Skip to content

Commit

Permalink
MINOR add cip64 support (#10614)
Browse files Browse the repository at this point in the history
* add cip64 support
fix signing test that was supposed to be testing eip1559
dont include fields that are not part of spec in the EncodedTransaction

* successfully recover cip64 transactions

* add cip64 support
dont include fields that are not part of spec in the EncodedTransaction

* successfully recover cip64 transactions

* while there IS sig here we need to ignore it for the math to check out.
  • Loading branch information
aaronmgdr committed Sep 25, 2023
1 parent 5bf6fd6 commit 33eb441
Show file tree
Hide file tree
Showing 6 changed files with 226 additions and 43 deletions.
9 changes: 7 additions & 2 deletions packages/sdk/connect/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ export { BlockNumber, EventLog, Log, PromiEvent, Sign } from 'web3-core'
export { Block, BlockHeader, Syncing } from 'web3-eth'
export { Contract, ContractSendMethod, PastEventOptions } from 'web3-eth-contract'

export type TransactionTypes = 'eip1559' | 'celo-legacy' | 'cip42'
export type TransactionTypes = 'eip1559' | 'celo-legacy' | 'cip42' | 'cip64'

interface CommonTXProperties {
nonce: string
Expand All @@ -90,6 +90,11 @@ export interface EIP1559TXProperties extends FeeMarketAndAccessListTXProperties
type: 'eip1559'
}

export interface CIP64TXProperties extends FeeMarketAndAccessListTXProperties {
feeCurrency: string
type: 'cip64'
}

export interface CIP42TXProperties extends FeeMarketAndAccessListTXProperties {
feeCurrency: string
gatewayFeeRecipient?: string
Expand All @@ -110,7 +115,7 @@ export interface LegacyTXProperties extends CommonTXProperties {

export interface EncodedTransaction {
raw: Hex
tx: LegacyTXProperties | CIP42TXProperties | EIP1559TXProperties
tx: LegacyTXProperties | CIP42TXProperties | EIP1559TXProperties | CIP64TXProperties
}

export type CeloTxPending = Transaction & Partial<CeloParams>
Expand Down
23 changes: 23 additions & 0 deletions packages/sdk/connect/src/utils/formatter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,29 @@ describe('inputCeloTxFormatter', () => {
`)
})
})
describe('valid cip64 tx', () => {
const cip64 = {
...base,
maxFeePerGas: '0x3e8',
maxPriorityFeePerGas: '0x3e8',
feeCurrency: '0x11f4d0A3c12e86B4b5F39B213F7E19D048276DAe',
}
it('formats', () => {
expect(inputCeloTxFormatter(cip64)).toMatchInlineSnapshot(`
{
"data": "0x",
"feeCurrency": "0x11f4d0a3c12e86b4b5f39b213f7e19d048276dae",
"from": "0x11f4d0a3c12e86b4b5f39b213f7e19d048276dae",
"gas": "0xf4240",
"maxFeePerGas": "0x3e8",
"maxPriorityFeePerGas": "0x3e8",
"nonce": "0x1",
"to": "0x11f4d0a3c12e86b4b5f39b213f7e19d048276dae",
"value": "0x241",
}
`)
})
})
describe('valid cip42 tx', () => {
const cip42 = {
...base,
Expand Down
57 changes: 36 additions & 21 deletions packages/sdk/wallets/wallet-base/src/signing-utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { celo } from 'viem/chains'
import Web3 from 'web3'
import {
extractSignature,
getSignerFromTxCIP42,
getSignerFromTxEIP2718TX,
isPriceToLow,
recoverTransaction,
rlpEncodedTx,
Expand Down Expand Up @@ -139,62 +139,51 @@ describe('rlpEncodedTx', () => {
})

describe('when maxFeePerGas and maxPriorityFeePerGas and feeCurrency are provided', () => {
it('orders fields in RLP as specified by CIP42', () => {
const CIP42Transaction = {
it('orders fields in RLP as specified by CIP64', () => {
const CIP64Transaction = {
...eip1559Transaction,
feeCurrency: '0x5409ED021D9299bf6814279A6A1411A7e866A631',
}
const result = rlpEncodedTx(CIP42Transaction)
const result = rlpEncodedTx(CIP64Transaction)
expect(result).toMatchInlineSnapshot(`
{
"rlpEncode": "0x7cf8400280630a63945409ed021d9299bf6814279a6a1411a7e866a6318080941be31a94361a391bbafb2a4ccd704f57dc04d4bb893635c9adc5dea0000083abcdefc0",
"rlpEncode": "0x7bf83e0280630a63941be31a94361a391bbafb2a4ccd704f57dc04d4bb893635c9adc5dea0000083abcdefc0945409ed021d9299bf6814279a6a1411a7e866a631",
"transaction": {
"chainId": 2,
"data": "0xabcdef",
"feeCurrency": "0x5409ed021d9299bf6814279a6a1411a7e866a631",
"from": "0x1be31a94361a391bbafb2a4ccd704f57dc04d4bb",
"gas": "0x63",
"gasPrice": "0x",
"gatewayFee": "0x",
"gatewayFeeRecipient": "0x",
"maxFeePerGas": "0x0a",
"maxPriorityFeePerGas": "0x63",
"nonce": 0,
"to": "0x1be31a94361a391bbafb2a4ccd704f57dc04d4bb",
"value": "0x3635c9adc5dea00000",
},
"type": "cip42",
"type": "cip64",
}
`)
})
})

describe('when maxFeePerGas and maxPriorityFeePerGas are provided', () => {
it('orders fields in RLP as specified by EIP1559', () => {
const CIP42Transaction = {
...eip1559Transaction,
feeCurrency: '0x5409ED021D9299bf6814279A6A1411A7e866A631',
}
const result = rlpEncodedTx(CIP42Transaction)
const result = rlpEncodedTx(eip1559Transaction)
expect(result).toMatchInlineSnapshot(`
{
"rlpEncode": "0x7cf8400280630a63945409ed021d9299bf6814279a6a1411a7e866a6318080941be31a94361a391bbafb2a4ccd704f57dc04d4bb893635c9adc5dea0000083abcdefc0",
"rlpEncode": "0x02e90280630a63941be31a94361a391bbafb2a4ccd704f57dc04d4bb893635c9adc5dea0000083abcdefc0",
"transaction": {
"chainId": 2,
"data": "0xabcdef",
"feeCurrency": "0x5409ed021d9299bf6814279a6a1411a7e866a631",
"from": "0x1be31a94361a391bbafb2a4ccd704f57dc04d4bb",
"gas": "0x63",
"gasPrice": "0x",
"gatewayFee": "0x",
"gatewayFeeRecipient": "0x",
"maxFeePerGas": "0x0a",
"maxPriorityFeePerGas": "0x63",
"nonce": 0,
"to": "0x1be31a94361a391bbafb2a4ccd704f57dc04d4bb",
"value": "0x3635c9adc5dea00000",
},
"type": "cip42",
"type": "eip1559",
}
`)
})
Expand Down Expand Up @@ -380,6 +369,32 @@ describe('recoverTransaction', () => {
]
`)
})
it('handles cip64 transactions', () => {
const cip64TX =
'0x7bf88282ad5a8063630a94588e4b68193001e4d10928660ab4165b813717c0880de0b6b3a764000083abcdefc094cd2a3d9f938e13cd947ec05abc7fe734df8dd82680a091b5504a59e529e7efa42dbb97fbc3311a91d035c873a94ab0789441fc989f84a02e8254d6b3101b63417e5d496833bc84f4832d4a8bf8a2b83e291d8f38c0f62d'
expect(recoverTransaction(cip64TX)).toMatchInlineSnapshot(`
[
{
"accessList": [],
"chainId": 44378,
"data": "0xabcdef",
"feeCurrency": "0xcd2a3d9f938e13cd947ec05abc7fe734df8dd826",
"gas": 10,
"maxFeePerGas": 99,
"maxPriorityFeePerGas": 99,
"nonce": 0,
"r": "0x91b5504a59e529e7efa42dbb97fbc3311a91d035c873a94ab0789441fc989f84",
"s": "0x2e8254d6b3101b63417e5d496833bc84f4832d4a8bf8a2b83e291d8f38c0f62d",
"to": "0x588e4b68193001e4d10928660ab4165b813717c0",
"type": "cip64",
"v": 27,
"value": 1000000000000000000,
"yParity": 0,
},
"0x1Be31A94361a391bBaFB2a4CCd704F57dc04d4bb",
]
`)
})
it('handles cip42 transactions', () => {
const cip42TX =
'0x7cf89a82ad5a8063630a94cd2a3d9f938e13cd947ec05abc7fe734df8dd826941be31a94361a391bbafb2a4ccd704f57dc04d4bb82567894588e4b68193001e4d10928660ab4165b813717c0880de0b6b3a764000083abcdefc01ba0c610507b2ac3cff80dd7017419021196807d605efce0970c18cde48db33c27d1a01799477e0f601f554f0ee6f7ac21490602124801e9f7a99d9605249b90f03112'
Expand Down Expand Up @@ -579,7 +594,7 @@ describe('getSignerFromTx', () => {
},
{ serializer: celo.serializers?.transaction }
)
expect(getSignerFromTxCIP42(signed)).toEqual(account.address)
expect(getSignerFromTxEIP2718TX(signed)).toEqual(account.address)
})
})

