diff --git a/packages/tx/src/dataContainerTypes.ts b/packages/tx/src/dataContainerTypes.ts new file mode 100644 index 00000000000..9e85c3c3a97 --- /dev/null +++ b/packages/tx/src/dataContainerTypes.ts @@ -0,0 +1,234 @@ +import type { + AccessList, + AccessListBytes, + AuthorizationList, + AuthorizationListBytes, + JSONTx, + TransactionType, +} from './types.js' +import type { + Address, + AddressLike, + BigIntLike, + BytesLike, + PrefixedHexString, +} from '@ethereumjs/util' + +// TODO +// Make a very simple "Features" class which handles supports/activate/deactivate (?) + +export enum Feature { + ReplayProtection = 'ReplayProtection', // For EIP-155 replay protection + ECDSASignable = 'ECDSASignable', // For unsigned/signed ECDSA containers + ECDSASigned = 'ECDSASigned', // For signed ECDSA containers + + LegacyGasMarket = 'LegacyGasMarket', // Txs with legacy gas market (pre-1559) + FeeMarket = 'FeeMarket', // Txs with EIP1559 gas market + + TypedTransaction = 'TypedTransaction', + + AccessLists = 'AccessLists', + EOACode = 'EOACode', + Blobs = 'Blobs', +} + +export type NestedUint8Array = (Uint8Array | NestedUint8Array)[] + +export interface TxContainerMethods { + supports(capability: Feature): boolean + type: TransactionType + + // Raw list of Uint8Arrays (can be nested) + raw(): NestedUint8Array // TODO make more tx-specific + + // The serialized version of the raw() one + // (current: RLP.encode) + serialize(): Uint8Array + + // Utility to convert to a JSON object + toJSON(): JSONTx + + /** Signature related stuff, TODO */ + /* + isSigned(): boolean + isValid(): boolean + verifySignature(): boolean + getSenderAddress(): Address + getSenderPublicKey(): Uint8Array + sign(privateKey: Uint8Array): Transaction[T] + errorStr(): string + + addSignature( + v: bigint, + r: Uint8Array | bigint, + s: Uint8Array | bigint, + convertV?: boolean, + ): Transaction[T] + + + // Get the non-hashed message to sign (this is input, but then hashed, is input to methods like ecsign) + getMessageToSign(): Uint8Array | Uint8Array[] + // Get the hashed message to sign (allows for flexibility over the hash method, now: keccak256) + getHashedMessageToSign(): Uint8Array + + // The hash of the transaction (note: hash currently has to do with signed txs but on L2 likely can also be of non-signed txs (?)) + hash(): Uint8Array + + */ +} + +// Container "fields" and container "interface" below +// Fields: used for the CONSTRUCTOR of the containers +// Interface: used for the resulting constructor, so each param of the field is converted to that type before resulting in the container + +export type DefaultFields = { + nonce?: BigIntLike + gasLimit?: BigIntLike + to?: AddressLike + value?: BigIntLike + data?: BytesLike | '' // Note: '' is for empty data (TODO look if we want to keep this) +} + +export interface DefaultContainerInterface { + readonly nonce: bigint + readonly gasLimit: bigint + readonly value: bigint + readonly data: Uint8Array +} + +export type CreateContractFields = { + to?: AddressLike | '' | null +} + +export interface CreateContractInterface { + to: Address | null +} + +// Equivalent of CreateContractDataFields but does not allow "null" or the empty string. +export type ToFields = { + to?: AddressLike +} + +export interface ToInterface { + to: Address +} + +export type ECDSAMaybeSignedFields = { + v?: BigIntLike + r?: BigIntLike + s?: BigIntLike +} + +export type ECDSASignedFields = Required + +// Note: only container interface with values which could be undefined due to unsigned containers +export interface ECDSAMaybeSignedInterface { + readonly v?: bigint + readonly r?: bigint + readonly s?: bigint +} + +type ECDSASignedInterfaceType = Required + +export interface ECDSASignedInterface extends ECDSASignedInterfaceType {} + +export type LegacyGasMarketFields = { + gasPrice?: BigIntLike +} + +export interface LegacyGasMarketInterface { + readonly gasPrice: bigint +} + +export interface LegacyTxInterface + extends DefaultContainerInterface, + CreateContractInterface, + LegacyGasMarketInterface, + ECDSAMaybeSignedInterface {} + +export type ContainerInterface = { + [TransactionType.Legacy]: LegacyTxInterface +} + +// EIP-2930 (Access Lists) related types and interfaces +export type ChainIdFields = { + chainId?: BigIntLike +} + +export interface ChainIdInterface { + chainId: bigint +} + +export type AccessListFields = { + accessList?: AccessListBytes | AccessList +} + +export type EIP2930Fields = ChainIdFields & AccessListFields + +export interface AccessListInterface { + accessList: AccessListBytes +} + +export interface AccessList2930Interface extends ChainIdInterface, AccessListInterface {} + +// EIP-1559 (Fee market) related types and interfaces +export type FeeMarketFields = { + maxPriorityFeePerGas?: BigIntLike + maxFeePerGas?: BigIntLike +} + +export interface FeeMarketInterface { + readonly maxPriorityFeePerGas: bigint + readonly maxFeePerGas: bigint +} + +// EIP-4844 (Shard blob transactions) related types and fields +export type BlobFields = { + blobVersionedHashes?: BytesLike[] + maxFeePerBlobGas?: BigIntLike + blobs?: BytesLike[] + kzgCommitments?: BytesLike[] + kzgProofs?: BytesLike[] + blobsData?: string[] // TODO why is this string and not something like PrefixedHexString? +} + +export interface BlobInterface { + readonly blobVersionedHashes: PrefixedHexString[] // TODO why is this a string and not uint8array? + readonly blobs?: PrefixedHexString[] + readonly kzgCommitments?: PrefixedHexString[] + readonly kzgProofs?: PrefixedHexString[] + readonly maxFeePerBlobGas: bigint +} + +// EIP-7702 (EOA code transactions) related types and fields +export type AuthorizationListFields = { + authorizationList?: AuthorizationListBytes | AuthorizationList | never +} + +export interface AuthorizationListInterface { + readonly authorizationList: AuthorizationListBytes +} + +// Below here: helper types +// Helper type which is common on the txs: +type DefaultFieldsMaybeSigned = DefaultFields & ECDSAMaybeSignedFields + +// Helper type for the constructor fields of the txs +export type TxConstructorFields = { + [TransactionType.Legacy]: DefaultFieldsMaybeSigned & CreateContractFields & LegacyGasMarketFields + [TransactionType.AccessListEIP2930]: TxConstructorFields[TransactionType.Legacy] & EIP2930Fields + [TransactionType.FeeMarketEIP1559]: Omit< + TxConstructorFields[TransactionType.AccessListEIP2930], + 'gasPrice' + > & + FeeMarketFields + [TransactionType.BlobEIP4844]: Omit & + ToFields & + BlobFields + [TransactionType.EOACodeEIP7702]: Omit< + TxConstructorFields[TransactionType.FeeMarketEIP1559], + 'to' + > & + ToFields & + AuthorizationListFields +} diff --git a/packages/tx/src/dataContainers/EOA7702DataContainer.ts b/packages/tx/src/dataContainers/EOA7702DataContainer.ts new file mode 100644 index 00000000000..805b7a0b477 --- /dev/null +++ b/packages/tx/src/dataContainers/EOA7702DataContainer.ts @@ -0,0 +1,125 @@ +import { RLP } from '@ethereumjs/rlp' +import { Address, bytesToBigInt, toBytes } from '@ethereumjs/util' + +import { Feature } from '../dataContainerTypes.js' +import { TransactionType } from '../types.js' +import { AccessLists, AuthorizationLists } from '../util.js' + +import type { + AccessList2930Interface, + AuthorizationListInterface, + DefaultContainerInterface, + ECDSAMaybeSignedInterface, + ECDSASignedInterface, + FeeMarketInterface, + ToInterface, + TxConstructorFields, + TxContainerMethods, +} from '../dataContainerTypes.js' +import type { AccessListBytes, AuthorizationListBytes, TxOptions } from '../types.js' + +type TxType = TransactionType.EOACodeEIP7702 + +const feeMarketFeatures = new Set([ + Feature.ECDSASignable, + Feature.FeeMarket, + Feature.AccessLists, + Feature.EOACode, +]) + +export class EOACode7702Container + implements + TxContainerMethods, + DefaultContainerInterface, + ToInterface, + FeeMarketInterface, + ECDSAMaybeSignedInterface, + AccessList2930Interface, + AuthorizationListInterface +{ + public type: number = TransactionType.EOACodeEIP7702 // Legacy tx type + + // Tx data part (part of the RLP) + public readonly maxFeePerGas: bigint + public readonly maxPriorityFeePerGas: bigint + public readonly nonce: bigint + public readonly gasLimit: bigint + public readonly value: bigint + public readonly data: Uint8Array + public readonly to: Address + public readonly accessList: AccessListBytes + public readonly authorizationList: AuthorizationListBytes + public readonly chainId: bigint + + // Props only for signed txs + public readonly v?: bigint + public readonly r?: bigint + public readonly s?: bigint + + constructor(txData: TxConstructorFields[TxType], txOptions: TxOptions) { + const { nonce, gasLimit, to, value, data, v, r, s, maxPriorityFeePerGas, maxFeePerGas } = txData + + // Set the tx properties + const toB = toBytes(to) + if (toB.length === 0) { + // TODO: better check + throw new Error('The to: field should be defined') + } + this.to = new Address(toB) + + this.nonce = bytesToBigInt(toBytes(nonce)) + this.gasLimit = bytesToBigInt(toBytes(gasLimit)) + this.value = bytesToBigInt(toBytes(value)) + this.data = toBytes(data === '' ? '0x' : data) + this.maxFeePerGas = bytesToBigInt(toBytes(maxFeePerGas)) + this.maxPriorityFeePerGas = bytesToBigInt(toBytes(maxPriorityFeePerGas)) + + // Set signature values (if the tx is signed) + + const vB = toBytes(v) + const rB = toBytes(r) + const sB = toBytes(s) + this.v = vB.length > 0 ? bytesToBigInt(vB) : undefined + this.r = rB.length > 0 ? bytesToBigInt(rB) : undefined + this.s = sB.length > 0 ? bytesToBigInt(sB) : undefined + + const { chainId, accessList, authorizationList } = txData + + // NOTE: previously there was a check against common's chainId, this is not here + // Common is not available in the tx container + // Likely, we should now check this chain id when signing the tx (to prevent people signing on the wrong chain) + this.chainId = bytesToBigInt(toBytes(chainId)) + + const accessListData = AccessLists.getAccessListData(accessList ?? []) + this.accessList = accessListData.accessList + + // Populate the authority list fields + const authorizationListData = AuthorizationLists.getAuthorizationListData( + authorizationList ?? [], + ) + this.authorizationList = authorizationListData.authorizationList + } + + raw() { + // TODO + return [new Uint8Array(), new Uint8Array()] + } + serialize() { + return RLP.encode(this.raw()) + } + + supports(feature: Feature) { + return feeMarketFeatures.has(feature) + } + + toJSON() { + return {} + } + + // TODO likely add common here: should check against the chain id in this container to prevent + // signing against the wrong chain id + sign(privateKey: Uint8Array): EOACode7702Container & ECDSASignedInterface { + // TODO + return this as EOACode7702Container & ECDSASignedInterface // Type return value to have v/r/s set + } +} diff --git a/packages/tx/src/dataContainers/accessList2930Container.ts b/packages/tx/src/dataContainers/accessList2930Container.ts new file mode 100644 index 00000000000..9e9af2897fd --- /dev/null +++ b/packages/tx/src/dataContainers/accessList2930Container.ts @@ -0,0 +1,109 @@ +import { RLP } from '@ethereumjs/rlp' +import { Address, bytesToBigInt, toBytes } from '@ethereumjs/util' + +import { Feature } from '../dataContainerTypes.js' +import { TransactionType } from '../types.js' +import { AccessLists } from '../util.js' + +import type { + AccessList2930Interface, + CreateContractInterface, + DefaultContainerInterface, + ECDSAMaybeSignedInterface, + ECDSASignedInterface, + LegacyGasMarketInterface, + TxConstructorFields, + TxContainerMethods, +} from '../dataContainerTypes.js' +import type { AccessListBytes, TxOptions } from '../types.js' + +type TxType = TransactionType.AccessListEIP2930 + +const accessListFeatures = new Set([ + Feature.ECDSASignable, + Feature.LegacyGasMarket, + Feature.AccessLists, +]) + +export class AccessList2930Container + implements + TxContainerMethods, + DefaultContainerInterface, + CreateContractInterface, + LegacyGasMarketInterface, + ECDSAMaybeSignedInterface, + AccessList2930Interface +{ + public type: number = TransactionType.AccessListEIP2930 // Legacy tx type + + // Tx data part (part of the RLP) + public readonly gasPrice: bigint + public readonly nonce: bigint + public readonly gasLimit: bigint + public readonly value: bigint + public readonly data: Uint8Array + public readonly to: Address | null + public readonly accessList: AccessListBytes + public readonly chainId: bigint + + // Props only for signed txs + public readonly v?: bigint + public readonly r?: bigint + public readonly s?: bigint + + constructor(txData: TxConstructorFields[TxType], txOptions: TxOptions) { + const { nonce, gasLimit, to, value, data, v, r, s } = txData + + // Set the tx properties + const toB = toBytes(to === '' ? '0x' : to) + this.to = toB.length > 0 ? new Address(toB) : null + + this.nonce = bytesToBigInt(toBytes(nonce)) + this.gasLimit = bytesToBigInt(toBytes(gasLimit)) + this.value = bytesToBigInt(toBytes(value)) + this.data = toBytes(data === '' ? '0x' : data) + this.gasPrice = bytesToBigInt(toBytes(txData.gasPrice)) + + // Set signature values (if the tx is signed) + + const vB = toBytes(v) + const rB = toBytes(r) + const sB = toBytes(s) + this.v = vB.length > 0 ? bytesToBigInt(vB) : undefined + this.r = rB.length > 0 ? bytesToBigInt(rB) : undefined + this.s = sB.length > 0 ? bytesToBigInt(sB) : undefined + + const { chainId, accessList } = txData + + // NOTE: previously there was a check against common's chainId, this is not here + // Common is not available in the tx container + // Likely, we should now check this chain id when signing the tx (to prevent people signing on the wrong chain) + this.chainId = bytesToBigInt(toBytes(chainId)) + + const accessListData = AccessLists.getAccessListData(accessList ?? []) + this.accessList = accessListData.accessList + } + + raw() { + // TODO + return [new Uint8Array(), new Uint8Array()] + } + serialize() { + return RLP.encode(this.raw()) + } + + supports(feature: Feature) { + return accessListFeatures.has(feature) + } + + toJSON() { + return {} + } + + // TODO likely add common here: should check against the chain id in this container to prevent + // signing against the wrong chain id + sign(privateKey: Uint8Array): AccessList2930Container & ECDSASignedInterface { + // TODO + return this as AccessList2930Container & ECDSASignedInterface // Type return value to have v/r/s set + } +} diff --git a/packages/tx/src/dataContainers/blob4844Container.ts b/packages/tx/src/dataContainers/blob4844Container.ts new file mode 100644 index 00000000000..621a70ba661 --- /dev/null +++ b/packages/tx/src/dataContainers/blob4844Container.ts @@ -0,0 +1,185 @@ +import { RLP } from '@ethereumjs/rlp' +import { Address, TypeOutput, bytesToBigInt, toBytes, toType } from '@ethereumjs/util' + +import { Feature } from '../dataContainerTypes.js' +import { TransactionType } from '../types.js' +import { AccessLists } from '../util.js' + +import type { + AccessList2930Interface, + BlobInterface, + DefaultContainerInterface, + ECDSAMaybeSignedInterface, + ECDSASignedInterface, + FeeMarketInterface, + ToInterface, + TxConstructorFields, + TxContainerMethods, +} from '../dataContainerTypes.js' +import type { AccessListBytes, TxOptions } from '../types.js' +import type { PrefixedHexString } from '@ethereumjs/util' + +type TxType = TransactionType.BlobEIP4844 + +const feeMarketFeatures = new Set([ + Feature.ECDSASignable, + Feature.FeeMarket, + Feature.AccessLists, + Feature.Blobs, +]) + +export class Blob4844Container + implements + TxContainerMethods, + DefaultContainerInterface, + ToInterface, + FeeMarketInterface, + ECDSAMaybeSignedInterface, + AccessList2930Interface, + BlobInterface +{ + public type: number = TransactionType.BlobEIP4844 // Legacy tx type + + // Tx data part (part of the RLP) + public readonly maxFeePerGas: bigint + public readonly maxPriorityFeePerGas: bigint + public readonly maxFeePerBlobGas: bigint + public readonly nonce: bigint + public readonly gasLimit: bigint + public readonly value: bigint + public readonly data: Uint8Array + public readonly to: Address + public readonly accessList: AccessListBytes + public readonly chainId: bigint + public readonly blobVersionedHashes: PrefixedHexString[] + + // Props only for signed txs + public readonly v?: bigint + public readonly r?: bigint + public readonly s?: bigint + + // Blob related properties + + public readonly blobs?: PrefixedHexString[] // This property should only be populated when the transaction is in the "Network Wrapper" format + public readonly kzgCommitments?: PrefixedHexString[] // This property should only be populated when the transaction is in the "Network Wrapper" format + public readonly kzgProofs?: PrefixedHexString[] // This property should only be populated when the transaction is in the "Network Wrapper" format + + constructor(txData: TxConstructorFields[TxType], txOptions: TxOptions) { + const { + nonce, + gasLimit, + to, + value, + data, + v, + r, + s, + maxPriorityFeePerGas, + maxFeePerGas, + maxFeePerBlobGas, + } = txData + + // Set the tx properties + const toB = toBytes(to) + if (toB.length === 0) { + // TODO: better check + throw new Error('The to: field should be defined') + } + this.to = new Address(toB) + + this.nonce = bytesToBigInt(toBytes(nonce)) + this.gasLimit = bytesToBigInt(toBytes(gasLimit)) + this.value = bytesToBigInt(toBytes(value)) + this.data = toBytes(data === '' ? '0x' : data) + this.maxFeePerGas = bytesToBigInt(toBytes(maxFeePerGas)) + this.maxPriorityFeePerGas = bytesToBigInt(toBytes(maxPriorityFeePerGas)) + + // Set signature values (if the tx is signed) + + const vB = toBytes(v) + const rB = toBytes(r) + const sB = toBytes(s) + this.v = vB.length > 0 ? bytesToBigInt(vB) : undefined + this.r = rB.length > 0 ? bytesToBigInt(rB) : undefined + this.s = sB.length > 0 ? bytesToBigInt(sB) : undefined + + const { chainId, accessList } = txData + + // NOTE: previously there was a check against common's chainId, this is not here + // Common is not available in the tx container + // Likely, we should now check this chain id when signing the tx (to prevent people signing on the wrong chain) + this.chainId = bytesToBigInt(toBytes(chainId)) + + const accessListData = AccessLists.getAccessListData(accessList ?? []) + this.accessList = accessListData.accessList + + // Blob related stuff here + this.maxFeePerBlobGas = bytesToBigInt( + toBytes((maxFeePerBlobGas ?? '') === '' ? '0x' : maxFeePerBlobGas), + ) + + this.blobVersionedHashes = (txData.blobVersionedHashes ?? []).map((vh) => + toType(vh, TypeOutput.PrefixedHexString), + ) + + /* Validation, should be done elsewhere + for (const hash of this.blobVersionedHashes) { + if (hash.length !== 66) { + // 66 is the length of a 32 byte hash as a PrefixedHexString + const msg = Legacy.errorMsg(this, 'versioned hash is invalid length') + throw new Error(msg) + } + if (BigInt(parseInt(hash.slice(2, 4))) !== this.common.param('blobCommitmentVersionKzg')) { + // We check the first "byte" of the hash (starts at position 2 since hash is a PrefixedHexString) + const msg = Legacy.errorMsg( + this, + 'versioned hash does not start with KZG commitment version', + ) + throw new Error(msg) + } + } + if (this.blobVersionedHashes.length > LIMIT_BLOBS_PER_TX) { + const msg = Legacy.errorMsg(this, `tx can contain at most ${LIMIT_BLOBS_PER_TX} blobs`) + throw new Error(msg) + } else if (this.blobVersionedHashes.length === 0) { + const msg = Legacy.errorMsg(this, `tx should contain at least one blob`) + throw new Error(msg) + } + if (this.to === undefined) { + const msg = Legacy.errorMsg( + this, + `tx should have a "to" field and cannot be used to create contracts`, + ) + throw new Error(msg) + } */ + + this.blobs = txData.blobs?.map((blob) => toType(blob, TypeOutput.PrefixedHexString)) + this.kzgCommitments = txData.kzgCommitments?.map((commitment) => + toType(commitment, TypeOutput.PrefixedHexString), + ) + this.kzgProofs = txData.kzgProofs?.map((proof) => toType(proof, TypeOutput.PrefixedHexString)) + } + + raw() { + // TODO + return [new Uint8Array(), new Uint8Array()] + } + serialize() { + return RLP.encode(this.raw()) + } + + supports(feature: Feature) { + return feeMarketFeatures.has(feature) + } + + toJSON() { + return {} + } + + // TODO likely add common here: should check against the chain id in this container to prevent + // signing against the wrong chain id + sign(privateKey: Uint8Array): Blob4844Container & ECDSASignedInterface { + // TODO + return this as Blob4844Container & ECDSASignedInterface // Type return value to have v/r/s set + } +} diff --git a/packages/tx/src/dataContainers/feeMarket1559Container.ts b/packages/tx/src/dataContainers/feeMarket1559Container.ts new file mode 100644 index 00000000000..102836a3aac --- /dev/null +++ b/packages/tx/src/dataContainers/feeMarket1559Container.ts @@ -0,0 +1,111 @@ +import { RLP } from '@ethereumjs/rlp' +import { Address, bytesToBigInt, toBytes } from '@ethereumjs/util' + +import { Feature } from '../dataContainerTypes.js' +import { TransactionType } from '../types.js' +import { AccessLists } from '../util.js' + +import type { + AccessList2930Interface, + CreateContractInterface, + DefaultContainerInterface, + ECDSAMaybeSignedInterface, + ECDSASignedInterface, + FeeMarketInterface, + TxConstructorFields, + TxContainerMethods, +} from '../dataContainerTypes.js' +import type { AccessListBytes, TxOptions } from '../types.js' + +type TxType = TransactionType.FeeMarketEIP1559 + +const feeMarketFeatures = new Set([ + Feature.ECDSASignable, + Feature.FeeMarket, + Feature.AccessLists, +]) + +export class FeeMarket1559Container + implements + TxContainerMethods, + DefaultContainerInterface, + CreateContractInterface, + FeeMarketInterface, + ECDSAMaybeSignedInterface, + AccessList2930Interface +{ + public type: number = TransactionType.FeeMarketEIP1559 // Legacy tx type + + // Tx data part (part of the RLP) + public readonly maxFeePerGas: bigint + public readonly maxPriorityFeePerGas: bigint + public readonly nonce: bigint + public readonly gasLimit: bigint + public readonly value: bigint + public readonly data: Uint8Array + public readonly to: Address | null + public readonly accessList: AccessListBytes + public readonly chainId: bigint + + // Props only for signed txs + public readonly v?: bigint + public readonly r?: bigint + public readonly s?: bigint + + constructor(txData: TxConstructorFields[TxType], txOptions: TxOptions) { + const { nonce, gasLimit, to, value, data, v, r, s, maxPriorityFeePerGas, maxFeePerGas } = txData + + // Set the tx properties + const toB = toBytes(to === '' ? '0x' : to) + this.to = toB.length > 0 ? new Address(toB) : null + + this.nonce = bytesToBigInt(toBytes(nonce)) + this.gasLimit = bytesToBigInt(toBytes(gasLimit)) + this.value = bytesToBigInt(toBytes(value)) + this.data = toBytes(data === '' ? '0x' : data) + this.maxFeePerGas = bytesToBigInt(toBytes(maxFeePerGas)) + this.maxPriorityFeePerGas = bytesToBigInt(toBytes(maxPriorityFeePerGas)) + + // Set signature values (if the tx is signed) + + const vB = toBytes(v) + const rB = toBytes(r) + const sB = toBytes(s) + this.v = vB.length > 0 ? bytesToBigInt(vB) : undefined + this.r = rB.length > 0 ? bytesToBigInt(rB) : undefined + this.s = sB.length > 0 ? bytesToBigInt(sB) : undefined + + const { chainId, accessList } = txData + + // NOTE: previously there was a check against common's chainId, this is not here + // Common is not available in the tx container + // Likely, we should now check this chain id when signing the tx (to prevent people signing on the wrong chain) + this.chainId = bytesToBigInt(toBytes(chainId)) + + const accessListData = AccessLists.getAccessListData(accessList ?? []) + this.accessList = accessListData.accessList + } + + raw() { + // TODO + return [new Uint8Array(), new Uint8Array()] + } + serialize() { + return RLP.encode(this.raw()) + } + + supports(feature: Feature) { + return feeMarketFeatures.has(feature) + } + + toJSON() { + return {} + } + + // TODO likely add common here: should check against the chain id in this container to prevent + // signing against the wrong chain id + sign(privateKey: Uint8Array): FeeMarket1559Container & ECDSASignedInterface { + // TODO + return this as FeeMarket1559Container & ECDSASignedInterface // Type return value to have v/r/s set + } +} diff --git a/packages/tx/src/dataContainers/legacyContainer.ts b/packages/tx/src/dataContainers/legacyContainer.ts new file mode 100644 index 00000000000..fdbf6dc6364 --- /dev/null +++ b/packages/tx/src/dataContainers/legacyContainer.ts @@ -0,0 +1,91 @@ +import { RLP } from '@ethereumjs/rlp' +import { Address, bytesToBigInt, toBytes } from '@ethereumjs/util' + +import { Feature } from '../dataContainerTypes.js' +import { TransactionType } from '../types.js' + +import type { + CreateContractInterface, + DefaultContainerInterface, + ECDSAMaybeSignedInterface, + ECDSASignedInterface, + LegacyGasMarketInterface, + TxConstructorFields, + TxContainerMethods, +} from '../dataContainerTypes.js' +import type { TxOptions } from '../types.js' + +type TxType = TransactionType.Legacy + +const legacyFeatures = new Set([Feature.ECDSASignable, Feature.LegacyGasMarket]) + +export class LegacyDataContainer + implements + TxContainerMethods, + DefaultContainerInterface, + CreateContractInterface, + LegacyGasMarketInterface, + ECDSAMaybeSignedInterface +{ + public type: number = TransactionType.Legacy // Legacy tx type + + // Tx data part (part of the RLP) + public readonly gasPrice: bigint + public readonly nonce: bigint + public readonly gasLimit: bigint + public readonly value: bigint + public readonly data: Uint8Array + public readonly to: Address | null + + // Props only for signed txs + public readonly v?: bigint + public readonly r?: bigint + public readonly s?: bigint + + // TODO: verify if txOptions is necessary + // TODO (optimizing): for reach tx we auto-convert the input values to the target values (mostly bigints) + // Is this necessary? What if we need the unconverted values? Convert it on the fly? + constructor(txData: TxConstructorFields[TxType], txOptions: TxOptions) { + const { nonce, gasLimit, to, value, data, v, r, s } = txData + + // Set the tx properties + const toB = toBytes(to === '' ? '0x' : to) + this.to = toB.length > 0 ? new Address(toB) : null + + this.nonce = bytesToBigInt(toBytes(nonce)) + this.gasLimit = bytesToBigInt(toBytes(gasLimit)) + this.value = bytesToBigInt(toBytes(value)) + this.data = toBytes(data === '' ? '0x' : data) + this.gasPrice = bytesToBigInt(toBytes(txData.gasPrice)) + + // Set signature values (if the tx is signed) + + const vB = toBytes(v) + const rB = toBytes(r) + const sB = toBytes(s) + this.v = vB.length > 0 ? bytesToBigInt(vB) : undefined + this.r = rB.length > 0 ? bytesToBigInt(rB) : undefined + this.s = sB.length > 0 ? bytesToBigInt(sB) : undefined + } + + raw() { + // TODO + return [] + } + serialize() { + return RLP.encode(this.raw()) + } + + supports(feature: Feature) { + return legacyFeatures.has(feature) + } + + toJSON() { + return {} + } + + sign(privateKey: Uint8Array): LegacyDataContainer & ECDSASignedInterface { + // TODO + return this as LegacyDataContainer & ECDSASignedInterface // Type return value to have v/r/s set + } +} diff --git a/packages/tx/src/dataContainers/template.ts b/packages/tx/src/dataContainers/template.ts new file mode 100644 index 00000000000..1153b47d3bf --- /dev/null +++ b/packages/tx/src/dataContainers/template.ts @@ -0,0 +1,22 @@ +import { RLP } from '@ethereumjs/rlp' + +import type { TxDataContainer } from '../dataContainerTypes.js' +import type { NestedUint8Array } from '@ethereumjs/rlp' + +export abstract class TemplateDataContainer implements TxDataContainer { + type = -1 + + abstract raw(): NestedUint8Array + serialize() { + // Defaults to use RLP.encode + return RLP.encode(this.raw()) + } + + supports(/*feature: Feature*/) { + return false + } + + toJSON() { + return {} + } +} diff --git a/packages/tx/src/txWorker.ts b/packages/tx/src/txWorker.ts new file mode 100644 index 00000000000..8310cf66fa2 --- /dev/null +++ b/packages/tx/src/txWorker.ts @@ -0,0 +1,77 @@ +/** + * TxWorker.ts: helper methods to extract relevant data from + */ + +import { BIGINT_0 } from '@ethereumjs/util' + +import { Feature } from './dataContainerTypes.js' +import { AccessLists, AuthorizationLists } from './util.js' + +import type { + AccessListInterface, + AuthorizationListInterface, + CreateContractInterface, + DefaultContainerInterface, + TxContainerMethods, +} from './dataContainerTypes.js' +import type { Common } from '@ethereumjs/common' + +/** + * Gets the data gas part of the tx, this consists of calldata, access lists and authority lists + * @param tx + * @param common + */ +export function getDataGas( + tx: TxContainerMethods & DefaultContainerInterface & CreateContractInterface, + common: Common, +): bigint { + // Side note: can also do this method without the entire tx container and just use `tx.data` instead as param? + const txDataZero = common.param('txDataZeroGas') + const txDataNonZero = common.param('txDataNonZeroGas') + + let cost = BIGINT_0 + for (let i = 0; i < tx.data.length; i++) { + tx.data[i] === 0 ? (cost += txDataZero) : (cost += txDataNonZero) + } + + if ((tx.to === undefined || tx.to === null) && common.isActivatedEIP(3860)) { + const dataLength = BigInt(Math.ceil(tx.data.length / 32)) + const initCodeCost = common.param('initCodeWordGas') * dataLength + cost += initCodeCost + } + + if (tx.supports(Feature.AccessLists)) { + // TODO: figure out how to get rid of the "unknown" + // (Likely: mark a type with all tx interfaces and mark them all as "optional") + const eip2930tx = tx as unknown as AccessListInterface + cost += BigInt(AccessLists.getDataGasEIP2930(eip2930tx.accessList, common)) + } + + if (tx.supports(Feature.EOACode)) { + const eip7702tx = tx as unknown as AuthorizationListInterface + cost += BigInt(AuthorizationLists.getDataGasEIP7702(eip7702tx.authorizationList, common)) + } + + return cost +} + +/** + * Gets the intrinsic gas which is the minimal gas limit a tx should have to be valid + * @param tx + * @param common + */ +export function getIntrinsicGas( + tx: TxContainerMethods & DefaultContainerInterface & CreateContractInterface, + common: Common, +): bigint { + // NOTE: TxDataContainer & DefaultContainerInterface + // This is the tx data container class interface WITH the default tx params + let intrincisGas = BIGINT_0 + const txFee = common.param('txGas') + if (txFee) intrincisGas += txFee + if (common.gteHardfork('homestead') && (tx.to === undefined || tx.to === null)) { + const txCreationFee = common.param('txCreationGas') + if (txCreationFee) intrincisGas += txCreationFee + } + return intrincisGas + getDataGas(tx, common) +}