Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

v2.2.8 #470

Merged
merged 11 commits into from
Sep 23, 2022
116 changes: 63 additions & 53 deletions docs/docs/signing.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ sidebar_position: 3

The Lattice1 is capable of signing messages on supported curves. For certain message types, it is capable of decoding and displaying the requests in more readable ways.

## ✍️ General Signing
# ✍️ General Signing

***This new signing mode was introduced Lattice firmare `v0.14.0`. GridPlus plans on deprecating the legacy signing mode and replacing it with general signing decoders. This document will be updated as that happens.***

Expand All @@ -17,7 +17,7 @@ You should import `Constants` when using general signing:
import { Constants } from `gridplus-sdk`
```

### 🖊️ Requesting Signatures
## 🖊️ Requesting Signatures

General signing allows you to request a signature on any message from a private key derived on any supported curve. Some curves (e.g. `secp256k1`) require a hashing algorithm to be specified in order to hash the message before signing. Other curves (e.g. `ed25519`) do not expect hashed messages prior to signing.

Expand All @@ -26,7 +26,7 @@ General signing allows you to request a signature on any message from a private
| Curve | `Constants.SIGNING.CURVES` | `SECP256K1`, `ED25519` | Curve on which to derive the signer's private key |
| Hash | `Constants.SIGNING.HASHES` | `KECCAK256`, `SHA256`, `NONE` | Hash to use prior to signing. Note that `ED25519` requires `NONE` as messages are not prehashed. |

#### Example: using generic signing
### Example: using generic signing

```ts
const msg = "I am the message to sign"
Expand All @@ -36,15 +36,19 @@ const req = {
0x80000000 + 60,
0x80000000,
]
curveType: Constants.SIGNING.CURVES.ED25519,
hashType: Constants.SIGNING.HASHES.NONE,
curveType: Constants.SIGNING.CURVES.SECP256K1,
hashType: Constants.SIGNING.HASHES.KECCAK256,
payload: msg
};

const sig = await client.sign(req)
```

### 📃 Message Decoders
:::note
When using the `gridplus-sdk` in a Node.js application with a version of Node lower than v18, you will need to patch the `fetch()` API in the global scope. One solution is to use the `node-fetch` package. See [the `node-fetch` README](https://github.com/node-fetch/node-fetch#installation) for instructions. Other options are available on NPM.
:::

## 📃 Message Decoders

By default, the message will be displayed on the Lattice's screen in either ASCII or hex -- if the message contains only ASCII, it will be displayed as such; otherwise it will get printed as a hex string. This means the Lattice can produce a signature for any message you like. However, there are additional decoders that make the request more readable on the Lattice. These decoders can be accessed inside of `Constants`:

Expand All @@ -55,12 +59,33 @@ const encodings = Constants.SIGNING.ENCODINGS
| Encoding | Description |
|:---------|:------------|
| `NONE` | Can also use `null` or not specify the `encodingType`. Lattice will display either an ASCII or a hex string depending on the payload. |
| `SOLANA` | Used to decode a Solana transaction. Transactions that cannot be decoded will be rejected. See `test/testGeneric.ts` for an example. |
| `TERRA` | Used to decode a Terra transaction. Only `MsgSend`, `MsgMultiSend`, and `MsgExecuteContract` are supported, but any transaction with unsupported message types will still decode -- the message type and calldata will be displayed raw. |
| `SOLANA` | Used to decode a Solana transaction. Transactions that cannot be decoded will be rejected. |
| `EVM` | Used to decode an EVM contract function call. May also be combined with ABI encoding data. To deploy a contract, set `to` as `null`. |

If you do not wish to specify a decoder, you can leave this field empty and the message will display either as ASCII or a hex string on the device.

#### Example: using the Solana decoder
### Example: Using the EVM Decoder

```ts
const tx = EthTxFactory.fromTxData(txData, { common: req.common });
const req = {
signerPath: [ // Derivation path of the first requested pubkey
0x80000000 + 44,
0x80000000 + 60,
0x80000000,
0,
0
]
curveType: Constants.SIGNING.CURVES.SECP256K1,
hashType: Constants.SIGNING.HASHES.KECCAK256,
encodingType: Constants.SIGNING.ENCODINGS.EVM,
payload: tx.getMessageToSign(false), // Pass serialized transaction
};

