From 3fabbb3f1028d9653c782e647647bf791df5798c Mon Sep 17 00:00:00 2001 From: Jochem Brouwer Date: Fri, 15 Jan 2021 22:44:36 +0100 Subject: [PATCH 01/32] common: add EIP-2718, EIP-2930 common: ensure that EIPs enforce required EIPs are active --- packages/common/src/eips/2718.json | 12 ++++++++++++ packages/common/src/eips/2930.json | 22 ++++++++++++++++++++++ packages/common/src/eips/index.ts | 2 ++ packages/common/src/index.ts | 8 ++++++++ packages/common/tests/eips.spec.ts | 21 +++++++++++++++++++++ 5 files changed, 65 insertions(+) create mode 100644 packages/common/src/eips/2718.json create mode 100644 packages/common/src/eips/2930.json diff --git a/packages/common/src/eips/2718.json b/packages/common/src/eips/2718.json new file mode 100644 index 00000000000..e0eb26850e8 --- /dev/null +++ b/packages/common/src/eips/2718.json @@ -0,0 +1,12 @@ +{ + "name": "EIP-2718", + "comment": "Typed Transaction Envelope", + "url": "https://eips.ethereum.org/EIPS/eip-2718", + "status": "Draft", + "minimumHardfork": "chainstart", + "gasConfig": {}, + "gasPrices": {}, + "vm": {}, + "pow": {} + } + \ No newline at end of file diff --git a/packages/common/src/eips/2930.json b/packages/common/src/eips/2930.json new file mode 100644 index 00000000000..5c82947f065 --- /dev/null +++ b/packages/common/src/eips/2930.json @@ -0,0 +1,22 @@ +{ + "name": "EIP-2929", + "comment": "Optional access lists", + "url": "https://eips.ethereum.org/EIPS/eip-2930", + "status": "Draft", + "minimumHardfork": "berlin", + "gasConfig": {}, + "gasPrices": { + "accessListStorageKeyCost": { + "v": 1900, + "d": "Gas cost per storage key in an Access List transaction" + }, + "accessListAddressCost": { + "v": 2400, + "d": "Gas cost per storage key in an Access List transaction" + } + }, + "requiredEIPs": [2718, 2929], + "vm": {}, + "pow": {} + } + \ No newline at end of file diff --git a/packages/common/src/eips/index.ts b/packages/common/src/eips/index.ts index ef24089b432..3911fd25b63 100644 --- a/packages/common/src/eips/index.ts +++ b/packages/common/src/eips/index.ts @@ -4,5 +4,7 @@ export const EIPs: eipsType = { 2315: require('./2315.json'), 2537: require('./2537.json'), 2565: require('./2565.json'), + 2718: require('./2718.json'), 2929: require('./2929.json'), + 2930: require('./2930.json'), } diff --git a/packages/common/src/index.ts b/packages/common/src/index.ts index 70951e78a4d..1250cee094e 100644 --- a/packages/common/src/index.ts +++ b/packages/common/src/index.ts @@ -274,6 +274,14 @@ export default class Common extends EventEmitter { `${eip} cannot be activated on hardfork ${this.hardfork()}, minimumHardfork: ${minHF}` ) } + if (EIPs[eip].requiredEIPs) { + // eslint-disable-next-line prettier/prettier + (EIPs[eip].requiredEIPs).forEach((elem: number) => { + if (!eips.includes(elem)) { + throw new Error(`${eip} requires EIP ${elem}, but is not included in the EIP list`) + } + }) + } } this._eips = eips } diff --git a/packages/common/tests/eips.spec.ts b/packages/common/tests/eips.spec.ts index 4a77bce7ffc..41b3830c965 100644 --- a/packages/common/tests/eips.spec.ts +++ b/packages/common/tests/eips.spec.ts @@ -44,4 +44,25 @@ tape('[Common]: Initialization / Chain params', function (t: tape.Test) { st.end() }) + + t.test( + 'Should throw when trying to initialize with an EIP which requires certain EIPs, but which are not included on the EIP list', + function (st: tape.Test) { + const eips = [2930] + const msg = + 'should throw when initializing with an EIP, which does not have required EIPs on the EIP list' + const f = () => { + new Common({ chain: 'mainnet', eips, hardfork: 'berlin' }) + } + st.throws(f, msg) + st.end() + } + ) + + t.test('Should not throw when initializing with a valid EIP list', function (st: tape.Test) { + const eips = [2718, 2929, 2930] + new Common({ chain: 'mainnet', eips, hardfork: 'berlin' }) + st.pass('initialized correctly') + st.end() + }) }) From 76af18f2533e08b8699c321ac770e5df741aec67 Mon Sep 17 00:00:00 2001 From: Jochem Brouwer Date: Mon, 1 Mar 2021 17:57:29 +0100 Subject: [PATCH 02/32] tx: introduce transactionFactory tx: introduce EIP2930Transaction --- packages/tx/src/eip2930Transaction.ts | 167 ++++++++++++++++++ packages/tx/src/index.ts | 4 +- .../{transaction.ts => legacyTransaction.ts} | 17 +- packages/tx/src/transactionFactory.ts | 63 +++++++ packages/tx/src/types.ts | 72 +++++++- 5 files changed, 311 insertions(+), 12 deletions(-) create mode 100644 packages/tx/src/eip2930Transaction.ts rename packages/tx/src/{transaction.ts => legacyTransaction.ts} (96%) create mode 100644 packages/tx/src/transactionFactory.ts diff --git a/packages/tx/src/eip2930Transaction.ts b/packages/tx/src/eip2930Transaction.ts new file mode 100644 index 00000000000..99021e4a2bb --- /dev/null +++ b/packages/tx/src/eip2930Transaction.ts @@ -0,0 +1,167 @@ +import Common from '@ethereumjs/common' +import { Address, BN, rlp, toBuffer } from 'ethereumjs-util' +import { DEFAULT_COMMON, EIP2930TxData, TxOptions } from './types' + +export default class EIP2930Transaction { + public readonly common: Common + public readonly chainId: BN + public readonly nonce: BN + public readonly gasLimit: BN + public readonly gasPrice: BN + public readonly to?: Address + public readonly value: BN + public readonly data: Buffer + public readonly accessList: any + public readonly yParity?: number + public readonly r?: BN + public readonly s?: BN + + get senderS() { + return this.s + } + + get senderR() { + return this.r + } + + public static fromTxData(txData: EIP2930TxData, opts?: TxOptions) { + return new EIP2930Transaction(txData, opts ?? {}) + } + + // Instantiate a transaction from the raw RLP serialized tx. This means that the RLP should start with 0x01. + public static fromRlpSerializedTx(serialized: Buffer, opts?: TxOptions) { + if (serialized[0] !== 1) { + throw 'This is not an EIP-2930 transaction' + } + + const values = rlp.decode(serialized) + + if (!Array.isArray(values)) { + throw new Error('Invalid serialized tx input. Must be array') + } + + return EIP2930Transaction.fromValuesArray(values, opts) + } + + // Create a transaction from a values array. + // The format is: chainId, nonce, gasPrice, gasLimit, to, value, data, access_list, [yParity, senderR, senderS] + public static fromValuesArray(values: Buffer[], opts?: TxOptions) { + if (values.length == 8) { + const [chainId, nonce, gasPrice, gasLimit, to, value, data, accessList] = values + const emptyBuffer = Buffer.from([]) + + return new EIP2930Transaction( + { + chainId: new BN(chainId), + nonce: new BN(nonce), + gasPrice: new BN(gasPrice), + gasLimit: new BN(gasLimit), + to: to && to.length > 0 ? new Address(to) : undefined, + value: new BN(value), + data: data ?? emptyBuffer, + accessList: accessList ?? emptyBuffer, + }, + opts ?? {} + ) + } else if (values.length == 11) { + // TODO: return EIP2930SignedTransaction + const [ + chainId, + nonce, + gasPrice, + gasLimit, + to, + value, + data, + accessList, + yParity, + r, + s, + ] = values + const emptyBuffer = Buffer.from([]) + + return new EIP2930Transaction( + { + chainId: new BN(chainId), + nonce: new BN(nonce), + gasPrice: new BN(gasPrice), + gasLimit: new BN(gasLimit), + to: to && to.length > 0 ? new Address(to) : undefined, + value: new BN(value), + data: data ?? emptyBuffer, + accessList: accessList ?? emptyBuffer, + yParity: !yParity?.equals(emptyBuffer) + ? parseInt(yParity.toString('hex'), 16) + : undefined, + r: !r?.equals(emptyBuffer) ? new BN(r) : undefined, + s: !s?.equals(emptyBuffer) ? new BN(s) : undefined, + }, + opts ?? {} + ) + } else { + throw new Error( + 'Invalid EIP-2930 transaction. Only expecting 8 values (for unsigned tx) or 11 values (for signed tx).' + ) + } + } + + private constructor(txData: EIP2930TxData, opts: TxOptions) { + this.common = opts.common ?? DEFAULT_COMMON + + const { + chainId, + nonce, + gasPrice, + gasLimit, + to, + value, + data, + accessList, + yParity, + r, + s, + } = txData + + if (!this.common.eips().includes(2718)) { + throw new Error('EIP-2718 not enabled on Common') + } else if (!this.common.eips().includes(2930)) { + throw new Error('EIP-2930 not enabled on Common') + } + + if (txData.chainId?.eqn(this.common.chainId())) { + throw new Error('The chain ID does not match the chain ID of Common') + } + + this.chainId = new BN(toBuffer(chainId)) + this.nonce = new BN(toBuffer(nonce)) + this.gasPrice = new BN(toBuffer(gasPrice)) + this.gasLimit = new BN(toBuffer(gasLimit)) + this.to = to ? new Address(toBuffer(to)) : undefined + this.value = new BN(toBuffer(value)) + this.data = toBuffer(data) + this.accessList = accessList ?? [] + this.yParity = yParity ?? 0 + this.r = r ? new BN(toBuffer(r)) : undefined + this.s = s ? new BN(toBuffer(s)) : undefined + + // Verify the access list format. + for (let key = 0; key < this.accessList.length; key++) { + const accessListItem = this.accessList[key] + const address: Buffer = accessListItem[0] + const storageSlots: Buffer[] = accessListItem[1] + if (address.length != 20) { + throw new Error('Invalid EIP-2930 transaction: address length should be 20 bytes') + } + for (let storageSlot = 0; storageSlot < storageSlots.length; storageSlot++) { + if (storageSlots[storageSlot].length != 32) { + throw new Error('Invalid EIP-2930 transaction: storage slot length should be 32 bytes') + } + } + } + + const freeze = opts?.freeze ?? true + if (freeze) { + Object.freeze(this) + } + } +} diff --git a/packages/tx/src/index.ts b/packages/tx/src/index.ts index f9795e60465..7893213c624 100644 --- a/packages/tx/src/index.ts +++ b/packages/tx/src/index.ts @@ -1,2 +1,4 @@ -export { default as Transaction } from './transaction' +export { default as LegacyTransaction } from './legacyTransaction' +export { default as EIP2930Transaction } from './eip2930Transaction' +export { default as TransactionFactory } from './transactionFactory' export * from './types' diff --git a/packages/tx/src/transaction.ts b/packages/tx/src/legacyTransaction.ts similarity index 96% rename from packages/tx/src/transaction.ts rename to packages/tx/src/legacyTransaction.ts index 2231294b444..a05cb1d5a87 100644 --- a/packages/tx/src/transaction.ts +++ b/packages/tx/src/legacyTransaction.ts @@ -16,7 +16,7 @@ import { MAX_INTEGER, } from 'ethereumjs-util' import Common from '@ethereumjs/common' -import { TxOptions, TxData, JsonTx } from './types' +import { TxOptions, LegacyTxData, JsonTx, DEFAULT_COMMON } from './types' // secp256k1n/2 const N_DIV_2 = new BN('7fffffffffffffffffffffffffffffff5d576e7357a4501ddfe92f46681b20a0', 16) @@ -24,7 +24,7 @@ const N_DIV_2 = new BN('7fffffffffffffffffffffffffffffff5d576e7357a4501ddfe92f46 /** * An Ethereum transaction. */ -export default class Transaction { +export default class LegacyTransaction { public readonly common: Common public readonly nonce: BN public readonly gasLimit: BN @@ -36,8 +36,8 @@ export default class Transaction { public readonly r?: BN public readonly s?: BN - public static fromTxData(txData: TxData, opts?: TxOptions) { - return new Transaction(txData, opts) + public static fromTxData(txData: LegacyTxData, opts?: TxOptions) { + return new LegacyTransaction(txData, opts) } public static fromRlpSerializedTx(serialized: Buffer, opts?: TxOptions) { @@ -61,7 +61,7 @@ export default class Transaction { const emptyBuffer = Buffer.from([]) - return new Transaction( + return new LegacyTransaction( { nonce: new BN(nonce), gasPrice: new BN(gasPrice), @@ -82,7 +82,7 @@ export default class Transaction { * Use the static factory methods to assist in creating a Transaction object from varying data types. * @note Transaction objects implement EIP155 by default. To disable it, pass in an `@ethereumjs/common` object set before EIP155 activation (i.e. before Spurious Dragon). */ - constructor(txData: TxData, opts?: TxOptions) { + private constructor(txData: LegacyTxData, opts?: TxOptions) { const { nonce, gasPrice, gasLimit, to, value, data, v, r, s } = txData this.nonce = new BN(toBuffer(nonce)) @@ -112,8 +112,7 @@ export default class Transaction { if (opts?.common) { this.common = Object.assign(Object.create(Object.getPrototypeOf(opts.common)), opts.common) } else { - const DEFAULT_CHAIN = 'mainnet' - this.common = new Common({ chain: DEFAULT_CHAIN }) + this.common = DEFAULT_COMMON } this._validateTxV(this.v) @@ -233,7 +232,7 @@ export default class Transaction { common: this.common, } - return new Transaction( + return new LegacyTransaction( { nonce: this.nonce, gasPrice: this.gasPrice, diff --git a/packages/tx/src/transactionFactory.ts b/packages/tx/src/transactionFactory.ts new file mode 100644 index 00000000000..19bf9f18d13 --- /dev/null +++ b/packages/tx/src/transactionFactory.ts @@ -0,0 +1,63 @@ +import Common from '@ethereumjs/common' +import { default as LegacyTransaction } from './legacyTransaction' +import { default as EIP2930Transaction } from './eip2930Transaction' +import { TxOptions, Transaction } from './types' + +const DEFAULT_COMMON = new Common({ chain: 'mainnet' }) + +export default class TransactionFactory { + // It is not possible to instantiate a TransactionFactory object. + private constructor() {} + + public static fromRawData(rawData: Buffer, transactionOptions: TxOptions): Transaction { + const common = transactionOptions.common ?? DEFAULT_COMMON + if (rawData[0] <= 0x7f) { + // It is an EIP-2718 Typed Transaction + if (!common.eips().includes(2718)) { + throw new Error('Cannot create a TypedTransaction: EIP-2718 is not enabled') + } + // Determine the type. + let EIP: number + switch (rawData[0]) { + case 1: + EIP = 2930 + break + default: + throw new Error(`TypedTransaction with ID ${rawData[0]} unknown`) + } + + if (!common.eips().includes(EIP)) { + throw new Error( + `Cannot create TypedTransaction with ID ${rawData[0]}: EIP ${EIP} not activated` + ) + } + + return EIP2930Transaction.fromRlpSerializedTx(rawData, transactionOptions) + } else { + return LegacyTransaction.fromRlpSerializedTx(rawData, transactionOptions) + } + } + + /** + * This helper method allows one to retrieve the class which matches the transactionID + * If transactionID is undefined, return the LegacyTransaction class. + * @param transactionID + * @param common + */ + public static getTransactionClass(transactionID?: number, common?: Common) { + const usedCommon = common ?? DEFAULT_COMMON + if (transactionID) { + if (!usedCommon.eips().includes(2718)) { + throw new Error('Cannot create a TypedTransaction: EIP-2718 is not enabled') + } + switch (transactionID) { + case 1: + return EIP2930Transaction + default: + throw new Error(`TypedTransaction with ID ${transactionID} unknown`) + } + } + + return LegacyTransaction + } +} diff --git a/packages/tx/src/types.ts b/packages/tx/src/types.ts index cb2681e730d..661dc8b261b 100644 --- a/packages/tx/src/types.ts +++ b/packages/tx/src/types.ts @@ -1,5 +1,7 @@ -import { AddressLike, BNLike, BufferLike } from 'ethereumjs-util' +import { AddressLike, BN, BNLike, BufferLike } from 'ethereumjs-util' import Common from '@ethereumjs/common' +import { default as LegacyTransaction } from './legacyTransaction' +import { default as EIP2930Transaction } from './eip2930Transaction' /** * The options for initializing a Transaction. @@ -32,7 +34,7 @@ export interface TxOptions { /** * An object with an optional field with each of the transaction's values. */ -export interface TxData { +export interface LegacyTxData { /** * The transaction's nonce. */ @@ -79,6 +81,70 @@ export interface TxData { s?: BNLike } +/** + * An object with an optional field with each of the transaction's values. + */ +export interface EIP2930TxData { + /** + * The transaction's chain ID + */ + chainId?: BN + + /** + * The transaction's nonce. + */ + nonce?: BN + + /** + * The transaction's gas price. + */ + gasPrice?: BN + + /** + * The transaction's gas limit. + */ + gasLimit?: BN + + /** + * The transaction's the address is sent to. + */ + to?: AddressLike + + /** + * The amount of Ether sent. + */ + value?: BN + + /** + * This will contain the data of the message or the init of a contract. + */ + data?: Buffer + + /** + * The access list which contains the addresses/storage slots which the transaction wishes to access + */ + accessList?: any // TODO: typesafe this + + /** + * Parity of the transaction + */ + yParity?: number + + /** + * EC signature parameter. (This is senderR in the EIP) + */ + r?: BN + + /** + * EC signature parameter. (This is senderS in the EIP) + */ + s?: BN +} + +export type TxData = LegacyTxData | EIP2930TxData + +export type Transaction = LegacyTransaction | EIP2930Transaction + /** * An object with all of the transaction's values represented as strings. */ @@ -93,3 +159,5 @@ export interface JsonTx { s?: string value?: string } + +export const DEFAULT_COMMON = new Common({ chain: 'mainnet' }) From 1847d8be406eab1728df1fe82e1f5d2d23baad9e Mon Sep 17 00:00:00 2001 From: Jochem Brouwer Date: Sat, 23 Jan 2021 23:14:05 +0100 Subject: [PATCH 03/32] tx: add some legacy transaction methods to EIP2930 --- packages/tx/src/eip2930Transaction.ts | 74 ++++++++++++++++++++++++++- 1 file changed, 73 insertions(+), 1 deletion(-) diff --git a/packages/tx/src/eip2930Transaction.ts b/packages/tx/src/eip2930Transaction.ts index 99021e4a2bb..3fdb1c9ced7 100644 --- a/packages/tx/src/eip2930Transaction.ts +++ b/packages/tx/src/eip2930Transaction.ts @@ -1,5 +1,5 @@ import Common from '@ethereumjs/common' -import { Address, BN, rlp, toBuffer } from 'ethereumjs-util' +import { Address, BN, bnToRlp, ecsign, rlp, rlphash, toBuffer } from 'ethereumjs-util' import { DEFAULT_COMMON, EIP2930TxData, TxOptions } from './types' export default class EIP2930Transaction { @@ -164,4 +164,76 @@ export default class EIP2930Transaction { Object.freeze(this) } } + + /** + * If the tx's `to` is to the creation address + */ + toCreationAddress(): boolean { + return this.to === undefined || this.to.buf.length === 0 + } + + /** + * Computes a sha3-256 hash of the unserialized tx. + * This hash is signed by the private key. It is different from the transaction hash, which are put in blocks. + * The transaction hash also hashes the data used to sign the transaction. + */ + rawTxHash(): Buffer { + const values = [ + Buffer.from('01', 'hex'), + this.chainId.toBuffer(), + this.nonce.toBuffer(), + this.gasPrice.toBuffer(), + this.gasLimit.toBuffer(), + this.to !== undefined ? this.to.buf : Buffer.from([]), + this.value.toBuffer(), + this.data, + this.accessList, + ] + + return rlphash(values) + } + + getMessageToSign() { + return this.rawTxHash() + } + + /** + * Returns chain ID + */ + getChainId(): number { + return this.common.chainId() + } + + sign(privateKey: Buffer) { + if (privateKey.length !== 32) { + throw new Error('Private key must be 32 bytes in length.') + } + + const msgHash = this.getMessageToSign() + + // Only `v` is reassigned. + /* eslint-disable-next-line prefer-const */ + let { v, r, s } = ecsign(msgHash, privateKey) + + const opts = { + common: this.common, + } + + return EIP2930Transaction.fromTxData( + { + chainId: this.chainId, + nonce: this.nonce, + gasPrice: this.gasPrice, + gasLimit: this.gasLimit, + to: this.to, + value: this.value, + data: this.data, + accessList: this.accessList, + yParity: v, // TODO: check if this is correct. Should be a number between 0/1 + r: new BN(r), + s: new BN(s), + }, + opts + ) + } } From 5f2a1c9f12af1653434cb3ab540c5f841df2d840 Mon Sep 17 00:00:00 2001 From: Jochem Brouwer Date: Mon, 1 Mar 2021 18:14:14 +0100 Subject: [PATCH 04/32] tx: eip2930: add more legacy methods --- packages/tx/src/eip2930Transaction.ts | 141 +++++++++++++++++++++++--- 1 file changed, 126 insertions(+), 15 deletions(-) diff --git a/packages/tx/src/eip2930Transaction.ts b/packages/tx/src/eip2930Transaction.ts index 3fdb1c9ced7..2e7908ca2d6 100644 --- a/packages/tx/src/eip2930Transaction.ts +++ b/packages/tx/src/eip2930Transaction.ts @@ -1,5 +1,5 @@ import Common from '@ethereumjs/common' -import { Address, BN, bnToRlp, ecsign, rlp, rlphash, toBuffer } from 'ethereumjs-util' +import { Address, BN, bnToHex, ecsign, rlp, rlphash, toBuffer } from 'ethereumjs-util' import { DEFAULT_COMMON, EIP2930TxData, TxOptions } from './types' export default class EIP2930Transaction { @@ -149,6 +149,11 @@ export default class EIP2930Transaction { const accessListItem = this.accessList[key] const address: Buffer = accessListItem[0] const storageSlots: Buffer[] = accessListItem[1] + if (accessListItem[2] !== undefined) { + throw new Error( + 'Access list item cannot have 3 elements. It can only have an address, and an array of storage slots.' + ) + } if (address.length != 20) { throw new Error('Invalid EIP-2930 transaction: address length should be 20 bytes') } @@ -178,23 +183,11 @@ export default class EIP2930Transaction { * The transaction hash also hashes the data used to sign the transaction. */ rawTxHash(): Buffer { - const values = [ - Buffer.from('01', 'hex'), - this.chainId.toBuffer(), - this.nonce.toBuffer(), - this.gasPrice.toBuffer(), - this.gasLimit.toBuffer(), - this.to !== undefined ? this.to.buf : Buffer.from([]), - this.value.toBuffer(), - this.data, - this.accessList, - ] - - return rlphash(values) + return this.getMessageToSign() } getMessageToSign() { - return this.rawTxHash() + return rlphash(this.raw()) } /** @@ -236,4 +229,122 @@ export default class EIP2930Transaction { opts ) } + + /** + * The amount of gas paid for the data in this tx + */ + getDataFee(): BN { + const txDataZero = this.common.param('gasPrices', 'txDataZero') + const txDataNonZero = this.common.param('gasPrices', 'txDataNonZero') + const accessListStorageKeyCost = this.common.param('gasPrices', 'accessListStorageKeyCost') + const accessListAddressCost = this.common.param('gasPrices', 'accessListAddressCost') + + let cost = 0 + for (let i = 0; i < this.data.length; i++) { + this.data[i] === 0 ? (cost += txDataZero) : (cost += txDataNonZero) + } + + let slots = 0 + for (let index = 0; index < this.accessList.length; index++) { + const item = this.accessList[index] + const storageSlots = item[1] + slots += storageSlots.length + } + + const addresses = this.accessList.length + cost += addresses * accessListAddressCost + slots * accessListStorageKeyCost + + return new BN(cost) + } + + /** + * The minimum amount of gas the tx must have (DataFee + TxFee + Creation Fee) + */ + getBaseFee(): BN { + const fee = this.getDataFee().addn(this.common.param('gasPrices', 'tx')) + if (this.common.gteHardfork('homestead') && this.toCreationAddress()) { + fee.iaddn(this.common.param('gasPrices', 'txCreation')) + } + return fee + } + + /** + * The up front amount that an account must have for this transaction to be valid + */ + getUpfrontCost(): BN { + return this.gasLimit.mul(this.gasPrice).add(this.value) + } + + /** + * Validates the signature and checks if + * the transaction has the minimum amount of gas required + * (DataFee + TxFee + Creation Fee). + */ + validate(): boolean + validate(stringError: false): boolean + validate(stringError: true): string[] + validate(stringError: boolean = false): boolean | string[] { + const errors = [] + + if (this.getBaseFee().gt(this.gasLimit)) { + errors.push(`gasLimit is too low. given ${this.gasLimit}, need at least ${this.getBaseFee()}`) + } + + return stringError ? errors : errors.length === 0 + } + + /** + * Returns a Buffer Array of the raw Buffers of this transaction, in order. + */ + raw(): Buffer[] { + return [ + Buffer.from('01', 'hex'), + this.chainId.toBuffer(), + this.nonce.toBuffer(), + this.gasPrice.toBuffer(), + this.gasLimit.toBuffer(), + this.to !== undefined ? this.to.buf : Buffer.from([]), + this.value.toBuffer(), + this.data, + this.accessList, + ] + } + + /** + * Returns the rlp encoding of the transaction. + */ + serialize(): Buffer { + return rlp.encode(this.raw()) + } + + /** + * Returns an object with the JSON representation of the transaction + */ + toJSON(): any { + // TODO: fix type + const accessListJSON = [] + for (let index = 0; index < this.accessList.length; index++) { + const item = this.accessList[index] + const JSONItem: any = ['0x' + item[0].toString('hex')] + const storageSlots = item[1] + const JSONSlots = [] + for (let slot = 0; slot < storageSlots.length; slot++) { + const storageSlot = storageSlots[slot] + JSONSlots.push('0x' + storageSlot.toString('hex')) + } + JSONItem.push(JSONSlots) + accessListJSON.push(JSONItem) + } + + return { + chainId: bnToHex(this.chainId), + nonce: bnToHex(this.nonce), + gasPrice: bnToHex(this.gasPrice), + gasLimit: bnToHex(this.gasLimit), + to: this.to !== undefined ? this.to.toString() : undefined, + value: bnToHex(this.value), + data: '0x' + this.data.toString('hex'), + accessList: accessListJSON, + } + } } From f9852cab3d0ffc213ad2df2c58a0eb07909880e0 Mon Sep 17 00:00:00 2001 From: Jochem Brouwer Date: Mon, 1 Mar 2021 18:14:26 +0100 Subject: [PATCH 05/32] tx: tests: update tests so that these run --- packages/tx/test/api.ts | 98 ++++++++++++++------------- packages/tx/test/transactionRunner.ts | 4 +- 2 files changed, 52 insertions(+), 50 deletions(-) diff --git a/packages/tx/test/api.ts b/packages/tx/test/api.ts index 1ecd1d75200..4ed5e09112e 100644 --- a/packages/tx/test/api.ts +++ b/packages/tx/test/api.ts @@ -10,22 +10,22 @@ import { unpadBuffer, } from 'ethereumjs-util' import Common from '@ethereumjs/common' -import { Transaction, TxData } from '../src' +import { LegacyTransaction, TxData } from '../src' import { TxsJsonEntry, VitaliksTestsDataEntry } from './types' const txFixtures: TxsJsonEntry[] = require('./json/txs.json') const txFixturesEip155: VitaliksTestsDataEntry[] = require('./json/ttTransactionTestEip155VitaliksTests.json') tape('[Transaction]: Basic functions', function (t) { - const transactions: Transaction[] = [] + const transactions: LegacyTransaction[] = [] t.test('should initialize correctly', function (st) { - let tx = Transaction.fromTxData({}) + let tx = LegacyTransaction.fromTxData({}) st.equal(tx.common.hardfork(), 'istanbul', 'should initialize with correct default HF') st.ok(Object.isFrozen(tx), 'tx should be frozen by default') const common = new Common({ chain: 'mainnet', hardfork: 'spuriousDragon' }) - tx = Transaction.fromTxData({}, { common }) + tx = LegacyTransaction.fromTxData({}, { common }) st.equal(tx.common.hardfork(), 'spuriousDragon', 'should initialize with correct HF provided') common.setHardfork('byzantium') @@ -35,7 +35,7 @@ tape('[Transaction]: Basic functions', function (t) { 'should stay on correct HF if outer common HF changes' ) - tx = Transaction.fromTxData({}, { freeze: false }) + tx = LegacyTransaction.fromTxData({}, { freeze: false }) st.ok(!Object.isFrozen(tx), 'tx should not be frozen when freeze deactivated in options') // Perform the same test as above, but now using a different construction method. This also implies that passing on the @@ -45,16 +45,16 @@ tape('[Transaction]: Basic functions', function (t) { const zero = Buffer.alloc(0) const valuesArray = [zero, zero, zero, zero, zero, zero] - tx = Transaction.fromRlpSerializedTx(rlpData) + tx = LegacyTransaction.fromRlpSerializedTx(rlpData) st.ok(Object.isFrozen(tx), 'tx should be frozen by default') - tx = Transaction.fromRlpSerializedTx(rlpData, { freeze: false }) + tx = LegacyTransaction.fromRlpSerializedTx(rlpData, { freeze: false }) st.ok(!Object.isFrozen(tx), 'tx should not be frozen when freeze deactivated in options') - tx = Transaction.fromValuesArray(valuesArray) + tx = LegacyTransaction.fromValuesArray(valuesArray) st.ok(Object.isFrozen(tx), 'tx should be frozen by default') - tx = Transaction.fromValuesArray(valuesArray, { freeze: false }) + tx = LegacyTransaction.fromValuesArray(valuesArray, { freeze: false }) st.ok(!Object.isFrozen(tx), 'tx should not be frozen when freeze deactivated in options') st.end() @@ -63,7 +63,7 @@ tape('[Transaction]: Basic functions', function (t) { t.test('should decode transactions', function (st) { txFixtures.slice(0, 4).forEach(function (tx: any) { const txData = tx.raw.map(toBuffer) - const pt = Transaction.fromValuesArray(txData) + const pt = LegacyTransaction.fromValuesArray(txData) st.equal(bufferToHex(unpadBuffer(toBuffer(pt.nonce))), tx.raw[0]) st.equal(bufferToHex(toBuffer(pt.gasPrice)), tx.raw[1]) @@ -94,7 +94,7 @@ tape('[Transaction]: Basic functions', function (t) { chain: 'mainnet', hardfork: 'tangerineWhistle', }) - const tx = Transaction.fromValuesArray(txFixtures[3].raw.map(toBuffer), { + const tx = LegacyTransaction.fromValuesArray(txFixtures[3].raw.map(toBuffer), { common, }) st.deepEqual( @@ -113,7 +113,7 @@ tape('[Transaction]: Basic functions', function (t) { }) t.test('should hash with defined chainId', function (st) { - const tx = Transaction.fromValuesArray(txFixtures[4].raw.map(toBuffer)) + const tx = LegacyTransaction.fromValuesArray(txFixtures[4].raw.map(toBuffer)) st.equal( tx.hash().toString('hex'), '0f09dc98ea85b7872f4409131a790b91e7540953992886fc268b7ba5c96820e4' @@ -131,19 +131,19 @@ tape('[Transaction]: Basic functions', function (t) { t.test('should verify Signatures', function (st) { transactions.forEach(function (tx) { - st.equals(tx.verifySignature(), true) + st.equals((tx).verifySignature(), true) }) st.end() }) t.test('should not verify invalid signatures', function (st) { - const txs: Transaction[] = [] + const txs: LegacyTransaction[] = [] txFixtures.slice(0, 4).forEach(function (txFixture: any) { const txData = txFixture.raw.map(toBuffer) // set `s` to zero txData[8] = zeros(32) - const tx = Transaction.fromValuesArray(txData) + const tx = LegacyTransaction.fromValuesArray(txData) txs.push(tx) }) @@ -214,11 +214,11 @@ tape('[Transaction]: Basic functions', function (t) { }) t.test('should round trip decode a tx', function (st) { - const tx = Transaction.fromTxData({ value: 5000 }) + const tx = LegacyTransaction.fromTxData({ value: 5000 }) const s1 = tx.serialize() const s1Rlp = toBuffer('0x' + s1.toString('hex')) - const tx2 = Transaction.fromRlpSerializedTx(s1Rlp) + const tx2 = LegacyTransaction.fromRlpSerializedTx(s1Rlp) const s2 = tx2.serialize() st.ok(s1.equals(s2)) @@ -226,29 +226,29 @@ tape('[Transaction]: Basic functions', function (t) { }) t.test('should accept lesser r values', function (st) { - const tx = Transaction.fromTxData({ r: new BN(toBuffer('0x0005')) }) + const tx = LegacyTransaction.fromTxData({ r: new BN(toBuffer('0x0005')) }) st.equals(tx.r!.toString('hex'), '5') st.end() }) t.test('should return data fee', function (st) { - let tx = Transaction.fromTxData({}) + let tx = LegacyTransaction.fromTxData({}) st.equals(tx.getDataFee().toNumber(), 0) - tx = Transaction.fromValuesArray(txFixtures[3].raw.map(toBuffer)) + tx = LegacyTransaction.fromValuesArray(txFixtures[3].raw.map(toBuffer)) st.equals(tx.getDataFee().toNumber(), 1716) st.end() }) t.test('should return base fee', function (st) { - const tx = Transaction.fromTxData({}) + const tx = LegacyTransaction.fromTxData({}) st.equals(tx.getBaseFee().toNumber(), 53000) st.end() }) t.test('should return upfront cost', function (st) { - const tx = Transaction.fromTxData({ + const tx = LegacyTransaction.fromTxData({ gasPrice: 1000, gasLimit: 10000000, value: 42, @@ -259,7 +259,7 @@ tape('[Transaction]: Basic functions', function (t) { t.test("Verify EIP155 Signature based on Vitalik's tests", function (st) { txFixturesEip155.forEach(function (tx) { - const pt = Transaction.fromRlpSerializedTx(toBuffer(tx.rlp)) + const pt = LegacyTransaction.fromRlpSerializedTx(toBuffer(tx.rlp)) st.equal(pt.getMessageToSign().toString('hex'), tx.hash) st.equal('0x' + pt.serialize().toString('hex'), tx.rlp) st.equal(pt.getSenderAddress().toString(), '0x' + tx.sender) @@ -281,7 +281,7 @@ tape('[Transaction]: Basic functions', function (t) { '4646464646464646464646464646464646464646464646464646464646464646', 'hex' ) - const pt = Transaction.fromValuesArray(txRaw.map(toBuffer)) + const pt = LegacyTransaction.fromValuesArray(txRaw.map(toBuffer)) // Note that Vitalik's example has a very similar value denoted "signing data". // It's not the output of `serialize()`, but the pre-image of the hash returned by `tx.hash(false)`. @@ -318,7 +318,7 @@ tape('[Transaction]: Basic functions', function (t) { 'hex' ) const common = new Common({ chain: 3 }) - const tx = Transaction.fromValuesArray(txRaw.map(toBuffer), { common }) + const tx = LegacyTransaction.fromValuesArray(txRaw.map(toBuffer), { common }) const signedTx = tx.sign(privateKey) st.equal( signedTx.serialize().toString('hex'), @@ -331,7 +331,7 @@ tape('[Transaction]: Basic functions', function (t) { t.test('sign tx with chainId specified in params', function (st) { const common = new Common({ chain: 42, hardfork: 'petersburg' }) - let tx = Transaction.fromTxData({}, { common }) + let tx = LegacyTransaction.fromTxData({}, { common }) st.equal(tx.getChainId(), 42) const privKey = Buffer.from(txFixtures[0].privateKey, 'hex') @@ -339,7 +339,7 @@ tape('[Transaction]: Basic functions', function (t) { const serialized = tx.serialize() - const reTx = Transaction.fromRlpSerializedTx(serialized, { common }) + const reTx = LegacyTransaction.fromRlpSerializedTx(serialized, { common }) st.equal(reTx.verifySignature(), true) st.equal(reTx.getChainId(), 42) @@ -347,7 +347,7 @@ tape('[Transaction]: Basic functions', function (t) { }) t.test('returns correct values for isSigned', function (st) { - let tx = Transaction.fromTxData({}) + let tx = LegacyTransaction.fromTxData({}) st.notOk(tx.isSigned()) const txData: TxData = { @@ -362,29 +362,29 @@ tape('[Transaction]: Basic functions', function (t) { '4646464646464646464646464646464646464646464646464646464646464646', 'hex' ) - tx = Transaction.fromTxData(txData) + tx = LegacyTransaction.fromTxData(txData) st.notOk(tx.isSigned()) tx = tx.sign(privateKey) st.ok(tx.isSigned()) - tx = new Transaction(txData) + tx = LegacyTransaction.fromTxData(txData) st.notOk(tx.isSigned()) const rawUnsigned = tx.serialize() tx = tx.sign(privateKey) const rawSigned = tx.serialize() st.ok(tx.isSigned()) - tx = Transaction.fromRlpSerializedTx(rawUnsigned) + tx = LegacyTransaction.fromRlpSerializedTx(rawUnsigned) st.notOk(tx.isSigned()) tx = tx.sign(privateKey) st.ok(tx.isSigned()) - tx = Transaction.fromRlpSerializedTx(rawSigned) + tx = LegacyTransaction.fromRlpSerializedTx(rawSigned) st.ok(tx.isSigned()) const signedValues = (rlp.decode(rawSigned) as any) as Buffer[] - tx = Transaction.fromValuesArray(signedValues) + tx = LegacyTransaction.fromValuesArray(signedValues) st.ok(tx.isSigned()) - tx = Transaction.fromValuesArray(signedValues.slice(0, 6)) + tx = LegacyTransaction.fromValuesArray(signedValues.slice(0, 6)) st.notOk(tx.isSigned()) st.end() }) @@ -393,12 +393,12 @@ tape('[Transaction]: Basic functions', function (t) { 'throws when creating a a transaction with incompatible chainid and v value', function (st) { const common = new Common({ chain: 42, hardfork: 'petersburg' }) - let tx = Transaction.fromTxData({}, { common }) + let tx = LegacyTransaction.fromTxData({}, { common }) st.equal(tx.getChainId(), 42) const privKey = Buffer.from(txFixtures[0].privateKey, 'hex') tx = tx.sign(privKey) const serialized = tx.serialize() - st.throws(() => Transaction.fromRlpSerializedTx(serialized)) + st.throws(() => LegacyTransaction.fromRlpSerializedTx(serialized)) st.end() } ) @@ -408,7 +408,7 @@ tape('[Transaction]: Basic functions', function (t) { function (st) { st.throws(() => { const common = new Common({ chain: 42, hardfork: 'petersburg' }) - Transaction.fromTxData({ v: new BN(1) }, { common }) + LegacyTransaction.fromTxData({ v: new BN(1) }, { common }) }) st.end() } @@ -417,13 +417,15 @@ tape('[Transaction]: Basic functions', function (t) { t.test('EIP155 hashing when singing', function (st) { const common = new Common({ chain: 1, hardfork: 'petersburg' }) txFixtures.slice(0, 3).forEach(function (txData) { - let tx = Transaction.fromValuesArray(txData.raw.slice(0, 6).map(toBuffer), { common }) + const tx = LegacyTransaction.fromValuesArray(txData.raw.slice(0, 6).map(toBuffer), { + common, + }) const privKey = Buffer.from(txData.privateKey, 'hex') - tx = tx.sign(privKey) + const txSigned = tx.sign(privKey) st.equal( - tx.getSenderAddress().toString(), + txSigned.getSenderAddress().toString(), '0x' + txData.sendersAddress, "computed sender address should equal the fixture's one" ) @@ -449,18 +451,18 @@ tape('[Transaction]: Basic functions', function (t) { 'hex' ) - const fixtureTxSignedWithEIP155 = Transaction.fromTxData(txData).sign(privateKey) + const fixtureTxSignedWithEIP155 = LegacyTransaction.fromTxData(txData).sign(privateKey) const common = new Common({ chain: 'mainnet', hardfork: 'tangerineWhistle', }) - const fixtureTxSignedWithoutEIP155 = Transaction.fromTxData(txData, { + const fixtureTxSignedWithoutEIP155 = LegacyTransaction.fromTxData(txData, { common, }).sign(privateKey) - let signedWithEIP155 = Transaction.fromTxData(fixtureTxSignedWithEIP155.toJSON()).sign( + let signedWithEIP155 = LegacyTransaction.fromTxData(fixtureTxSignedWithEIP155.toJSON()).sign( privateKey ) @@ -468,7 +470,7 @@ tape('[Transaction]: Basic functions', function (t) { st.notEqual(signedWithEIP155.v?.toString('hex'), '1c') st.notEqual(signedWithEIP155.v?.toString('hex'), '1b') - signedWithEIP155 = Transaction.fromTxData(fixtureTxSignedWithoutEIP155.toJSON()).sign( + signedWithEIP155 = LegacyTransaction.fromTxData(fixtureTxSignedWithoutEIP155.toJSON()).sign( privateKey ) @@ -476,7 +478,7 @@ tape('[Transaction]: Basic functions', function (t) { st.notEqual(signedWithEIP155.v?.toString('hex'), '1c') st.notEqual(signedWithEIP155.v?.toString('hex'), '1b') - let signedWithoutEIP155 = Transaction.fromTxData(fixtureTxSignedWithEIP155.toJSON(), { + let signedWithoutEIP155 = LegacyTransaction.fromTxData(fixtureTxSignedWithEIP155.toJSON(), { common, }).sign(privateKey) @@ -487,7 +489,7 @@ tape('[Transaction]: Basic functions', function (t) { "v shouldn't be EIP155 encoded" ) - signedWithoutEIP155 = Transaction.fromTxData(fixtureTxSignedWithoutEIP155.toJSON(), { + signedWithoutEIP155 = LegacyTransaction.fromTxData(fixtureTxSignedWithoutEIP155.toJSON(), { common, }).sign(privateKey) @@ -504,10 +506,10 @@ tape('[Transaction]: Basic functions', function (t) { t.test('should return correct data fee for istanbul', function (st) { const common = new Common({ chain: 'mainnet', hardfork: 'istanbul' }) - let tx = Transaction.fromTxData({}, { common }) + let tx = LegacyTransaction.fromTxData({}, { common }) st.equals(tx.getDataFee().toNumber(), 0) - tx = Transaction.fromValuesArray(txFixtures[3].raw.map(toBuffer), { + tx = LegacyTransaction.fromValuesArray(txFixtures[3].raw.map(toBuffer), { common, }) st.equals(tx.getDataFee().toNumber(), 1716) diff --git a/packages/tx/test/transactionRunner.ts b/packages/tx/test/transactionRunner.ts index 1e305bc9d50..68802e48187 100644 --- a/packages/tx/test/transactionRunner.ts +++ b/packages/tx/test/transactionRunner.ts @@ -2,7 +2,7 @@ import tape from 'tape' import minimist from 'minimist' import { toBuffer } from 'ethereumjs-util' import Common from '@ethereumjs/common' -import Transaction from '../src/transaction' +import LegacyTransaction from '../src/legacyTransaction' import { ForkName, ForkNamesMap, OfficialTransactionTestData } from './types' const testing = require('./testLoader') @@ -46,7 +46,7 @@ tape('TransactionTests', (t) => { const rawTx = toBuffer(testData.rlp) const hardfork = forkNameMap[forkName] const common = new Common({ chain: 1, hardfork }) - const tx = Transaction.fromRlpSerializedTx(rawTx, { common }) + const tx = LegacyTransaction.fromRlpSerializedTx(rawTx, { common }) const sender = tx.getSenderAddress().toString() const hash = tx.hash().toString('hex') From 7a35c756a9a317c6afc7f4a0ef8e05365fc16f52 Mon Sep 17 00:00:00 2001 From: Jochem Brouwer Date: Mon, 1 Mar 2021 18:27:25 +0100 Subject: [PATCH 06/32] tx: add transactionFactory tests tx: small EIP2930Transaction fixes --- packages/tx/src/eip2930Transaction.ts | 7 +- packages/tx/src/transactionFactory.ts | 7 +- packages/tx/test/eip2930.ts | 0 packages/tx/test/transactionFactory.spec.ts | 80 +++++++++++++++++++++ 4 files changed, 87 insertions(+), 7 deletions(-) create mode 100644 packages/tx/test/eip2930.ts create mode 100644 packages/tx/test/transactionFactory.spec.ts diff --git a/packages/tx/src/eip2930Transaction.ts b/packages/tx/src/eip2930Transaction.ts index 2e7908ca2d6..cf03e9922f6 100644 --- a/packages/tx/src/eip2930Transaction.ts +++ b/packages/tx/src/eip2930Transaction.ts @@ -34,7 +34,7 @@ export default class EIP2930Transaction { throw 'This is not an EIP-2930 transaction' } - const values = rlp.decode(serialized) + const values = rlp.decode(serialized.slice(1)) if (!Array.isArray(values)) { throw new Error('Invalid serialized tx input. Must be array') @@ -298,7 +298,6 @@ export default class EIP2930Transaction { */ raw(): Buffer[] { return [ - Buffer.from('01', 'hex'), this.chainId.toBuffer(), this.nonce.toBuffer(), this.gasPrice.toBuffer(), @@ -314,7 +313,9 @@ export default class EIP2930Transaction { * Returns the rlp encoding of the transaction. */ serialize(): Buffer { - return rlp.encode(this.raw()) + const RLPEncodedTx = rlp.encode(this.raw()) + + return Buffer.concat([Buffer.from('01', 'hex'), RLPEncodedTx]) } /** diff --git a/packages/tx/src/transactionFactory.ts b/packages/tx/src/transactionFactory.ts index 19bf9f18d13..7118ced7b92 100644 --- a/packages/tx/src/transactionFactory.ts +++ b/packages/tx/src/transactionFactory.ts @@ -47,17 +47,16 @@ export default class TransactionFactory { public static getTransactionClass(transactionID?: number, common?: Common) { const usedCommon = common ?? DEFAULT_COMMON if (transactionID) { - if (!usedCommon.eips().includes(2718)) { + if (transactionID !== 0 && !usedCommon.eips().includes(2718)) { throw new Error('Cannot create a TypedTransaction: EIP-2718 is not enabled') } switch (transactionID) { + case 0: + return LegacyTransaction case 1: return EIP2930Transaction default: throw new Error(`TypedTransaction with ID ${transactionID} unknown`) } } - - return LegacyTransaction - } } diff --git a/packages/tx/test/eip2930.ts b/packages/tx/test/eip2930.ts new file mode 100644 index 00000000000..e69de29bb2d diff --git a/packages/tx/test/transactionFactory.spec.ts b/packages/tx/test/transactionFactory.spec.ts new file mode 100644 index 00000000000..c34701d850d --- /dev/null +++ b/packages/tx/test/transactionFactory.spec.ts @@ -0,0 +1,80 @@ +import Common from '@ethereumjs/common' +import tape from 'tape' +import { + EIP2930Transaction, + TransactionFactory, + LegacyTransaction +} from '../src' + +const EIP2930Common = new Common({ + eips: [2718, 2930], + chain: 'mainnet', + hardfork: 'berlin', +}) + +const simpleUnsignedEIP2930Transaction = EIP2930Transaction.fromTxData( + {}, + { common: EIP2930Common } +) + +tape('[TransactionFactory]: Basic functions', function (t) { + t.test('should return the right type', function (st) { + const serialized = simpleUnsignedEIP2930Transaction.serialize() + const factoryTx = TransactionFactory.fromRawData(serialized, { common: EIP2930Common }) + st.equals(factoryTx.constructor.name, EIP2930Transaction.name) + + const legacyTx = LegacyTransaction.fromTxData({}) + const serializedLegacyTx = legacyTx.serialize() + const factoryLegacyTx = TransactionFactory.fromRawData(serializedLegacyTx, {}) + st.equals(factoryLegacyTx.constructor.name, LegacyTransaction.name) + + st.end() + }) + + t.test( + 'should throw when trying to create EIP-2718 typed transactions when not allowed in Common', + function (st) { + st.throws(() => { + TransactionFactory.fromRawData(simpleUnsignedEIP2930Transaction.serialize(), {}) + }) + st.end() + } + ) + + t.test( + 'should throw when trying to create EIP-2718 typed transactions when not allowed in Common', + function (st) { + st.throws(() => { + const serialized = simpleUnsignedEIP2930Transaction.serialize() + serialized[0] = 2 // edit the transaction type + TransactionFactory.fromRawData(serialized, { common: EIP2930Common }) + }) + st.end() + } + ) + + t.test('should give me the right classes in getTransactionClass', function (st) { + let legacyTx = TransactionFactory.getTransactionClass() + st.equals(legacyTx!.name, LegacyTransaction.name) + + let eip2930Tx = TransactionFactory.getTransactionClass(1, EIP2930Common) + st.equals(eip2930Tx!.name, EIP2930Transaction.name) + + st.end() + }) + + t.test('should throw when getting an invalid transaction type', function (st) { + st.throws(() => { + TransactionFactory.getTransactionClass(2, EIP2930Common) + }) + + st.end() + }) + + t.test('should throw when getting typed transactions without EIP-2718 activated', function (st) { + st.throws(() => { + TransactionFactory.getTransactionClass(1) + }) + st.end() + }) +}) From 64cb4c7b8d9ce40e005803ddca3ed6fc1ffa98af Mon Sep 17 00:00:00 2001 From: Jochem Brouwer Date: Mon, 1 Mar 2021 18:29:25 +0100 Subject: [PATCH 07/32] tx: add some EIP2930 tests tx: fix transactionFactory build --- packages/tx/src/transactionFactory.ts | 1 + packages/tx/test/eip2930.ts | 136 ++++++++++++++++++++++++++ 2 files changed, 137 insertions(+) diff --git a/packages/tx/src/transactionFactory.ts b/packages/tx/src/transactionFactory.ts index 7118ced7b92..8ddff22c12f 100644 --- a/packages/tx/src/transactionFactory.ts +++ b/packages/tx/src/transactionFactory.ts @@ -59,4 +59,5 @@ export default class TransactionFactory { throw new Error(`TypedTransaction with ID ${transactionID} unknown`) } } + } } diff --git a/packages/tx/test/eip2930.ts b/packages/tx/test/eip2930.ts index e69de29bb2d..9977352b323 100644 --- a/packages/tx/test/eip2930.ts +++ b/packages/tx/test/eip2930.ts @@ -0,0 +1,136 @@ +import Common from '@ethereumjs/common' +import { Address } from 'ethereumjs-util' +import tape from 'tape' +import { EIP2930Transaction } from '../src' + +const common = new Common({ + eips: [2718, 2930], + chain: 'mainnet', + hardfork: 'berlin', +}) + +const validAddress = Buffer.from('01'.repeat(20), 'hex') +const validSlot = Buffer.from('01'.repeat(32), 'hex') + +tape('[EIP2930 transactions]: Basic functions', function (t) { + t.test('should throw on invalid access list data', function (st) { + let accessList: any[] = [ + [ + Buffer.from('01'.repeat(21), 'hex'), // Address of 21 bytes instead of 20 + [], + ], + ] + + st.throws(() => { + EIP2930Transaction.fromTxData({ accessList }, { common }) + }) + + accessList = [ + [ + validAddress, + [ + Buffer.from('01'.repeat(31), 'hex'), // Slot of 31 bytes instead of 32 + ], + ], + ] + + st.throws(() => { + EIP2930Transaction.fromTxData({ accessList }, { common }) + }) + + accessList = [[]] // Address does not exist + + st.throws(() => { + EIP2930Transaction.fromTxData({ accessList }, { common }) + }) + + accessList = [[validAddress]] // Slots does not exist + + st.throws(() => { + EIP2930Transaction.fromTxData({ accessList }, { common }) + }) + + accessList = [[validAddress, validSlot]] // Slots is not an array + + st.throws(() => { + EIP2930Transaction.fromTxData({ accessList }, { common }) + }) + + accessList = [[validAddress, [], []]] // 3 items where 2 are expected + + st.throws(() => { + EIP2930Transaction.fromTxData({ accessList }, { common }) + }) + + st.end() + }) + + t.test('should return right upfront cost', (st) => { + let tx = EIP2930Transaction.fromTxData( + { + data: Buffer.from('010200', 'hex'), + to: validAddress, + accessList: [[validAddress, [validSlot]]], + }, + { common } + ) + // Cost should be: + // Base fee + 2*TxDataNonZero + TxDataZero + AccessListAddressCost + AccessListSlotCost + const txDataZero = common.param('gasPrices', 'txDataZero') + const txDataNonZero = common.param('gasPrices', 'txDataNonZero') + const accessListStorageKeyCost = common.param('gasPrices', 'accessListStorageKeyCost') + const accessListAddressCost = common.param('gasPrices', 'accessListAddressCost') + const baseFee = common.param('gasPrices', 'tx') + const creationFee = common.param('gasPrices', 'txCreation') + + st.ok( + tx + .getBaseFee() + .eqn( + txDataNonZero * 2 + + txDataZero + + baseFee + + accessListAddressCost + + accessListStorageKeyCost + ) + ) + + // In this Tx, `to` is `undefined`, so we should charge homestead creation gas. + tx = EIP2930Transaction.fromTxData( + { + data: Buffer.from('010200', 'hex'), + accessList: [[validAddress, [validSlot]]], + }, + { common } + ) + + st.ok( + tx + .getBaseFee() + .eqn( + txDataNonZero * 2 + + txDataZero + + creationFee + + baseFee + + accessListAddressCost + + accessListStorageKeyCost + ) + ) + + // Explicilty check that even if we have duplicates in our list, we still charge for those + tx = EIP2930Transaction.fromTxData( + { + to: validAddress, + accessList: [ + [validAddress, [validSlot]], + [validAddress, [validSlot, validSlot]], + ], + }, + { common } + ) + + st.ok(tx.getBaseFee().eqn(baseFee + accessListAddressCost * 2 + accessListStorageKeyCost * 3)) + + st.end() + }) +}) From 946e35a30ed02beb94d1a4ac173d3a73c5103b07 Mon Sep 17 00:00:00 2001 From: Jochem Brouwer Date: Mon, 1 Mar 2021 19:46:25 +0100 Subject: [PATCH 08/32] tx: implement BaseTransaction --- packages/tx/src/baseTransaction.ts | 94 +++++++++++++++++++++++++++ packages/tx/src/eip2930Transaction.ts | 45 ++++--------- packages/tx/src/legacyTransaction.ts | 34 +++------- packages/tx/src/types.ts | 33 +++++++++- 4 files changed, 149 insertions(+), 57 deletions(-) create mode 100644 packages/tx/src/baseTransaction.ts diff --git a/packages/tx/src/baseTransaction.ts b/packages/tx/src/baseTransaction.ts new file mode 100644 index 00000000000..d994283dce3 --- /dev/null +++ b/packages/tx/src/baseTransaction.ts @@ -0,0 +1,94 @@ +import Common from '@ethereumjs/common' +import { Address, BN, toBuffer } from 'ethereumjs-util' +import { BaseTransactionData, BaseTxOptions, DEFAULT_COMMON } from './types' + +export abstract class BaseTransaction { + public readonly to?: Address + public readonly common: Common + + constructor(txData: BaseTransactionData, txOptions?: BaseTxOptions) { + const { to } = txData + + this.to = to ? new Address(toBuffer(to)) : undefined + + this.common = txOptions?.common ?? DEFAULT_COMMON + } + + /** + * If the tx's `to` is to the creation address + */ + toCreationAddress(): boolean { + return this.to === undefined || this.to.buf.length === 0 + } + + /** + * Computes a sha3-256 hash of the serialized unsigned tx, which is used to sign the transaction. + */ + rawTxHash(): Buffer { + return this.getMessageToSign() + } + + abstract getMessageToSign(): Buffer + + /** + * Returns chain ID + */ + getChainId(): number { + return this.common.chainId() + } + + abstract sign(privateKey: Buffer): SignedTxType + + /** + * The amount of gas paid for the data in this tx + */ + abstract getDataFee(): BN + + /** + * The minimum amount of gas the tx must have (DataFee + TxFee + Creation Fee) + */ + abstract getBaseFee(): BN + + /** + * The up front amount that an account must have for this transaction to be valid + */ + abstract getUpfrontCost(): BN + + /** + * Checks if the transaction has the minimum amount of gas required + * (DataFee + TxFee + Creation Fee). + */ + abstract validate(): boolean + abstract validate(stringError: false): boolean | string[] + abstract validate(stringError: true): string[] + + /** + * Returns a Buffer Array of the raw Buffers of this transaction, in order. + */ + abstract raw(): Buffer[] + + /** + * Returns the encoding of the transaction. + */ + abstract serialize(): Buffer + + /** + * Returns an object with the JSON representation of the transaction + */ + abstract toJSON(): JsonTx + + abstract isSigned(): boolean +} + +export interface SignedTransactionInterface { + raw(): Buffer[] + hash(): Buffer + + getMessageToVerifySignature(): Buffer + getSenderAddress(): Address + getSenderPublicKey(): Buffer + + verifySignature(): boolean + + sign(privateKey: Buffer): never +} diff --git a/packages/tx/src/eip2930Transaction.ts b/packages/tx/src/eip2930Transaction.ts index cf03e9922f6..8c9649120de 100644 --- a/packages/tx/src/eip2930Transaction.ts +++ b/packages/tx/src/eip2930Transaction.ts @@ -1,9 +1,11 @@ -import Common from '@ethereumjs/common' import { Address, BN, bnToHex, ecsign, rlp, rlphash, toBuffer } from 'ethereumjs-util' -import { DEFAULT_COMMON, EIP2930TxData, TxOptions } from './types' +import { BaseTransaction } from './baseTransaction' +import { EIP2930TxData, TxOptions, JsonEIP2930Tx } from './types' -export default class EIP2930Transaction { - public readonly common: Common +export default class EIP2930Transaction extends BaseTransaction< + EIP2930Transaction, + JsonEIP2930Tx +> { public readonly chainId: BN public readonly nonce: BN public readonly gasLimit: BN @@ -105,9 +107,7 @@ export default class EIP2930Transaction { } } - private constructor(txData: EIP2930TxData, opts: TxOptions) { - this.common = opts.common ?? DEFAULT_COMMON - + protected constructor(txData: EIP2930TxData, opts: TxOptions) { const { chainId, nonce, @@ -122,6 +122,8 @@ export default class EIP2930Transaction { s, } = txData + super({ to }, opts) + if (!this.common.eips().includes(2718)) { throw new Error('EIP-2718 not enabled on Common') } else if (!this.common.eips().includes(2930)) { @@ -170,33 +172,10 @@ export default class EIP2930Transaction { } } - /** - * If the tx's `to` is to the creation address - */ - toCreationAddress(): boolean { - return this.to === undefined || this.to.buf.length === 0 - } - - /** - * Computes a sha3-256 hash of the unserialized tx. - * This hash is signed by the private key. It is different from the transaction hash, which are put in blocks. - * The transaction hash also hashes the data used to sign the transaction. - */ - rawTxHash(): Buffer { - return this.getMessageToSign() - } - getMessageToSign() { return rlphash(this.raw()) } - /** - * Returns chain ID - */ - getChainId(): number { - return this.common.chainId() - } - sign(privateKey: Buffer) { if (privateKey.length !== 32) { throw new Error('Private key must be 32 bytes in length.') @@ -321,7 +300,7 @@ export default class EIP2930Transaction { /** * Returns an object with the JSON representation of the transaction */ - toJSON(): any { + toJSON(): JsonEIP2930Tx { // TODO: fix type const accessListJSON = [] for (let index = 0; index < this.accessList.length; index++) { @@ -348,4 +327,8 @@ export default class EIP2930Transaction { accessList: accessListJSON, } } + + public isSigned(): boolean { + return false + } } diff --git a/packages/tx/src/legacyTransaction.ts b/packages/tx/src/legacyTransaction.ts index a05cb1d5a87..1771f470bae 100644 --- a/packages/tx/src/legacyTransaction.ts +++ b/packages/tx/src/legacyTransaction.ts @@ -15,8 +15,8 @@ import { publicToAddress, MAX_INTEGER, } from 'ethereumjs-util' -import Common from '@ethereumjs/common' -import { TxOptions, LegacyTxData, JsonTx, DEFAULT_COMMON } from './types' +import { TxOptions, LegacyTxData, JsonLegacyTx } from './types' +import { BaseTransaction, SignedTransactionInterface } from './baseTransaction' // secp256k1n/2 const N_DIV_2 = new BN('7fffffffffffffffffffffffffffffff5d576e7357a4501ddfe92f46681b20a0', 16) @@ -24,8 +24,10 @@ const N_DIV_2 = new BN('7fffffffffffffffffffffffffffffff5d576e7357a4501ddfe92f46 /** * An Ethereum transaction. */ -export default class LegacyTransaction { - public readonly common: Common +export default class LegacyTransaction extends BaseTransaction< + LegacyTransaction, + JsonLegacyTx +> { public readonly nonce: BN public readonly gasLimit: BN public readonly gasPrice: BN @@ -85,6 +87,8 @@ export default class LegacyTransaction { private constructor(txData: LegacyTxData, opts?: TxOptions) { const { nonce, gasPrice, gasLimit, to, value, data, v, r, s } = txData + super({ to }, opts) + this.nonce = new BN(toBuffer(nonce)) this.gasPrice = new BN(toBuffer(gasPrice)) this.gasLimit = new BN(toBuffer(gasLimit)) @@ -109,12 +113,6 @@ export default class LegacyTransaction { } } - if (opts?.common) { - this.common = Object.assign(Object.create(Object.getPrototypeOf(opts.common)), opts.common) - } else { - this.common = DEFAULT_COMMON - } - this._validateTxV(this.v) const freeze = opts?.freeze ?? true @@ -123,13 +121,6 @@ export default class LegacyTransaction { } } - /** - * If the tx's `to` is to the creation address - */ - toCreationAddress(): boolean { - return this.to === undefined || this.to.buf.length === 0 - } - /** * Computes a sha3-256 hash of the serialized tx */ @@ -145,13 +136,6 @@ export default class LegacyTransaction { return this._getMessageToSign(this._signedTxImplementsEIP155()) } - /** - * Returns chain ID - */ - getChainId(): number { - return this.common.chainId() - } - /** * Returns the sender's address */ @@ -329,7 +313,7 @@ export default class LegacyTransaction { /** * Returns an object with the JSON representation of the transaction */ - toJSON(): JsonTx { + toJSON(): JsonLegacyTx { return { nonce: bnToHex(this.nonce), gasPrice: bnToHex(this.gasPrice), diff --git a/packages/tx/src/types.ts b/packages/tx/src/types.ts index 661dc8b261b..84f1cefc8be 100644 --- a/packages/tx/src/types.ts +++ b/packages/tx/src/types.ts @@ -31,6 +31,13 @@ export interface TxOptions { freeze?: boolean } +/** + * The options for initializing a Transaction. + */ +export interface BaseTxOptions { + common?: Common +} + /** * An object with an optional field with each of the transaction's values. */ @@ -145,10 +152,32 @@ export type TxData = LegacyTxData | EIP2930TxData export type Transaction = LegacyTransaction | EIP2930Transaction +export type BaseTransactionData = { + /** + * The transaction's the address is sent to. + */ + to?: AddressLike +} + +/** + * An object with all of the transaction's values represented as strings. + */ +export interface JsonLegacyTx { + nonce?: string + gasPrice?: string + gasLimit?: string + to?: string + data?: string + v?: string + r?: string + s?: string + value?: string +} + /** * An object with all of the transaction's values represented as strings. */ -export interface JsonTx { +export interface JsonEIP2930Tx { nonce?: string gasPrice?: string gasLimit?: string @@ -158,6 +187,8 @@ export interface JsonTx { r?: string s?: string value?: string + chainId: string + accessList: string[] } export const DEFAULT_COMMON = new Common({ chain: 'mainnet' }) From ac694fb184f9cece8b0cba003e9e03170901d66c Mon Sep 17 00:00:00 2001 From: Jochem Brouwer Date: Mon, 1 Mar 2021 20:50:52 +0100 Subject: [PATCH 09/32] tx: make txs both support signed/unsigned transactions tx: extend base class with common methods tx: fix tests --- packages/tx/src/baseTransaction.ts | 137 ++++++-- packages/tx/src/eip2930Transaction.ts | 139 +++----- packages/tx/src/legacyTransaction.ts | 362 ++++++++------------ packages/tx/src/types.ts | 25 ++ packages/tx/test/transactionFactory.spec.ts | 4 +- packages/tx/test/transactionRunner.ts | 2 +- 6 files changed, 320 insertions(+), 349 deletions(-) diff --git a/packages/tx/src/baseTransaction.ts b/packages/tx/src/baseTransaction.ts index d994283dce3..ff1db36b638 100644 --- a/packages/tx/src/baseTransaction.ts +++ b/packages/tx/src/baseTransaction.ts @@ -1,19 +1,54 @@ import Common from '@ethereumjs/common' -import { Address, BN, toBuffer } from 'ethereumjs-util' +import { + Address, + BN, + toBuffer, + MAX_INTEGER, + unpadBuffer, + ecsign, + publicToAddress, +} from 'ethereumjs-util' import { BaseTransactionData, BaseTxOptions, DEFAULT_COMMON } from './types' -export abstract class BaseTransaction { +export abstract class BaseTransaction { + public readonly nonce: BN + public readonly gasLimit: BN + public readonly gasPrice: BN public readonly to?: Address + public readonly value: BN + public readonly data: Buffer public readonly common: Common constructor(txData: BaseTransactionData, txOptions?: BaseTxOptions) { - const { to } = txData + const { nonce, gasLimit, gasPrice, to, value, data } = txData + this.nonce = new BN(toBuffer(nonce)) + this.gasPrice = new BN(toBuffer(gasPrice)) + this.gasLimit = new BN(toBuffer(gasLimit)) this.to = to ? new Address(toBuffer(to)) : undefined + this.value = new BN(toBuffer(value)) + this.data = toBuffer(data) + + const validateCannotExceedMaxInteger = { + nonce: this.nonce, + gasPrice: this.gasPrice, + gasLimit: this.gasLimit, + value: this.value, + } + + this.validateExcdeedsMaxInteger(validateCannotExceedMaxInteger) this.common = txOptions?.common ?? DEFAULT_COMMON } + protected validateExcdeedsMaxInteger(validateCannotExceedMaxInteger: { [key: string]: BN }) { + for (const [key, value] of Object.entries(validateCannotExceedMaxInteger)) { + if (value && value.gt(MAX_INTEGER)) { + throw new Error(`${key} cannot exceed MAX_INTEGER, given ${value}`) + } + } + } + /** * If the tx's `to` is to the creation address */ @@ -37,35 +72,62 @@ export abstract class BaseTransaction { return this.common.chainId() } - abstract sign(privateKey: Buffer): SignedTxType - /** * The amount of gas paid for the data in this tx */ - abstract getDataFee(): BN + getDataFee(): BN { + const txDataZero = this.common.param('gasPrices', 'txDataZero') + const txDataNonZero = this.common.param('gasPrices', 'txDataNonZero') + + let cost = 0 + for (let i = 0; i < this.data.length; i++) { + this.data[i] === 0 ? (cost += txDataZero) : (cost += txDataNonZero) + } + return new BN(cost) + } /** * The minimum amount of gas the tx must have (DataFee + TxFee + Creation Fee) */ - abstract getBaseFee(): BN + getBaseFee(): BN { + const fee = this.getDataFee().addn(this.common.param('gasPrices', 'tx')) + if (this.common.gteHardfork('homestead') && this.toCreationAddress()) { + fee.iaddn(this.common.param('gasPrices', 'txCreation')) + } + return fee + } /** * The up front amount that an account must have for this transaction to be valid */ - abstract getUpfrontCost(): BN + getUpfrontCost(): BN { + return this.gasLimit.mul(this.gasPrice).add(this.value) + } /** * Checks if the transaction has the minimum amount of gas required * (DataFee + TxFee + Creation Fee). */ - abstract validate(): boolean - abstract validate(stringError: false): boolean | string[] - abstract validate(stringError: true): string[] - /** - * Returns a Buffer Array of the raw Buffers of this transaction, in order. + * Checks if the transaction has the minimum amount of gas required + * (DataFee + TxFee + Creation Fee). */ - abstract raw(): Buffer[] + validate(): boolean + validate(stringError: false): boolean + validate(stringError: true): string[] + validate(stringError: boolean = false): boolean | string[] { + const errors = [] + + if (this.getBaseFee().gt(this.gasLimit)) { + errors.push(`gasLimit is too low. given ${this.gasLimit}, need at least ${this.getBaseFee()}`) + } + + if (this.isSigned() && !this.verifySignature()) { + errors.push('Invalid Signature') + } + + return stringError ? errors : errors.length === 0 + } /** * Returns the encoding of the transaction. @@ -78,17 +140,46 @@ export abstract class BaseTransaction { abstract toJSON(): JsonTx abstract isSigned(): boolean -} -export interface SignedTransactionInterface { - raw(): Buffer[] - hash(): Buffer + /** + * Determines if the signature is valid + */ + verifySignature(): boolean { + try { + // Main signature verification is done in `getSenderPublicKey()` + const publicKey = this.getSenderPublicKey() + return unpadBuffer(publicKey).length !== 0 + } catch (e) { + return false + } + } + + abstract raw(): Buffer[] + abstract hash(): Buffer + + abstract getMessageToVerifySignature(): Buffer + /** + * Returns the sender's address + */ + getSenderAddress(): Address { + return new Address(publicToAddress(this.getSenderPublicKey())) + } + abstract getSenderPublicKey(): Buffer - getMessageToVerifySignature(): Buffer - getSenderAddress(): Address - getSenderPublicKey(): Buffer + sign(privateKey: Buffer): TransactionObject { + if (privateKey.length !== 32) { + throw new Error('Private key must be 32 bytes in length.') + } - verifySignature(): boolean + const msgHash = this.getMessageToSign() + + // Only `v` is reassigned. + /* eslint-disable-next-line prefer-const */ + let { v, r, s } = ecsign(msgHash, privateKey) + + return this.processSignature(v, r, s) + } - sign(privateKey: Buffer): never + // Accept the v,r,s values from the `sign` method, and convert this into a TransactionObject + protected abstract processSignature(v: number, r: Buffer, s: Buffer): TransactionObject } diff --git a/packages/tx/src/eip2930Transaction.ts b/packages/tx/src/eip2930Transaction.ts index 8c9649120de..e2fcf7de762 100644 --- a/packages/tx/src/eip2930Transaction.ts +++ b/packages/tx/src/eip2930Transaction.ts @@ -1,27 +1,20 @@ -import { Address, BN, bnToHex, ecsign, rlp, rlphash, toBuffer } from 'ethereumjs-util' +import { Address, BN, bnToHex, rlp, rlphash, toBuffer } from 'ethereumjs-util' import { BaseTransaction } from './baseTransaction' import { EIP2930TxData, TxOptions, JsonEIP2930Tx } from './types' -export default class EIP2930Transaction extends BaseTransaction< - EIP2930Transaction, - JsonEIP2930Tx -> { +export default class EIP2930Transaction extends BaseTransaction { public readonly chainId: BN - public readonly nonce: BN - public readonly gasLimit: BN - public readonly gasPrice: BN - public readonly to?: Address - public readonly value: BN - public readonly data: Buffer public readonly accessList: any public readonly yParity?: number public readonly r?: BN public readonly s?: BN + // EIP-2930 alias for `s` get senderS() { return this.s } + // EIP-2930 alias for `r` get senderR() { return this.r } @@ -48,25 +41,7 @@ export default class EIP2930Transaction extends BaseTransaction< // Create a transaction from a values array. // The format is: chainId, nonce, gasPrice, gasLimit, to, value, data, access_list, [yParity, senderR, senderS] public static fromValuesArray(values: Buffer[], opts?: TxOptions) { - if (values.length == 8) { - const [chainId, nonce, gasPrice, gasLimit, to, value, data, accessList] = values - const emptyBuffer = Buffer.from([]) - - return new EIP2930Transaction( - { - chainId: new BN(chainId), - nonce: new BN(nonce), - gasPrice: new BN(gasPrice), - gasLimit: new BN(gasLimit), - to: to && to.length > 0 ? new Address(to) : undefined, - value: new BN(value), - data: data ?? emptyBuffer, - accessList: accessList ?? emptyBuffer, - }, - opts ?? {} - ) - } else if (values.length == 11) { - // TODO: return EIP2930SignedTransaction + if (values.length == 8 || values.length == 11) { const [ chainId, nonce, @@ -92,11 +67,11 @@ export default class EIP2930Transaction extends BaseTransaction< value: new BN(value), data: data ?? emptyBuffer, accessList: accessList ?? emptyBuffer, - yParity: !yParity?.equals(emptyBuffer) + yParity: yParity !== undefined && !yParity?.equals(emptyBuffer) ? parseInt(yParity.toString('hex'), 16) : undefined, - r: !r?.equals(emptyBuffer) ? new BN(r) : undefined, - s: !s?.equals(emptyBuffer) ? new BN(s) : undefined, + r: r !== undefined && !r.equals(emptyBuffer) ? new BN(r) : undefined, + s: s !== undefined && !s.equals(emptyBuffer) ? new BN(s) : undefined, }, opts ?? {} ) @@ -122,7 +97,7 @@ export default class EIP2930Transaction extends BaseTransaction< s, } = txData - super({ to }, opts) + super({ nonce, gasPrice, gasLimit, to, value, data }, opts) if (!this.common.eips().includes(2718)) { throw new Error('EIP-2718 not enabled on Common') @@ -134,18 +109,24 @@ export default class EIP2930Transaction extends BaseTransaction< throw new Error('The chain ID does not match the chain ID of Common') } + if (txData.yParity && txData.yParity != 0 && txData.yParity != 1) { + throw new Error('The y-parity of the transaction should either be 0 or 1') + } + + // TODO: verify the signature. + + this.yParity = txData.yParity + this.r = txData.r + this.s = txData.s + this.chainId = new BN(toBuffer(chainId)) - this.nonce = new BN(toBuffer(nonce)) - this.gasPrice = new BN(toBuffer(gasPrice)) - this.gasLimit = new BN(toBuffer(gasLimit)) - this.to = to ? new Address(toBuffer(to)) : undefined - this.value = new BN(toBuffer(value)) - this.data = toBuffer(data) this.accessList = accessList ?? [] this.yParity = yParity ?? 0 this.r = r ? new BN(toBuffer(r)) : undefined this.s = s ? new BN(toBuffer(s)) : undefined + // todo verify max BN of r,s + // Verify the access list format. for (let key = 0; key < this.accessList.length; key++) { const accessListItem = this.accessList[key] @@ -176,17 +157,7 @@ export default class EIP2930Transaction extends BaseTransaction< return rlphash(this.raw()) } - sign(privateKey: Buffer) { - if (privateKey.length !== 32) { - throw new Error('Private key must be 32 bytes in length.') - } - - const msgHash = this.getMessageToSign() - - // Only `v` is reassigned. - /* eslint-disable-next-line prefer-const */ - let { v, r, s } = ecsign(msgHash, privateKey) - + processSignature(v: number, r: Buffer, s: Buffer) { const opts = { common: this.common, } @@ -213,16 +184,10 @@ export default class EIP2930Transaction extends BaseTransaction< * The amount of gas paid for the data in this tx */ getDataFee(): BN { - const txDataZero = this.common.param('gasPrices', 'txDataZero') - const txDataNonZero = this.common.param('gasPrices', 'txDataNonZero') + const cost = super.getDataFee() const accessListStorageKeyCost = this.common.param('gasPrices', 'accessListStorageKeyCost') const accessListAddressCost = this.common.param('gasPrices', 'accessListAddressCost') - let cost = 0 - for (let i = 0; i < this.data.length; i++) { - this.data[i] === 0 ? (cost += txDataZero) : (cost += txDataNonZero) - } - let slots = 0 for (let index = 0; index < this.accessList.length; index++) { const item = this.accessList[index] @@ -231,45 +196,9 @@ export default class EIP2930Transaction extends BaseTransaction< } const addresses = this.accessList.length - cost += addresses * accessListAddressCost + slots * accessListStorageKeyCost - - return new BN(cost) - } - - /** - * The minimum amount of gas the tx must have (DataFee + TxFee + Creation Fee) - */ - getBaseFee(): BN { - const fee = this.getDataFee().addn(this.common.param('gasPrices', 'tx')) - if (this.common.gteHardfork('homestead') && this.toCreationAddress()) { - fee.iaddn(this.common.param('gasPrices', 'txCreation')) - } - return fee - } - - /** - * The up front amount that an account must have for this transaction to be valid - */ - getUpfrontCost(): BN { - return this.gasLimit.mul(this.gasPrice).add(this.value) - } + cost.addn(addresses * accessListAddressCost + slots * accessListStorageKeyCost) - /** - * Validates the signature and checks if - * the transaction has the minimum amount of gas required - * (DataFee + TxFee + Creation Fee). - */ - validate(): boolean - validate(stringError: false): boolean - validate(stringError: true): string[] - validate(stringError: boolean = false): boolean | string[] { - const errors = [] - - if (this.getBaseFee().gt(this.gasLimit)) { - errors.push(`gasLimit is too low. given ${this.gasLimit}, need at least ${this.getBaseFee()}`) - } - - return stringError ? errors : errors.length === 0 + return cost } /** @@ -304,9 +233,9 @@ export default class EIP2930Transaction extends BaseTransaction< // TODO: fix type const accessListJSON = [] for (let index = 0; index < this.accessList.length; index++) { - const item = this.accessList[index] - const JSONItem: any = ['0x' + item[0].toString('hex')] - const storageSlots = item[1] + const item: any = this.accessList[index] + const JSONItem: any = ['0x' + (item[0]).toString('hex')] + const storageSlots: Buffer[] = item[1] const JSONSlots = [] for (let slot = 0; slot < storageSlots.length; slot++) { const storageSlot = storageSlots[slot] @@ -331,4 +260,16 @@ export default class EIP2930Transaction extends BaseTransaction< public isSigned(): boolean { return false } + + public hash(): Buffer { + throw new Error('Implement me') + } + + public getMessageToVerifySignature(): Buffer { + throw new Error('Implement me') + } + + public getSenderPublicKey(): Buffer { + throw new Error('Implement me') + } } diff --git a/packages/tx/src/legacyTransaction.ts b/packages/tx/src/legacyTransaction.ts index 1771f470bae..3d87c83eb86 100644 --- a/packages/tx/src/legacyTransaction.ts +++ b/packages/tx/src/legacyTransaction.ts @@ -16,7 +16,7 @@ import { MAX_INTEGER, } from 'ethereumjs-util' import { TxOptions, LegacyTxData, JsonLegacyTx } from './types' -import { BaseTransaction, SignedTransactionInterface } from './baseTransaction' +import { BaseTransaction } from './baseTransaction' // secp256k1n/2 const N_DIV_2 = new BN('7fffffffffffffffffffffffffffffff5d576e7357a4501ddfe92f46681b20a0', 16) @@ -28,12 +28,6 @@ export default class LegacyTransaction extends BaseTransaction< LegacyTransaction, JsonLegacyTx > { - public readonly nonce: BN - public readonly gasLimit: BN - public readonly gasPrice: BN - public readonly to?: Address - public readonly value: BN - public readonly data: Buffer public readonly v?: BN public readonly r?: BN public readonly s?: BN @@ -59,24 +53,32 @@ export default class LegacyTransaction extends BaseTransaction< ) } - const [nonce, gasPrice, gasLimit, to, value, data, v, r, s] = values - - const emptyBuffer = Buffer.from([]) - - return new LegacyTransaction( - { - nonce: new BN(nonce), - gasPrice: new BN(gasPrice), - gasLimit: new BN(gasLimit), - to: to && to.length > 0 ? new Address(to) : undefined, - value: new BN(value), - data: data ?? emptyBuffer, - v: v && !v.equals(emptyBuffer) ? new BN(v) : undefined, - r: r && !r.equals(emptyBuffer) ? new BN(r) : undefined, - s: s && !s.equals(emptyBuffer) ? new BN(s) : undefined, - }, - opts - ) + // If length is not 6, it has length 9. If v/r/s are empty Buffers, it is still an unsigned transaction + // This happens if you get the RLP data from `raw()` + if (values.length === 6 || values.length == 9) { + const [nonce, gasPrice, gasLimit, to, value, data, v, r, s] = values + + const emptyBuffer = Buffer.from([]) + + return new LegacyTransaction( + { + nonce: new BN(nonce), + gasPrice: new BN(gasPrice), + gasLimit: new BN(gasLimit), + to: to && to.length > 0 ? new Address(to) : undefined, + value: new BN(value), + data: data ?? emptyBuffer, + v: v !== undefined && !v.equals(emptyBuffer) ? new BN(v) : undefined, + r: r !== undefined && !r.equals(emptyBuffer) ? new BN(r) : undefined, + s: s !== undefined && !s.equals(emptyBuffer) ? new BN(s) : undefined, + }, + opts + ) + } else { + throw new Error( + 'Invalid transaction. Only expecting 6 values (for unsigned tx) or 9 values (for signed tx).' + ) + } } /** @@ -84,33 +86,24 @@ export default class LegacyTransaction extends BaseTransaction< * Use the static factory methods to assist in creating a Transaction object from varying data types. * @note Transaction objects implement EIP155 by default. To disable it, pass in an `@ethereumjs/common` object set before EIP155 activation (i.e. before Spurious Dragon). */ - private constructor(txData: LegacyTxData, opts?: TxOptions) { + protected constructor(txData: LegacyTxData, opts?: TxOptions) { const { nonce, gasPrice, gasLimit, to, value, data, v, r, s } = txData - super({ to }, opts) + super({ nonce, gasPrice, gasLimit, to, value, data }, opts) - this.nonce = new BN(toBuffer(nonce)) - this.gasPrice = new BN(toBuffer(gasPrice)) - this.gasLimit = new BN(toBuffer(gasLimit)) - this.to = to ? new Address(toBuffer(to)) : undefined - this.value = new BN(toBuffer(value)) - this.data = toBuffer(data) this.v = v ? new BN(toBuffer(v)) : undefined this.r = r ? new BN(toBuffer(r)) : undefined this.s = s ? new BN(toBuffer(s)) : undefined const validateCannotExceedMaxInteger = { - nonce: this.nonce, - gasPrice: this.gasPrice, - gasLimit: this.gasLimit, - value: this.value, - r: this.r, - s: this.s, + r: this.r ?? new BN(0), + s: this.s ?? new BN(0), } - for (const [key, value] of Object.entries(validateCannotExceedMaxInteger)) { - if (value && value.gt(MAX_INTEGER)) { - throw new Error(`${key} cannot exceed MAX_INTEGER, given ${value}`) - } + + this.validateExcdeedsMaxInteger(validateCannotExceedMaxInteger) + + if (this.v) { + this._validateTxV(this.v) } this._validateTxV(this.v) @@ -122,168 +115,10 @@ export default class LegacyTransaction extends BaseTransaction< } /** - * Computes a sha3-256 hash of the serialized tx - */ - hash(): Buffer { - return rlphash(this.raw()) - } - - getMessageToSign() { - return this._getMessageToSign(this._unsignedTxImplementsEIP155()) - } - - getMessageToVerifySignature() { - return this._getMessageToSign(this._signedTxImplementsEIP155()) - } - - /** - * Returns the sender's address - */ - getSenderAddress(): Address { - return new Address(publicToAddress(this.getSenderPublicKey())) - } - - /** - * Returns the public key of the sender - */ - getSenderPublicKey(): Buffer { - const msgHash = this.getMessageToVerifySignature() - - // All transaction signatures whose s-value is greater than secp256k1n/2 are considered invalid. - if (this.common.gteHardfork('homestead') && this.s && this.s.gt(N_DIV_2)) { - throw new Error( - 'Invalid Signature: s-values greater than secp256k1n/2 are considered invalid' - ) - } - - const { v, r, s } = this - if (!v || !r || !s) { - throw new Error('Missing values to derive sender public key from signed tx') - } - - try { - return ecrecover( - msgHash, - v.toNumber(), - bnToRlp(r), - bnToRlp(s), - this._signedTxImplementsEIP155() ? this.getChainId() : undefined - ) - } catch (e) { - throw new Error('Invalid Signature') - } - } - - /** - * Determines if the signature is valid - */ - verifySignature(): boolean { - try { - // Main signature verification is done in `getSenderPublicKey()` - const publicKey = this.getSenderPublicKey() - return unpadBuffer(publicKey).length !== 0 - } catch (e) { - return false - } - } - - /** - * Sign a transaction with a given private key. - * Returns a new Transaction object (the original tx will not be modified). - * Example: - * ```typescript - * const unsignedTx = Transaction.fromTxData(txData) - * const signedTx = unsignedTx.sign(privKey) - * ``` - * @param privateKey Must be 32 bytes in length. - */ - sign(privateKey: Buffer) { - if (privateKey.length !== 32) { - throw new Error('Private key must be 32 bytes in length.') - } - - const msgHash = this.getMessageToSign() - - // Only `v` is reassigned. - /* eslint-disable-next-line prefer-const */ - let { v, r, s } = ecsign(msgHash, privateKey) - - if (this._unsignedTxImplementsEIP155()) { - v += this.getChainId() * 2 + 8 - } - - const opts = { - common: this.common, - } - - return new LegacyTransaction( - { - nonce: this.nonce, - gasPrice: this.gasPrice, - gasLimit: this.gasLimit, - to: this.to, - value: this.value, - data: this.data, - v: new BN(v), - r: new BN(r), - s: new BN(s), - }, - opts - ) - } - - /** - * The amount of gas paid for the data in this tx - */ - getDataFee(): BN { - const txDataZero = this.common.param('gasPrices', 'txDataZero') - const txDataNonZero = this.common.param('gasPrices', 'txDataNonZero') - - let cost = 0 - for (let i = 0; i < this.data.length; i++) { - this.data[i] === 0 ? (cost += txDataZero) : (cost += txDataNonZero) - } - return new BN(cost) - } - - /** - * The minimum amount of gas the tx must have (DataFee + TxFee + Creation Fee) - */ - getBaseFee(): BN { - const fee = this.getDataFee().addn(this.common.param('gasPrices', 'tx')) - if (this.common.gteHardfork('homestead') && this.toCreationAddress()) { - fee.iaddn(this.common.param('gasPrices', 'txCreation')) - } - return fee - } - - /** - * The up front amount that an account must have for this transaction to be valid - */ - getUpfrontCost(): BN { - return this.gasLimit.mul(this.gasPrice).add(this.value) - } - - /** - * Validates the signature and checks if - * the transaction has the minimum amount of gas required - * (DataFee + TxFee + Creation Fee). + * Returns the rlp encoding of the transaction. */ - validate(): boolean - validate(stringError: false): boolean - validate(stringError: true): string[] - validate(stringError: boolean = false): boolean | string[] { - const errors = [] - - if (!this.verifySignature()) { - errors.push('Invalid Signature') - } - - if (this.getBaseFee().gt(this.gasLimit)) { - errors.push(`gasLimit is too low. given ${this.gasLimit}, need at least ${this.getBaseFee()}`) - } - - return stringError ? errors : errors.length === 0 + serialize(): Buffer { + return rlp.encode(this.raw()) } /** @@ -304,10 +139,10 @@ export default class LegacyTransaction extends BaseTransaction< } /** - * Returns the rlp encoding of the transaction. + * Computes a sha3-256 hash of the serialized tx */ - serialize(): Buffer { - return rlp.encode(this.raw()) + hash(): Buffer { + return rlphash(this.raw()) } /** @@ -336,33 +171,95 @@ export default class LegacyTransaction extends BaseTransaction< return this.common.gteHardfork('spuriousDragon') } - private _signedTxImplementsEIP155() { - if (!this.isSigned()) { - throw Error('This transaction is not signed') + private _getMessageToSign(withEIP155: boolean) { + const values = [ + bnToRlp(this.nonce), + bnToRlp(this.gasPrice), + bnToRlp(this.gasLimit), + this.to !== undefined ? this.to.buf : Buffer.from([]), + bnToRlp(this.value), + this.data, + ] + + if (withEIP155) { + values.push(toBuffer(this.getChainId())) + values.push(unpadBuffer(toBuffer(0))) + values.push(unpadBuffer(toBuffer(0))) } - const onEIP155BlockOrLater = this.common.gteHardfork('spuriousDragon') + return rlphash(values) + } - // EIP155 spec: - // If block.number >= 2,675,000 and v = CHAIN_ID * 2 + 35 or v = CHAIN_ID * 2 + 36, then when computing the hash of a transaction for purposes of signing or recovering, instead of hashing only the first six elements (i.e. nonce, gasprice, startgas, to, value, data), hash nine elements, with v replaced by CHAIN_ID, r = 0 and s = 0. - const v = this.v?.toNumber() + getMessageToSign() { + return this._getMessageToSign(this._unsignedTxImplementsEIP155()) + } - const vAndChainIdMeetEIP155Conditions = - v === this.getChainId() * 2 + 35 || v === this.getChainId() * 2 + 36 + /** + * Process the v, r, s values from the `sign` method of the base transaction. + */ + protected processSignature(v: number, r: Buffer, s: Buffer) { + if (this._unsignedTxImplementsEIP155()) { + v += this.getChainId() * 2 + 8 + } - return vAndChainIdMeetEIP155Conditions && onEIP155BlockOrLater + const opts = { + common: this.common, + } + + return LegacyTransaction.fromTxData( + { + nonce: this.nonce, + gasPrice: this.gasPrice, + gasLimit: this.gasLimit, + to: this.to, + value: this.value, + data: this.data, + v: new BN(v), + r: new BN(r), + s: new BN(s), + }, + opts + ) } - private _getMessageToSign(withEIP155: boolean) { - const values = this.raw().slice(0, 6) + getMessageToVerifySignature() { + const withEIP155 = this._signedTxImplementsEIP155() - if (withEIP155) { - values.push(toBuffer(this.getChainId())) - values.push(unpadBuffer(toBuffer(0))) - values.push(unpadBuffer(toBuffer(0))) + return this._getMessageToSign(withEIP155) + } + + /** + * Returns the public key of the sender + */ + /** + * Returns the public key of the sender + */ + getSenderPublicKey(): Buffer { + const msgHash = this.getMessageToVerifySignature() + + // All transaction signatures whose s-value is greater than secp256k1n/2 are considered invalid. + if (this.common.gteHardfork('homestead') && this.s && this.s.gt(N_DIV_2)) { + throw new Error( + 'Invalid Signature: s-values greater than secp256k1n/2 are considered invalid' + ) } - return rlphash(values) + const { v, r, s } = this + if (!v || !r || !s) { + throw new Error('Missing values to derive sender public key from signed tx') + } + + try { + return ecrecover( + msgHash, + v.toNumber(), + bnToRlp(r), + bnToRlp(s), + this._signedTxImplementsEIP155() ? this.getChainId() : undefined + ) + } catch (e) { + throw new Error('Invalid Signature') + } } /** @@ -392,4 +289,21 @@ export default class LegacyTransaction extends BaseTransaction< ) } } + + private _signedTxImplementsEIP155() { + if (!this.isSigned()) { + throw Error('This transaction is not signed') + } + + const onEIP155BlockOrLater = this.common.gteHardfork('spuriousDragon') + + // EIP155 spec: + // If block.number >= 2,675,000 and v = CHAIN_ID * 2 + 35 or v = CHAIN_ID * 2 + 36, then when computing the hash of a transaction for purposes of signing or recovering, instead of hashing only the first six elements (i.e. nonce, gasprice, startgas, to, value, data), hash nine elements, with v replaced by CHAIN_ID, r = 0 and s = 0. + const v = this.v?.toNumber() + + const vAndChainIdMeetEIP155Conditions = + v === this.getChainId() * 2 + 35 || v === this.getChainId() * 2 + 36 + + return vAndChainIdMeetEIP155Conditions && onEIP155BlockOrLater + } } diff --git a/packages/tx/src/types.ts b/packages/tx/src/types.ts index 84f1cefc8be..a67bb2f89ac 100644 --- a/packages/tx/src/types.ts +++ b/packages/tx/src/types.ts @@ -153,10 +153,35 @@ export type TxData = LegacyTxData | EIP2930TxData export type Transaction = LegacyTransaction | EIP2930Transaction export type BaseTransactionData = { + /** + * The transaction's nonce. + */ + nonce?: BNLike + + /** + * The transaction's gas price. + */ + gasPrice?: BNLike + + /** + * The transaction's gas limit. + */ + gasLimit?: BNLike + /** * The transaction's the address is sent to. */ to?: AddressLike + + /** + * The amount of Ether sent. + */ + value?: BNLike + + /** + * This will contain the data of the message or the init of a contract. + */ + data?: BufferLike } /** diff --git a/packages/tx/test/transactionFactory.spec.ts b/packages/tx/test/transactionFactory.spec.ts index c34701d850d..9b2191f8bb3 100644 --- a/packages/tx/test/transactionFactory.spec.ts +++ b/packages/tx/test/transactionFactory.spec.ts @@ -54,10 +54,10 @@ tape('[TransactionFactory]: Basic functions', function (t) { ) t.test('should give me the right classes in getTransactionClass', function (st) { - let legacyTx = TransactionFactory.getTransactionClass() + const legacyTx = TransactionFactory.getTransactionClass() st.equals(legacyTx!.name, LegacyTransaction.name) - let eip2930Tx = TransactionFactory.getTransactionClass(1, EIP2930Common) + const eip2930Tx = TransactionFactory.getTransactionClass(1, EIP2930Common) st.equals(eip2930Tx!.name, EIP2930Transaction.name) st.end() diff --git a/packages/tx/test/transactionRunner.ts b/packages/tx/test/transactionRunner.ts index 68802e48187..3a89bf8e5dc 100644 --- a/packages/tx/test/transactionRunner.ts +++ b/packages/tx/test/transactionRunner.ts @@ -2,7 +2,7 @@ import tape from 'tape' import minimist from 'minimist' import { toBuffer } from 'ethereumjs-util' import Common from '@ethereumjs/common' -import LegacyTransaction from '../src/legacyTransaction' +import { LegacyTransaction } from '../src/' import { ForkName, ForkNamesMap, OfficialTransactionTestData } from './types' const testing = require('./testLoader') From 2943360e41a93e2b1eb165e0105fb87767bd9534 Mon Sep 17 00:00:00 2001 From: Jochem Brouwer Date: Thu, 28 Jan 2021 00:05:42 +0100 Subject: [PATCH 10/32] tx: decode EIP2930 transactions from block tx: fix undefined v/r/s/yParity values --- packages/tx/src/eip2930Transaction.ts | 149 ++++++++++++++------ packages/tx/src/transactionFactory.ts | 25 ++++ packages/tx/test/eip2930.ts | 28 +++- packages/tx/test/transactionFactory.spec.ts | 2 +- 4 files changed, 160 insertions(+), 44 deletions(-) diff --git a/packages/tx/src/eip2930Transaction.ts b/packages/tx/src/eip2930Transaction.ts index e2fcf7de762..089db130608 100644 --- a/packages/tx/src/eip2930Transaction.ts +++ b/packages/tx/src/eip2930Transaction.ts @@ -1,7 +1,20 @@ -import { Address, BN, bnToHex, rlp, rlphash, toBuffer } from 'ethereumjs-util' +import { + Address, + BN, + bnToHex, + bnToRlp, + ecrecover, + keccak256, + rlp, + rlphash, + toBuffer, +} from 'ethereumjs-util' import { BaseTransaction } from './baseTransaction' import { EIP2930TxData, TxOptions, JsonEIP2930Tx } from './types' +// secp256k1n/2 +const N_DIV_2 = new BN('7fffffffffffffffffffffffffffffff5d576e7357a4501ddfe92f46681b20a0', 16) + export default class EIP2930Transaction extends BaseTransaction { public readonly chainId: BN public readonly accessList: any @@ -67,9 +80,10 @@ export default class EIP2930Transaction extends BaseTransaction Date: Thu, 28 Jan 2021 17:39:47 +0100 Subject: [PATCH 11/32] tx: fix eip2930 hash method tx: fix eip2930 not throwing on wrong chain id --- packages/tx/src/eip2930Transaction.ts | 4 +- packages/tx/test/eip2930.ts | 59 +++++++++++++++++++++++---- 2 files changed, 53 insertions(+), 10 deletions(-) diff --git a/packages/tx/src/eip2930Transaction.ts b/packages/tx/src/eip2930Transaction.ts index 089db130608..83381029098 100644 --- a/packages/tx/src/eip2930Transaction.ts +++ b/packages/tx/src/eip2930Transaction.ts @@ -119,7 +119,7 @@ export default class EIP2930Transaction extends BaseTransaction { - EIP2930Transaction.fromTxData({ accessList }, { common }) + EIP2930Transaction.fromTxData({ chainId, accessList }, { common }) }) accessList = [ @@ -38,31 +55,31 @@ tape('[EIP2930 transactions]: Basic functions', function (t) { ] st.throws(() => { - EIP2930Transaction.fromTxData({ accessList }, { common }) + EIP2930Transaction.fromTxData({ chainId, accessList }, { common }) }) accessList = [[]] // Address does not exist st.throws(() => { - EIP2930Transaction.fromTxData({ accessList }, { common }) + EIP2930Transaction.fromTxData({ chainId, accessList }, { common }) }) accessList = [[validAddress]] // Slots does not exist st.throws(() => { - EIP2930Transaction.fromTxData({ accessList }, { common }) + EIP2930Transaction.fromTxData({ chainId, accessList }, { common }) }) accessList = [[validAddress, validSlot]] // Slots is not an array st.throws(() => { - EIP2930Transaction.fromTxData({ accessList }, { common }) + EIP2930Transaction.fromTxData({ chainId, accessList }, { common }) }) accessList = [[validAddress, [], []]] // 3 items where 2 are expected st.throws(() => { - EIP2930Transaction.fromTxData({ accessList }, { common }) + EIP2930Transaction.fromTxData({ chainId, accessList }, { common }) }) st.end() @@ -74,6 +91,7 @@ tape('[EIP2930 transactions]: Basic functions', function (t) { data: Buffer.from('010200', 'hex'), to: validAddress, accessList: [[validAddress, [validSlot]]], + chainId, }, { common } ) @@ -103,6 +121,7 @@ tape('[EIP2930 transactions]: Basic functions', function (t) { { data: Buffer.from('010200', 'hex'), accessList: [[validAddress, [validSlot]]], + chainId, }, { common } ) @@ -128,6 +147,7 @@ tape('[EIP2930 transactions]: Basic functions', function (t) { [validAddress, [validSlot]], [validAddress, [validSlot, validSlot]], ], + chainId, }, { common } ) @@ -143,6 +163,7 @@ tape('[EIP2930 transactions]: Basic functions', function (t) { data: Buffer.from('010200', 'hex'), to: validAddress, accessList: [[validAddress, [validSlot]]], + chainId, }, { common } ) @@ -155,4 +176,26 @@ tape('[EIP2930 transactions]: Basic functions', function (t) { t.end() }) + + t.test('should reject transactions with wrong chain ID', function (t) { + t.throws(() => { + EIP2930Transaction.fromTxData( + { + chainId: chainId.addn(1), + }, + { common } + ) + }) + t.end() + }) + + t.test('should produce right hash-to-sign values', function (t) { + const hash = GethUnsignedEIP2930Transaction.getMessageToSign() + const expected = Buffer.from( + 'c44faa8f50803df8edd97e72c4dbae32343b2986c91e382fc3e329e6c9a36f31', + 'hex' + ) + t.ok(hash.equals(expected)) + t.end() + }) }) From 73f4b35d4133b45f260bd3dd6ce70b642c5d642b Mon Sep 17 00:00:00 2001 From: Jochem Brouwer Date: Mon, 1 Mar 2021 21:08:18 +0100 Subject: [PATCH 12/32] tx: pack both legacy and typed transactions in TxData tx: add yParity test tx: fix chainId check for big numbers --- packages/tx/src/baseTransaction.ts | 4 +- packages/tx/src/eip2930Transaction.ts | 79 +++++++------------ packages/tx/src/legacyTransaction.ts | 13 ++-- packages/tx/src/types.ts | 86 ++++----------------- packages/tx/test/eip2930.ts | 12 +++ packages/tx/test/transactionFactory.spec.ts | 3 +- 6 files changed, 61 insertions(+), 136 deletions(-) diff --git a/packages/tx/src/baseTransaction.ts b/packages/tx/src/baseTransaction.ts index ff1db36b638..d8d29869b0a 100644 --- a/packages/tx/src/baseTransaction.ts +++ b/packages/tx/src/baseTransaction.ts @@ -8,9 +8,9 @@ import { ecsign, publicToAddress, } from 'ethereumjs-util' -import { BaseTransactionData, BaseTxOptions, DEFAULT_COMMON } from './types' +import { BaseTransactionData, BaseTxOptions, DEFAULT_COMMON, JsonTx } from './types' -export abstract class BaseTransaction { +export abstract class BaseTransaction { public readonly nonce: BN public readonly gasLimit: BN public readonly gasPrice: BN diff --git a/packages/tx/src/eip2930Transaction.ts b/packages/tx/src/eip2930Transaction.ts index 83381029098..07f81df9827 100644 --- a/packages/tx/src/eip2930Transaction.ts +++ b/packages/tx/src/eip2930Transaction.ts @@ -10,15 +10,15 @@ import { toBuffer, } from 'ethereumjs-util' import { BaseTransaction } from './baseTransaction' -import { EIP2930TxData, TxOptions, JsonEIP2930Tx } from './types' +import { JsonTx, TxData, TxOptions } from './types' // secp256k1n/2 const N_DIV_2 = new BN('7fffffffffffffffffffffffffffffff5d576e7357a4501ddfe92f46681b20a0', 16) -export default class EIP2930Transaction extends BaseTransaction { +export default class EIP2930Transaction extends BaseTransaction { public readonly chainId: BN public readonly accessList: any - public readonly yParity?: number + public readonly v?: BN public readonly r?: BN public readonly s?: BN @@ -32,7 +32,13 @@ export default class EIP2930Transaction extends BaseTransaction { +export default class LegacyTransaction extends BaseTransaction { public readonly v?: BN public readonly r?: BN public readonly s?: BN - public static fromTxData(txData: LegacyTxData, opts?: TxOptions) { + public static fromTxData(txData: TxData, opts?: TxOptions) { return new LegacyTransaction(txData, opts) } @@ -86,7 +83,7 @@ export default class LegacyTransaction extends BaseTransaction< * Use the static factory methods to assist in creating a Transaction object from varying data types. * @note Transaction objects implement EIP155 by default. To disable it, pass in an `@ethereumjs/common` object set before EIP155 activation (i.e. before Spurious Dragon). */ - protected constructor(txData: LegacyTxData, opts?: TxOptions) { + protected constructor(txData: TxData, opts?: TxOptions) { const { nonce, gasPrice, gasLimit, to, value, data, v, r, s } = txData super({ nonce, gasPrice, gasLimit, to, value, data }, opts) @@ -148,7 +145,7 @@ export default class LegacyTransaction extends BaseTransaction< /** * Returns an object with the JSON representation of the transaction */ - toJSON(): JsonLegacyTx { + toJSON(): JsonTx { return { nonce: bnToHex(this.nonce), gasPrice: bnToHex(this.gasPrice), diff --git a/packages/tx/src/types.ts b/packages/tx/src/types.ts index a67bb2f89ac..c756ca415e3 100644 --- a/packages/tx/src/types.ts +++ b/packages/tx/src/types.ts @@ -1,4 +1,4 @@ -import { AddressLike, BN, BNLike, BufferLike } from 'ethereumjs-util' +import { AddressLike, BNLike, BufferLike } from 'ethereumjs-util' import Common from '@ethereumjs/common' import { default as LegacyTransaction } from './legacyTransaction' import { default as EIP2930Transaction } from './eip2930Transaction' @@ -41,7 +41,12 @@ export interface BaseTxOptions { /** * An object with an optional field with each of the transaction's values. */ -export interface LegacyTxData { +export interface TxData { + /** + * The transaction's chain ID + */ + chainId?: BNLike + /** * The transaction's nonce. */ @@ -86,46 +91,6 @@ export interface LegacyTxData { * EC signature parameter. */ s?: BNLike -} - -/** - * An object with an optional field with each of the transaction's values. - */ -export interface EIP2930TxData { - /** - * The transaction's chain ID - */ - chainId?: BN - - /** - * The transaction's nonce. - */ - nonce?: BN - - /** - * The transaction's gas price. - */ - gasPrice?: BN - - /** - * The transaction's gas limit. - */ - gasLimit?: BN - - /** - * The transaction's the address is sent to. - */ - to?: AddressLike - - /** - * The amount of Ether sent. - */ - value?: BN - - /** - * This will contain the data of the message or the init of a contract. - */ - data?: Buffer /** * The access list which contains the addresses/storage slots which the transaction wishes to access @@ -133,23 +98,12 @@ export interface EIP2930TxData { accessList?: any // TODO: typesafe this /** - * Parity of the transaction - */ - yParity?: number - - /** - * EC signature parameter. (This is senderR in the EIP) + * The transaction type */ - r?: BN - /** - * EC signature parameter. (This is senderS in the EIP) - */ - s?: BN + type?: BNLike } -export type TxData = LegacyTxData | EIP2930TxData - export type Transaction = LegacyTransaction | EIP2930Transaction export type BaseTransactionData = { @@ -187,22 +141,7 @@ export type BaseTransactionData = { /** * An object with all of the transaction's values represented as strings. */ -export interface JsonLegacyTx { - nonce?: string - gasPrice?: string - gasLimit?: string - to?: string - data?: string - v?: string - r?: string - s?: string - value?: string -} - -/** - * An object with all of the transaction's values represented as strings. - */ -export interface JsonEIP2930Tx { +export interface JsonTx { nonce?: string gasPrice?: string gasLimit?: string @@ -212,8 +151,9 @@ export interface JsonEIP2930Tx { r?: string s?: string value?: string - chainId: string - accessList: string[] + chainId?: string + accessList?: string[] + type?: string } export const DEFAULT_COMMON = new Common({ chain: 'mainnet' }) diff --git a/packages/tx/test/eip2930.ts b/packages/tx/test/eip2930.ts index 36cd6481a65..2c43a1e3faf 100644 --- a/packages/tx/test/eip2930.ts +++ b/packages/tx/test/eip2930.ts @@ -189,6 +189,18 @@ tape('[EIP2930 transactions]: Basic functions', function (t) { t.end() }) + t.test('should reject transactions with invalid yParity (v) values', function (t) { + t.throws(() => { + EIP2930Transaction.fromTxData( + { + v: 2, + }, + { common } + ) + }) + t.end() + }) + t.test('should produce right hash-to-sign values', function (t) { const hash = GethUnsignedEIP2930Transaction.getMessageToSign() const expected = Buffer.from( diff --git a/packages/tx/test/transactionFactory.spec.ts b/packages/tx/test/transactionFactory.spec.ts index 31dc3f5d304..2609d9935f0 100644 --- a/packages/tx/test/transactionFactory.spec.ts +++ b/packages/tx/test/transactionFactory.spec.ts @@ -1,4 +1,5 @@ import Common from '@ethereumjs/common' +import { BN } from 'ethereumjs-util' import tape from 'tape' import { EIP2930Transaction, @@ -13,7 +14,7 @@ const EIP2930Common = new Common({ }) const simpleUnsignedEIP2930Transaction = EIP2930Transaction.fromTxData( - {}, + { chainId: new BN(1) }, { common: EIP2930Common } ) From 25ec7d5c6a732164753f15a0a0835524229bc0a4 Mon Sep 17 00:00:00 2001 From: Jochem Brouwer Date: Thu, 28 Jan 2021 18:24:52 +0100 Subject: [PATCH 13/32] tx: factory: add fromTxData tx: set default TxOpts value --- packages/tx/src/eip2930Transaction.ts | 10 +++--- packages/tx/src/legacyTransaction.ts | 8 ++--- packages/tx/src/transactionFactory.ts | 50 ++++++++++++++++++++++----- 3 files changed, 51 insertions(+), 17 deletions(-) diff --git a/packages/tx/src/eip2930Transaction.ts b/packages/tx/src/eip2930Transaction.ts index 07f81df9827..31630e0fc72 100644 --- a/packages/tx/src/eip2930Transaction.ts +++ b/packages/tx/src/eip2930Transaction.ts @@ -38,12 +38,12 @@ export default class EIP2930Transaction extends BaseTransaction Date: Mon, 1 Mar 2021 21:23:05 +0100 Subject: [PATCH 14/32] block: integrate EIP2930 blocks --- packages/block/src/block.ts | 18 ++++++++---------- packages/block/src/from-rpc.ts | 4 ++-- 2 files changed, 10 insertions(+), 12 deletions(-) diff --git a/packages/block/src/block.ts b/packages/block/src/block.ts index 912e0e8e783..2f688076e0c 100644 --- a/packages/block/src/block.ts +++ b/packages/block/src/block.ts @@ -3,7 +3,7 @@ import { BaseTrie as Trie } from 'merkle-patricia-tree' import { BN, rlp, keccak256, KECCAK256_RLP } from 'ethereumjs-util' import Common from '@ethereumjs/common' -import { Transaction, TxOptions } from '@ethereumjs/tx' +import { TransactionFactory, Transaction, TxOptions } from '@ethereumjs/tx' import { BlockHeader } from './header' import { BlockData, BlockOptions, JsonBlock, BlockBuffer, Blockchain } from './types' @@ -31,7 +31,7 @@ export class Block { // parse transactions const transactions = [] for (const txData of txsData || []) { - const tx = Transaction.fromTxData(txData, { + const tx = TransactionFactory.fromTxData(txData, { ...opts, // Use header common in case of hardforkByBlockNumber being activated common: header._common, @@ -90,13 +90,11 @@ export class Block { // parse transactions const transactions = [] for (const txData of txsData || []) { - transactions.push( - Transaction.fromValuesArray(txData, { - ...opts, - // Use header common in case of hardforkByBlockNumber being activated - common: header._common, - }) - ) + transactions.push(TransactionFactory.fromBlockBodyData(txData, { + ...opts, + // Use header common in case of hardforkByBlockNumber being activated + common: header._common, + })) } // parse uncle headers @@ -223,7 +221,7 @@ export class Block { const errors: string[] = [] this.transactions.forEach(function (tx, i) { - const errs = tx.validate(true) + const errs = tx.validate(true) if (errs.length > 0) { errors.push(`errors at tx ${i}: ${errs.join(', ')}`) } diff --git a/packages/block/src/from-rpc.ts b/packages/block/src/from-rpc.ts index 60bd0495371..27b4dec518d 100644 --- a/packages/block/src/from-rpc.ts +++ b/packages/block/src/from-rpc.ts @@ -1,4 +1,4 @@ -import { Transaction, TxData } from '@ethereumjs/tx' +import { TransactionFactory, Transaction, TxData } from '@ethereumjs/tx' import { toBuffer, setLengthLeft } from 'ethereumjs-util' import { Block, BlockOptions } from './index' @@ -37,7 +37,7 @@ export default function blockFromRpc(blockParams: any, uncles: any[] = [], optio const opts = { common: header._common } for (const _txParams of blockParams.transactions) { const txParams = normalizeTxParams(_txParams) - const tx = Transaction.fromTxData(txParams as TxData, opts) + const tx = TransactionFactory.fromTxData(txParams as TxData, opts) transactions.push(tx) } } From c5c58d0fcd609e7d88990c12bc09ebeb5f5378e0 Mon Sep 17 00:00:00 2001 From: Jochem Brouwer Date: Fri, 29 Jan 2021 22:26:53 +0100 Subject: [PATCH 15/32] tx: add AccessListItem type tx: allow either buffers or json-types for access lists --- packages/tx/src/eip2930Transaction.ts | 86 +++++++++++++++++++++++---- packages/tx/src/types.ts | 29 ++++++++- packages/tx/test/eip2930.ts | 46 +++++++++++++- 3 files changed, 148 insertions(+), 13 deletions(-) diff --git a/packages/tx/src/eip2930Transaction.ts b/packages/tx/src/eip2930Transaction.ts index 31630e0fc72..6744a3abf04 100644 --- a/packages/tx/src/eip2930Transaction.ts +++ b/packages/tx/src/eip2930Transaction.ts @@ -3,6 +3,7 @@ import { BN, bnToHex, bnToRlp, + bufferToHex, ecrecover, keccak256, rlp, @@ -10,18 +11,42 @@ import { toBuffer, } from 'ethereumjs-util' import { BaseTransaction } from './baseTransaction' -import { JsonTx, TxData, TxOptions } from './types' +import { + AccessList, + AccessListBuffer, + AccessListItem, + isAccessList, + JsonTx, + TxData, + TxOptions, +} from './types' // secp256k1n/2 const N_DIV_2 = new BN('7fffffffffffffffffffffffffffffff5d576e7357a4501ddfe92f46681b20a0', 16) +type EIP2930ValuesArray = [ + Buffer, + Buffer, + Buffer, + Buffer, + Buffer, + Buffer, + Buffer, + AccessListBuffer, + Buffer?, + Buffer?, + Buffer? +] + export default class EIP2930Transaction extends BaseTransaction { public readonly chainId: BN - public readonly accessList: any + public readonly accessList: AccessListBuffer public readonly v?: BN public readonly r?: BN public readonly s?: BN + public readonly AccessListJSON: AccessList + // EIP-2930 alias for `s` get senderS() { return this.s @@ -59,9 +84,11 @@ export default class EIP2930Transaction extends BaseTransactionvalues const emptyBuffer = Buffer.from([]) return new EIP2930Transaction( @@ -87,7 +114,7 @@ export default class EIP2930Transaction extends BaseTransactionaccessListItem[0] + const storageSlots = accessListItem[1] + if ((accessListItem)[2] !== undefined) { throw new Error( 'Access list item cannot have 3 elements. It can only have an address, and an array of storage slots.' ) @@ -183,7 +249,7 @@ export default class EIP2930Transaction extends BaseTransaction[ bnToRlp(this.chainId), bnToRlp(this.nonce), bnToRlp(this.gasPrice), diff --git a/packages/tx/src/types.ts b/packages/tx/src/types.ts index c756ca415e3..9cf1c3f5e64 100644 --- a/packages/tx/src/types.ts +++ b/packages/tx/src/types.ts @@ -38,6 +38,33 @@ export interface BaseTxOptions { common?: Common } +export type AccessListItem = { + address: string + storageKeys: string[] +} + +export type AccessListBufferItem = [Buffer, Buffer[]] + +export type AccessList = AccessListItem[] +export type AccessListBuffer = AccessListBufferItem[] + +export function isAccessListBuffer( + input: AccessListBuffer | AccessList +): input is AccessListBuffer { + if (input.length === 0) { + return true + } + const firstItem = input[0] + if (Array.isArray(firstItem)) { + return true + } + return false +} + +export function isAccessList(input: AccessListBuffer | AccessList): input is AccessList { + return !isAccessListBuffer(input) // This is exactly the same method, except the output is negated. +} + /** * An object with an optional field with each of the transaction's values. */ @@ -95,7 +122,7 @@ export interface TxData { /** * The access list which contains the addresses/storage slots which the transaction wishes to access */ - accessList?: any // TODO: typesafe this + accessList?: AccessListBuffer | AccessList /** * The transaction type diff --git a/packages/tx/test/eip2930.ts b/packages/tx/test/eip2930.ts index 2c43a1e3faf..fece6d4338c 100644 --- a/packages/tx/test/eip2930.ts +++ b/packages/tx/test/eip2930.ts @@ -1,7 +1,7 @@ import Common from '@ethereumjs/common' -import { Address, BN, privateToAddress } from 'ethereumjs-util' +import { Address, BN, bufferToHex, privateToAddress } from 'ethereumjs-util' import tape from 'tape' -import { EIP2930Transaction } from '../src' +import { AccessList, EIP2930Transaction } from '../src' const pKey = Buffer.from('4646464646464646464646464646464646464646464646464646464646464646', 'hex') const address = privateToAddress(pKey) @@ -33,6 +33,48 @@ const GethUnsignedEIP2930Transaction = EIP2930Transaction.fromTxData( const chainId = new BN(1) tape('[EIP2930 transactions]: Basic functions', function (t) { + t.test('should allow json-typed access lists', function (st) { + const access: AccessList = [ + { + address: bufferToHex(validAddress), + storageKeys: [bufferToHex(validSlot)], + }, + ] + const txn = EIP2930Transaction.fromTxData( + { + accessList: access, + chainId: 1, + }, + { common } + ) + + // Check if everything is converted + + const BufferArray = txn.accessList + const JSON = txn.AccessListJSON + + st.ok(BufferArray[0][0].equals(validAddress)) + st.ok(BufferArray[0][1][0].equals(validSlot)) + + st.deepEqual(JSON, access) + + // also verify that we can always get the json access list, even if we don't provide one. + + const txnRaw = EIP2930Transaction.fromTxData( + { + accessList: BufferArray, + chainId: 1, + }, + { common } + ) + + const JSONRaw = txnRaw.AccessListJSON + + st.deepEqual(JSONRaw, access) + + st.end() + }) + t.test('should throw on invalid access list data', function (st) { let accessList: any[] = [ [ From cd6a58f4abdb2dd8392bcae2ef97fd2463bf3f5c Mon Sep 17 00:00:00 2001 From: Jochem Brouwer Date: Sat, 30 Jan 2021 17:54:52 +0100 Subject: [PATCH 16/32] tx: fix EIP2930 v value of 0 tx: run new tests by default tx: fix hash(), raw() of EIP2930Transaction tx: fix EIP2930 hash scheme --- packages/tx/src/eip2930Transaction.ts | 9 ++++----- packages/tx/test/index.ts | 2 ++ 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/packages/tx/src/eip2930Transaction.ts b/packages/tx/src/eip2930Transaction.ts index 6744a3abf04..409de7f38dc 100644 --- a/packages/tx/src/eip2930Transaction.ts +++ b/packages/tx/src/eip2930Transaction.ts @@ -101,7 +101,7 @@ export default class EIP2930Transaction extends BaseTransaction Date: Sat, 20 Feb 2021 19:13:27 +0100 Subject: [PATCH 17/32] vm: implement new tx package --- packages/tx/src/eip2930Transaction.ts | 2 +- packages/tx/src/legacyTransaction.ts | 2 +- packages/tx/test/json/eip2930blockRLP.json | 3 +++ .../vm/examples/run-solidity-contract/index.ts | 6 +++--- .../vm/examples/run-transactions-complete/index.ts | 4 ++-- packages/vm/tests/api/events.spec.ts | 14 +++++++------- packages/vm/tests/api/runBlock.spec.ts | 6 +++--- packages/vm/tests/api/runTx.spec.ts | 8 ++++---- packages/vm/tests/util.ts | 4 ++-- 9 files changed, 26 insertions(+), 23 deletions(-) create mode 100644 packages/tx/test/json/eip2930blockRLP.json diff --git a/packages/tx/src/eip2930Transaction.ts b/packages/tx/src/eip2930Transaction.ts index 409de7f38dc..7a9ebcdc3b4 100644 --- a/packages/tx/src/eip2930Transaction.ts +++ b/packages/tx/src/eip2930Transaction.ts @@ -114,7 +114,7 @@ export default class EIP2930Transaction extends BaseTransaction { emitted = val }) - const tx = Transaction.fromTxData({ + const tx = LegacyTransaction.fromTxData({ gasPrice: 40000, gasLimit: 90000, to: '0x1111111111111111111111111111111111111111', @@ -79,7 +79,7 @@ tape('VM events', (t) => { emitted = val }) - const tx = Transaction.fromTxData({ + const tx = LegacyTransaction.fromTxData({ gasPrice: 40000, gasLimit: 90000, to: '0x1111111111111111111111111111111111111111', @@ -101,7 +101,7 @@ tape('VM events', (t) => { emitted = val }) - const tx = Transaction.fromTxData({ + const tx = LegacyTransaction.fromTxData({ gasPrice: 40000, gasLimit: 90000, to: '0x1111111111111111111111111111111111111111', @@ -124,7 +124,7 @@ tape('VM events', (t) => { emitted = val }) - const tx = Transaction.fromTxData({ + const tx = LegacyTransaction.fromTxData({ gasPrice: 40000, gasLimit: 90000, to: '0x1111111111111111111111111111111111111111', @@ -149,7 +149,7 @@ tape('VM events', (t) => { // This a deployment transaction that pushes 0x41 (i.e. ascii A) followed by 31 0s to // the stack, stores that in memory, and then returns the first byte from memory. // This deploys a contract which a single byte of code, 0x41. - const tx = Transaction.fromTxData({ + const tx = LegacyTransaction.fromTxData({ gasPrice: 40000, gasLimit: 90000, data: '0x7f410000000000000000000000000000000000000000000000000000000000000060005260016000f3', @@ -173,7 +173,7 @@ tape('VM events', (t) => { // This a deployment transaction that pushes 0x41 (i.e. ascii A) followed by 31 0s to // the stack, stores that in memory, and then returns the first byte from memory. // This deploys a contract which a single byte of code, 0x41. - const tx = Transaction.fromTxData({ + const tx = LegacyTransaction.fromTxData({ gasPrice: 40000, gasLimit: 90000, data: '0x7f410000000000000000000000000000000000000000000000000000000000000060005260016000f3', diff --git a/packages/vm/tests/api/runBlock.spec.ts b/packages/vm/tests/api/runBlock.spec.ts index d8bc5d63fa7..3b20e0f43a8 100644 --- a/packages/vm/tests/api/runBlock.spec.ts +++ b/packages/vm/tests/api/runBlock.spec.ts @@ -2,7 +2,7 @@ import tape from 'tape' import { Address, BN, rlp } from 'ethereumjs-util' import Common from '@ethereumjs/common' import { Block } from '@ethereumjs/block' -import { Transaction } from '@ethereumjs/tx' +import { LegacyTransaction } from '@ethereumjs/tx' import { DefaultStateManager } from '../../lib/state' import runBlock from '../../lib/runBlock' import { setupPreConditions, getDAOCommon } from '../util' @@ -98,7 +98,7 @@ tape('should fail when tx gas limit higher than block gas limit', async (t) => { const gasLimit = new BN(Buffer.from('3fefba', 'hex')) const opts = { common: block._common } - block.transactions[0] = new Transaction( + block.transactions[0] = new LegacyTransaction( { nonce, gasPrice, gasLimit, to, value, data, v, r, s }, opts ) @@ -135,7 +135,7 @@ tape('should correctly use the hardforkByBlockNumber option', async (t) => { number: new BN(10000000), }, transactions: [ - Transaction.fromTxData( + LegacyTransaction.fromTxData( { data: '0x600154', // PUSH 01 SLOAD gasLimit: new BN(100000), diff --git a/packages/vm/tests/api/runTx.spec.ts b/packages/vm/tests/api/runTx.spec.ts index 09da7913eb6..4d8361d85f7 100644 --- a/packages/vm/tests/api/runTx.spec.ts +++ b/packages/vm/tests/api/runTx.spec.ts @@ -2,7 +2,7 @@ import tape from 'tape' import { Account, Address, BN, MAX_INTEGER } from 'ethereumjs-util' import { Block } from '@ethereumjs/block' import Common from '@ethereumjs/common' -import { Transaction } from '@ethereumjs/tx' +import { LegacyTransaction } from '@ethereumjs/tx' import VM from '../../lib' import { DefaultStateManager } from '../../lib/state' import runTx from '../../lib/runTx' @@ -129,7 +129,7 @@ tape('should clear storage cache after every transaction', async (t) => { Buffer.from('00'.repeat(32), 'hex'), Buffer.from('00'.repeat(31) + '01', 'hex') ) - const tx = Transaction.fromTxData( + const tx = LegacyTransaction.fromTxData( { nonce: '0x00', gasPrice: 1, @@ -165,7 +165,7 @@ tape('should be possible to disable the block gas limit validation', async (t) = const transferCost = 21000 - const unsignedTx = Transaction.fromTxData({ + const unsignedTx = LegacyTransaction.fromTxData({ to: address, gasLimit: transferCost, gasPrice: 1, @@ -234,7 +234,7 @@ function getTransaction(sign = false, value = '0x00', createContract = false) { data, } - const tx = Transaction.fromTxData(txParams) + const tx = LegacyTransaction.fromTxData(txParams) if (sign) { const privateKey = Buffer.from( diff --git a/packages/vm/tests/util.ts b/packages/vm/tests/util.ts index f30edf4f7d5..44b0632c1b6 100644 --- a/packages/vm/tests/util.ts +++ b/packages/vm/tests/util.ts @@ -8,7 +8,7 @@ import { setLengthLeft, toBuffer, } from 'ethereumjs-util' -import { Transaction, TxOptions } from '@ethereumjs/tx' +import { LegacyTransaction, TxOptions } from '@ethereumjs/tx' import { Block, BlockHeader, BlockOptions } from '@ethereumjs/block' import Common from '@ethereumjs/common' @@ -99,7 +99,7 @@ const format = (exports.format = function ( * @returns {Transaction} Transaction to be passed to VM.runTx function */ export function makeTx(txData: any, opts?: TxOptions) { - const tx = Transaction.fromTxData(txData, opts) + const tx = LegacyTransaction.fromTxData(txData, opts) if (txData.secretKey) { const privKey = toBuffer(txData.secretKey) From 0b8643d5c2bbc5e938e0764cb80212942d2c3aa4 Mon Sep 17 00:00:00 2001 From: Jochem Brouwer Date: Sat, 20 Feb 2021 20:58:23 +0100 Subject: [PATCH 18/32] tx: add transaction type getter package: lint tx: fix tests and run linter tx: fix EIP2930 signatures/serialization/hash tx: fix tests --- packages/block/src/block.ts | 12 ++-- packages/tx/src/baseTransaction.ts | 10 ++- packages/tx/src/eip2930Transaction.ts | 29 ++++---- packages/tx/src/legacyTransaction.ts | 4 ++ packages/tx/src/transactionFactory.ts | 44 ++++++------ packages/tx/test/api.ts | 30 +++++---- packages/tx/test/eip2930.ts | 96 +++++++++++++++++++-------- 7 files changed, 142 insertions(+), 83 deletions(-) diff --git a/packages/block/src/block.ts b/packages/block/src/block.ts index 2f688076e0c..1f60afe6a25 100644 --- a/packages/block/src/block.ts +++ b/packages/block/src/block.ts @@ -90,11 +90,13 @@ export class Block { // parse transactions const transactions = [] for (const txData of txsData || []) { - transactions.push(TransactionFactory.fromBlockBodyData(txData, { - ...opts, - // Use header common in case of hardforkByBlockNumber being activated - common: header._common, - })) + transactions.push( + TransactionFactory.fromBlockBodyData(txData, { + ...opts, + // Use header common in case of hardforkByBlockNumber being activated + common: header._common, + }) + ) } // parse uncle headers diff --git a/packages/tx/src/baseTransaction.ts b/packages/tx/src/baseTransaction.ts index d8d29869b0a..0d1e3c3be25 100644 --- a/packages/tx/src/baseTransaction.ts +++ b/packages/tx/src/baseTransaction.ts @@ -19,7 +19,7 @@ export abstract class BaseTransaction { public readonly data: Buffer public readonly common: Common - constructor(txData: BaseTransactionData, txOptions?: BaseTxOptions) { + constructor(txData: BaseTransactionData, txOptions: BaseTxOptions = {}) { const { nonce, gasLimit, gasPrice, to, value, data } = txData this.nonce = new BN(toBuffer(nonce)) @@ -38,7 +38,10 @@ export abstract class BaseTransaction { this.validateExcdeedsMaxInteger(validateCannotExceedMaxInteger) - this.common = txOptions?.common ?? DEFAULT_COMMON + this.common = + (txOptions.common && + Object.assign(Object.create(Object.getPrototypeOf(txOptions.common)), txOptions.common)) ?? + DEFAULT_COMMON } protected validateExcdeedsMaxInteger(validateCannotExceedMaxInteger: { [key: string]: BN }) { @@ -113,8 +116,11 @@ export abstract class BaseTransaction { * (DataFee + TxFee + Creation Fee). */ validate(): boolean + /* eslint-disable-next-line no-dupe-class-members */ validate(stringError: false): boolean + /* eslint-disable-next-line no-dupe-class-members */ validate(stringError: true): string[] + /* eslint-disable-next-line no-dupe-class-members */ validate(stringError: boolean = false): boolean | string[] { const errors = [] diff --git a/packages/tx/src/eip2930Transaction.ts b/packages/tx/src/eip2930Transaction.ts index 7a9ebcdc3b4..5a7d528ad21 100644 --- a/packages/tx/src/eip2930Transaction.ts +++ b/packages/tx/src/eip2930Transaction.ts @@ -7,7 +7,6 @@ import { ecrecover, keccak256, rlp, - rlphash, toBuffer, } from 'ethereumjs-util' import { BaseTransaction } from './baseTransaction' @@ -45,6 +44,10 @@ export default class EIP2930Transaction extends BaseTransaction= 0x80 && transactionID <= 0xff) + + if (legacyTxn) { + return LegacyTransaction + } + + switch (transactionID) { + case 1: + return EIP2930Transaction + default: + throw new Error(`TypedTransaction with ID ${transactionID} unknown`) + } throw new Error(`TypedTransaction with ID ${transactionID} unknown`) } diff --git a/packages/tx/test/api.ts b/packages/tx/test/api.ts index 4ed5e09112e..dedce0e4ea1 100644 --- a/packages/tx/test/api.ts +++ b/packages/tx/test/api.ts @@ -462,25 +462,28 @@ tape('[Transaction]: Basic functions', function (t) { common, }).sign(privateKey) - let signedWithEIP155 = LegacyTransaction.fromTxData(fixtureTxSignedWithEIP155.toJSON()).sign( - privateKey - ) + let signedWithEIP155 = LegacyTransaction.fromTxData( + fixtureTxSignedWithEIP155.toJSON() + ).sign(privateKey) st.true(signedWithEIP155.verifySignature()) st.notEqual(signedWithEIP155.v?.toString('hex'), '1c') st.notEqual(signedWithEIP155.v?.toString('hex'), '1b') - signedWithEIP155 = LegacyTransaction.fromTxData(fixtureTxSignedWithoutEIP155.toJSON()).sign( - privateKey - ) + signedWithEIP155 = LegacyTransaction.fromTxData( + fixtureTxSignedWithoutEIP155.toJSON() + ).sign(privateKey) st.true(signedWithEIP155.verifySignature()) st.notEqual(signedWithEIP155.v?.toString('hex'), '1c') st.notEqual(signedWithEIP155.v?.toString('hex'), '1b') - let signedWithoutEIP155 = LegacyTransaction.fromTxData(fixtureTxSignedWithEIP155.toJSON(), { - common, - }).sign(privateKey) + let signedWithoutEIP155 = LegacyTransaction.fromTxData( + fixtureTxSignedWithEIP155.toJSON(), + { + common, + } + ).sign(privateKey) st.true(signedWithoutEIP155.verifySignature()) st.true( @@ -489,9 +492,12 @@ tape('[Transaction]: Basic functions', function (t) { "v shouldn't be EIP155 encoded" ) - signedWithoutEIP155 = LegacyTransaction.fromTxData(fixtureTxSignedWithoutEIP155.toJSON(), { - common, - }).sign(privateKey) + signedWithoutEIP155 = LegacyTransaction.fromTxData( + fixtureTxSignedWithoutEIP155.toJSON(), + { + common, + } + ).sign(privateKey) st.true(signedWithoutEIP155.verifySignature()) st.true( diff --git a/packages/tx/test/eip2930.ts b/packages/tx/test/eip2930.ts index fece6d4338c..33a2c7574bb 100644 --- a/packages/tx/test/eip2930.ts +++ b/packages/tx/test/eip2930.ts @@ -15,21 +15,6 @@ const common = new Common({ const validAddress = Buffer.from('01'.repeat(20), 'hex') const validSlot = Buffer.from('01'.repeat(32), 'hex') -// tests from https://github.com/ethereum/go-ethereum/blob/ac8e5900e6d38f7577251e7e36da9b371b2e5488/core/types/transaction_test.go#L56 -const GethUnsignedEIP2930Transaction = EIP2930Transaction.fromTxData( - { - chainId: new BN(1), - nonce: new BN(3), - to: new Address(Buffer.from('b94f5374fce5edbc8e2a8697c15331677e6ebf0b', 'hex')), - value: new BN(10), - gasLimit: new BN(25000), - gasPrice: new BN(1), - data: Buffer.from('5544', 'hex'), - accessList: [], - }, - { common } -) - const chainId = new BN(1) tape('[EIP2930 transactions]: Basic functions', function (t) { @@ -139,12 +124,12 @@ tape('[EIP2930 transactions]: Basic functions', function (t) { ) // Cost should be: // Base fee + 2*TxDataNonZero + TxDataZero + AccessListAddressCost + AccessListSlotCost - const txDataZero = common.param('gasPrices', 'txDataZero') - const txDataNonZero = common.param('gasPrices', 'txDataNonZero') - const accessListStorageKeyCost = common.param('gasPrices', 'accessListStorageKeyCost') - const accessListAddressCost = common.param('gasPrices', 'accessListAddressCost') - const baseFee = common.param('gasPrices', 'tx') - const creationFee = common.param('gasPrices', 'txCreation') + const txDataZero: number = common.param('gasPrices', 'txDataZero') + const txDataNonZero: number = common.param('gasPrices', 'txDataNonZero') + const accessListStorageKeyCost: number = common.param('gasPrices', 'accessListStorageKeyCost') + const accessListAddressCost: number = common.param('gasPrices', 'accessListAddressCost') + const baseFee: number = common.param('gasPrices', 'tx') + const creationFee: number = common.param('gasPrices', 'txCreation') st.ok( tx @@ -243,13 +228,72 @@ tape('[EIP2930 transactions]: Basic functions', function (t) { t.end() }) - t.test('should produce right hash-to-sign values', function (t) { - const hash = GethUnsignedEIP2930Transaction.getMessageToSign() - const expected = Buffer.from( - 'c44faa8f50803df8edd97e72c4dbae32343b2986c91e382fc3e329e6c9a36f31', + // Data from + // https://github.com/INFURA/go-ethlibs/blob/75b2a52a39d353ed8206cffaf68d09bd1b154aae/eth/transaction_signing_test.go#L87 + + t.test('should sign transaction correctly', function (t) { + const address = Buffer.from('0000000000000000000000000000000000001337', 'hex') + const slot1 = Buffer.from( + '0000000000000000000000000000000000000000000000000000000000000000', + 'hex' + ) + const txData = { + data: Buffer.from('', 'hex'), + gasLimit: 0x62d4, + gasPrice: 0x3b9aca00, + nonce: 0x00, + to: new Address(Buffer.from('df0a88b2b68c673713a8ec826003676f272e3573', 'hex')), + value: 0x01, + chainId: new BN(Buffer.from('796f6c6f763378', 'hex')), + accessList: [[address, [slot1]]], + } + + const customChainParams = { + name: 'custom', + chainId: parseInt(txData.chainId.toString()), + eips: [2718, 2929, 2930], + } + const usedCommon = Common.forCustomChain('mainnet', customChainParams, 'berlin') + usedCommon.setEIPs([2718, 2929, 2930]) + + const expectedUnsignedRaw = Buffer.from( + '01f86587796f6c6f76337880843b9aca008262d494df0a88b2b68c673713a8ec826003676f272e35730180f838f7940000000000000000000000000000000000001337e1a00000000000000000000000000000000000000000000000000000000000000000808080', + 'hex' + ) + const pkey = Buffer.from( + 'fad9c8855b740a0b7ed4c221dbad0f33a83a49cad6b3fe8d5817ac83d38b6a19', + 'hex' + ) + const expectedSigned = Buffer.from( + '01f8a587796f6c6f76337880843b9aca008262d494df0a88b2b68c673713a8ec826003676f272e35730180f838f7940000000000000000000000000000000000001337e1a0000000000000000000000000000000000000000000000000000000000000000080a0294ac94077b35057971e6b4b06dfdf55a6fbed819133a6c1d31e187f1bca938da00be950468ba1c25a5cb50e9f6d8aa13c8cd21f24ba909402775b262ac76d374d', + 'hex' + ) + const expectedHash = Buffer.from( + 'bbd570a3c6acc9bb7da0d5c0322fe4ea2a300db80226f7df4fef39b2d6649eec', 'hex' ) - t.ok(hash.equals(expected)) + const v = new BN(0) + const r = new BN( + Buffer.from('294ac94077b35057971e6b4b06dfdf55a6fbed819133a6c1d31e187f1bca938d', 'hex') + ) + const s = new BN( + Buffer.from('0be950468ba1c25a5cb50e9f6d8aa13c8cd21f24ba909402775b262ac76d374d', 'hex') + ) + + const unsignedTx = EIP2930Transaction.fromTxData(txData, { common: usedCommon }) + + const serializedMessageRaw = unsignedTx.serialize() + + t.ok(expectedUnsignedRaw.equals(serializedMessageRaw), 'serialized unsigned message correct') + + const signed = unsignedTx.sign(pkey) + + t.ok(v.eq(signed.v!), 'v correct') + t.ok(r.eq(signed.r!), 'r correct') + t.ok(s.eq(signed.s!), 's correct') + t.ok(expectedSigned.equals(signed.serialize()), 'serialized signed message correct') + t.ok(expectedHash.equals(signed.hash()), 'hash correct') + t.end() }) }) From 87955c8e01999b7e5c603d98a32f8fe340936ce4 Mon Sep 17 00:00:00 2001 From: Jochem Brouwer Date: Wed, 24 Feb 2021 22:32:01 +0100 Subject: [PATCH 19/32] vm: fix VM tests vm: add support for EIP2930 vm: add EIP2930-specific test --- packages/vm/lib/index.ts | 2 +- packages/vm/lib/runBlock.ts | 45 +++++++--- packages/vm/lib/runTx.ts | 24 +++++- packages/vm/tests/api/EIPs/eip-2929.spec.ts | 6 +- .../api/EIPs/eip-2930-accesslists.spec.ts | 82 +++++++++++++++++++ 5 files changed, 139 insertions(+), 20 deletions(-) create mode 100644 packages/vm/tests/api/EIPs/eip-2930-accesslists.spec.ts diff --git a/packages/vm/lib/index.ts b/packages/vm/lib/index.ts index 2ca2648bca0..181c1e42622 100644 --- a/packages/vm/lib/index.ts +++ b/packages/vm/lib/index.ts @@ -163,7 +163,7 @@ export default class VM extends AsyncEventEmitter { if (opts.common) { //EIPs - const supportedEIPs = [2537, 2565, 2929] + const supportedEIPs = [2537, 2565, 2718, 2929, 2930] for (const eip of opts.common.eips()) { if (!supportedEIPs.includes(eip)) { throw new Error(`${eip} is not supported by the VM`) diff --git a/packages/vm/lib/runBlock.ts b/packages/vm/lib/runBlock.ts index eb906fec482..1d99c291d85 100644 --- a/packages/vm/lib/runBlock.ts +++ b/packages/vm/lib/runBlock.ts @@ -61,7 +61,7 @@ export interface RunBlockResult { /** * Receipts generated for transactions in the block */ - receipts: (PreByzantiumTxReceipt | PostByzantiumTxReceipt)[] + receipts: (PreByzantiumTxReceipt | PostByzantiumTxReceipt | EIP2930Receipt)[] /** * Results of executing the transactions in the block */ @@ -108,6 +108,9 @@ export interface PostByzantiumTxReceipt extends TxReceipt { status: 0 | 1 } +// EIP290Receipt, which has the same fields as PostByzantiumTxReceipt +export interface EIP2930Receipt extends PostByzantiumTxReceipt {} + export interface AfterBlockEvent extends RunBlockResult { // The block which just finished processing block: Block @@ -328,30 +331,46 @@ async function applyTransactions(this: VM, block: Block, opts: RunBlockOpts) { logs: txRes.execResult.logs || [], } let txReceipt + let encodedReceipt let receiptLog = `Generate tx receipt gasUsed=${gasUsed} bitvector=${short( abstractTxReceipt.bitvector )} (${abstractTxReceipt.bitvector.length} bytes) logs=${abstractTxReceipt.logs.length}` - if (this._common.gteHardfork('byzantium')) { + if (tx.transactionType == 0) { + receiptLog += 'transactionType=0' + if (this._common.gteHardfork('byzantium')) { + txReceipt = { + status: txRes.execResult.exceptionError ? 0 : 1, // Receipts have a 0 as status on error + ...abstractTxReceipt, + } as PostByzantiumTxReceipt + const statusInfo = txRes.execResult.exceptionError ? 'error' : 'ok' + receiptLog += ` status=${txReceipt.status} (${statusInfo}) (>= Byzantium)` + } else { + const stateRoot = await this.stateManager.getStateRoot(true) + txReceipt = { + stateRoot: stateRoot, + ...abstractTxReceipt, + } as PreByzantiumTxReceipt + receiptLog += ` stateRoot=${txReceipt.stateRoot.toString('hex')} (< Byzantium)` + } + encodedReceipt = encode(Object.values(txReceipt)) + } else if (tx.transactionType == 1) { txReceipt = { - status: txRes.execResult.exceptionError ? 0 : 1, // Receipts have a 0 as status on error + status: txRes.execResult.exceptionError ? 0 : 1, ...abstractTxReceipt, - } as PostByzantiumTxReceipt - const statusInfo = txRes.execResult.exceptionError ? 'error' : 'ok' - receiptLog += ` status=${txReceipt.status} (${statusInfo}) (>= Byzantium)` + } as EIP2930Receipt + + // rlp([status, cumulativeGasUsed, logsBloom, logs]) + + encodedReceipt = Buffer.concat([Buffer.from('01', 'hex'), encode(Object.values(txReceipt))]) } else { - const stateRoot = await this.stateManager.getStateRoot(true) - txReceipt = { - stateRoot: stateRoot, - ...abstractTxReceipt, - } as PreByzantiumTxReceipt - receiptLog += ` stateRoot=${txReceipt.stateRoot.toString('hex')} (< Byzantium)` + throw new Error(`Unsupported transaction type ${tx.transactionType}`) } debug(receiptLog) receipts.push(txReceipt) // Add receipt to trie to later calculate receipt root - await receiptTrie.put(encode(txIdx), encode(Object.values(txReceipt))) + await receiptTrie.put(encode(txIdx), encodedReceipt) } return { diff --git a/packages/vm/lib/runTx.ts b/packages/vm/lib/runTx.ts index 9b4c8c53459..3c0ce5469fd 100644 --- a/packages/vm/lib/runTx.ts +++ b/packages/vm/lib/runTx.ts @@ -1,7 +1,7 @@ import { debug as createDebugLogger } from 'debug' import { Address, BN } from 'ethereumjs-util' import { Block } from '@ethereumjs/block' -import { Transaction } from '@ethereumjs/tx' +import { AccessListItem, EIP2930Transaction, Transaction } from '@ethereumjs/tx' import VM from './index' import Bloom from './bloom' import { default as EVM, EVMResult } from './evm/evm' @@ -9,6 +9,7 @@ import { short } from './evm/opcodes/util' import Message from './evm/message' import TxContext from './evm/txContext' import { getActivePrecompiles } from './evm/precompiles' +import { EIP2929StateManager } from './state/interface' const debug = createDebugLogger('vm:tx') const debugGas = createDebugLogger('vm:tx:gas') @@ -85,8 +86,8 @@ export default async function runTx(this: VM, opts: RunTxOpts): Promisethis.stateManager // Ensure we start with a clear warmed accounts Map if (this._common.isActivatedEIP(2929)) { @@ -97,6 +98,23 @@ export default async function runTx(this: VM, opts: RunTxOpts): Promiseopts.tx + + castedTx.AccessListJSON.forEach((accessListItem: AccessListItem) => { + const address = Buffer.from(accessListItem.address.slice(2), 'hex') + state.addWarmedAddress(address) + accessListItem.storageKeys.forEach((storageKey: string) => { + state.addWarmedStorage(address, Buffer.from(storageKey.slice(2), 'hex')) + }) + }) + } + try { const result = await _runTx.bind(this)(opts) await state.commit() diff --git a/packages/vm/tests/api/EIPs/eip-2929.spec.ts b/packages/vm/tests/api/EIPs/eip-2929.spec.ts index 105ce5ae836..b3ef5d0db6b 100644 --- a/packages/vm/tests/api/EIPs/eip-2929.spec.ts +++ b/packages/vm/tests/api/EIPs/eip-2929.spec.ts @@ -2,7 +2,7 @@ import tape from 'tape' import { Account, Address, BN } from 'ethereumjs-util' import VM from '../../../lib' import Common from '@ethereumjs/common' -import { Transaction } from '@ethereumjs/tx' +import { LegacyTransaction } from '@ethereumjs/tx' // Test cases source: https://gist.github.com/holiman/174548cad102096858583c6fbbb0649a tape('EIP 2929: gas cost tests', (t) => { @@ -50,7 +50,7 @@ tape('EIP 2929: gas cost tests', (t) => { await vm.stateManager.putContractCode(address, Buffer.from(test.code, 'hex')) - const unsignedTx = Transaction.fromTxData({ + const unsignedTx = LegacyTransaction.fromTxData({ gasLimit: initialGas, // ensure we pass a lot of gas, so we do not run out of gas to: address, // call to the contract address, }) @@ -80,7 +80,7 @@ tape('EIP 2929: gas cost tests', (t) => { await vm.stateManager.putContractCode(contractAddress, Buffer.from(code, 'hex')) // setup the contract code // setup the call arguments - const unsignedTx = Transaction.fromTxData({ + const unsignedTx = LegacyTransaction.fromTxData({ gasLimit: new BN(21000 + 9000), // ensure we pass a lot of gas, so we do not run out of gas to: contractAddress, // call to the contract address, value: new BN(1), diff --git a/packages/vm/tests/api/EIPs/eip-2930-accesslists.spec.ts b/packages/vm/tests/api/EIPs/eip-2930-accesslists.spec.ts new file mode 100644 index 00000000000..2bdf884d095 --- /dev/null +++ b/packages/vm/tests/api/EIPs/eip-2930-accesslists.spec.ts @@ -0,0 +1,82 @@ +import tape from 'tape' +import { Account, Address, BN, bufferToHex } from 'ethereumjs-util' +import Common from '@ethereumjs/common' +import VM from '../../../lib' +import { EIP2930Transaction } from '@ethereumjs/tx' + +const common = new Common({ + eips: [2718, 2929, 2930], + chain: 'mainnet', + hardfork: 'berlin', +}) + +const validAddress = Buffer.from('00000000000000000000000000000000000000ff', 'hex') +const validSlot = Buffer.from('00'.repeat(32), 'hex') + +// setup the accounts for this test +const privateKey = Buffer.from( + 'e331b6d69882b4cb4ea581d88e0b604039a3de5967688d3dcffdd2270c0fd109', + 'hex' +) +const contractAddress = new Address(validAddress) + +tape('EIP-2930 Optional Access Lists tests', (t) => { + t.test('VM should charge the right gas when using access list transactions', async (st) => { + const access = [ + { + address: bufferToHex(validAddress), + storageKeys: [bufferToHex(validSlot)], + }, + ] + const txnWithAccessList = EIP2930Transaction.fromTxData( + { + accessList: access, + chainId: 1, + gasLimit: 100000, + to: contractAddress, + }, + { common } + ).sign(privateKey) + const txnWithoutAccessList = EIP2930Transaction.fromTxData( + { + accessList: [], + chainId: 1, + gasLimit: 100000, + to: contractAddress, + }, + { common } + ).sign(privateKey) + const vm = new VM({ common }) + + // contract code PUSH1 0x00 SLOAD STOP + await vm.stateManager.putContractCode(contractAddress, Buffer.from('60005400', 'hex')) + + const address = Address.fromPrivateKey(privateKey) + const initialBalance = new BN(10).pow(new BN(18)) + + const account = await vm.stateManager.getAccount(address) + await vm.stateManager.putAccount( + address, + Account.fromAccountData({ ...account, balance: initialBalance }) + ) + + let trace: any = [] + + vm.on('step', (o: any) => { + trace.push([o.opcode.name, o.gasLeft]) + }) + + await vm.runTx({ tx: txnWithAccessList }) + st.ok(trace[1][0] == 'SLOAD') + let gasUsed = trace[1][1].sub(trace[2][1]).toNumber() + st.equal(gasUsed, 100, 'charge warm sload gas') + + trace = [] + await vm.runTx({ tx: txnWithoutAccessList }) + st.ok(trace[1][0] == 'SLOAD') + gasUsed = trace[1][1].sub(trace[2][1]).toNumber() + st.equal(gasUsed, 2100, 'charge cold sload gas') + + st.end() + }) +}) From a638d8bcb1d1c95d04ed159b05c044d99ca46cc6 Mon Sep 17 00:00:00 2001 From: holgerd77 Date: Thu, 25 Feb 2021 11:20:40 +0100 Subject: [PATCH 20/32] tx -> EIP-2930: some variable renaming, moved tests to *.spec.ts files --- packages/tx/src/eip2930Transaction.ts | 6 ++-- packages/tx/src/transactionFactory.ts | 29 +++++++++---------- packages/tx/test/{api.ts => api.spec.ts} | 0 .../tx/test/{eip2930.ts => eip2930.spec.ts} | 0 packages/tx/test/index.ts | 6 ++-- 5 files changed, 20 insertions(+), 21 deletions(-) rename packages/tx/test/{api.ts => api.spec.ts} (100%) rename packages/tx/test/{eip2930.ts => eip2930.spec.ts} (100%) diff --git a/packages/tx/src/eip2930Transaction.ts b/packages/tx/src/eip2930Transaction.ts index 5a7d528ad21..e9c90bac1b8 100644 --- a/packages/tx/src/eip2930Transaction.ts +++ b/packages/tx/src/eip2930Transaction.ts @@ -73,12 +73,14 @@ export default class EIP2930Transaction extends BaseTransaction Date: Thu, 25 Feb 2021 11:51:26 +0100 Subject: [PATCH 21/32] tx -> EIP-2930: common related improvements and fixes --- packages/common/src/eips/2930.json | 4 +-- packages/common/src/hardforks/berlin.json | 2 +- packages/common/src/index.ts | 2 +- packages/common/tests/eips.spec.ts | 34 ++++++++++------------- packages/tx/src/eip2930Transaction.ts | 4 +-- packages/tx/src/transactionFactory.ts | 10 +++---- 6 files changed, 26 insertions(+), 30 deletions(-) diff --git a/packages/common/src/eips/2930.json b/packages/common/src/eips/2930.json index 5c82947f065..dc4c34c4025 100644 --- a/packages/common/src/eips/2930.json +++ b/packages/common/src/eips/2930.json @@ -3,7 +3,8 @@ "comment": "Optional access lists", "url": "https://eips.ethereum.org/EIPS/eip-2930", "status": "Draft", - "minimumHardfork": "berlin", + "minimumHardfork": "istanbul", + "requiredEIPs": [2718, 2929], "gasConfig": {}, "gasPrices": { "accessListStorageKeyCost": { @@ -15,7 +16,6 @@ "d": "Gas cost per storage key in an Access List transaction" } }, - "requiredEIPs": [2718, 2929], "vm": {}, "pow": {} } diff --git a/packages/common/src/hardforks/berlin.json b/packages/common/src/hardforks/berlin.json index abe3fc79eef..14c80c0c628 100644 --- a/packages/common/src/hardforks/berlin.json +++ b/packages/common/src/hardforks/berlin.json @@ -3,5 +3,5 @@ "comment": "HF targeted for July 2020 following the Muir Glacier HF", "url": "https://eips.ethereum.org/EIPS/eip-2070", "status": "Draft", - "eips": [ 2315, 2565, 2929 ] + "eips": [ 2315, 2565, 2929, 2718, 2930 ] } diff --git a/packages/common/src/index.ts b/packages/common/src/index.ts index 1250cee094e..5a9a51ad6ab 100644 --- a/packages/common/src/index.ts +++ b/packages/common/src/index.ts @@ -277,7 +277,7 @@ export default class Common extends EventEmitter { if (EIPs[eip].requiredEIPs) { // eslint-disable-next-line prettier/prettier (EIPs[eip].requiredEIPs).forEach((elem: number) => { - if (!eips.includes(elem)) { + if (!(eips.includes(elem) || this.isActivatedEIP(elem))) { throw new Error(`${eip} requires EIP ${elem}, but is not included in the EIP list`) } }) diff --git a/packages/common/tests/eips.spec.ts b/packages/common/tests/eips.spec.ts index 41b3830c965..8c8a5289049 100644 --- a/packages/common/tests/eips.spec.ts +++ b/packages/common/tests/eips.spec.ts @@ -3,9 +3,22 @@ import Common from '../src/' tape('[Common]: Initialization / Chain params', function (t: tape.Test) { t.test('Correct initialization', function (st: tape.Test) { - const eips = [2537, 2929] + let eips = [2537, 2929] const c = new Common({ chain: 'mainnet', eips }) st.equal(c.eips(), eips, 'should initialize with supported EIP') + + eips = [2718, 2929, 2930] + new Common({ chain: 'mainnet', eips, hardfork: 'istanbul' }) + st.pass('Should not throw when initializing with a consistent EIP list') + + eips = [2930] + const msg = + 'should throw when initializing with an EIP with required EIPs not being activated along' + const f = () => { + new Common({ chain: 'mainnet', eips, hardfork: 'istanbul' }) + } + st.throws(f, msg) + st.end() }) @@ -45,24 +58,7 @@ tape('[Common]: Initialization / Chain params', function (t: tape.Test) { st.end() }) - t.test( - 'Should throw when trying to initialize with an EIP which requires certain EIPs, but which are not included on the EIP list', - function (st: tape.Test) { - const eips = [2930] - const msg = - 'should throw when initializing with an EIP, which does not have required EIPs on the EIP list' - const f = () => { - new Common({ chain: 'mainnet', eips, hardfork: 'berlin' }) - } - st.throws(f, msg) - st.end() - } - ) - - t.test('Should not throw when initializing with a valid EIP list', function (st: tape.Test) { - const eips = [2718, 2929, 2930] - new Common({ chain: 'mainnet', eips, hardfork: 'berlin' }) - st.pass('initialized correctly') + t.test('Initialization', function (st: tape.Test) { st.end() }) }) diff --git a/packages/tx/src/eip2930Transaction.ts b/packages/tx/src/eip2930Transaction.ts index e9c90bac1b8..37e05e086d8 100644 --- a/packages/tx/src/eip2930Transaction.ts +++ b/packages/tx/src/eip2930Transaction.ts @@ -123,9 +123,9 @@ export default class EIP2930Transaction extends BaseTransaction Date: Mon, 1 Mar 2021 21:36:59 +0100 Subject: [PATCH 22/32] tx: fix transaction type of LegacyTransaction --- packages/tx/src/legacyTransaction.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/tx/src/legacyTransaction.ts b/packages/tx/src/legacyTransaction.ts index 46f8b6cd3c1..3f6d9d2dbe4 100644 --- a/packages/tx/src/legacyTransaction.ts +++ b/packages/tx/src/legacyTransaction.ts @@ -30,7 +30,7 @@ export default class LegacyTransaction extends BaseTransaction Date: Thu, 25 Feb 2021 14:29:08 +0100 Subject: [PATCH 23/32] tx: fix transaction type of LegacyTransaction vm: setup EIP2930 tests --- .gitmodules | 4 ++-- packages/ethereum-tests | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.gitmodules b/.gitmodules index e6b8bc1676e..928adbbe6a6 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,4 +1,4 @@ [submodule "ethereum-tests"] path = packages/ethereum-tests - url = https://github.com/ethereum/tests.git - branch = develop + url = https://github.com/qbzzt/tests.git + branch = eip2930 diff --git a/packages/ethereum-tests b/packages/ethereum-tests index 1508126ea04..0966f565365 160000 --- a/packages/ethereum-tests +++ b/packages/ethereum-tests @@ -1 +1 @@ -Subproject commit 1508126ea04cd61495b60db2f036ac823de274b1 +Subproject commit 0966f56536511200326e5ca939385da462730240 From af6782f085661d6982f1a546a9099ab286df7dcb Mon Sep 17 00:00:00 2001 From: holgerd77 Date: Thu, 25 Feb 2021 16:52:51 +0100 Subject: [PATCH 24/32] tx, vm -> EIP-2930: smaller fixes and cleanups --- packages/tx/src/eip2930Transaction.ts | 5 ++--- packages/tx/src/transactionFactory.ts | 13 ++++--------- packages/vm/lib/runBlock.ts | 13 +++++++------ packages/vm/lib/runTx.ts | 2 +- 4 files changed, 14 insertions(+), 19 deletions(-) diff --git a/packages/tx/src/eip2930Transaction.ts b/packages/tx/src/eip2930Transaction.ts index 37e05e086d8..1761a635afa 100644 --- a/packages/tx/src/eip2930Transaction.ts +++ b/packages/tx/src/eip2930Transaction.ts @@ -123,9 +123,8 @@ export default class EIP2930Transaction extends BaseTransaction Date: Thu, 25 Feb 2021 18:24:03 +0100 Subject: [PATCH 25/32] block/tx: fix `raw` method to return right value tx: fix browser tests vm: state tests to use EIP2930 --- packages/block/src/block.ts | 2 +- packages/tx/karma.conf.js | 2 +- packages/tx/src/baseTransaction.ts | 4 +++- packages/tx/src/eip2930Transaction.ts | 10 +++++++--- packages/vm/tests/util.ts | 9 +++++++-- 5 files changed, 19 insertions(+), 8 deletions(-) diff --git a/packages/block/src/block.ts b/packages/block/src/block.ts index 1f60afe6a25..f710502f341 100644 --- a/packages/block/src/block.ts +++ b/packages/block/src/block.ts @@ -154,7 +154,7 @@ export class Block { raw(): BlockBuffer { return [ this.header.raw(), - this.transactions.map((tx) => tx.raw()), + this.transactions.map((tx) => tx.raw()), this.uncleHeaders.map((uh) => uh.raw()), ] } diff --git a/packages/tx/karma.conf.js b/packages/tx/karma.conf.js index 413873c1032..a577e868fc1 100644 --- a/packages/tx/karma.conf.js +++ b/packages/tx/karma.conf.js @@ -5,7 +5,7 @@ module.exports = function (config) { browserNoActivityTimeout: 60000, frameworks: ['browserify', 'tap'], // the official transaction's test suite is disabled for now, see https://github.com/ethereumjs/ethereumjs-testing/issues/40 - files: ['./test-build/test/api.js'], + files: ['./test-build/test/legacy.spec.js'], preprocessors: { './test-build/**/*.js': ['browserify'], }, diff --git a/packages/tx/src/baseTransaction.ts b/packages/tx/src/baseTransaction.ts index 0d1e3c3be25..11caa69f582 100644 --- a/packages/tx/src/baseTransaction.ts +++ b/packages/tx/src/baseTransaction.ts @@ -160,7 +160,9 @@ export abstract class BaseTransaction { } } - abstract raw(): Buffer[] + // In case of a LegacyTransaction, this is a Buffer[] + // For a TypedTransaction, this is a Buffer + abstract raw(): Buffer[] | Buffer abstract hash(): Buffer abstract getMessageToVerifySignature(): Buffer diff --git a/packages/tx/src/eip2930Transaction.ts b/packages/tx/src/eip2930Transaction.ts index 1761a635afa..7a452e079b4 100644 --- a/packages/tx/src/eip2930Transaction.ts +++ b/packages/tx/src/eip2930Transaction.ts @@ -250,7 +250,7 @@ export default class EIP2930Transaction extends BaseTransaction[ bnToRlp(this.chainId), bnToRlp(this.nonce), @@ -264,14 +264,18 @@ export default class EIP2930Transaction extends BaseTransactionthis.raw() } /** diff --git a/packages/vm/tests/util.ts b/packages/vm/tests/util.ts index 44b0632c1b6..c5bec853d6f 100644 --- a/packages/vm/tests/util.ts +++ b/packages/vm/tests/util.ts @@ -8,7 +8,7 @@ import { setLengthLeft, toBuffer, } from 'ethereumjs-util' -import { LegacyTransaction, TxOptions } from '@ethereumjs/tx' +import { EIP2930Transaction, LegacyTransaction, TxOptions } from '@ethereumjs/tx' import { Block, BlockHeader, BlockOptions } from '@ethereumjs/block' import Common from '@ethereumjs/common' @@ -99,7 +99,12 @@ const format = (exports.format = function ( * @returns {Transaction} Transaction to be passed to VM.runTx function */ export function makeTx(txData: any, opts?: TxOptions) { - const tx = LegacyTransaction.fromTxData(txData, opts) + let tx + if (txData.accessLists) { + tx = EIP2930Transaction.fromTxData(txData, opts) + } else { + tx = LegacyTransaction.fromTxData(txData, opts) + } if (txData.secretKey) { const privKey = toBuffer(txData.secretKey) From 4a7a547e06314a67c1fe321f3071779169766854 Mon Sep 17 00:00:00 2001 From: holgerd77 Date: Thu, 25 Feb 2021 19:49:17 +0100 Subject: [PATCH 26/32] tx -> EIP-2930: set default HF from common to berlin, small fixes --- packages/tx/src/baseTransaction.ts | 4 ++-- packages/tx/src/legacyTransaction.ts | 2 +- packages/tx/src/types.ts | 2 +- packages/tx/test/api.spec.ts | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/tx/src/baseTransaction.ts b/packages/tx/src/baseTransaction.ts index 11caa69f582..79df5ef1f86 100644 --- a/packages/tx/src/baseTransaction.ts +++ b/packages/tx/src/baseTransaction.ts @@ -36,7 +36,7 @@ export abstract class BaseTransaction { value: this.value, } - this.validateExcdeedsMaxInteger(validateCannotExceedMaxInteger) + this.validateExceedsMaxInteger(validateCannotExceedMaxInteger) this.common = (txOptions.common && @@ -44,7 +44,7 @@ export abstract class BaseTransaction { DEFAULT_COMMON } - protected validateExcdeedsMaxInteger(validateCannotExceedMaxInteger: { [key: string]: BN }) { + protected validateExceedsMaxInteger(validateCannotExceedMaxInteger: { [key: string]: BN }) { for (const [key, value] of Object.entries(validateCannotExceedMaxInteger)) { if (value && value.gt(MAX_INTEGER)) { throw new Error(`${key} cannot exceed MAX_INTEGER, given ${value}`) diff --git a/packages/tx/src/legacyTransaction.ts b/packages/tx/src/legacyTransaction.ts index 3f6d9d2dbe4..ca13ad49d32 100644 --- a/packages/tx/src/legacyTransaction.ts +++ b/packages/tx/src/legacyTransaction.ts @@ -101,7 +101,7 @@ export default class LegacyTransaction extends BaseTransaction Date: Thu, 25 Feb 2021 21:28:52 +0100 Subject: [PATCH 27/32] vm: fix state tests for EIP2930 vm/tx/block: add changelog tx: add some docs tx: check s value OK, make json-tx geth compatible common: remove empty test vm: remove unnecesary comment --- packages/block/CHANGELOG.md | 4 ++++ packages/common/tests/eips.spec.ts | 4 ---- packages/tx/CHANGELOG.md | 10 ++++++++ packages/tx/src/baseTransaction.ts | 6 +++-- packages/tx/src/eip2930Transaction.ts | 25 +++++++++++++------- packages/tx/src/transactionFactory.ts | 10 ++++++++ packages/tx/src/types.ts | 4 +++- packages/vm/CHANGELOG.md | 5 ++++ packages/vm/lib/runBlock.ts | 2 -- packages/vm/tests/GeneralStateTestsRunner.ts | 7 ++++++ 10 files changed, 59 insertions(+), 18 deletions(-) diff --git a/packages/block/CHANGELOG.md b/packages/block/CHANGELOG.md index 22455fba3a7..ed2a52964ed 100644 --- a/packages/block/CHANGELOG.md +++ b/packages/block/CHANGELOG.md @@ -6,6 +6,10 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) (modification: no type change headlines) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). +## UNRELEASED + +- Integration of [EIP2718](https://eips.ethereum.org/EIPS/eip-2718) (Typed Transactions) and [EIP2930](https://eips.ethereum.org/EIPS/eip-2930) (Access List Transaction), PR [#1048](https://github.com/ethereumjs/ethereumjs-monorepo/pull/1048). It is now possible to create blocks with access list transactions. + ## 3.1.0 - 2021-02-22 ### Clique/PoA Support diff --git a/packages/common/tests/eips.spec.ts b/packages/common/tests/eips.spec.ts index 8c8a5289049..9aa3aa244b2 100644 --- a/packages/common/tests/eips.spec.ts +++ b/packages/common/tests/eips.spec.ts @@ -57,8 +57,4 @@ tape('[Common]: Initialization / Chain params', function (t: tape.Test) { st.end() }) - - t.test('Initialization', function (st: tape.Test) { - st.end() - }) }) diff --git a/packages/tx/CHANGELOG.md b/packages/tx/CHANGELOG.md index f5867b56afe..614daec8c39 100644 --- a/packages/tx/CHANGELOG.md +++ b/packages/tx/CHANGELOG.md @@ -6,6 +6,16 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) (modification: no type change headlines) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). +## UNRELEASED + +- Integration of [EIP2718](https://eips.ethereum.org/EIPS/eip-2718) (Typed Transactions) and [EIP2930](https://eips.ethereum.org/EIPS/eip-2930) (Access List Transaction), PR [#1048](https://github.com/ethereumjs/ethereumjs-monorepo/pull/1048). + +This PR integrates the Typed Transactions. In order to produce the right transactions, there is a new class called the `TransactionFactory`. This factory helps to create the correct transaction. When decoding directly from blocks, use the `fromBlockBodyData` method. The PR also refactors the internals of the legacy transaction and the new typed transaction: there is a base transaction class, which both transaction classes extends. It is also possible to import `EIP2930Transaction` (an access list transaction). + +### How to migrate + +The old `Transaction` class has been renamed to `LegacyTransaction`. This class works just how it used to work. + ## 3.0.2 - 2021-02-16 diff --git a/packages/tx/src/baseTransaction.ts b/packages/tx/src/baseTransaction.ts index 79df5ef1f86..f7f0691c674 100644 --- a/packages/tx/src/baseTransaction.ts +++ b/packages/tx/src/baseTransaction.ts @@ -160,8 +160,10 @@ export abstract class BaseTransaction { } } - // In case of a LegacyTransaction, this is a Buffer[] - // For a TypedTransaction, this is a Buffer + /** + * Returns the raw `Buffer[]` (LegacyTransaction) or `Buffer` (typed transaction). + * This is the data which is found in the transactions of the block body. + */ abstract raw(): Buffer[] | Buffer abstract hash(): Buffer diff --git a/packages/tx/src/eip2930Transaction.ts b/packages/tx/src/eip2930Transaction.ts index 7a452e079b4..293c51cd05e 100644 --- a/packages/tx/src/eip2930Transaction.ts +++ b/packages/tx/src/eip2930Transaction.ts @@ -7,6 +7,7 @@ import { ecrecover, keccak256, rlp, + setLengthLeft, toBuffer, } from 'ethereumjs-util' import { BaseTransaction } from './baseTransaction' @@ -183,7 +184,11 @@ export default class EIP2930Transaction extends BaseTransaction[ @@ -272,7 +277,8 @@ export default class EIP2930Transaction extends BaseTransactionthis.raw() @@ -282,18 +288,19 @@ export default class EIP2930Transaction extends BaseTransactionitem[0]).toString('hex')] + const JSONItem: any = { + address: '0x' + setLengthLeft(item[0], 20).toString('hex'), + storageKeys: [], + } const storageSlots: Buffer[] = item[1] - const JSONSlots = [] for (let slot = 0; slot < storageSlots.length; slot++) { const storageSlot = storageSlots[slot] - JSONSlots.push('0x' + storageSlot.toString('hex')) + JSONItem.storageKeys.push('0x' + setLengthLeft(storageSlot, 32).toString('hex')) } - JSONItem.push(JSONSlots) accessListJSON.push(JSONItem) } diff --git a/packages/tx/src/transactionFactory.ts b/packages/tx/src/transactionFactory.ts index a42e7326da5..37452dcf9dc 100644 --- a/packages/tx/src/transactionFactory.ts +++ b/packages/tx/src/transactionFactory.ts @@ -10,6 +10,11 @@ export default class TransactionFactory { // It is not possible to instantiate a TransactionFactory object. private constructor() {} + /** + * Create a transaction from a `txData` object + * @param txData - The transaction data. The `type` field will determine which transaction type is returned (if undefined, create a LegacyTransaction) + * @param txOptions - Options to pass on to the constructor of the transaction + */ public static fromTxData(txData: TxData, txOptions: TxOptions = {}): Transaction { const common = txOptions.common ?? DEFAULT_COMMON if (txData.type === undefined) { @@ -107,6 +112,11 @@ export default class TransactionFactory { throw new Error(`TypedTransaction with ID ${transactionID} unknown`) } + /** + * Check if a typed transaction eip is supported by common + * @param common - The common to use + * @param eip - The EIP to check + */ public static eipSupport(common: Common, eip: number): boolean { if (!common.isActivatedEIP(2718)) { return false diff --git a/packages/tx/src/types.ts b/packages/tx/src/types.ts index 1d15528936d..7a793b3d37e 100644 --- a/packages/tx/src/types.ts +++ b/packages/tx/src/types.ts @@ -165,6 +165,8 @@ export type BaseTransactionData = { data?: BufferLike } +type JsonAccessListItem = { address: string; storageKeys: string[] } + /** * An object with all of the transaction's values represented as strings. */ @@ -179,7 +181,7 @@ export interface JsonTx { s?: string value?: string chainId?: string - accessList?: string[] + accessList?: JsonAccessListItem[] type?: string } diff --git a/packages/vm/CHANGELOG.md b/packages/vm/CHANGELOG.md index 1b9981f1141..bd6c9b29e0b 100644 --- a/packages/vm/CHANGELOG.md +++ b/packages/vm/CHANGELOG.md @@ -6,6 +6,11 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) (modification: no type change headlines) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). +## UNRELEASED + +- Fixes for [EIP2929](https://eips.ethereum.org/EIPS/eip-2929) (Gas cost increases for state access opcodes), PR [#1124](https://github.com/ethereumjs/ethereumjs-monorepo/pull/1124) +- Integration of [EIP2718](https://eips.ethereum.org/EIPS/eip-2718) (Typed Transactions) and [EIP2930](https://eips.ethereum.org/EIPS/eip-2930) (Access List Transaction), PR [#1048](https://github.com/ethereumjs/ethereumjs-monorepo/pull/1048). VM now has support for access list transactions. + ### 5.1.0 - 2021-02-22 ### Clique/PoA Support diff --git a/packages/vm/lib/runBlock.ts b/packages/vm/lib/runBlock.ts index 55d5c86951e..d9485ceabfb 100644 --- a/packages/vm/lib/runBlock.ts +++ b/packages/vm/lib/runBlock.ts @@ -360,8 +360,6 @@ async function applyTransactions(this: VM, block: Block, opts: RunBlockOpts) { ...abstractTxReceipt, } as EIP2930Receipt - // rlp([status, cumulativeGasUsed, logsBloom, logs]) - encodedReceipt = Buffer.concat([Buffer.from('01', 'hex'), encode(Object.values(txReceipt))]) } else { throw new Error(`Unsupported transaction type ${tx.transactionType}`) diff --git a/packages/vm/tests/GeneralStateTestsRunner.ts b/packages/vm/tests/GeneralStateTestsRunner.ts index 6b2385a611f..d187fa2e6b0 100644 --- a/packages/vm/tests/GeneralStateTestsRunner.ts +++ b/packages/vm/tests/GeneralStateTestsRunner.ts @@ -34,6 +34,13 @@ function parseTestCases( tx.gasLimit = testData.transaction.gasLimit[testIndexes['gas']] tx.value = testData.transaction.value[testIndexes['value']] + if (tx.accessLists) { + tx.accessList = testData.transaction.accessLists[testIndexes['data']] + if (tx.chainId == undefined) { + tx.chainId = 1 + } + } + return { transaction: tx, postStateRoot: testCase['hash'], From ceeedc67e29e24a41c0608e67ceae754059c7940 Mon Sep 17 00:00:00 2001 From: holgerd77 Date: Fri, 26 Feb 2021 11:55:04 +0100 Subject: [PATCH 28/32] tx -> EIP-2929: started to separate dedicated legacy and base tests, test clean-up, add more tests tx -> EIP-2930: fixed missing toBuffer conversion for v value in EIP2930Transaction constructor tx -> EIP-2930: added generic API tests for serialize() and raw() tx -> EIP-2930: added generic verifySignature() tests tx -> EIP-2920: added generic verifySignature() error test cases tx -> EIP-2930: small test fix --- packages/tx/src/eip2930Transaction.ts | 4 +- packages/tx/test/api.spec.ts | 97 ++--------------- packages/tx/test/base.spec.ts | 150 ++++++++++++++++++++++++++ packages/tx/test/eip2930.spec.ts | 47 ++++---- packages/tx/test/index.ts | 21 +++- packages/tx/test/json/eip2930txs.json | 51 +++++++++ packages/tx/test/json/txs.json | 65 ++++++++++- 7 files changed, 310 insertions(+), 125 deletions(-) create mode 100644 packages/tx/test/base.spec.ts create mode 100644 packages/tx/test/json/eip2930txs.json diff --git a/packages/tx/src/eip2930Transaction.ts b/packages/tx/src/eip2930Transaction.ts index 293c51cd05e..ef0fcdc2083 100644 --- a/packages/tx/src/eip2930Transaction.ts +++ b/packages/tx/src/eip2930Transaction.ts @@ -170,9 +170,9 @@ export default class EIP2930Transaction extends BaseTransaction decode with fromValuesArray()', function (st) { txFixtures.slice(0, 4).forEach(function (tx: any) { const txData = tx.raw.map(toBuffer) const pt = LegacyTransaction.fromValuesArray(txData) @@ -80,7 +31,7 @@ tape('[Transaction]: Basic functions', function (t) { st.end() }) - t.test('should serialize', function (st) { + t.test('serialize()', function (st) { transactions.forEach(function (tx, i) { const s1 = tx.serialize() const s2 = rlp.encode(txFixtures[i].raw) @@ -89,7 +40,7 @@ tape('[Transaction]: Basic functions', function (t) { st.end() }) - t.test('should hash', function (st) { + t.test('hash()', function (st) { const common = new Common({ chain: 'mainnet', hardfork: 'tangerineWhistle', @@ -112,7 +63,7 @@ tape('[Transaction]: Basic functions', function (t) { st.end() }) - t.test('should hash with defined chainId', function (st) { + t.test('hash() -> with defined chainId', function (st) { const tx = LegacyTransaction.fromValuesArray(txFixtures[4].raw.map(toBuffer)) st.equal( tx.hash().toString('hex'), @@ -129,43 +80,11 @@ tape('[Transaction]: Basic functions', function (t) { st.end() }) - t.test('should verify Signatures', function (st) { - transactions.forEach(function (tx) { - st.equals((tx).verifySignature(), true) - }) - st.end() - }) - - t.test('should not verify invalid signatures', function (st) { - const txs: LegacyTransaction[] = [] - - txFixtures.slice(0, 4).forEach(function (txFixture: any) { - const txData = txFixture.raw.map(toBuffer) - // set `s` to zero - txData[8] = zeros(32) - const tx = LegacyTransaction.fromValuesArray(txData) - txs.push(tx) - }) - - txs.forEach(function (tx) { - st.equals(tx.verifySignature(), false) - - st.ok( - tx.validate(true).includes('Invalid Signature'), - 'should give a string about not verifying signatures' - ) - - st.notOk(tx.validate(), 'should validate correctly') - }) - - st.end() - }) - - t.test('should sign tx', function (st) { + t.test('sign()', function (st) { transactions.forEach(function (tx, i) { const { privateKey } = txFixtures[i] if (privateKey) { - st.ok(tx.sign(Buffer.from(privateKey, 'hex'))) + st.ok(tx.sign(Buffer.from(privateKey, 'hex')), 'should sign tx') } }) st.end() diff --git a/packages/tx/test/base.spec.ts b/packages/tx/test/base.spec.ts new file mode 100644 index 00000000000..a1c17cd1621 --- /dev/null +++ b/packages/tx/test/base.spec.ts @@ -0,0 +1,150 @@ +import tape from 'tape' +import Common from '@ethereumjs/common' +import { LegacyTransaction, EIP2930Transaction } from '../src' +import { TxsJsonEntry } from './types' +import { BaseTransaction } from '../src/baseTransaction' + +tape('[BaseTransaction]', function (t) { + const legacyFixtures: TxsJsonEntry[] = require('./json/txs.json') + const legacyTxs: BaseTransaction[] = [] + legacyFixtures.slice(0, 4).forEach(function (tx: any) { + legacyTxs.push(LegacyTransaction.fromTxData(tx.data)) + }) + + const eip2930Fixtures = require('./json/eip2930txs.json') + const eip2930Txs: BaseTransaction[] = [] + eip2930Fixtures.forEach(function (tx: any) { + eip2930Txs.push(EIP2930Transaction.fromTxData(tx.data)) + }) + + const zero = Buffer.alloc(0) + const txTypes = [ + { + class: LegacyTransaction, + name: 'LegacyTransaction', + values: Array(6).fill(zero), + txs: legacyTxs, + fixtures: legacyFixtures, + }, + { + class: EIP2930Transaction, + name: 'EIP2930Transaction', + values: [Buffer.from([1])].concat(Array(7).fill(zero)), + txs: eip2930Txs, + fixtures: eip2930Fixtures, + }, + ] + + t.test('Initialization', function (st) { + for (const txType of txTypes) { + let tx = txType.class.fromTxData({}) + st.equal( + tx.common.hardfork(), + 'berlin', + `${txType.name}: should initialize with correct default HF` + ) + st.ok(Object.isFrozen(tx), `${txType.name}: tx should be frozen by default`) + + const common = new Common({ + chain: 'mainnet', + hardfork: 'istanbul', + eips: [2718, 2929, 2930], + }) + tx = txType.class.fromTxData({}, { common }) + st.equal( + tx.common.hardfork(), + 'istanbul', + `${txType.name}: should initialize with correct HF provided` + ) + + common.setHardfork('byzantium') + st.equal( + tx.common.hardfork(), + 'istanbul', + `${txType.name}: should stay on correct HF if outer common HF changes` + ) + + tx = txType.class.fromTxData({}, { freeze: false }) + tx = txType.class.fromTxData({}, { freeze: false }) + st.ok( + !Object.isFrozen(tx), + `${txType.name}: tx should not be frozen when freeze deactivated in options` + ) + + // Perform the same test as above, but now using a different construction method. This also implies that passing on the + // options object works as expected. + tx = txType.class.fromTxData({}, { freeze: false }) + const rlpData = tx.serialize() + + tx = txType.class.fromRlpSerializedTx(rlpData) + st.ok(Object.isFrozen(tx), `${txType.name}: tx should be frozen by default`) + + tx = txType.class.fromRlpSerializedTx(rlpData, { freeze: false }) + st.ok( + !Object.isFrozen(tx), + `${txType.name}: tx should not be frozen when freeze deactivated in options` + ) + + tx = txType.class.fromValuesArray(txType.values) + st.ok(Object.isFrozen(tx), `${txType.name}: tx should be frozen by default`) + + tx = txType.class.fromValuesArray(txType.values, { freeze: false }) + st.ok( + !Object.isFrozen(tx), + `${txType.name}: tx should not be frozen when freeze deactivated in options` + ) + } + st.end() + }) + + t.test('serialize()', function (st) { + for (const txType of txTypes) { + txType.txs.forEach(function (tx: any) { + st.ok( + txType.class.fromRlpSerializedTx(tx.serialize()), + `${txType.name}: should do roundtrip serialize() -> fromRlpSerializedTx()` + ) + }) + } + st.end() + }) + + t.test('raw()', function (st) { + for (const txType of txTypes) { + txType.txs.forEach(function (tx: any) { + st.ok( + txType.class.fromValuesArray(tx.raw(true)), + `${txType.name}: should do roundtrip raw() -> fromValuesArray()` + ) + }) + } + st.end() + }) + + t.test('verifySignature()', function (st) { + for (const txType of txTypes) { + txType.txs.forEach(function (tx: any) { + st.equals(tx.verifySignature(), true, `${txType.name}: signature should be valid`) + }) + } + st.end() + }) + + t.test('verifySignature() -> invalid', function (st) { + for (const txType of txTypes) { + txType.fixtures.slice(0, 4).forEach(function (txFixture: any) { + // set `s` to zero + txFixture.data.s = `0x` + '0'.repeat(16) + const tx = txType.class.fromTxData(txFixture.data) + st.equals(tx.verifySignature(), false, `${txType.name}: signature should not be valid`) + st.ok( + (tx.validate(true)).includes('Invalid Signature'), + `${txType.name}: should return an error string about not verifying signatures` + ) + st.notOk(tx.validate(), `${txType.name}: should not validate correctly`) + }) + } + + st.end() + }) +}) diff --git a/packages/tx/test/eip2930.spec.ts b/packages/tx/test/eip2930.spec.ts index 33a2c7574bb..3ea53ad24c0 100644 --- a/packages/tx/test/eip2930.spec.ts +++ b/packages/tx/test/eip2930.spec.ts @@ -17,7 +17,28 @@ const validSlot = Buffer.from('01'.repeat(32), 'hex') const chainId = new BN(1) -tape('[EIP2930 transactions]: Basic functions', function (t) { +tape('[EIP2930Transaction]', function (t) { + t.test('Initialization / Getter', function (t) { + t.throws(() => { + EIP2930Transaction.fromTxData( + { + chainId: chainId.addn(1), + }, + { common } + ) + }, 'should reject transactions with wrong chain ID') + + t.throws(() => { + EIP2930Transaction.fromTxData( + { + v: 2, + }, + { common } + ) + }, 'should reject transactions with invalid yParity (v) values') + t.end() + }) + t.test('should allow json-typed access lists', function (st) { const access: AccessList = [ { @@ -204,30 +225,6 @@ tape('[EIP2930 transactions]: Basic functions', function (t) { t.end() }) - t.test('should reject transactions with wrong chain ID', function (t) { - t.throws(() => { - EIP2930Transaction.fromTxData( - { - chainId: chainId.addn(1), - }, - { common } - ) - }) - t.end() - }) - - t.test('should reject transactions with invalid yParity (v) values', function (t) { - t.throws(() => { - EIP2930Transaction.fromTxData( - { - v: 2, - }, - { common } - ) - }) - t.end() - }) - // Data from // https://github.com/INFURA/go-ethlibs/blob/75b2a52a39d353ed8206cffaf68d09bd1b154aae/eth/transaction_signing_test.go#L87 diff --git a/packages/tx/test/index.ts b/packages/tx/test/index.ts index f2fcfd22726..6b0f37c6ec0 100644 --- a/packages/tx/test/index.ts +++ b/packages/tx/test/index.ts @@ -2,13 +2,26 @@ import minimist from 'minimist' const argv = minimist(process.argv.slice(2)) -if (argv.a) { - require('./api.spec') +if (argv.b) { + require('./base.spec') +} else if (argv.l) { + require('./legacy.spec') +} else if (argv.e) { + require('./eip2930.spec') } else if (argv.t) { require('./transactionRunner') +} else if (argv.f) { + require('./transactionFactory.spec') +} else if (argv.a) { + // All manual API tests + require('./base.spec') + require('./legacy.spec') + require('./eip2930.spec') + require('./transactionFactory.spec') } else { - require('./api.spec') require('./transactionRunner') - require('./transactionFactory.spec') + require('./base.spec') + require('./legacy.spec') require('./eip2930.spec') + require('./transactionFactory.spec') } diff --git a/packages/tx/test/json/eip2930txs.json b/packages/tx/test/json/eip2930txs.json new file mode 100644 index 00000000000..e0fcc6dc857 --- /dev/null +++ b/packages/tx/test/json/eip2930txs.json @@ -0,0 +1,51 @@ +[ + { + "privateKey": "e0a462586887362a18a318b128dbc1e3a0cae6d4b0739f5d0419ec25114bc722", + "sendersAddress": "d13d825eb15c87b247c4c26331d66f225a5f632e", + "type": "message", + "raw": [ + "0x01", + "0x", + "0x01", + "0x02625a00", + "0xcccccccccccccccccccccccccccccccccccccccc", + "0x0186a0", + "0x1a8451e600000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + [ + [ + "0x0000000000000000000000000000000000000101", + [ + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x00000000000000000000000000000000000000000000000000000000000060a7" + ] + ] + ], + "0x01", + "0xafb6e247b1c490e284053c87ab5f6b59e219d51f743f7a4d83e400782bc7e4b9", + "0x479a268e0e0acd4de3f1e28e4fac2a6b32a4195e8dfa9d19147abe8807aa6f64" + ], + "data": { + "data": "0x1a8451e600000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "gasLimit": "0x02625a00", + "gasPrice": "0x01", + "nonce": "0x00", + "to": "0xcccccccccccccccccccccccccccccccccccccccc", + "value": "0x0186a0", + "v": "0x01", + "r": "0xafb6e247b1c490e284053c87ab5f6b59e219d51f743f7a4d83e400782bc7e4b9", + "s": "0x479a268e0e0acd4de3f1e28e4fac2a6b32a4195e8dfa9d19147abe8807aa6f64", + "chainId": "0x01", + "accessList": [ + { + "address": "0x0000000000000000000000000000000000000101", + "storageKeys": [ + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x00000000000000000000000000000000000000000000000000000000000060a7" + ] + } + ], + "type": "0x01" + } + } + +] \ No newline at end of file diff --git a/packages/tx/test/json/txs.json b/packages/tx/test/json/txs.json index 6e913a5e8f1..5b3aff00b77 100644 --- a/packages/tx/test/json/txs.json +++ b/packages/tx/test/json/txs.json @@ -14,7 +14,18 @@ "0x1c", "0x5e1d3a76fbf824220eafc8c79ad578ad2b67d01b0c2425eb1f1347e8f50882ab", "0x5bd428537f05f9830e93792f90ea6a3e2d1ee84952dd96edbae9f658f831ab13" - ] + ], + "data": { + "nonce": "0x", + "gasPrice": "0x09184e72a000", + "gasLimit": "0x2710", + "to": "0x0000000000000000000000000000000000000000", + "value": "0x", + "data": "0x7f7465737432000000000000000000000000000000000000000000000000000000600057", + "v": "0x1c", + "r": "0x5e1d3a76fbf824220eafc8c79ad578ad2b67d01b0c2425eb1f1347e8f50882ab", + "s": "0x5bd428537f05f9830e93792f90ea6a3e2d1ee84952dd96edbae9f658f831ab13" + } }, { "privateKey": "4646464646464646464646464646464646464646464646464646464646464646", @@ -31,7 +42,18 @@ "0x25", "0x28ef61340bd939bc2195fe537567866003e1a15d3c71ff63e1590620aa636276", "0x67cbe9d8997f761aecb703304b3800ccf555c9f3dc64214b297fb1966a3b6d83" - ] + ], + "data": { + "nonce": "0x09", + "gasPrice": "0x04a817c800", + "gasLimit": "0x2710", + "to": "0x3535353535353535353535353535353535353535", + "value": "0x0de0b6b3a7640000", + "data": "0x", + "v": "0x25", + "r": "0x28ef61340bd939bc2195fe537567866003e1a15d3c71ff63e1590620aa636276", + "s": "0x67cbe9d8997f761aecb703304b3800ccf555c9f3dc64214b297fb1966a3b6d83" + } }, { "privateKey": "e0a462586887362a18a318b128dbc1e3a0cae6d4b0739f5d0419ec25114bc722", @@ -48,7 +70,18 @@ "0x1c", "0x24a484bfa7380860e9fa0a9f5e4b64b985e860ca31abd36e66583f9030c2e29d", "0x4d5ef07d9e73fa2fbfdad059591b4f13d0aa79e7634a2bb00174c9200cabb04d" - ] + ], + "data": { + "nonce": "0x06", + "gasPrice": "0x09184e72a000", + "gasLimit": "0x01f4", + "to": "0xbe862ad9abfe6f22bcb087716c7d89a26051f74c", + "value": "0x016345785d8a0000", + "data": "0x", + "v": "0x1c", + "r": "0x24a484bfa7380860e9fa0a9f5e4b64b985e860ca31abd36e66583f9030c2e29d", + "s": "0x4d5ef07d9e73fa2fbfdad059591b4f13d0aa79e7634a2bb00174c9200cabb04d" + } }, { "privateKey": "164122e5d39e9814ca723a749253663bafb07f6af91704d9754c361eb315f0c1", @@ -65,7 +98,18 @@ "0x1c", "0x5e9361ca27e14f3af0e6b28466406ad8be026d3b0f2ae56e3c064043fb73ec77", "0x29ae9893dac4f9afb1af743e25fbb6a63f7879a61437203cb48c997b0fcefc3a" - ] + ], + "data": { + "nonce": "0x06", + "gasPrice": "0x09184e72a000", + "gasLimit": "0x0974", + "to": "0xbe862ad9abfe6f22bcb087716c7d89a26051f74c", + "value": "0x016345785d8a0000", + "data": "0x00000000000000000000000000000000000000000000000000000000000000ad000000000000000000000000000000000000000000000000000000000000fafa0000000000000000000000000000000000000000000000000000000000000dfa0000000000000000000000000000000000000000000000000000000000000dfa00000000000000000000000000000000000000000000000000000000000000ad000000000000000000000000000000000000000000000000000000000000000f000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000df000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000df000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000d", + "v": "0x1c", + "r": "0x5e9361ca27e14f3af0e6b28466406ad8be026d3b0f2ae56e3c064043fb73ec77", + "s": "0x29ae9893dac4f9afb1af743e25fbb6a63f7879a61437203cb48c997b0fcefc3a" + } }, { "privateKey": "not-available", @@ -82,6 +126,17 @@ "0x26", "0xef903f6bbcb7d6214d478df27db6591d857b1063954eade1bb24e69e58511f96", "0x5433f8e1abf886cbec64891f38a2ea6fd9f9ffe078421f5e238b9fec03eea97a" - ] + ], + "data": { + "nonce": "0x0b", + "gasPrice": "0x051f4d5c00", + "gasLimit": "0x5208", + "to": "0x656e929d6fc0cac52d3d9526d288fe02dcd56fbd", + "value": "0x2386f26fc10000", + "data": "0x", + "v": "0x26", + "r": "0xef903f6bbcb7d6214d478df27db6591d857b1063954eade1bb24e69e58511f96", + "s": "0x5433f8e1abf886cbec64891f38a2ea6fd9f9ffe078421f5e238b9fec03eea97a" + } } ] From 7d12beb6df72f706fcc02130886ca44d270ad129 Mon Sep 17 00:00:00 2001 From: holgerd77 Date: Fri, 26 Feb 2021 20:47:49 +0100 Subject: [PATCH 29/32] tx -> EIP-2930: added generic sign(), getSenderAddress(), getSenderPublicKey(), verifySignature() tests --- packages/tx/test/api.spec.ts | 47 +-------------------------- packages/tx/test/base.spec.ts | 60 +++++++++++++++++++++++++++++++++++ 2 files changed, 61 insertions(+), 46 deletions(-) diff --git a/packages/tx/test/api.spec.ts b/packages/tx/test/api.spec.ts index c6ea0340f0b..314a9b004cb 100644 --- a/packages/tx/test/api.spec.ts +++ b/packages/tx/test/api.spec.ts @@ -1,6 +1,6 @@ import tape from 'tape' import { Buffer } from 'buffer' -import { BN, rlp, privateToPublic, toBuffer, bufferToHex, unpadBuffer } from 'ethereumjs-util' +import { BN, rlp, toBuffer, bufferToHex, unpadBuffer } from 'ethereumjs-util' import Common from '@ethereumjs/common' import { LegacyTransaction, TxData } from '../src' import { TxsJsonEntry, VitaliksTestsDataEntry } from './types' @@ -80,51 +80,6 @@ tape('[Transaction]', function (t) { st.end() }) - t.test('sign()', function (st) { - transactions.forEach(function (tx, i) { - const { privateKey } = txFixtures[i] - if (privateKey) { - st.ok(tx.sign(Buffer.from(privateKey, 'hex')), 'should sign tx') - } - }) - st.end() - }) - - t.test("should get sender's address after signing it", function (st) { - transactions.forEach(function (tx, i) { - const { privateKey, sendersAddress } = txFixtures[i] - if (privateKey) { - const signedTx = tx.sign(Buffer.from(privateKey, 'hex')) - st.equals(signedTx.getSenderAddress().toString(), '0x' + sendersAddress) - } - }) - st.end() - }) - - t.test("should get sender's public key after signing it", function (st) { - transactions.forEach(function (tx, i) { - const { privateKey } = txFixtures[i] - if (privateKey) { - const signedTx = tx.sign(Buffer.from(privateKey, 'hex')) - const txPubKey = signedTx.getSenderPublicKey() - const pubKeyFromPriv = privateToPublic(Buffer.from(privateKey, 'hex')) - st.ok(txPubKey.equals(pubKeyFromPriv)) - } - }) - st.end() - }) - - t.test('should verify signing it', function (st) { - transactions.forEach(function (tx, i) { - const { privateKey } = txFixtures[i] - if (privateKey) { - const signedTx = tx.sign(Buffer.from(privateKey, 'hex')) - st.ok(signedTx.verifySignature()) - } - }) - st.end() - }) - t.test('should validate with string option', function (st) { transactions.forEach(function (tx) { st.ok(typeof tx.validate(true)[0] === 'string') diff --git a/packages/tx/test/base.spec.ts b/packages/tx/test/base.spec.ts index a1c17cd1621..54fed02cb87 100644 --- a/packages/tx/test/base.spec.ts +++ b/packages/tx/test/base.spec.ts @@ -3,6 +3,7 @@ import Common from '@ethereumjs/common' import { LegacyTransaction, EIP2930Transaction } from '../src' import { TxsJsonEntry } from './types' import { BaseTransaction } from '../src/baseTransaction' +import { privateToPublic } from 'ethereumjs-util' tape('[BaseTransaction]', function (t) { const legacyFixtures: TxsJsonEntry[] = require('./json/txs.json') @@ -144,7 +145,66 @@ tape('[BaseTransaction]', function (t) { st.notOk(tx.validate(), `${txType.name}: should not validate correctly`) }) } + st.end() + }) + + t.test('sign()', function (st) { + for (const txType of txTypes) { + txType.txs.forEach(function (tx: any, i: number) { + const { privateKey } = txType.fixtures[i] + if (privateKey) { + st.ok(tx.sign(Buffer.from(privateKey, 'hex')), `${txType.name}: should sign tx`) + } + }) + } + st.end() + }) + + t.test('getSenderAddress()', function (st) { + for (const txType of txTypes) { + txType.txs.forEach(function (tx: any, i: number) { + const { privateKey, sendersAddress } = txType.fixtures[i] + if (privateKey) { + const signedTx = tx.sign(Buffer.from(privateKey, 'hex')) + st.equals( + signedTx.getSenderAddress().toString(), + `0x${sendersAddress}`, + `${txType.name}: should get sender's address after signing it` + ) + } + }) + } + st.end() + }) + + t.test('getSenderPublicKey()', function (st) { + for (const txType of txTypes) { + txType.txs.forEach(function (tx: any, i: number) { + const { privateKey } = txType.fixtures[i] + if (privateKey) { + const signedTx = tx.sign(Buffer.from(privateKey, 'hex')) + const txPubKey = signedTx.getSenderPublicKey() + const pubKeyFromPriv = privateToPublic(Buffer.from(privateKey, 'hex')) + st.ok( + txPubKey.equals(pubKeyFromPriv), + `${txType.name}: should get sender's public key after signing it` + ) + } + }) + } + st.end() + }) + t.test('verifySignature()', function (st) { + for (const txType of txTypes) { + txType.txs.forEach(function (tx: any, i: number) { + const { privateKey } = txType.fixtures[i] + if (privateKey) { + const signedTx = tx.sign(Buffer.from(privateKey, 'hex')) + st.ok(signedTx.verifySignature(), `${txType.name}: should verify signing it`) + } + }) + } st.end() }) }) From 861d1c0bc3ed654e68b5e846bb4998624e55d109 Mon Sep 17 00:00:00 2001 From: Jochem Brouwer Date: Fri, 26 Feb 2021 21:54:20 +0100 Subject: [PATCH 30/32] tx: increase test coverage --- packages/tx/src/transactionFactory.ts | 3 -- packages/tx/test/eip2930.spec.ts | 20 +++++++++- packages/tx/test/transactionFactory.spec.ts | 41 +++++++++++++++++++++ 3 files changed, 60 insertions(+), 4 deletions(-) diff --git a/packages/tx/src/transactionFactory.ts b/packages/tx/src/transactionFactory.ts index 37452dcf9dc..cf04a44098b 100644 --- a/packages/tx/src/transactionFactory.ts +++ b/packages/tx/src/transactionFactory.ts @@ -21,9 +21,6 @@ export default class TransactionFactory { // Assume LegacyTransaction return LegacyTransaction.fromTxData(txData, txOptions) } else { - if (!common.isActivatedEIP(2718)) { - throw new Error('Common support for TypedTransactions (EIP-2718) not activated') - } const txType = new BN(txData.type).toNumber() return TransactionFactory.getTransactionClass(txType, common).fromTxData(txData, txOptions) } diff --git a/packages/tx/test/eip2930.spec.ts b/packages/tx/test/eip2930.spec.ts index 3ea53ad24c0..df9f65019b2 100644 --- a/packages/tx/test/eip2930.spec.ts +++ b/packages/tx/test/eip2930.spec.ts @@ -228,7 +228,7 @@ tape('[EIP2930Transaction]', function (t) { // Data from // https://github.com/INFURA/go-ethlibs/blob/75b2a52a39d353ed8206cffaf68d09bd1b154aae/eth/transaction_signing_test.go#L87 - t.test('should sign transaction correctly', function (t) { + t.test('should sign transaction correctly and return expected JSON', function (t) { const address = Buffer.from('0000000000000000000000000000000000001337', 'hex') const slot1 = Buffer.from( '0000000000000000000000000000000000000000000000000000000000000000', @@ -291,6 +291,24 @@ tape('[EIP2930Transaction]', function (t) { t.ok(expectedSigned.equals(signed.serialize()), 'serialized signed message correct') t.ok(expectedHash.equals(signed.hash()), 'hash correct') + const expectedJSON = { + chainId: '0x796f6c6f763378', + nonce: '0x0', + gasPrice: '0x3b9aca00', + gasLimit: '0x62d4', + to: '0xdf0a88b2b68c673713a8ec826003676f272e3573', + value: '0x1', + data: '0x', + accessList: [ + { + address: '0x0000000000000000000000000000000000001337', + storageKeys: ['0x0000000000000000000000000000000000000000000000000000000000000000'], + }, + ], + } + + t.deepEqual(signed.toJSON(), expectedJSON) + t.end() }) }) diff --git a/packages/tx/test/transactionFactory.spec.ts b/packages/tx/test/transactionFactory.spec.ts index 2609d9935f0..ee79232e86e 100644 --- a/packages/tx/test/transactionFactory.spec.ts +++ b/packages/tx/test/transactionFactory.spec.ts @@ -13,11 +13,18 @@ const EIP2930Common = new Common({ hardfork: 'berlin', }) +const pKey = Buffer.from('4646464646464646464646464646464646464646464646464646464646464646', 'hex') + const simpleUnsignedEIP2930Transaction = EIP2930Transaction.fromTxData( { chainId: new BN(1) }, { common: EIP2930Common } ) +const simpleUnsignedLegacyTransaction = LegacyTransaction.fromTxData({}) + +const simpleSignedEIP2930Transaction = simpleUnsignedEIP2930Transaction.sign(pKey) +const simpleSignedLegacyTransaction = simpleUnsignedLegacyTransaction.sign(pKey) + tape('[TransactionFactory]: Basic functions', function (t) { t.test('should return the right type', function (st) { const serialized = simpleUnsignedEIP2930Transaction.serialize() @@ -78,4 +85,38 @@ tape('[TransactionFactory]: Basic functions', function (t) { }) st.end() }) + + t.test('should decode raw block body data', function (st) { + const rawLegacy = simpleSignedLegacyTransaction.raw() + const rawEIP2930 = simpleSignedEIP2930Transaction.raw() + + const legacyTx = TransactionFactory.fromBlockBodyData(rawLegacy) + const eip2930Tx = TransactionFactory.fromBlockBodyData(rawEIP2930, { common: EIP2930Common }) + + st.equals(legacyTx.constructor.name, LegacyTransaction.name) + st.equals(eip2930Tx.constructor.name, EIP2930Transaction.name) + st.end() + }) + + t.test('should create the right transaction types from tx data', function (st) { + const legacyTx = TransactionFactory.fromTxData({ type: 0 }) + const legacyTx2 = TransactionFactory.fromTxData({}) + const eip2930Tx = TransactionFactory.fromTxData({ type: 1 }, { common: EIP2930Common }) + st.throws(() => { + TransactionFactory.fromTxData({ type: 1 }) + }) + + st.equals(legacyTx.constructor.name, LegacyTransaction.name) + st.equals(legacyTx2.constructor.name, LegacyTransaction.name) + st.equals(eip2930Tx.constructor.name, EIP2930Transaction.name) + st.end() + }) + + t.test('if eip2718 is not activated, always return that the eip is not activated', function (st) { + const newCommon = new Common({ chain: 'mainnet', hardfork: 'istanbul' }) + + const eip2930Active = TransactionFactory.eipSupport(newCommon, 2930) + st.ok(!eip2930Active) + st.end() + }) }) From be27b7118a20a286e175ca868b4fc01f2b6052ff Mon Sep 17 00:00:00 2001 From: Jochem Brouwer Date: Fri, 26 Feb 2021 22:15:04 +0100 Subject: [PATCH 31/32] tx: simplify getMessageToSign tx: add fromSerializedTx method tx: remove TODOs tx: add fromSerializedTx to legacy transaction --- packages/tx/src/eip2930Transaction.ts | 24 +++++++++--------------- packages/tx/src/legacyTransaction.ts | 5 +++++ packages/tx/test/base.spec.ts | 4 ++++ 3 files changed, 18 insertions(+), 15 deletions(-) diff --git a/packages/tx/src/eip2930Transaction.ts b/packages/tx/src/eip2930Transaction.ts index ef0fcdc2083..355a211fd00 100644 --- a/packages/tx/src/eip2930Transaction.ts +++ b/packages/tx/src/eip2930Transaction.ts @@ -71,8 +71,8 @@ export default class EIP2930Transaction extends BaseTransaction fromRlpSerializedTx()` ) + st.ok( + txType.class.fromSerializedTx(tx.serialize()), + `${txType.name}: should do roundtrip serialize() -> fromRlpSerializedTx()` + ) }) } st.end() From 9c6f2f690ab76bc88abf367bf7570db3f7453518 Mon Sep 17 00:00:00 2001 From: Jochem Brouwer Date: Mon, 1 Mar 2021 22:25:44 +0100 Subject: [PATCH 32/32] monorepo: reset ethereum-tests to develop tx: move api test to legacy test tx: lint --- .gitmodules | 2 +- packages/tx/src/legacyTransaction.ts | 3 --- packages/tx/src/transactionFactory.ts | 1 - packages/tx/test/{api.spec.ts => legacy.spec.ts} | 0 packages/tx/test/transactionFactory.spec.ts | 6 +----- 5 files changed, 2 insertions(+), 10 deletions(-) rename packages/tx/test/{api.spec.ts => legacy.spec.ts} (100%) diff --git a/.gitmodules b/.gitmodules index 928adbbe6a6..22a3a91624a 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,4 +1,4 @@ [submodule "ethereum-tests"] path = packages/ethereum-tests url = https://github.com/qbzzt/tests.git - branch = eip2930 + branch = develop diff --git a/packages/tx/src/legacyTransaction.ts b/packages/tx/src/legacyTransaction.ts index b794d3eb070..e88ed36a802 100644 --- a/packages/tx/src/legacyTransaction.ts +++ b/packages/tx/src/legacyTransaction.ts @@ -7,13 +7,10 @@ import { bnToHex, bnToRlp, ecrecover, - ecsign, rlp, rlphash, toBuffer, unpadBuffer, - publicToAddress, - MAX_INTEGER, } from 'ethereumjs-util' import { TxOptions, TxData, JsonTx } from './types' import { BaseTransaction } from './baseTransaction' diff --git a/packages/tx/src/transactionFactory.ts b/packages/tx/src/transactionFactory.ts index cf04a44098b..aa761a4f9ac 100644 --- a/packages/tx/src/transactionFactory.ts +++ b/packages/tx/src/transactionFactory.ts @@ -106,7 +106,6 @@ export default class TransactionFactory { default: throw new Error(`TypedTransaction with ID ${transactionID} unknown`) } - throw new Error(`TypedTransaction with ID ${transactionID} unknown`) } /** diff --git a/packages/tx/test/api.spec.ts b/packages/tx/test/legacy.spec.ts similarity index 100% rename from packages/tx/test/api.spec.ts rename to packages/tx/test/legacy.spec.ts diff --git a/packages/tx/test/transactionFactory.spec.ts b/packages/tx/test/transactionFactory.spec.ts index ee79232e86e..56c6f606caf 100644 --- a/packages/tx/test/transactionFactory.spec.ts +++ b/packages/tx/test/transactionFactory.spec.ts @@ -1,11 +1,7 @@ import Common from '@ethereumjs/common' import { BN } from 'ethereumjs-util' import tape from 'tape' -import { - EIP2930Transaction, - TransactionFactory, - LegacyTransaction -} from '../src' +import { EIP2930Transaction, TransactionFactory, LegacyTransaction } from '../src' const EIP2930Common = new Common({ eips: [2718, 2929, 2930],