Expand Down
124 changes: 109 additions & 15 deletions packages/sdk/wallets/wallet-base/src/signing-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,26 @@ export function rlpEncodedTx(tx: CeloTx): RLPEncodedTx {
transaction.maxPriorityFeePerGas = stringNumberOrBNToHex(tx.maxPriorityFeePerGas)

let rlpEncode: Hex
if (isCIP42(tx)) {
if (isCIP64(tx)) {
// https://github.com/celo-org/celo-proposals/blob/master/CIPs/cip-0064.md
// 0x7b || rlp([chainId, nonce, maxPriorityFeePerGas, maxFeePerGas, gasLimit, to, value, data, accessList, feeCurrency, signatureYParity, signatureR, signatureS]).
rlpEncode = RLP.encode([
stringNumberToHex(transaction.chainId),
stringNumberToHex(transaction.nonce),
transaction.maxPriorityFeePerGas || '0x',
transaction.maxFeePerGas || '0x',
transaction.gas || '0x',
transaction.to || '0x',
transaction.value || '0x',
transaction.data || '0x',
transaction.accessList || [],
transaction.feeCurrency || '0x',
])
delete transaction.gatewayFee
delete transaction.gatewayFeeRecipient
delete transaction.gasPrice
return { transaction, rlpEncode: concatHex([TxTypeToPrefix.cip64, rlpEncode]), type: 'cip64' }
} else if (isCIP42(tx)) {
// There shall be a typed transaction with the code 0x7c that has the following format:
// 0x7c || rlp([chain_id, nonce, max_priority_fee_per_gas, max_fee_per_gas, gas_limit, feecurrency, gatewayFeeRecipient, gatewayfee, destination, amount, data, access_list, signature_y_parity, signature_r, signature_s]).
// This will be in addition to the type 0x02 transaction as specified in EIP-1559.
Expand All @@ -147,7 +166,8 @@ export function rlpEncodedTx(tx: CeloTx): RLPEncodedTx {
transaction.data || '0x',
transaction.accessList || [],
])
return { transaction, rlpEncode: concatHex(['0x7c', rlpEncode]), type: 'cip42' }
delete transaction.gasPrice
return { transaction, rlpEncode: concatHex([TxTypeToPrefix.cip42, rlpEncode]), type: 'cip42' }
} else if (isEIP1559(tx)) {
// https://eips.ethereum.org/EIPS/eip-1559
// 0x02 || rlp([chain_id, nonce, max_priority_fee_per_gas, max_fee_per_gas, gas_limit, destination, amount, data, access_list, signature_y_parity, signature_r, signature_s]).
Expand All @@ -162,7 +182,15 @@ export function rlpEncodedTx(tx: CeloTx): RLPEncodedTx {
transaction.data || '0x',
transaction.accessList || [],
])
return { transaction, rlpEncode: concatHex(['0x02', rlpEncode]), type: 'eip1559' }
delete transaction.feeCurrency
delete transaction.gatewayFee
delete transaction.gatewayFeeRecipient
delete transaction.gasPrice
return {
transaction,
rlpEncode: concatHex([TxTypeToPrefix.eip1559, rlpEncode]),
type: 'eip1559',
}
} else {
// This order should match the order in Geth.
// https://github.com/celo-org/celo-blockchain/blob/027dba2e4584936cc5a8e8993e4e27d28d5247b8/core/types/transaction.go#L65
Expand All @@ -187,6 +215,7 @@ export function rlpEncodedTx(tx: CeloTx): RLPEncodedTx {
enum TxTypeToPrefix {
'celo-legacy' = '',
cip42 = '0x7c',
cip64 = '0x7b',
eip1559 = '0x02',
}

Expand Down Expand Up @@ -255,6 +284,15 @@ function isEIP1559(tx: CeloTx): boolean {
return isPresent(tx.maxFeePerGas) && isPresent(tx.maxPriorityFeePerGas)
}

function isCIP64(tx: CeloTx) {
return (
isEIP1559(tx) &&
isPresent(tx.feeCurrency) &&
!isPresent(tx.gatewayFeeRecipient) &&
!isPresent(tx.gatewayFeeRecipient)
)
}

function isCIP42(tx: CeloTx): boolean {
return (
isEIP1559(tx) &&
Expand Down Expand Up @@ -349,10 +387,13 @@ function prefixAwareRLPDecode(rlpEncode: string, type: TransactionTypes): string
return type === 'celo-legacy' ? RLP.decode(rlpEncode) : RLP.decode(`0x${rlpEncode.slice(4)}`)
}

function correctLengthWithSignatureOf(type: TransactionTypes) {
function correctLengthOf(type: TransactionTypes, includeSig: boolean = true) {
switch (type) {
case 'cip64': {
return includeSig ? 13 : 10
}
case 'cip42':
return 15
return includeSig ? 15 : 12
case 'celo-legacy':
case 'eip1559':
return 12
Expand All @@ -363,9 +404,9 @@ export function extractSignature(rawTx: string) {
const type = determineTXType(rawTx)
const rawValues = prefixAwareRLPDecode(rawTx, type)
const length = rawValues.length
if (correctLengthWithSignatureOf(type) !== length) {
if (correctLengthOf(type) !== length) {
throw new Error(
`@extractSignature: provided transaction has ${length} elements but ${type} txs with a signature have ${correctLengthWithSignatureOf(
`@extractSignature: provided transaction has ${length} elements but ${type} txs with a signature have ${correctLengthOf(
type
)} ${JSON.stringify(rawValues)}`
)
Expand Down Expand Up @@ -399,6 +440,8 @@ export function recoverTransaction(rawTx: string): [CeloTx, string] {
}

switch (determineTXType(rawTx)) {
case 'cip64':
return recoverTransactionCIP64(rawTx as Hex)
case 'cip42':
return recoverTransactionCIP42(rawTx as Hex)
case 'eip1559':
Expand Down Expand Up @@ -433,9 +476,10 @@ export function recoverTransaction(rawTx: string): [CeloTx, string] {
}

// inspired by @ethereumjs/tx
function getPublicKeyofSignerFromTx(transactionArray: string[]) {
const base = transactionArray.slice(0, 12) // 12 is length of cip42 without vrs fields
const message = concatHex([TxTypeToPrefix.cip42, RLP.encode(base).slice(2)])
function getPublicKeyofSignerFromTx(transactionArray: string[], type: TransactionTypes) {
// this needs to be 10 for cip64, 12 for cip42 and eip1559
const base = transactionArray.slice(0, correctLengthOf(type, false))
const message = concatHex([TxTypeToPrefix[type], RLP.encode(base).slice(2)])
const msgHash = keccak256(hexToBytes(message))

const { v, r, s } = extractSignatureFromDecoded(transactionArray)
Expand All @@ -451,19 +495,24 @@ function getPublicKeyofSignerFromTx(transactionArray: string[]) {
}
}

export function getSignerFromTxCIP42(serializedTransaction: string): string {
export function getSignerFromTxEIP2718TX(serializedTransaction: string): string {
const transactionArray: any[] = RLP.decode(`0x${serializedTransaction.slice(4)}`)
const signer = getPublicKeyofSignerFromTx(transactionArray)
const signer = getPublicKeyofSignerFromTx(
transactionArray,
determineTXType(serializedTransaction)
)
return toChecksumAddress(Address.fromPublicKey(signer).toString())
}

function determineTXType(serializedTransaction: string): TransactionTypes {
const prefix = serializedTransaction.slice(0, 4)

if (prefix === '0x02') {
if (prefix === TxTypeToPrefix.eip1559) {
return 'eip1559'
} else if (prefix === '0x7c') {
} else if (prefix === TxTypeToPrefix.cip42) {
return 'cip42'
} else if (prefix === TxTypeToPrefix.cip64) {
return 'cip64'
}
return 'celo-legacy'
}
Expand Down Expand Up @@ -523,7 +572,52 @@ function recoverTransactionCIP42(serializedTransaction: Hex): [CeloTxWithSig, st
}

const signer =
transactionArray.length === 15 ? getSignerFromTxCIP42(serializedTransaction) : 'unsigned'
transactionArray.length === 15 ? getSignerFromTxEIP2718TX(serializedTransaction) : 'unsigned'
return [celoTX, signer]
}

function recoverTransactionCIP64(serializedTransaction: Hex): [CeloTxWithSig, string] {
const transactionArray: any[] = prefixAwareRLPDecode(serializedTransaction, 'cip64')
debug('signing-utils@recoverTransactionCIP64: values are %s', transactionArray)
if (transactionArray.length !== 13 && transactionArray.length !== 10) {
throw new Error(
`Invalid transaction length for type CIP64: ${transactionArray.length} instead of 13 or 10. array: ${transactionArray}`
)
}
const [
chainId,
nonce,
maxPriorityFeePerGas,
maxFeePerGas,
gas,
to,
value,
data,
accessList,
feeCurrency,
vRaw,
r,
s,
] = transactionArray

const celoTX: CeloTxWithSig = {
type: 'cip64',
nonce: nonce.toLowerCase() === '0x' ? 0 : parseInt(nonce, 16),
maxPriorityFeePerGas:
maxPriorityFeePerGas.toLowerCase() === '0x' ? 0 : parseInt(maxPriorityFeePerGas, 16),
maxFeePerGas: maxFeePerGas.toLowerCase() === '0x' ? 0 : parseInt(maxFeePerGas, 16),
gas: gas.toLowerCase() === '0x' ? 0 : parseInt(gas, 16),
feeCurrency,
to,
value: value.toLowerCase() === '0x' ? 0 : parseInt(value, 16),
data,
chainId: chainId.toLowerCase() === '0x' ? 0 : parseInt(chainId, 16),
accessList: parseAccessList(accessList),
...vrsForRecovery(vRaw, r, s),
}

const signer =
transactionArray.length === 13 ? getSignerFromTxEIP2718TX(serializedTransaction) : 'unsigned'
return [celoTX, signer]
}

Expand Down
Loading

0 comments on commit 33eb441

Please sign in to comment.