diff --git a/.gitmodules b/.gitmodules index e6b8bc1676..22a3a91624 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,4 +1,4 @@ [submodule "ethereum-tests"] path = packages/ethereum-tests - url = https://github.com/ethereum/tests.git + url = https://github.com/qbzzt/tests.git branch = develop diff --git a/packages/block/CHANGELOG.md b/packages/block/CHANGELOG.md index 22455fba3a..ed2a52964e 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/block/src/block.ts b/packages/block/src/block.ts index 912e0e8e78..f710502f34 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, @@ -91,7 +91,7 @@ export class Block { const transactions = [] for (const txData of txsData || []) { transactions.push( - Transaction.fromValuesArray(txData, { + TransactionFactory.fromBlockBodyData(txData, { ...opts, // Use header common in case of hardforkByBlockNumber being activated common: header._common, @@ -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()), ] } @@ -223,7 +223,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 60bd049537..27b4dec518 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) } } diff --git a/packages/common/src/eips/2718.json b/packages/common/src/eips/2718.json new file mode 100644 index 0000000000..e0eb26850e --- /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 0000000000..dc4c34c402 --- /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": "istanbul", + "requiredEIPs": [2718, 2929], + "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" + } + }, + "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 ef24089b43..3911fd25b6 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/hardforks/berlin.json b/packages/common/src/hardforks/berlin.json index abe3fc79ee..14c80c0c62 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 70951e78a4..5a9a51ad6a 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) || this.isActivatedEIP(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 4a77bce7ff..9aa3aa244b 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() }) diff --git a/packages/ethereum-tests b/packages/ethereum-tests index 1508126ea0..0966f56536 160000 --- a/packages/ethereum-tests +++ b/packages/ethereum-tests @@ -1 +1 @@ -Subproject commit 1508126ea04cd61495b60db2f036ac823de274b1 +Subproject commit 0966f56536511200326e5ca939385da462730240 diff --git a/packages/tx/CHANGELOG.md b/packages/tx/CHANGELOG.md index f5867b56af..614daec8c3 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/karma.conf.js b/packages/tx/karma.conf.js index 413873c103..a577e868fc 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 new file mode 100644 index 0000000000..f7f0691c67 --- /dev/null +++ b/packages/tx/src/baseTransaction.ts @@ -0,0 +1,195 @@ +import Common from '@ethereumjs/common' +import { + Address, + BN, + toBuffer, + MAX_INTEGER, + unpadBuffer, + ecsign, + publicToAddress, +} from 'ethereumjs-util' +import { BaseTransactionData, BaseTxOptions, DEFAULT_COMMON, JsonTx } from './types' + +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 { 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.validateExceedsMaxInteger(validateCannotExceedMaxInteger) + + this.common = + (txOptions.common && + Object.assign(Object.create(Object.getPrototypeOf(txOptions.common)), txOptions.common)) ?? + DEFAULT_COMMON + } + + 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}`) + } + } + } + + /** + * 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() + } + + /** + * 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) + } + + /** + * Checks if the transaction has the minimum amount of gas required + * (DataFee + TxFee + Creation Fee). + */ + /** + * Checks if the transaction has the minimum amount of gas required + * (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 = [] + + 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. + */ + abstract serialize(): Buffer + + /** + * Returns an object with the JSON representation of the transaction + */ + abstract toJSON(): JsonTx + + abstract isSigned(): boolean + + /** + * 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 + } + } + + /** + * 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 + + abstract getMessageToVerifySignature(): Buffer + /** + * Returns the sender's address + */ + getSenderAddress(): Address { + return new Address(publicToAddress(this.getSenderPublicKey())) + } + abstract getSenderPublicKey(): Buffer + + sign(privateKey: Buffer): TransactionObject { + 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) + + return this.processSignature(v, r, s) + } + + // 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 new file mode 100644 index 0000000000..355a211fd0 --- /dev/null +++ b/packages/tx/src/eip2930Transaction.ts @@ -0,0 +1,385 @@ +import { + Address, + BN, + bnToHex, + bnToRlp, + bufferToHex, + ecrecover, + keccak256, + rlp, + setLengthLeft, + toBuffer, +} from 'ethereumjs-util' +import { BaseTransaction } from './baseTransaction' +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: AccessListBuffer + public readonly v?: BN + public readonly r?: BN + public readonly s?: BN + + get transactionType(): number { + return 1 + } + + public readonly AccessListJSON: AccessList + + // EIP-2930 alias for `s` + get senderS() { + return this.s + } + + // EIP-2930 alias for `r` + get senderR() { + return this.r + } + + // EIP-2930 alias for `v` + + get yParity() { + return this.v + } + + public static fromTxData(txData: TxData, opts: TxOptions = {}) { + return new EIP2930Transaction(txData, opts) + } + + // Instantiate a transaction from the serialized tx. This means that the Buffer should start with 0x01. + public static fromSerializedTx(serialized: Buffer, opts: TxOptions = {}) { + if (serialized[0] !== 1) { + throw new Error( + `Invalid serialized tx input: not an EIP-2930 transaction (wrong tx type, expected: 1, received: ${serialized[0]}` + ) + } + + const values = rlp.decode(serialized.slice(1)) + if (!Array.isArray(values)) { + throw new Error('Invalid serialized tx input: must be array') + } + + return EIP2930Transaction.fromValuesArray(values, opts) + } + + // Instantiate a transaction from the serialized tx. This means that the Buffer should start with 0x01. + // Alias of fromSerializedTx + public static fromRlpSerializedTx(serialized: Buffer, opts: TxOptions = {}) { + return EIP2930Transaction.fromSerializedTx(serialized, 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 | AccessListBuffer)[], opts: TxOptions = {}) { + if (values.length == 8 || values.length == 11) { + const [chainId, nonce, gasPrice, gasLimit, to, value, data, accessList, v, r, s] = < + EIP2930ValuesArray + >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, + v: v !== undefined ? new BN(v) : undefined, // EIP2930 supports v's with value 0 (empty Buffer) + 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 EIP-2930 transaction. Only expecting 8 values (for unsigned tx) or 11 values (for signed tx).' + ) + } + } + + public constructor(txData: TxData, opts: TxOptions = {}) { + const { chainId, nonce, gasPrice, gasLimit, to, value, data, accessList, v, r, s } = txData + + super({ nonce, gasPrice, gasLimit, to, value, data }, opts) + + // EIP-2718 check is done in Common + if (!this.common.isActivatedEIP(2930)) { + throw new Error('EIP-2930 not enabled on Common') + } + + // check the type of AccessList. If it's a JSON-type, we have to convert it to a buffer. + + let usedAccessList + if (accessList && isAccessList(accessList)) { + this.AccessListJSON = accessList + + const newAccessList: AccessListBuffer = [] + + for (let i = 0; i < accessList.length; i++) { + const item: AccessListItem = accessList[i] + //const addItem: AccessListBufferItem = [] + const addressBuffer = toBuffer(item.address) + const storageItems: Buffer[] = [] + for (let index = 0; index < item.storageKeys.length; index++) { + storageItems.push(toBuffer(item.storageKeys[index])) + } + newAccessList.push([addressBuffer, storageItems]) + } + usedAccessList = newAccessList + } else { + usedAccessList = accessList ?? [] + // build the JSON + const json: AccessList = [] + for (let i = 0; i < usedAccessList.length; i++) { + const data = usedAccessList[i] + const address = bufferToHex(data[0]) + const storageKeys: string[] = [] + for (let item = 0; item < data[1].length; item++) { + storageKeys.push(bufferToHex(data[1][item])) + } + const jsonItem: AccessListItem = { + address, + storageKeys, + } + json.push(jsonItem) + } + this.AccessListJSON = json + } + + this.chainId = chainId ? new BN(toBuffer(chainId)) : new BN(this.common.chainId()) + this.accessList = usedAccessList + this.v = v ? new BN(toBuffer(v)) : undefined + this.r = r ? new BN(toBuffer(r)) : undefined + this.s = s ? new BN(toBuffer(s)) : undefined + + if (!this.chainId.eq(new BN(this.common.chainId().toString()))) { + throw new Error('The chain ID does not match the chain ID of Common') + } + + if (this.v && !this.v.eqn(0) && !this.v.eqn(1)) { + throw new Error('The y-parity of the transaction should either be 0 or 1') + } + + 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' + ) + } + + // Verify the access list format. + for (let key = 0; key < this.accessList.length; key++) { + const accessListItem = this.accessList[key] + const address = accessListItem[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.' + ) + } + 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) + } + } + + getMessageToSign() { + const base = this.raw(true).slice(0, 8) + return keccak256(Buffer.concat([Buffer.from('01', 'hex'), rlp.encode(base)])) + } + + /** + * The amount of gas paid for the data in this tx + */ + getDataFee(): BN { + const cost = super.getDataFee() + const accessListStorageKeyCost = this.common.param('gasPrices', 'accessListStorageKeyCost') + const accessListAddressCost = this.common.param('gasPrices', 'accessListAddressCost') + + 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.iaddn(addresses * accessListAddressCost + slots * accessListStorageKeyCost) + return cost + } + + /** + * Returns a Buffer Array of the raw Buffers of this transaction, in order. + * @param asList - By default, this method returns a concatenated Buffer + * If this is not desired, then set this to `true`, to get a Buffer array. + */ + raw(asList = false): Buffer[] | Buffer { + const base = [ + bnToRlp(this.chainId), + bnToRlp(this.nonce), + bnToRlp(this.gasPrice), + bnToRlp(this.gasLimit), + this.to !== undefined ? this.to.buf : Buffer.from([]), + bnToRlp(this.value), + this.data, + this.accessList, + this.v !== undefined ? bnToRlp(this.v) : Buffer.from([]), + this.r !== undefined ? bnToRlp(this.r) : Buffer.from([]), + this.s !== undefined ? bnToRlp(this.s) : Buffer.from([]), + ] + if (!asList) { + return Buffer.concat([Buffer.from('01', 'hex'), rlp.encode(base)]) + } else { + return base + } + } + + /** + * Returns the encoding of the transaction. For typed transaction, this is the raw Buffer. + * In LegacyTransaction, this is a Buffer array. + */ + serialize(): Buffer { + return this.raw() + } + + /** + * Returns an object with the JSON representation of the transaction + */ + toJSON(): JsonTx { + const accessListJSON = [] + + for (let index = 0; index < this.accessList.length; index++) { + const item: any = this.accessList[index] + const JSONItem: any = { + address: '0x' + setLengthLeft(item[0], 20).toString('hex'), + storageKeys: [], + } + const storageSlots: Buffer[] = item[1] + for (let slot = 0; slot < storageSlots.length; slot++) { + const storageSlot = storageSlots[slot] + JSONItem.storageKeys.push('0x' + setLengthLeft(storageSlot, 32).toString('hex')) + } + 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, + } + } + + public isSigned(): boolean { + const { yParity, r, s } = this + return yParity !== undefined && !!r && !!s + } + + public hash(): Buffer { + if (!this.isSigned()) { + throw new Error('Cannot call hash method if transaction is not signed') + } + + return keccak256(this.serialize()) + } + + public getMessageToVerifySignature(): Buffer { + return this.getMessageToSign() + } + + public getSenderPublicKey(): Buffer { + if (!this.isSigned()) { + throw new Error('Cannot call this method if transaction is not signed') + } + + const msgHash = this.getMessageToVerifySignature() + + // All transaction signatures whose s-value is greater than secp256k1n/2 are considered invalid. + // TODO: verify if this is the case for EIP-2930 + 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 { yParity, r, s } = this + if (yParity === undefined || !r || !s) { + throw new Error('Missing values to derive sender public key from signed tx') + } + + try { + return ecrecover( + msgHash, + yParity.toNumber() + 27, // Recover the 27 which was stripped from ecsign + bnToRlp(r), + bnToRlp(s) + ) + } catch (e) { + throw new Error('Invalid Signature') + } + } + + processSignature(v: number, r: Buffer, s: Buffer) { + 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, + v: new BN(v - 27), // This looks extremely hacky: ethereumjs-util actually adds 27 to the value, the recovery bit is either 0 or 1. + r: new BN(r), + s: new BN(s), + }, + opts + ) + } +} diff --git a/packages/tx/src/index.ts b/packages/tx/src/index.ts index f9795e6046..7893213c62 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 55% rename from packages/tx/src/transaction.ts rename to packages/tx/src/legacyTransaction.ts index 2231294b44..e88ed36a80 100644 --- a/packages/tx/src/transaction.ts +++ b/packages/tx/src/legacyTransaction.ts @@ -7,16 +7,13 @@ import { bnToHex, bnToRlp, ecrecover, - ecsign, rlp, rlphash, toBuffer, unpadBuffer, - publicToAddress, - MAX_INTEGER, } from 'ethereumjs-util' -import Common from '@ethereumjs/common' import { TxOptions, TxData, JsonTx } from './types' +import { BaseTransaction } from './baseTransaction' // secp256k1n/2 const N_DIV_2 = new BN('7fffffffffffffffffffffffffffffff5d576e7357a4501ddfe92f46681b20a0', 16) @@ -24,23 +21,20 @@ const N_DIV_2 = new BN('7fffffffffffffffffffffffffffffff5d576e7357a4501ddfe92f46 /** * An Ethereum transaction. */ -export default class Transaction { - public readonly common: Common - public readonly nonce: BN - public readonly gasLimit: BN - public readonly gasPrice: BN - public readonly to?: Address - public readonly value: BN - public readonly data: Buffer +export default class LegacyTransaction extends BaseTransaction { public readonly v?: BN public readonly r?: BN public readonly s?: BN - public static fromTxData(txData: TxData, opts?: TxOptions) { - return new Transaction(txData, opts) + get transactionType(): number { + return 0 } - public static fromRlpSerializedTx(serialized: Buffer, opts?: TxOptions) { + public static fromTxData(txData: TxData, opts: TxOptions = {}) { + return new LegacyTransaction(txData, opts) + } + + public static fromRlpSerializedTx(serialized: Buffer, opts: TxOptions = {}) { const values = rlp.decode(serialized) if (!Array.isArray(values)) { @@ -50,31 +44,44 @@ export default class Transaction { return this.fromValuesArray(values, opts) } - public static fromValuesArray(values: Buffer[], opts?: TxOptions) { + // alias of fromRlpSerializedTx + public static fromSerializedTx(serialized: Buffer, opts: TxOptions = {}) { + return LegacyTransaction.fromRlpSerializedTx(serialized, opts) + } + + public static fromValuesArray(values: Buffer[], opts: TxOptions = {}) { if (values.length !== 6 && values.length !== 9) { throw new Error( 'Invalid transaction. Only expecting 6 values (for unsigned tx) or 9 values (for signed tx).' ) } - const [nonce, gasPrice, gasLimit, to, value, data, v, r, s] = values - - const emptyBuffer = Buffer.from([]) - - return new Transaction( - { - 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).' + ) + } } /** @@ -82,38 +89,24 @@ 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) { + public constructor(txData: TxData, opts: TxOptions = {}) { const { nonce, gasPrice, gasLimit, to, value, data, v, r, s } = 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) + super({ nonce, gasPrice, gasLimit, to, value, data }, opts) + 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, - } - for (const [key, value] of Object.entries(validateCannotExceedMaxInteger)) { - if (value && value.gt(MAX_INTEGER)) { - throw new Error(`${key} cannot exceed MAX_INTEGER, given ${value}`) - } + r: this.r ?? new BN(0), + s: this.s ?? new BN(0), } - 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.validateExceedsMaxInteger(validateCannotExceedMaxInteger) + + if (this.v) { + this._validateTxV(this.v) } this._validateTxV(this.v) @@ -125,10 +118,27 @@ export default class Transaction { } /** - * If the tx's `to` is to the creation address + * Returns the rlp encoding of the transaction. */ - toCreationAddress(): boolean { - return this.to === undefined || this.to.buf.length === 0 + serialize(): Buffer { + return rlp.encode(this.raw()) + } + + /** + * Returns a Buffer Array of the raw Buffers of this transaction, in order. + */ + raw(): Buffer[] { + return [ + bnToRlp(this.nonce), + bnToRlp(this.gasPrice), + bnToRlp(this.gasLimit), + this.to !== undefined ? this.to.buf : Buffer.from([]), + bnToRlp(this.value), + this.data, + this.v !== undefined ? bnToRlp(this.v) : Buffer.from([]), + this.r !== undefined ? bnToRlp(this.r) : Buffer.from([]), + this.s !== undefined ? bnToRlp(this.s) : Buffer.from([]), + ] } /** @@ -138,93 +148,59 @@ export default class Transaction { return rlphash(this.raw()) } - getMessageToSign() { - return this._getMessageToSign(this._unsignedTxImplementsEIP155()) - } - - getMessageToVerifySignature() { - return this._getMessageToSign(this._signedTxImplementsEIP155()) - } - /** - * Returns chain ID + * Returns an object with the JSON representation of the transaction */ - getChainId(): number { - return this.common.chainId() + toJSON(): JsonTx { + return { + 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'), + v: this.v !== undefined ? bnToHex(this.v) : undefined, + r: this.r !== undefined ? bnToHex(this.r) : undefined, + s: this.s !== undefined ? bnToHex(this.s) : undefined, + } } - /** - * Returns the sender's address - */ - getSenderAddress(): Address { - return new Address(publicToAddress(this.getSenderPublicKey())) + public isSigned(): boolean { + const { v, r, s } = this + return !!v && !!r && !!s } - /** - * Returns the public key of the sender - */ - getSenderPublicKey(): Buffer { - const msgHash = this.getMessageToVerifySignature() + private _unsignedTxImplementsEIP155() { + return this.common.gteHardfork('spuriousDragon') + } - // 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' - ) - } + 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, + ] - const { v, r, s } = this - if (!v || !r || !s) { - throw new Error('Missing values to derive sender public key from signed tx') + if (withEIP155) { + values.push(toBuffer(this.getChainId())) + values.push(unpadBuffer(toBuffer(0))) + values.push(unpadBuffer(toBuffer(0))) } - try { - return ecrecover( - msgHash, - v.toNumber(), - bnToRlp(r), - bnToRlp(s), - this._signedTxImplementsEIP155() ? this.getChainId() : undefined - ) - } catch (e) { - throw new Error('Invalid Signature') - } + return rlphash(values) } - /** - * 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 - } + getMessageToSign() { + return this._getMessageToSign(this._unsignedTxImplementsEIP155()) } /** - * 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. + * Process the v, r, s values from the `sign` method of the base transaction. */ - 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) - + protected processSignature(v: number, r: Buffer, s: Buffer) { if (this._unsignedTxImplementsEIP155()) { v += this.getChainId() * 2 + 8 } @@ -233,7 +209,7 @@ export default class Transaction { common: this.common, } - return new Transaction( + return LegacyTransaction.fromTxData( { nonce: this.nonce, gasPrice: this.gasPrice, @@ -249,137 +225,44 @@ export default class Transaction { ) } - /** - * 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). - */ - 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()}`) - } + getMessageToVerifySignature() { + const withEIP155 = this._signedTxImplementsEIP155() - return stringError ? errors : errors.length === 0 + return this._getMessageToSign(withEIP155) } /** - * Returns a Buffer Array of the raw Buffers of this transaction, in order. + * Returns the public key of the sender */ - raw(): Buffer[] { - return [ - bnToRlp(this.nonce), - bnToRlp(this.gasPrice), - bnToRlp(this.gasLimit), - this.to !== undefined ? this.to.buf : Buffer.from([]), - bnToRlp(this.value), - this.data, - this.v !== undefined ? bnToRlp(this.v) : Buffer.from([]), - this.r !== undefined ? bnToRlp(this.r) : Buffer.from([]), - this.s !== undefined ? bnToRlp(this.s) : Buffer.from([]), - ] - } - /** - * Returns the rlp encoding of the transaction. + * Returns the public key of the sender */ - serialize(): Buffer { - return rlp.encode(this.raw()) - } + getSenderPublicKey(): Buffer { + const msgHash = this.getMessageToVerifySignature() - /** - * Returns an object with the JSON representation of the transaction - */ - toJSON(): JsonTx { - return { - 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'), - v: this.v !== undefined ? bnToHex(this.v) : undefined, - r: this.r !== undefined ? bnToHex(this.r) : undefined, - s: this.s !== undefined ? bnToHex(this.s) : undefined, + // 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' + ) } - } - public isSigned(): boolean { const { v, r, s } = this - return !!v && !!r && !!s - } - - private _unsignedTxImplementsEIP155() { - return this.common.gteHardfork('spuriousDragon') - } - - private _signedTxImplementsEIP155() { - if (!this.isSigned()) { - throw Error('This transaction is not signed') + if (!v || !r || !s) { + throw new Error('Missing values to derive sender public key from signed tx') } - 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 - } - - private _getMessageToSign(withEIP155: boolean) { - const values = this.raw().slice(0, 6) - - if (withEIP155) { - values.push(toBuffer(this.getChainId())) - values.push(unpadBuffer(toBuffer(0))) - values.push(unpadBuffer(toBuffer(0))) + try { + return ecrecover( + msgHash, + v.toNumber(), + bnToRlp(r), + bnToRlp(s), + this._signedTxImplementsEIP155() ? this.getChainId() : undefined + ) + } catch (e) { + throw new Error('Invalid Signature') } - - return rlphash(values) } /** @@ -409,4 +292,21 @@ export default class Transaction { ) } } + + 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/transactionFactory.ts b/packages/tx/src/transactionFactory.ts new file mode 100644 index 0000000000..aa761a4f9a --- /dev/null +++ b/packages/tx/src/transactionFactory.ts @@ -0,0 +1,122 @@ +import Common from '@ethereumjs/common' +import { default as LegacyTransaction } from './legacyTransaction' +import { default as EIP2930Transaction } from './eip2930Transaction' +import { TxOptions, Transaction, TxData } from './types' +import BN from 'bn.js' + +const DEFAULT_COMMON = new Common({ chain: 'mainnet' }) + +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) { + // Assume LegacyTransaction + return LegacyTransaction.fromTxData(txData, txOptions) + } else { + const txType = new BN(txData.type).toNumber() + return TransactionFactory.getTransactionClass(txType, common).fromTxData(txData, txOptions) + } + } + + /** + * This method tries to decode `raw` data. It is somewhat equivalent to `fromRlpSerializedTx`. + * However, it could be that the data is not directly RLP-encoded (it is a Typed Transaction) + * @param rawData - The raw data buffer + * @param txOptions - The transaction options + */ + public static fromRawData(rawData: Buffer, txOptions: TxOptions = {}): Transaction { + const common = txOptions.common ?? DEFAULT_COMMON + if (rawData[0] <= 0x7f) { + // It is an EIP-2718 Typed Transaction + if (!common.isActivatedEIP(2718)) { + throw new Error('Common support for TypedTransactions (EIP-2718) not activated') + } + // 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 (!TransactionFactory.eipSupport(common, EIP)) { + throw new Error( + `Cannot create TypedTransaction with ID ${rawData[0]}: EIP ${EIP} not activated` + ) + } + + return EIP2930Transaction.fromRlpSerializedTx(rawData, txOptions) + } else { + return LegacyTransaction.fromRlpSerializedTx(rawData, txOptions) + } + } + + /** + * When decoding a BlockBody, in the transactions field, a field is either: + * A Buffer (a TypedTransaction - encoded as TransactionType || rlp(TransactionPayload)) + * A Buffer[] (LegacyTransaction) + * This method returns the right transaction. + * @param rawData - Either a Buffer or a Buffer[] + * @param txOptions - The transaction options + */ + public static fromBlockBodyData(rawData: Buffer | Buffer[], txOptions: TxOptions = {}) { + if (Buffer.isBuffer(rawData)) { + return this.fromRawData(rawData, txOptions) + } else if (Array.isArray(rawData)) { + // It is a LegacyTransaction + return LegacyTransaction.fromValuesArray(rawData, txOptions) + } else { + throw new Error('Cannot decode transaction: unknown type input') + } + } + + /** + * 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 = 0, common?: Common) { + const usedCommon = common ?? DEFAULT_COMMON + if (transactionID !== 0) { + if (!usedCommon.isActivatedEIP(2718)) { + throw new Error('Common support for TypedTransactions (EIP-2718) not activated') + } + } + + const legacyTxn = transactionID == 0 || (transactionID >= 0x80 && transactionID <= 0xff) + + if (legacyTxn) { + return LegacyTransaction + } + + switch (transactionID) { + case 1: + return EIP2930Transaction + default: + 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 + } + return common.isActivatedEIP(eip) + } +} diff --git a/packages/tx/src/types.ts b/packages/tx/src/types.ts index cb2681e730..7a793b3d37 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 Common from '@ethereumjs/common' +import { default as LegacyTransaction } from './legacyTransaction' +import { default as EIP2930Transaction } from './eip2930Transaction' /** * The options for initializing a Transaction. @@ -29,10 +31,49 @@ export interface TxOptions { freeze?: boolean } +/** + * The options for initializing a Transaction. + */ +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. */ export interface TxData { + /** + * The transaction's chain ID + */ + chainId?: BNLike + /** * The transaction's nonce. */ @@ -77,8 +118,55 @@ export interface TxData { * EC signature parameter. */ s?: BNLike + + /** + * The access list which contains the addresses/storage slots which the transaction wishes to access + */ + accessList?: AccessListBuffer | AccessList + + /** + * The transaction type + */ + + type?: BNLike +} + +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 } +type JsonAccessListItem = { address: string; storageKeys: string[] } + /** * An object with all of the transaction's values represented as strings. */ @@ -92,4 +180,9 @@ export interface JsonTx { r?: string s?: string value?: string + chainId?: string + accessList?: JsonAccessListItem[] + type?: string } + +export const DEFAULT_COMMON = new Common({ chain: 'mainnet', hardfork: 'berlin' }) diff --git a/packages/tx/test/base.spec.ts b/packages/tx/test/base.spec.ts new file mode 100644 index 0000000000..dc3f5af31b --- /dev/null +++ b/packages/tx/test/base.spec.ts @@ -0,0 +1,214 @@ +import tape from 'tape' +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') + 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.ok( + txType.class.fromSerializedTx(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() + }) + + 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() + }) +}) diff --git a/packages/tx/test/eip2930.spec.ts b/packages/tx/test/eip2930.spec.ts new file mode 100644 index 0000000000..df9f65019b --- /dev/null +++ b/packages/tx/test/eip2930.spec.ts @@ -0,0 +1,314 @@ +import Common from '@ethereumjs/common' +import { Address, BN, bufferToHex, privateToAddress } from 'ethereumjs-util' +import tape from 'tape' +import { AccessList, EIP2930Transaction } from '../src' + +const pKey = Buffer.from('4646464646464646464646464646464646464646464646464646464646464646', 'hex') +const address = privateToAddress(pKey) + +const common = new Common({ + eips: [2718, 2929, 2930], + chain: 'mainnet', + hardfork: 'berlin', +}) + +const validAddress = Buffer.from('01'.repeat(20), 'hex') +const validSlot = Buffer.from('01'.repeat(32), 'hex') + +const chainId = new BN(1) + +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 = [ + { + 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[] = [ + [ + Buffer.from('01'.repeat(21), 'hex'), // Address of 21 bytes instead of 20 + [], + ], + ] + + st.throws(() => { + EIP2930Transaction.fromTxData({ chainId, accessList }, { common }) + }) + + accessList = [ + [ + validAddress, + [ + Buffer.from('01'.repeat(31), 'hex'), // Slot of 31 bytes instead of 32 + ], + ], + ] + + st.throws(() => { + EIP2930Transaction.fromTxData({ chainId, accessList }, { common }) + }) + + accessList = [[]] // Address does not exist + + st.throws(() => { + EIP2930Transaction.fromTxData({ chainId, accessList }, { common }) + }) + + accessList = [[validAddress]] // Slots does not exist + + st.throws(() => { + EIP2930Transaction.fromTxData({ chainId, accessList }, { common }) + }) + + accessList = [[validAddress, validSlot]] // Slots is not an array + + st.throws(() => { + EIP2930Transaction.fromTxData({ chainId, accessList }, { common }) + }) + + accessList = [[validAddress, [], []]] // 3 items where 2 are expected + + st.throws(() => { + EIP2930Transaction.fromTxData({ chainId, 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]]], + chainId, + }, + { common } + ) + // Cost should be: + // Base fee + 2*TxDataNonZero + TxDataZero + AccessListAddressCost + AccessListSlotCost + 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 + .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]]], + chainId, + }, + { 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]], + ], + chainId, + }, + { common } + ) + + st.ok(tx.getBaseFee().eqn(baseFee + accessListAddressCost * 2 + accessListStorageKeyCost * 3)) + + st.end() + }) + + t.test('should sign a transaction', function (t) { + const tx = EIP2930Transaction.fromTxData( + { + data: Buffer.from('010200', 'hex'), + to: validAddress, + accessList: [[validAddress, [validSlot]]], + chainId, + }, + { common } + ) + const signed = tx.sign(pKey) + const signedAddress = signed.getSenderAddress() + + t.ok(signedAddress.buf.equals(address)) + + signed.verifySignature() // If this throws, test will not end. + + t.end() + }) + + // Data from + // https://github.com/INFURA/go-ethlibs/blob/75b2a52a39d353ed8206cffaf68d09bd1b154aae/eth/transaction_signing_test.go#L87 + + t.test('should sign transaction correctly and return expected JSON', 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' + ) + 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') + + 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/index.ts b/packages/tx/test/index.ts index 9d1556bc36..6b0f37c6ec 100644 --- a/packages/tx/test/index.ts +++ b/packages/tx/test/index.ts @@ -2,11 +2,26 @@ import minimist from 'minimist' const argv = minimist(process.argv.slice(2)) -if (argv.a) { - require('./api') +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') require('./transactionRunner') + require('./base.spec') + require('./legacy.spec') + require('./eip2930.spec') + require('./transactionFactory.spec') } diff --git a/packages/tx/test/json/eip2930blockRLP.json b/packages/tx/test/json/eip2930blockRLP.json new file mode 100644 index 0000000000..a77b02d173 --- /dev/null +++ b/packages/tx/test/json/eip2930blockRLP.json @@ -0,0 +1,3 @@ +{ + "rlp": "f90319f90211a00000000000000000000000000000000000000000000000000000000000000000a01dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347948888f1f195afa192cfee860698584c030f4c9db1a0ef1552a40b7165c3cd773806b9e0c165b75356e0314bf0706f279c729f51e017a0e6e49996c7ec59f7a23d22b83239a60151512c65613bf84a0d7da336399ebc4aa0cafe75574d59780665a97fbfd11365c7545aa8f1abf4e5e12e8243334ef7286bb901000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000083020000820200832fefd882a410845506eb0796636f6f6c65737420626c6f636b206f6e20636861696ea0bd4472abb6659ebe3ee06ee4d7b72a00a9f4d001caca51342001075469aff49888a13a5a8c8f2bb1c4f90101f85f800a82c35094095e7baea6a6c7c4c2dfeb977efac326af552d870a801ba09bea4c4daac7c7c52e093e6a4c35dbbcf8856f1af7b059ba20253e70848d094fa08a8fae537ce25ed8cb5af9adac3f141af69bd515bd2ba031522df09b97dd72b1b89e01f89b01800a8301e24194095e7baea6a6c7c4c2dfeb977efac326af552d878080f838f7940000000000000000000000000000000000000001e1a0000000000000000000000000000000000000000000000000000000000000000001a03dbacc8d0259f2508625e97fdfc57cd85fdd16e5821bc2c10bdd1a52649e8335a0476e10695b183a87b0aa292a7f4b78ef0c3fbe62aa2c42c84e1d9c3da159ef14c0" +} \ No newline at end of file diff --git a/packages/tx/test/json/eip2930txs.json b/packages/tx/test/json/eip2930txs.json new file mode 100644 index 0000000000..e0fcc6dc85 --- /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 6e913a5e8f..5b3aff00b7 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" + } } ] diff --git a/packages/tx/test/api.ts b/packages/tx/test/legacy.spec.ts similarity index 60% rename from packages/tx/test/api.ts rename to packages/tx/test/legacy.spec.ts index 1ecd1d7520..314a9b004c 100644 --- a/packages/tx/test/api.ts +++ b/packages/tx/test/legacy.spec.ts @@ -1,69 +1,20 @@ import tape from 'tape' import { Buffer } from 'buffer' -import { - BN, - rlp, - zeros, - privateToPublic, - toBuffer, - bufferToHex, - unpadBuffer, -} from 'ethereumjs-util' +import { BN, rlp, toBuffer, bufferToHex, 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[] = [] +tape('[Transaction]', function (t) { + const transactions: LegacyTransaction[] = [] - t.test('should initialize correctly', function (st) { - let tx = Transaction.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 }) - st.equal(tx.common.hardfork(), 'spuriousDragon', 'should initialize with correct HF provided') - - common.setHardfork('byzantium') - st.equal( - tx.common.hardfork(), - 'spuriousDragon', - 'should stay on correct HF if outer common HF changes' - ) - - tx = Transaction.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 - // options object works as expected. - const rlpData = tx.serialize() - - const zero = Buffer.alloc(0) - const valuesArray = [zero, zero, zero, zero, zero, zero] - - tx = Transaction.fromRlpSerializedTx(rlpData) - st.ok(Object.isFrozen(tx), 'tx should be frozen by default') - - tx = Transaction.fromRlpSerializedTx(rlpData, { freeze: false }) - st.ok(!Object.isFrozen(tx), 'tx should not be frozen when freeze deactivated in options') - - tx = Transaction.fromValuesArray(valuesArray) - st.ok(Object.isFrozen(tx), 'tx should be frozen by default') - - tx = Transaction.fromValuesArray(valuesArray, { freeze: false }) - st.ok(!Object.isFrozen(tx), 'tx should not be frozen when freeze deactivated in options') - - st.end() - }) - - t.test('should decode transactions', function (st) { + t.test('Initialization -> decode with fromValuesArray()', 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]) @@ -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,12 +40,12 @@ 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', }) - const tx = Transaction.fromValuesArray(txFixtures[3].raw.map(toBuffer), { + const tx = LegacyTransaction.fromValuesArray(txFixtures[3].raw.map(toBuffer), { common, }) st.deepEqual( @@ -112,8 +63,8 @@ tape('[Transaction]: Basic functions', function (t) { st.end() }) - t.test('should hash with defined chainId', function (st) { - const tx = Transaction.fromValuesArray(txFixtures[4].raw.map(toBuffer)) + t.test('hash() -> with defined chainId', function (st) { + const tx = LegacyTransaction.fromValuesArray(txFixtures[4].raw.map(toBuffer)) st.equal( tx.hash().toString('hex'), '0f09dc98ea85b7872f4409131a790b91e7540953992886fc268b7ba5c96820e4' @@ -129,83 +80,6 @@ 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: Transaction[] = [] - - 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) - 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) { - transactions.forEach(function (tx, i) { - const { privateKey } = txFixtures[i] - if (privateKey) { - st.ok(tx.sign(Buffer.from(privateKey, 'hex'))) - } - }) - 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') @@ -214,11 +88,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 +100,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 +133,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 +155,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 +192,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 +205,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 +213,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 +221,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 +236,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 +267,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 +282,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 +291,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,36 +325,39 @@ 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( - 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 = Transaction.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 = Transaction.fromTxData(fixtureTxSignedWithEIP155.toJSON(), { - common, - }).sign(privateKey) + let signedWithoutEIP155 = LegacyTransaction.fromTxData( + fixtureTxSignedWithEIP155.toJSON(), + { + common, + } + ).sign(privateKey) st.true(signedWithoutEIP155.verifySignature()) st.true( @@ -487,9 +366,12 @@ tape('[Transaction]: Basic functions', function (t) { "v shouldn't be EIP155 encoded" ) - signedWithoutEIP155 = Transaction.fromTxData(fixtureTxSignedWithoutEIP155.toJSON(), { - common, - }).sign(privateKey) + signedWithoutEIP155 = LegacyTransaction.fromTxData( + fixtureTxSignedWithoutEIP155.toJSON(), + { + common, + } + ).sign(privateKey) st.true(signedWithoutEIP155.verifySignature()) st.true( @@ -504,10 +386,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/transactionFactory.spec.ts b/packages/tx/test/transactionFactory.spec.ts new file mode 100644 index 0000000000..56c6f606ca --- /dev/null +++ b/packages/tx/test/transactionFactory.spec.ts @@ -0,0 +1,118 @@ +import Common from '@ethereumjs/common' +import { BN } from 'ethereumjs-util' +import tape from 'tape' +import { EIP2930Transaction, TransactionFactory, LegacyTransaction } from '../src' + +const EIP2930Common = new Common({ + eips: [2718, 2929, 2930], + chain: 'mainnet', + 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() + 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) { + const legacyTx = TransactionFactory.getTransactionClass() + st.equals(legacyTx!.name, LegacyTransaction.name) + + const 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() + }) + + 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() + }) +}) diff --git a/packages/tx/test/transactionRunner.ts b/packages/tx/test/transactionRunner.ts index 1e305bc9d5..3a89bf8e5d 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/' 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') diff --git a/packages/vm/CHANGELOG.md b/packages/vm/CHANGELOG.md index 1b9981f114..bd6c9b29e0 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/examples/run-solidity-contract/index.ts b/packages/vm/examples/run-solidity-contract/index.ts index c002156f02..e1cfe75ffa 100644 --- a/packages/vm/examples/run-solidity-contract/index.ts +++ b/packages/vm/examples/run-solidity-contract/index.ts @@ -3,7 +3,7 @@ import { join } from 'path' import { readFileSync } from 'fs' import { defaultAbiCoder as AbiCoder, Interface } from '@ethersproject/abi' import { Account, Address, BN } from 'ethereumjs-util' -import { Transaction } from '@ethereumjs/tx' +import { LegacyTransaction } from '@ethereumjs/tx' import VM from '../../dist' const solc = require('solc') @@ -96,7 +96,7 @@ async function deployContract( nonce: await getAccountNonce(vm, senderPrivateKey), } - const tx = Transaction.fromTxData(txData).sign(senderPrivateKey) + const tx = LegacyTransaction.fromTxData(txData).sign(senderPrivateKey) const deploymentResult = await vm.runTx({ tx }) @@ -124,7 +124,7 @@ async function setGreeting( nonce: await getAccountNonce(vm, senderPrivateKey), } - const tx = Transaction.fromTxData(txData).sign(senderPrivateKey) + const tx = LegacyTransaction.fromTxData(txData).sign(senderPrivateKey) const setGreetingResult = await vm.runTx({ tx }) diff --git a/packages/vm/examples/run-transactions-complete/index.ts b/packages/vm/examples/run-transactions-complete/index.ts index 0bca72b17d..41ffa6ce56 100644 --- a/packages/vm/examples/run-transactions-complete/index.ts +++ b/packages/vm/examples/run-transactions-complete/index.ts @@ -1,5 +1,5 @@ import { Account, BN, toBuffer, pubToAddress, bufferToHex } from 'ethereumjs-util' -import { Transaction, TxData } from '@ethereumjs/tx' +import { LegacyTransaction, TxData } from '@ethereumjs/tx' import VM from '../..' async function main() { @@ -50,7 +50,7 @@ async function main() { } async function runTx(vm: VM, txData: TxData, privateKey: Buffer) { - const tx = Transaction.fromTxData(txData).sign(privateKey) + const tx = LegacyTransaction.fromTxData(txData).sign(privateKey) console.log('----running tx-------') const results = await vm.runTx({ tx }) diff --git a/packages/vm/lib/index.ts b/packages/vm/lib/index.ts index 2ca2648bca..181c1e4262 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 eb906fec48..d9485ceabf 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,45 @@ async function applyTransactions(this: VM, block: Block, opts: RunBlockOpts) { logs: txRes.execResult.logs || [], } let txReceipt - let receiptLog = `Generate tx receipt gasUsed=${gasUsed} bitvector=${short( - abstractTxReceipt.bitvector - )} (${abstractTxReceipt.bitvector.length} bytes) logs=${abstractTxReceipt.logs.length}` - if (this._common.gteHardfork('byzantium')) { + let encodedReceipt + let receiptLog = `Generate tx receipt transactionType=${ + tx.transactionType + } gasUsed=${gasUsed} bitvector=${short(abstractTxReceipt.bitvector)} (${ + abstractTxReceipt.bitvector.length + } bytes) logs=${abstractTxReceipt.logs.length}` + if (tx.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 + + 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 9b4c8c5345..de7dea0ac3 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/GeneralStateTestsRunner.ts b/packages/vm/tests/GeneralStateTestsRunner.ts index 6b2385a611..d187fa2e6b 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'], diff --git a/packages/vm/tests/api/EIPs/eip-2929.spec.ts b/packages/vm/tests/api/EIPs/eip-2929.spec.ts index 105ce5ae83..b3ef5d0db6 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 0000000000..2bdf884d09 --- /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() + }) +}) diff --git a/packages/vm/tests/api/events.spec.ts b/packages/vm/tests/api/events.spec.ts index 325db22df6..054514bd9f 100644 --- a/packages/vm/tests/api/events.spec.ts +++ b/packages/vm/tests/api/events.spec.ts @@ -1,6 +1,6 @@ import tape from 'tape' import { toBuffer, bufferToHex } from 'ethereumjs-util' -import { Transaction } from '@ethereumjs/tx' +import { LegacyTransaction } from '@ethereumjs/tx' import { Block } from '@ethereumjs/block' import VM from '../../lib/index' @@ -58,7 +58,7 @@ tape('VM events', (t) => { 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 d8bc5d63fa..3b20e0f43a 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 09da7913eb..4d8361d85f 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 f30edf4f7d..c5bec853d6 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 { 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 = Transaction.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)