const sig = await client.sign(req)
```

### Example: Using the Solana Decoder

```ts
const msg = solTx.compileMessage().serialize()
Expand All @@ -79,7 +104,7 @@ const req = {
const sig = await client.sign(req)
```

### 💾 Calldata Decoding
## 💾 Calldata Decoding

:::note
All available calldata decoding options will be documented in this section. More may be added as time goes on.
Expand All @@ -88,75 +113,60 @@ All available calldata decoding options will be documented in this section. More
Certain transaction decoder types may support calldata decoding for request data. You can use this feature by including "calldata decoder data" (explained shortly) in a general signing request using the `decoder` request param:

```ts
req.decoder = <calldata decoder data>
req.data = {
payload: <Raw message to be signed, e.g. serialized transaction>,
decoder: <Optional serialized information about decoding the payload>
}
await client.sign(req);
```

If you include a valid calldata decoder, the appearance of the transaction's data on the user's Lattice should transform from a raw hex string to a markdown-style version which displays the function name, parameter names, and values.

#### Storing Calldata Decoders

Although not necessary, in certain situations it may be advantageous to pre-save decoders to the Lattice. One advantage is that if the decoder is saved, you do not need to include it in the transaction request, which frees up some space. Additionally, pre-saving data may unlock certain security features depending on the decoder type.
### 1️⃣ EVM Calldata Decoding

You can use the following API:
EVM transactions serialize calldata according to the [Ethereum ABI specification](https://docs.soliditylang.org/en/latest/abi-spec.html). The first four bytes of a transaction's `data` represent the "function selector", which is (sort of) a unique identifier for a given function.

> Please see API docs for all options. Also see tests in `test/signing/evm.ts` for examples on usage.

* `addDecoders`: Allows the user to add a series of calldata decoders for a specific decoder type (e.g. EVM). This will prompt the user to approve these decoders on the target Lattice before returning success.
* `getDecoders`: Fetch `n` consecutive decoders for a specific type, starting a specific index.
* `removeDecoders`: Remove a set of included decoders for a specific type. You can also set a flag to remove all decoders for a specific type.

#### 1️⃣ EVM
:::note
We do not support 100% of all edge cases in the ABI specification, but we do support the vast majority of types. Please open a pull request or an issue if your request fails to decode on a Lattice.
:::

EVM transactions serialize calldata according to the [Ethereum ABI specification](https://docs.soliditylang.org/en/latest/abi-spec.html). The first four bytes of a transaction's `data` represent the "function selector", which is (sort of) a unique identifier for a given function. You can build the calldata decoder data by either parsing a [Solidity JSON ABI](https://docs.ethers.io/v5/api/utils/abi/formats/#abi-formats--solidity) object (which you can fetch from [Etherscan](https://etherscan.io)) or by parsing an ABI canonical name (you can get this from [4byte](https://www.4byte.directory)). *Using the Solidity JSON ABI is recommended*.
We expose a method `Utils.fetchCalldataDecoder`, which will attempt to search [Etherscan](https://etherscan.io) (or the relevant clone, depending on `chainId`) for the function definition. If none is found it will try [4byte](https://4byte.directory) instead. If a function definition is found, `fetchCalldataDecoder` will parse and serialize it for the Lattice. `fetchCalldataDecoder` will return `{ abi, def }` and you will need to pass `def` into the signing request.

:::note
We do not support 100% of all edge cases in the ABI specification, but we do support the vast majority of types. Please open a pull request or an issue if your request fails to decode on a Lattice.
`fetchCalldataDecoder` takes in params `(tx.input, tx.to, tx.chainId, shouldRecurse)`. The first 3 come from the transaction object (note that `chainId` must be a regular integer), while `shouldRecurse` is used to flag whether to look up nested definitions, as is typical with contract patterns like `multicall`. You can only use `shouldRecurse` with Lattice firmware v0.16.0 and above.
:::

##### Example Usage (see `test/signing/evm.ts` for more examples)
#### Example Usage (see `test/signing/evm-abi.ts` for more examples)

```ts
import { Calldata } from 'gridplus-sdk';
import { Calldata, Utils } from 'gridplus-sdk';
const EVMCalldata = Calldata.EVM;

const tx = {an @ethereumjs/tx object}
const selector = tx.data.slice(0, 4).toString('hex'); // must be a hex string

// 1. Test JSON ABI object

// First get the decoder data
const abi = {a Solidity JSON ABI object fetched from Etherscan}
// Get the decoder data. This will attempt to look up an ABI using Etherscan
// and, if that fails, 4byte.directory.
// Arguments are: [`data`, `to`, `chainId`, recurse]
// NOTE: Setting `recurse = true` may result in additional requests. It is
// used for nested contract patterns such as `multicall`. It is only suppored
// by Lattice firmware v0.16.0 and up.
const { def } = await Utils.fetchCalldataDecoder(tx.input, tx.to, tx.chainId, true);
// Add the decoder to the request and the transaction should get marked down
const req = {
signerPath,
curveType: Constants.SIGNING.CURVES.SECP256K1,
hashType: Constants.SIGNING.HASHES.KECCAK256,
encodingType: Constants.SIGNING.ENCODINGS.EVM,
payload: tx.getMessageToSign(false), // will serialize the transaction
decoder: EVMCalldata.parsers.parseSolidityJSONABI(selector, abi)
decoder: def
};
const sig = await client.sign(req)

// 2. Test canonical name type

const canonicalName = 'myFunction(bytes,uint256)'; // assume this is the function being used
req.decoder = EVMCalldata.parsers.parseCanonicalName(selector, canonicalName);
const sig = await client.sign(req)
```

#### Param Names

There are two things to note about parameter names in EVM calldata decoding:

* The canonical name alone validates the function name and the parameter types, but it does *not* validate the parameter names (look at any canonical name and you will not find parameter names defined). This means that while we can send calldata decoder info in a request, a user cannot validate the *parameter* names unless the decoder has been pre-saved to the device. If a decoder was pre-saved, its param names will show a ✔️ icon on the decoder screen.
* Using `parseCanonicalName` will result in your decoder's param names being numerical values (#1, #2, etc) instead of the parameter names. This is because, again, the canonical name does not include parameter names. Therefore we do not recommend using `parseCanonicalName` if you have a Solidity JSON ABI object available and we definitely do not recommend *saving* decoders parsed from canonical names.

## 📜 Legacy Signing
# 📜 Legacy Signing

Prior to general signing, request data was sent to the Lattice in preformatted ways and was used to build the transaction in firmware. We are phasing out this mechanism, but for now it is how you request Ethereum, Bitcoin, and Ethereum-Message signatures. These signing methods are accessed using the `currency` flag in the request data.

### Ξ Ethereum (Transaction)
## Ξ Ethereum (Transaction)

All six Ethereum transactions must be specified in the request data along with a signer path.

Expand Down Expand Up @@ -190,7 +200,7 @@ const reqData = {
const sig = await client.sign(reqData)
```

### Ξ Ethereum (Message)
## Ξ Ethereum (Message)

Two message protocols are supported for Ethereum: `personal_sign` and `sign_typed_data`.

Expand All @@ -200,7 +210,7 @@ This is a protocol to display a simple, human readable message. It includes a pr

**`protocol` must be specified as `"signPersonal"`**.

##### Example: requesting signature on Ethereum `personal_sign` message
#### Example: requesting signature on Ethereum `personal_sign` message

```ts
const reqData = {
Expand All @@ -221,7 +231,7 @@ const reqData = {
const sig = await client.sign(reqData)
```

#### `sign_typed_data`
### `sign_typed_data`

This is used in protocols such as EIP712. It is meant to be an encoding for JSON-like data that can be more human readable.

Expand Down Expand Up @@ -254,7 +264,7 @@ const reqData = {
const sig = await client.sign(reqData)
```

### ₿ Bitcoin
## ₿ Bitcoin

Bitcoin transactions can be requested by including a set of UTXOs, which include the signer derivation path and spend type. The same `purpose` values are used to determine how UTXOs should be signed:

Expand All @@ -264,7 +274,7 @@ Bitcoin transactions can be requested by including a set of UTXOs, which include

The `purpose` of the `signerPath` in the given previous output (a.k.a. UTXO) is used to make the above determination.

#### Example: requesting BTC transactions
### Example: requesting BTC transactions

```ts
const p2wpkhInputs = [
Expand Down
31 changes: 20 additions & 11 deletions docs/docs/testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ The following options can be used after `env` with any test.
|:------|:--------|:------------|
| `REUSE_KEY` | Must be `1` | Indicates we will be creating a new pairing with a Lattice and stashing that connection |
| `DEVICE_ID` | A six character string | The device ID of the target Lattice |
| `ETHERSCAN_KEY` | Any string | API key for making requests to Etherscan. This is needed specifically for `e2e-sign-evm-abi`. |
| `name` | Any 5-25 character string (default="SDK Test") | The name of the pairing you will create |
| `baseUrl` | Any URL (default="https://signing.gridpl.us") | URL describing where to send HTTP requests. Should be changed if your Lattice is on non-default message routing infrastructure. |

Expand All @@ -55,14 +56,22 @@ See table in the next section.

## Reference: Tests and Options

This section gives an overview of each test and which options can be passed for the specific test (in addition to global options)

| Test | Description | Uses Test Runner | Additional `env` Options |
|:-----|:------------|:-----------------|:--------------|
| `npm run test` | Sets up test connection and tests basic functionality like `getAddresses` and `sign`. You need to run this with `REUSE_KEY=1` and pair before running any other tests. | No | N/A |
| `npm run test-signing` | Tests various aspects of the message signing path as well as all known decoders. | Yes | `SEED` (random string to seed a random number generator)<br/>`ETHERSCAN_KEY` (API key for making Etherscan requests. Used in EVM tests.) |
| `npm run test-btc` | *(Legacy pathway)* Tests spending different types of BTC inputs. Signatures validated against `bitcoinjs-lib` using seed exported by test harness. | Yes | `N` (number of random vectors to populate)<br/>`SEED` (random string to seed a random number generator)<br/>`testnet` (if true, testnet addresses and transactions will also be tested) |
| `npm run test-eth-msg` | *(Legacy pathway)* Tests Ethereum message requests `signPersonal` and `signTypedData`. Tests boundary conditions of EIP712 messages. | No | `N` (number of random vectors to populate)<br/>`SEED` (random string to seed a random number generator) |
| `npm run test-kv` | Tests loading and using kv (key-value) files. These are used for address tags. | No | N/A |
| `npm run test-non-exportable` | Tests to validate signatures from a SafeCards with a non-exportable seed (legacy) | No | N/A |
| `npm run test-wallet-jobs` | Tests exported addresses and public keys against those from reference libraries using seed exported by test harness. | Yes | N/A |
You can run the following tests with `npm run <test name>`.

| Test | Description | Requires `FEATURE_TEST_RUNNER=1` |
|:-----|:------------|:-----------------|
| `test` | Runs integration tests. Does not use Lattice. | No |
| `test-unit` | Runs SDK unit tests. Does not use Lattice. | No |
| `e2e` | Runs all end-to-end tests. | Yes |
| `e2e-btc` | Tests BTC signatures (legacy signing) | Yes |
| `e2e-eth` | Tests EIP712 and `personal_sign` messages (legacy signing) | No |
| `e2e-gen` | Tests seveal Lattice message routes and some SDK functionality. Bit of a legacy test but still useful. | No |
| `e2e-kv` | Tests KV-files, which are used primarily for tags. | No |
| `e2e-ne` | Tests non-exportable seeded SafeCards (legacy). | No |
| `e2e-sign` | Runs all signing tests. | Yes |
| `e2e-sign-determinism` | Tests determinism of signatures using known seed loading. | Yes |
| `e2e-sign-evm-abi` | Tests ABI decoding and fetching for EVM transactions. | Yes |
| `e2e-sign-evm-tx` | Tests EVM transaction types. | Yes |
| `e2e-sign-solana` | Tests Solana transactions and address derivation. | Yes |
| `e2e-sign-unformatted` | Tests signing unformatted payloads (ASCII or hex strings). | Yes |
| `e2e-wj` | Tests wallet jobs, validating path derivations, seed management, etc. | Yes |
Loading