gridplus-sdk
Advanced tools
Comparing version 0.7.27 to 0.8.0
{ | ||
"name": "gridplus-sdk", | ||
"version": "0.7.27", | ||
"version": "0.8.0", | ||
"description": "SDK to interact with GridPlus Lattice1 device", | ||
@@ -34,3 +34,3 @@ "scripts": { | ||
"elliptic": "6.5.4", | ||
"ethers": "^5.0.31", | ||
"ethers": "^5.4.2", | ||
"ethers-eip712": "^0.2.0", | ||
@@ -55,3 +55,3 @@ "js-sha3": "^0.8.0", | ||
"eslint-plugin-standard": "^4.0.0", | ||
"ethereumjs-tx": "^2.1.2", | ||
"ethereumjs-util": "^7.1.0", | ||
"it-each": "^0.4.0", | ||
@@ -58,0 +58,0 @@ "lodash": ">=4.17.21", |
@@ -745,3 +745,3 @@ const bitwise = require('bitwise'); | ||
// Determine the `v` param and add it to the sig before returning | ||
const rawTx = ethereum.buildEthRawTx(req, sig, ethAddr, req.useEIP155); | ||
const rawTx = ethereum.buildEthRawTx(req, sig, ethAddr); | ||
returnData.data = { | ||
@@ -748,0 +748,0 @@ tx: `0x${rawTx}`, |
@@ -276,2 +276,41 @@ // Consistent with Lattice's IV | ||
const legacy = (v.length === 0); | ||
// BASE FIELDS | ||
//-------------------------------------- | ||
// Various size constants have changed on the firmware side over time and | ||
// are captured here | ||
if (!legacy && gte(v, [0, 10, 4])) { | ||
// >=0.10.3 | ||
c.reqMaxDataSz = 1678; | ||
c.ethMaxGasPrice = 20000000000000; // 20000 gwei | ||
c.addrFlagsAllowed = true; | ||
} else if (!legacy && gte(v, [0, 10, 0])) { | ||
// >=0.10.0 | ||
c.reqMaxDataSz = 1678; | ||
c.ethMaxGasPrice = 20000000000000; // 20000 gwei | ||
c.addrFlagsAllowed = true; | ||
} else { | ||
// Legacy or <0.10.0 | ||
c.reqMaxDataSz = 1152; | ||
c.ethMaxGasPrice = 500000000000; // 500 gwei | ||
c.addrFlagsAllowed = false; | ||
} | ||
// These transformations apply to all versions | ||
c.ethMaxDataSz = c.reqMaxDataSz - 128; | ||
c.ethMaxMsgSz = c.ethMaxDataSz; | ||
// EXTRA FIELDS ADDED IN LATER VERSIONS | ||
//------------------------------------- | ||
// V0.10.12 allows new ETH transaction types | ||
if (!legacy && gte(v, [0, 11, 0])) { | ||
c.allowedEthTxTypesVersion = 1; | ||
c.allowedEthTxTypes = [ | ||
1, // eip2930 | ||
2, // eip1559 | ||
] | ||
c.totalExtraEthTxDataSz = 10; | ||
} | ||
// V0.10.10 allows a user to sign a prehashed ETH message if payload too big | ||
@@ -299,26 +338,3 @@ if (!legacy && gte(v, [0, 10, 10])) { | ||
} | ||
// Various size constants have changed on the firmware side over time and | ||
// are captured here | ||
if (!legacy && gte(v, [0, 10, 4])) { | ||
// >=0.10.3 | ||
c.reqMaxDataSz = 1678; | ||
c.ethMaxDataSz = c.reqMaxDataSz - 128; | ||
c.ethMaxMsgSz = c.ethMaxDataSz; | ||
c.ethMaxGasPrice = 20000000000000; // 20000 gwei | ||
c.addrFlagsAllowed = true; | ||
} else if (!legacy && gte(v, [0, 10, 0])) { | ||
// >=0.10.0 | ||
c.reqMaxDataSz = 1678; | ||
c.ethMaxDataSz = c.reqMaxDataSz - 128; | ||
c.ethMaxMsgSz = c.ethMaxDataSz; | ||
c.ethMaxGasPrice = 20000000000000; // 20000 gwei | ||
c.addrFlagsAllowed = true; | ||
} else { | ||
// Legacy or <0.10.0 | ||
c.reqMaxDataSz = 1152; | ||
c.ethMaxDataSz = c.reqMaxDataSz - 128; | ||
c.ethMaxMsgSz = c.ethMaxDataSz; | ||
c.ethMaxGasPrice = 500000000000; // 500 gwei | ||
c.addrFlagsAllowed = false; | ||
} | ||
return c; | ||
@@ -325,0 +341,0 @@ } |
@@ -49,3 +49,3 @@ // Utils for Ethereum transactions. This is effecitvely a shim of ethereumjs-util, which | ||
Buffer.from(keccak256(Buffer.concat([get_personal_sign_prefix(msg.length), msg])), 'hex'); | ||
return addRecoveryParam(hash, sig, signer, 1, false) | ||
return addRecoveryParam(hash, sig, signer, { chainId: 1, useEIP155: false }) | ||
} else if (input.protocol === 'eip712') { | ||
@@ -62,6 +62,6 @@ const digest = prehash ? prehash : eip712.TypedDataUtils.encodeDigest(req.input.payload); | ||
let { chainId=1 } = data; | ||
const { signerPath, eip155=null, fwConstants } = data; | ||
const { signerPath, eip155=null, fwConstants, type=null } = data; | ||
const { extraDataFrameSz, extraDataMaxFrames, prehashAllowed } = fwConstants; | ||
const EXTRA_DATA_ALLOWED = extraDataFrameSz > 0 && extraDataMaxFrames > 0; | ||
const MAX_BASE_DATA_SZ = fwConstants.ethMaxDataSz; | ||
let MAX_BASE_DATA_SZ = fwConstants.ethMaxDataSz; | ||
const VAR_PATH_SZ = fwConstants.varAddrPathSzAllowed; | ||
@@ -80,3 +80,12 @@ | ||
throw new Error('`signerPath` not provided'); | ||
// We support eip1559 and eip2930 types (as well as legacy) | ||
const eip1559IsAllowed = (fwConstants.allowedEthTxTypes && | ||
fwConstants.allowedEthTxTypes.indexOf(2) > -1); | ||
const eip2930IsAllowed = (fwConstants.allowedEthTxTypes && | ||
fwConstants.allowedEthTxTypes.indexOf(1) > -1); | ||
const isEip1559 = (eip1559IsAllowed && (type === 2 || type === 'eip1559')); | ||
const isEip2930 = (eip2930IsAllowed && (type === 1 || type === 'eip2930')); | ||
if (type !== null && !isEip1559 && !isEip2930) | ||
throw new Error('Unsupported Ethereum transaction type'); | ||
// Determine if we should use EIP155 given the chainID. | ||
@@ -87,4 +96,8 @@ // If we are explicitly told to use eip155, we will use it. Otherwise, | ||
let useEIP155 = chainUsesEIP155(chainId); | ||
if (eip155 !== null && typeof eip155 === 'boolean') | ||
if (eip155 !== null && typeof eip155 === 'boolean') { | ||
useEIP155 = eip155; | ||
} else if (isEip1559 || isEip2930) { | ||
// Newer transaction types do not use EIP155 since the chainId is serialized | ||
useEIP155 = false; | ||
} | ||
@@ -102,4 +115,5 @@ // Hack for metamask, which sends value=null for 0 ETH transactions | ||
// Build the transaction buffer array | ||
const chainIdBytes = ensureHexBuffer(chainId); | ||
const nonceBytes = ensureHexBuffer(data.nonce); | ||
const gasPriceBytes = ensureHexBuffer(data.gasPrice); | ||
let gasPriceBytes; | ||
const gasLimitBytes = ensureHexBuffer(data.gasLimit); | ||
@@ -109,4 +123,28 @@ const toBytes = ensureHexBuffer(data.to); | ||
const dataBytes = ensureHexBuffer(data.data); | ||
if (isEip1559 || isEip2930) { | ||
// EIP1559 and EIP2930 transactions have a chainID field | ||
rawTx.push(chainIdBytes); | ||
} | ||
rawTx.push(nonceBytes); | ||
rawTx.push(gasPriceBytes); | ||
let maxPriorityFeePerGasBytes, maxFeePerGasBytes; | ||
if (isEip1559) { | ||
if (!data.maxPriorityFeePerGas) | ||
throw new Error('EIP1559 transactions must include `maxPriorityFeePerGas`'); | ||
if (!data.maxPriorityFeePerGas) | ||
throw new Error('EIP1559 transactions must include `maxFeePerGas`'); | ||
maxPriorityFeePerGasBytes = ensureHexBuffer(data.maxPriorityFeePerGas); | ||
rawTx.push(maxPriorityFeePerGasBytes); | ||
maxFeePerGasBytes = ensureHexBuffer(data.maxFeePerGas); | ||
rawTx.push(maxFeePerGasBytes); | ||
// EIP1559 renamed "gasPrice" to "maxFeePerGas", but firmware still | ||
// uses `gasPrice` in the struct, so update that value here. | ||
gasPriceBytes = maxFeePerGasBytes; | ||
if (0 > Buffer.compare(maxFeePerGasBytes, maxPriorityFeePerGasBytes)) | ||
throw new Error('EIP1559 requirement not met: (maxFeePerGasBytes > maxPriorityFeePerGasBytes)') | ||
} else { | ||
// EIP1559 transactions do not have the gasPrice field | ||
gasPriceBytes = ensureHexBuffer(data.gasPrice); | ||
rawTx.push(gasPriceBytes); | ||
} | ||
rawTx.push(gasLimitBytes); | ||
@@ -116,5 +154,21 @@ rawTx.push(toBytes); | ||
rawTx.push(dataBytes); | ||
// Add empty v,r,s values | ||
if (useEIP155 === true) { | ||
rawTx.push(ensureHexBuffer(chainId)); // v | ||
// We do not currently support accessList in firmware so we need to prehash if | ||
// the list is non-null | ||
let PREHASH_FROM_ACCESS_LIST = false; | ||
if (isEip1559 || isEip2930) { | ||
const accessList = []; | ||
if (Array.isArray(data.accessList)) { | ||
data.accessList.forEach((listItem) => { | ||
const keys = []; | ||
listItem.storageKeys.forEach((key) => { | ||
keys.push(ensureHexBuffer(key)) | ||
}) | ||
accessList.push([ ensureHexBuffer(listItem.address), keys ]) | ||
PREHASH_FROM_ACCESS_LIST = true; | ||
}) | ||
} | ||
rawTx.push(accessList); | ||
} else if (useEIP155 === true) { | ||
// Add empty v,r,s values for EIP155 legacy transactions | ||
rawTx.push(chainIdBytes); // v (which is the same as chainId in EIP155 txs) | ||
rawTx.push(ensureHexBuffer(null)); // r | ||
@@ -127,2 +181,9 @@ rawTx.push(ensureHexBuffer(null)); // s | ||
const ETH_TX_NON_DATA_SZ = 122; // Accounts for metadata and non-data params | ||
let ETH_TX_EXTRA_FIELDS_SZ = 0; // Accounts for newer ETH tx types (e.g. eip1559) | ||
if (fwConstants.allowedEthTxTypesVersion === 1) { | ||
// eip1559 and eip2930 | ||
// Add extra params and shrink the data region (extraData blocks are unaffected) | ||
ETH_TX_EXTRA_FIELDS_SZ = fwConstants.totalExtraEthTxDataSz; | ||
MAX_BASE_DATA_SZ -= ETH_TX_EXTRA_FIELDS_SZ; | ||
} | ||
const txReqPayload = Buffer.alloc(MAX_BASE_DATA_SZ + ETH_TX_NON_DATA_SZ); | ||
@@ -152,3 +213,2 @@ let off = 0; | ||
} | ||
// 2. Signer Path | ||
@@ -177,2 +237,26 @@ //------------------ | ||
valueBytes.copy(txReqPayload, off + (32 - valueBytes.length)); off += 32; | ||
// Extra Tx data comes before `data` in the struct | ||
let PREHASH_UNSUPPORTED = false; | ||
if (fwConstants.allowedEthTxTypesVersion === 1) { | ||
const extraEthTxDataSz = fwConstants.totalExtraEthTxDataSz || 0; | ||
// Some types may not be supported by firmware, so we will need to prehash | ||
if (PREHASH_FROM_ACCESS_LIST) { | ||
PREHASH_UNSUPPORTED = true; | ||
} | ||
txReqPayload.writeUint8(PREHASH_UNSUPPORTED === true, off); off += 1; | ||
// EIP1559 & EIP2930 struct version | ||
if (isEip1559) { | ||
txReqPayload.writeUint8(2, off); off += 1; // Eip1559 type enum value | ||
if (maxPriorityFeePerGasBytes.length > 8) | ||
throw new Error('maxPriorityFeePerGasBytes too large'); | ||
maxPriorityFeePerGasBytes.copy(txReqPayload, off + (8 - maxPriorityFeePerGasBytes.length)); off += 8; | ||
} else if (isEip2930) { | ||
txReqPayload.writeUint8(1, off); off += 1; // Eip2930 type enum value | ||
off += extraEthTxDataSz - 2; // Skip EIP1559 params | ||
} else { | ||
off += extraEthTxDataSz - 1; // Skip EIP1559 and EIP2930 params | ||
} | ||
} | ||
// Flow data into extraData requests, which will follow-up transaction requests, if supported/applicable | ||
@@ -196,6 +280,5 @@ const extraDataPayloads = []; | ||
} | ||
if (prehashAllowed && totalSz > maxSzAllowed) { | ||
// If this payload is too large to send, but the Lattice allows a prehashed message, do that | ||
prehash = Buffer.from(keccak256(rlp.encode(rawTx)), 'hex') | ||
prehash = Buffer.from(keccak256(get_rlp_encoded_preimage(rawTx, type)), 'hex') | ||
} else { | ||
@@ -212,3 +295,8 @@ if ((!EXTRA_DATA_ALLOWED) || (EXTRA_DATA_ALLOWED && totalSz > maxSzAllowed)) | ||
} | ||
} else if (PREHASH_UNSUPPORTED) { | ||
// If something is unsupported in firmware but we want to allow such transactions, | ||
// we prehash the message here. | ||
prehash = Buffer.from(keccak256(get_rlp_encoded_preimage(rawTx, type)), 'hex') | ||
} | ||
// Write the data size (does *NOT* include the chainId buffer, if that exists) | ||
@@ -230,2 +318,3 @@ txReqPayload.writeUInt16BE(dataBytes.length, off); off += 2; | ||
rawTx, | ||
type, | ||
payload: txReqPayload.slice(0, off), | ||
@@ -255,9 +344,9 @@ extraDataPayloads, | ||
// and attah the full signature to the end of the transaction payload | ||
exports.buildEthRawTx = function(tx, sig, address, useEIP155=true) { | ||
exports.buildEthRawTx = function(tx, sig, address) { | ||
// RLP-encode the data we sent to the lattice | ||
const rlpEncoded = rlp.encode(tx.rawTx); | ||
const hash = Buffer.from(keccak256(rlpEncoded), 'hex') | ||
const newSig = addRecoveryParam(hash, sig, address, tx.chainId, useEIP155); | ||
const hash = Buffer.from(keccak256(get_rlp_encoded_preimage(tx.rawTx, tx.type)), 'hex'); | ||
const newSig = addRecoveryParam(hash, sig, address, tx); | ||
// Use the signature to generate a new raw transaction payload | ||
const newRawTx = tx.rawTx.slice(0, 6); | ||
// Strip the last 3 items and replace them with signature components | ||
const newRawTx = tx.useEIP155 ? tx.rawTx.slice(0, -3) : tx.rawTx; | ||
newRawTx.push(newSig.v); | ||
@@ -268,7 +357,11 @@ // Per `ethereumjs-tx`, RLP encoding should include signature components w/ stripped zeros | ||
newRawTx.push(stripZeros(newSig.s)); | ||
return rlp.encode(newRawTx).toString('hex'); | ||
let rlpEncodedWithSig = rlp.encode(newRawTx); | ||
if (tx.type) { | ||
rlpEncodedWithSig = Buffer.concat([Buffer.from([tx.type]), rlpEncodedWithSig]) | ||
} | ||
return rlpEncodedWithSig.toString('hex'); | ||
} | ||
// Attach a recovery parameter to a signature by brute-forcing ECRecover | ||
function addRecoveryParam(hashBuf, sig, address, chainId, useEIP155) { | ||
function addRecoveryParam(hashBuf, sig, address, txData={}) { | ||
try { | ||
@@ -286,3 +379,3 @@ // Rebuild the keccak256 hash here so we can `ecrecover` | ||
if (pubToAddrStr(pubkey) === address.toString('hex')) { | ||
sig.v = getRecoveryParam(v, useEIP155, chainId); | ||
sig.v = getRecoveryParam(v, txData); | ||
return sig; | ||
@@ -294,3 +387,3 @@ } | ||
if (pubToAddrStr(pubkey) === address.toString('hex')) { | ||
sig.v = getRecoveryParam(v, useEIP155, chainId); | ||
sig.v = getRecoveryParam(v, txData); | ||
return sig; | ||
@@ -328,6 +421,15 @@ } else { | ||
// * For EIP155 transactions, return `(CHAIN_ID*2) + 35 + v` | ||
function getRecoveryParam(v, useEIP155, chainId=null) { | ||
function getRecoveryParam(v, txData={}) { | ||
const { chainId, useEIP155, type } = txData; | ||
// For EIP1559 and EIP2930 transactions, we want the recoveryParam (0 or 1) | ||
// rather than the `v` value because the `chainId` is already included in the | ||
// transaction payload. | ||
if (type === 1 || type === 2) { | ||
return ensureHexBuffer(v, true); // 0 or 1, with 0 expected as an empty buffer | ||
} | ||
// If we are not using EIP155, convert v directly to a buffer and return it | ||
if (false === useEIP155 || chainId === null) | ||
return Buffer.from(new BN(v).plus(27).toString(16), 'hex'); | ||
// We will use EIP155 in most cases. Convert v to a bignum and operate on it. | ||
@@ -724,2 +826,10 @@ // Note that the protocol calls for v = (CHAIN_ID*2) + 35/36, where 35 or 36 | ||
); | ||
} | ||
function get_rlp_encoded_preimage(rawTx, txType) { | ||
if (txType) { | ||
return Buffer.concat([Buffer.from([txType]), rlp.encode(rawTx)]); | ||
} else { | ||
return rlp.encode(rawTx); | ||
} | ||
} |
111782
2538
Updatedethers@^5.4.2