diff --git a/packages/tx/src/baseTransaction.ts b/packages/tx/src/baseTransaction.ts index 8845f21811..6db6caebc5 100644 --- a/packages/tx/src/baseTransaction.ts +++ b/packages/tx/src/baseTransaction.ts @@ -18,23 +18,14 @@ import { checkMaxInitCodeSize } from './util.js' import type { JsonTx, Transaction, + TransactionCache, TransactionInterface, TxData, TxOptions, TxValuesArray, } from './types.js' -import type { Hardfork } from '@ethereumjs/common' import type { BigIntLike } from '@ethereumjs/util' -interface TransactionCache { - hash?: Uint8Array - dataFee?: { - value: bigint - hardfork: string | Hardfork - } - senderPubKey?: Uint8Array -} - /** * This base class will likely be subject to further * refactoring along the introduction of additional tx types @@ -59,7 +50,7 @@ export abstract class BaseTransaction public readonly common!: Common - protected cache: TransactionCache = { + public cache: TransactionCache = { hash: undefined, dataFee: undefined, senderPubKey: undefined, diff --git a/packages/tx/src/capabilities/eip1559.ts b/packages/tx/src/capabilities/eip1559.ts index 8c51884994..d10375c9af 100644 --- a/packages/tx/src/capabilities/eip1559.ts +++ b/packages/tx/src/capabilities/eip1559.ts @@ -1,10 +1,6 @@ -import type { Transaction, TransactionType } from '../types.js' +import type { EIP1559CompatibleTx } from '../types.js' -type TypedTransactionEIP1559 = - | Transaction[TransactionType.BlobEIP4844] - | Transaction[TransactionType.FeeMarketEIP1559] - -export function getUpfrontCost(tx: TypedTransactionEIP1559, baseFee: bigint): bigint { +export function getUpfrontCost(tx: EIP1559CompatibleTx, baseFee: bigint): bigint { const prio = tx.maxPriorityFeePerGas const maxBase = tx.maxFeePerGas - baseFee const inclusionFeePerGas = prio < maxBase ? prio : maxBase diff --git a/packages/tx/src/capabilities/eip2718.ts b/packages/tx/src/capabilities/eip2718.ts new file mode 100644 index 0000000000..f4cf4528bd --- /dev/null +++ b/packages/tx/src/capabilities/eip2718.ts @@ -0,0 +1,26 @@ +import { RLP } from '@ethereumjs/rlp' +import { concatBytes } from '@ethereumjs/util' +import { keccak256 } from 'ethereum-cryptography/keccak.js' + +import { txTypeBytes } from '../util.js' + +import { errorMsg } from './legacy.js' + +import type { EIP2718CompatibleTx } from '../types' +import type { Input } from '@ethereumjs/rlp' + +export function getHashedMessageToSign(tx: EIP2718CompatibleTx): Uint8Array { + return keccak256(tx.getMessageToSign()) +} + +export function serialize(tx: EIP2718CompatibleTx, base?: Input): Uint8Array { + return concatBytes(txTypeBytes(tx.type), RLP.encode(base ?? tx.raw())) +} + +export function validateYParity(tx: EIP2718CompatibleTx) { + const { v } = tx + if (v !== undefined && v !== BigInt(0) && v !== BigInt(1)) { + const msg = errorMsg(tx, 'The y-parity of the transaction should either be 0 or 1') + throw new Error(msg) + } +} diff --git a/packages/tx/src/capabilities/eip2930.ts b/packages/tx/src/capabilities/eip2930.ts index c97261c083..1d9daa12b5 100644 --- a/packages/tx/src/capabilities/eip2930.ts +++ b/packages/tx/src/capabilities/eip2930.ts @@ -1,42 +1,12 @@ -import { RLP } from '@ethereumjs/rlp' -import { concatBytes, hexToBytes } from '@ethereumjs/util' -import { keccak256 } from 'ethereum-cryptography/keccak.js' - -import { BaseTransaction } from '../baseTransaction.js' -import { type TypedTransaction } from '../types.js' import { AccessLists } from '../util.js' -import type { Transaction, TransactionType } from '../types.js' +import * as Legacy from './legacy.js' -type TypedTransactionEIP2930 = Exclude +import type { EIP2930CompatibleTx } from '../types.js' /** * The amount of gas paid for the data in this tx */ -export function getDataFee(tx: TypedTransactionEIP2930): bigint { - if (tx['cache'].dataFee && tx['cache'].dataFee.hardfork === tx.common.hardfork()) { - return tx['cache'].dataFee.value - } - - let cost = BaseTransaction.prototype.getDataFee.bind(tx)() - cost += BigInt(AccessLists.getDataFeeEIP2930(tx.accessList, tx.common)) - - if (Object.isFrozen(tx)) { - tx['cache'].dataFee = { - value: cost, - hardfork: tx.common.hardfork(), - } - } - - return cost -} - -export function getHashedMessageToSign(tx: TypedTransactionEIP2930): Uint8Array { - return keccak256(tx.getMessageToSign()) -} - -export function serialize(tx: TypedTransactionEIP2930): Uint8Array { - const base = tx.raw() - const txTypeBytes = hexToBytes('0x' + tx.type.toString(16).padStart(2, '0')) - return concatBytes(txTypeBytes, RLP.encode(base)) +export function getDataFee(tx: EIP2930CompatibleTx): bigint { + return Legacy.getDataFee(tx, BigInt(AccessLists.getDataFeeEIP2930(tx.accessList, tx.common))) } diff --git a/packages/tx/src/capabilities/generic.ts b/packages/tx/src/capabilities/legacy.ts similarity index 56% rename from packages/tx/src/capabilities/generic.ts rename to packages/tx/src/capabilities/legacy.ts index b57dc7e7d3..91f95dbfd0 100644 --- a/packages/tx/src/capabilities/generic.ts +++ b/packages/tx/src/capabilities/legacy.ts @@ -1,9 +1,16 @@ import { SECP256K1_ORDER_DIV_2, bigIntToUnpaddedBytes, ecrecover } from '@ethereumjs/util' import { keccak256 } from 'ethereum-cryptography/keccak.js' -import { Capability, type TypedTransaction } from '../types.js' +import { BaseTransaction } from '../baseTransaction.js' +import { Capability } from '../types.js' -export function isSigned(tx: TypedTransaction): boolean { +import type { LegacyTxInterface } from '../types.js' + +export function errorMsg(tx: LegacyTxInterface, msg: string) { + return `${msg} (${tx.errorStr()})` +} + +export function isSigned(tx: LegacyTxInterface): boolean { const { v, r, s } = tx if (v === undefined || r === undefined || s === undefined) { return false @@ -12,21 +19,37 @@ export function isSigned(tx: TypedTransaction): boolean { } } -export function errorMsg(tx: TypedTransaction, msg: string) { - return `${msg} (${tx.errorStr()})` +/** + * The amount of gas paid for the data in this tx + */ +export function getDataFee(tx: LegacyTxInterface, extraCost?: bigint): bigint { + if (tx.cache.dataFee && tx.cache.dataFee.hardfork === tx.common.hardfork()) { + return tx.cache.dataFee.value + } + + const cost = BaseTransaction.prototype.getDataFee.bind(tx)() + (extraCost ?? 0n) + + if (Object.isFrozen(tx)) { + tx.cache.dataFee = { + value: cost, + hardfork: tx.common.hardfork(), + } + } + + return cost } -export function hash(tx: TypedTransaction): Uint8Array { +export function hash(tx: LegacyTxInterface): Uint8Array { if (!tx.isSigned()) { const msg = errorMsg(tx, 'Cannot call hash method if transaction is not signed') throw new Error(msg) } if (Object.isFrozen(tx)) { - if (!tx['cache'].hash) { - tx['cache'].hash = keccak256(tx.serialize()) + if (!tx.cache.hash) { + tx.cache.hash = keccak256(tx.serialize()) } - return tx['cache'].hash + return tx.cache.hash } return keccak256(tx.serialize()) @@ -36,7 +59,7 @@ export function hash(tx: TypedTransaction): Uint8Array { * EIP-2: All transaction signatures whose s-value is greater than secp256k1n/2are considered invalid. * Reasoning: https://ethereum.stackexchange.com/a/55728 */ -export function validateHighS(tx: TypedTransaction): void { +export function validateHighS(tx: LegacyTxInterface): void { const { s } = tx if (tx.common.gteHardfork('homestead') && s !== undefined && s > SECP256K1_ORDER_DIV_2) { const msg = errorMsg( @@ -47,17 +70,9 @@ export function validateHighS(tx: TypedTransaction): void { } } -export function validateYParity(tx: TypedTransaction) { - const { v } = tx - if (v !== undefined && v !== BigInt(0) && v !== BigInt(1)) { - const msg = errorMsg(tx, 'The y-parity of the transaction should either be 0 or 1') - throw new Error(msg) - } -} - -export function getSenderPublicKey(tx: TypedTransaction): Uint8Array { - if (tx['cache'].senderPubKey !== undefined) { - return tx['cache'].senderPubKey +export function getSenderPublicKey(tx: LegacyTxInterface): Uint8Array { + if (tx.cache.senderPubKey !== undefined) { + return tx.cache.senderPubKey } const msgHash = tx.getMessageToVerifySignature() @@ -75,7 +90,7 @@ export function getSenderPublicKey(tx: TypedTransaction): Uint8Array { tx.supports(Capability.EIP155ReplayProtection) ? tx.common.chainId() : undefined ) if (Object.isFrozen(tx)) { - tx['cache'].senderPubKey = sender + tx.cache.senderPubKey = sender } return sender } catch (e: any) { diff --git a/packages/tx/src/eip1559Transaction.ts b/packages/tx/src/eip1559Transaction.ts index 13635a692e..ee7ca4a419 100644 --- a/packages/tx/src/eip1559Transaction.ts +++ b/packages/tx/src/eip1559Transaction.ts @@ -5,19 +5,18 @@ import { bigIntToUnpaddedBytes, bytesToBigInt, bytesToHex, - concatBytes, equalsBytes, - hexToBytes, toBytes, validateNoLeadingZeroes, } from '@ethereumjs/util' import { BaseTransaction } from './baseTransaction.js' import * as EIP1559 from './capabilities/eip1559.js' +import * as EIP2718 from './capabilities/eip2718.js' import * as EIP2930 from './capabilities/eip2930.js' -import * as Generic from './capabilities/generic.js' +import * as Legacy from './capabilities/legacy.js' import { TransactionType } from './types.js' -import { AccessLists } from './util.js' +import { AccessLists, txTypeBytes } from './util.js' import type { AccessList, @@ -32,10 +31,6 @@ import type { Common } from '@ethereumjs/common' type TxData = AllTypesTxData[TransactionType.FeeMarketEIP1559] type TxValuesArray = AllTypesTxValuesArray[TransactionType.FeeMarketEIP1559] -const TRANSACTION_TYPE_BYTES = hexToBytes( - '0x' + TransactionType.FeeMarketEIP1559.toString(16).padStart(2, '0') -) - /** * Typed transaction with a new gas fee market mechanism * @@ -43,6 +38,7 @@ const TRANSACTION_TYPE_BYTES = hexToBytes( * - EIP: [EIP-1559](https://eips.ethereum.org/EIPS/eip-1559) */ export class FeeMarketEIP1559Transaction extends BaseTransaction { + // implements EIP1559CompatibleTx public readonly chainId: bigint public readonly accessList: AccessListBytes public readonly AccessListJSON: AccessList @@ -72,7 +68,10 @@ export class FeeMarketEIP1559Transaction extends BaseTransaction toBytes(vh)) - Generic.validateYParity(this) - Generic.validateHighS(this) + EIP2718.validateYParity(this) + Legacy.validateHighS(this) for (const hash of this.versionedHashes) { if (hash.length !== 32) { @@ -215,7 +210,10 @@ export class BlobEIP4844Transaction extends BaseTransaction { * The amount of gas paid for the data in this tx */ getDataFee(): bigint { - if (this.cache.dataFee && this.cache.dataFee.hardfork === this.common.hardfork()) { - return this.cache.dataFee.value - } - - if (Object.isFrozen(this)) { - this.cache.dataFee = { - value: super.getDataFee(), - hardfork: this.common.hardfork(), - } - } - - return super.getDataFee() + return Legacy.getDataFee(this) } /** @@ -257,7 +246,7 @@ export class LegacyTransaction extends BaseTransaction { * Use {@link Transaction.getMessageToSign} to get a tx hash for the purpose of signing. */ hash(): Uint8Array { - return Generic.hash(this) + return Legacy.hash(this) } /** @@ -275,7 +264,7 @@ export class LegacyTransaction extends BaseTransaction { * Returns the public key of the sender */ getSenderPublicKey(): Uint8Array { - return Generic.getSenderPublicKey(this) + return Legacy.getSenderPublicKey(this) } /** @@ -377,6 +366,6 @@ export class LegacyTransaction extends BaseTransaction { * @hidden */ protected _errorMsg(msg: string) { - return Generic.errorMsg(this, msg) + return Legacy.errorMsg(this, msg) } } diff --git a/packages/tx/src/types.ts b/packages/tx/src/types.ts index 9924a9a039..cfae11a2fe 100644 --- a/packages/tx/src/types.ts +++ b/packages/tx/src/types.ts @@ -4,7 +4,7 @@ import type { FeeMarketEIP1559Transaction } from './eip1559Transaction.js' import type { AccessListEIP2930Transaction } from './eip2930Transaction.js' import type { BlobEIP4844Transaction } from './eip4844Transaction.js' import type { LegacyTransaction } from './legacyTransaction.js' -import type { AccessList, AccessListBytes, Common } from '@ethereumjs/common' +import type { AccessList, AccessListBytes, Common, Hardfork } from '@ethereumjs/common' import type { Address, AddressLike, BigIntLike, BytesLike } from '@ethereumjs/util' export type { AccessList, @@ -14,7 +14,7 @@ export type { } from '@ethereumjs/common' /** - * Can be used in conjunction with {@link Transaction.supports} + * Can be used in conjunction with {@link Transaction[TransactionType].supports} * to query on tx capabilities */ export enum Capability { @@ -93,6 +93,15 @@ export function isAccessList(input: AccessListBytes | AccessList): input is Acce return !isAccessListBytes(input) // This is exactly the same method, except the output is negated. } +export interface TransactionCache { + hash?: Uint8Array + dataFee?: { + value: bigint + hardfork: string | Hardfork + } + senderPubKey?: Uint8Array +} + /** * Encompassing type for all transaction types. */ @@ -128,9 +137,19 @@ export function isBlobEIP4844Tx(tx: TypedTransaction): tx is BlobEIP4844Transact return tx.type === TransactionType.BlobEIP4844 } -export interface TransactionInterface { +export interface TransactionInterface { + readonly common: Common + readonly nonce: bigint + readonly gasLimit: bigint + readonly to?: Address + readonly value: bigint + readonly data: Uint8Array + readonly v?: bigint + readonly r?: bigint + readonly s?: bigint + readonly cache: TransactionCache supports(capability: Capability): boolean - type: number + type: TransactionType getBaseFee(): bigint getDataFee(): bigint getUpfrontCost(): bigint @@ -152,6 +171,38 @@ export interface TransactionInterface { errorStr(): string } +export interface LegacyTxInterface + extends TransactionInterface {} + +export interface EIP2718CompatibleTx + extends TransactionInterface { + readonly chainId: bigint + getMessageToSign(): Uint8Array +} + +export interface EIP2930CompatibleTx + extends EIP2718CompatibleTx { + readonly accessList: AccessListBytes + readonly AccessListJSON: AccessList +} + +export interface EIP1559CompatibleTx + extends EIP2930CompatibleTx { + readonly maxPriorityFeePerGas: bigint + readonly maxFeePerGas: bigint +} + +export interface EIP4844CompatibleTx + extends EIP1559CompatibleTx { + readonly maxFeePerBlobGas: bigint + versionedHashes: Uint8Array[] + blobs?: Uint8Array[] + kzgCommitments?: Uint8Array[] + kzgProofs?: Uint8Array[] + serializeNetworkWrapper(): Uint8Array + numBlobs(): number +} + export interface TxData { [TransactionType.Legacy]: LegacyTxData [TransactionType.AccessListEIP2930]: AccessListEIP2930TxData diff --git a/packages/tx/src/util.ts b/packages/tx/src/util.ts index 663d5a06fe..cbc3c97d34 100644 --- a/packages/tx/src/util.ts +++ b/packages/tx/src/util.ts @@ -2,7 +2,7 @@ import { bytesToHex, hexToBytes, setLengthLeft } from '@ethereumjs/util' import { isAccessList } from './types.js' -import type { AccessList, AccessListBytes, AccessListItem } from './types.js' +import type { AccessList, AccessListBytes, AccessListItem, TransactionType } from './types.js' import type { Common } from '@ethereumjs/common' export function checkMaxInitCodeSize(common: Common, length: number) { @@ -115,3 +115,7 @@ export class AccessLists { return addresses * Number(accessListAddressCost) + slots * Number(accessListStorageKeyCost) } } + +export function txTypeBytes(txType: TransactionType): Uint8Array { + return hexToBytes('0x' + txType.toString(16).padStart(2, '0')) +}