diff --git a/packages/tx/src/1559/tx.ts b/packages/tx/src/1559/tx.ts index 00423967c81..f4d845e3e83 100644 --- a/packages/tx/src/1559/tx.ts +++ b/packages/tx/src/1559/tx.ts @@ -11,8 +11,8 @@ import { 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 { getBaseJSON, sharedConstructor, valueBoundaryCheck } from '../features/util.js' import { TransactionType } from '../types.js' import { AccessLists } from '../util.js' @@ -86,7 +86,7 @@ export class FeeMarket1559Tx implements TransactionInterface MAX_INTEGER) { const msg = Legacy.errorMsg(this, 'gasLimit * gasPrice cannot exceed MAX_INTEGER') @@ -305,7 +305,7 @@ export class AccessList2930Tx implements TransactionInterface SECP256K1_ORDER_DIV_2) { + /*const msg = errorMsg( + tx, + 'Invalid Signature: s-values greater than secp256k1n/2 are considered invalid', + )*/ + throw new Error('Invalid Signature: s-values greater than secp256k1n/2 are considered invalid') + } +} + +export function getSenderPublicKey( + tx: ECDSASignableInterface, + getHashedMessageToSign: (tx: TxInterface) => Uint8Array, +): Uint8Array { + if (tx.cache.senderPubKey !== undefined) { + return tx.cache.senderPubKey + } + + const msgHash = getHashedMessageToSign(tx) + + const { v, r, s } = tx + + validateHighS(tx) + + try { + const ecrecoverFunction = tx.common.customCrypto.ecrecover ?? ecrecover + const sender = ecrecoverFunction( + msgHash, + v!, + bigIntToUnpaddedBytes(r!), + bigIntToUnpaddedBytes(s!), + tx.activeCapabilities.includes(Capability.EIP155ReplayProtection) + ? tx.common.chainId() + : undefined, + ) + if (Object.isFrozen(tx)) { + tx.cache.senderPubKey = sender + } + return sender + } catch (e: any) { + //const msg = errorMsg(tx, 'Invalid Signature') // TODO: generic errorMsg handling? + throw new Error('Invalid Signature') + } +} + +/** + * Determines if the signature is valid + */ +export function verifySignature( + tx: ECDSASignableInterface, + getMessageToVerifySignature: (tx: TxInterface) => Uint8Array, +): boolean { + try { + // Main signature verification is done in `getSenderPublicKey()` + const publicKey = getSenderPublicKey(tx, getMessageToVerifySignature) + return unpadBytes(publicKey).length !== 0 + } catch (e: any) { + return false + } +} + +/** + * Validates the transaction signature and minimum gas requirements. + * @returns {boolean} true if the transaction is valid, false otherwise + */ +// TODO: we can likely remove this method, it is used nowhere (?) +/*export function isValid(tx: LegacyTxInterface): boolean { + const errors = tx.getValidationErrors() + + return errors.length === 0 +}*/ + +export function getSenderAddress(tx: ECDSASignableInterface): Address { + return new Address(publicToAddress(getSenderPublicKey(tx))) +} diff --git a/packages/tx/src/capabilities/eip1559.ts b/packages/tx/src/capabilities/gasMarket/feeGasMarket.ts similarity index 71% rename from packages/tx/src/capabilities/eip1559.ts rename to packages/tx/src/capabilities/gasMarket/feeGasMarket.ts index 7c9047ff9aa..1446b7763b4 100644 --- a/packages/tx/src/capabilities/eip1559.ts +++ b/packages/tx/src/capabilities/gasMarket/feeGasMarket.ts @@ -1,6 +1,8 @@ -import type { EIP1559CompatibleTx } from '../types.js' +import type { EIP1559CompatibleTx, FeeGasMarketInterface } from '../../types.js' -export function getUpfrontCost(tx: EIP1559CompatibleTx, baseFee: bigint): bigint { +// NOTE: this file is currently essentially the "FeeMarket" capability + +export function getUpfrontCost(tx: FeeGasMarketInterface, baseFee: bigint): bigint { const prio = tx.maxPriorityFeePerGas const maxBase = tx.maxFeePerGas - baseFee const inclusionFeePerGas = prio < maxBase ? prio : maxBase @@ -9,7 +11,7 @@ export function getUpfrontCost(tx: EIP1559CompatibleTx, baseFee: bigint): bigint } export function getEffectivePriorityFee( - tx: EIP1559CompatibleTx, + tx: FeeGasMarketInterface, baseFee: bigint | undefined, ): bigint { if (baseFee === undefined || baseFee > tx.maxFeePerGas) { diff --git a/packages/tx/src/capabilities/gasMarket/legacyGasMarket.ts b/packages/tx/src/capabilities/gasMarket/legacyGasMarket.ts new file mode 100644 index 00000000000..2f04f808757 --- /dev/null +++ b/packages/tx/src/capabilities/gasMarket/legacyGasMarket.ts @@ -0,0 +1,28 @@ +import { Capability } from '../../types.js' + +import type { LegacyGasMarketInterface } from '../../types.js' + +export function getUpfrontCost(tx: LegacyGasMarketInterface): bigint { + if (!tx.activeCapabilities.includes(Capability.LegacyGasMarket)) { + throw new Error('Tx does not support legacy gas market') + } + return tx.gasLimit * tx.gasPrice + tx.value +} + +export function getEffectivePriorityFee( + tx: LegacyGasMarketInterface, + baseFee: bigint | undefined, +): bigint { + if (!tx.activeCapabilities.includes(Capability.LegacyGasMarket)) { + throw new Error('Tx does not support legacy gas market') + } + if (baseFee !== undefined && baseFee > tx.gasPrice) { + throw new Error('Tx cannot pay baseFee') + } + + if (baseFee === undefined) { + return tx.gasPrice + } + + return tx.gasPrice - baseFee +} diff --git a/packages/tx/src/features/util.ts b/packages/tx/src/capabilities/generic.ts similarity index 93% rename from packages/tx/src/features/util.ts rename to packages/tx/src/capabilities/generic.ts index 36bd9546187..9b8cbecbc88 100644 --- a/packages/tx/src/features/util.ts +++ b/packages/tx/src/capabilities/generic.ts @@ -12,7 +12,16 @@ import { import { paramsTx } from '../params.js' import { checkMaxInitCodeSize, validateNotArray } from '../util.js' -import type { TransactionInterface, TransactionType, TxData, TxOptions } from '../types.js' +import type { + ContractCreationInterface, + LegacyTxInterface, + ToInterface, + TransactionInterface, + TransactionType, + TxData, + TxInterface, + TxOptions, +} from '../types.js' export function getCommon(common?: Common): Common { return common?.copy() ?? new Common({ chain: Mainnet }) @@ -137,3 +146,7 @@ export function getBaseJSON(tx: TransactionInterface) { yParity: tx.v === 0n || tx.v === 1n ? bigIntToHex(tx.v) : undefined, } } + +export function toCreationAddress(tx: ContractCreationInterface | ToInterface): boolean { + return tx.to === undefined || tx.to.bytes.length === 0 +} diff --git a/packages/tx/src/capabilities/legacy.ts b/packages/tx/src/capabilities/legacy.ts index be560641687..e69de29bb2d 100644 --- a/packages/tx/src/capabilities/legacy.ts +++ b/packages/tx/src/capabilities/legacy.ts @@ -1,283 +0,0 @@ -import { - Address, - BIGINT_0, - SECP256K1_ORDER_DIV_2, - bigIntToUnpaddedBytes, - bytesToHex, - ecrecover, - ecsign, - publicToAddress, - unpadBytes, -} from '@ethereumjs/util' -import { keccak256 } from 'ethereum-cryptography/keccak.js' - -import { Capability, TransactionType } from '../types.js' - -import type { LegacyTxInterface, Transaction } 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 - } else { - return true - } -} - -/** - * The amount of gas paid for the data in this tx - */ -export function getDataGas(tx: LegacyTxInterface, extraCost?: bigint): bigint { - if (tx.cache.dataFee && tx.cache.dataFee.hardfork === tx.common.hardfork()) { - return tx.cache.dataFee.value - } - - const txDataZero = tx.common.param('txDataZeroGas') - const txDataNonZero = tx.common.param('txDataNonZeroGas') - - let cost = extraCost ?? 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) && tx.common.isActivatedEIP(3860)) { - const dataLength = BigInt(Math.ceil(tx.data.length / 32)) - const initCodeCost = tx.common.param('initCodeWordGas') * dataLength - cost += initCodeCost - } - - if (Object.isFrozen(tx)) { - tx.cache.dataFee = { - value: cost, - hardfork: tx.common.hardfork(), - } - } - - return cost -} - -/** - * The minimum gas limit which the tx to have to be valid. - * This covers costs as the standard fee (21000 gas), the data fee (paid for each calldata byte), - * the optional creation fee (if the transaction creates a contract), and if relevant the gas - * to be paid for access lists (EIP-2930) and authority lists (EIP-7702). - */ -export function getIntrinsicGas(tx: LegacyTxInterface): bigint { - const txFee = tx.common.param('txGas') - let fee = tx.getDataGas() - if (txFee) fee += txFee - if (tx.common.gteHardfork('homestead') && tx.toCreationAddress()) { - const txCreationFee = tx.common.param('txCreationGas') - if (txCreationFee) fee += txCreationFee - } - return fee -} - -export function toCreationAddress(tx: LegacyTxInterface): boolean { - return tx.to === undefined || tx.to.bytes.length === 0 -} - -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) - } - - const keccakFunction = tx.common.customCrypto.keccak256 ?? keccak256 - - if (Object.isFrozen(tx)) { - if (!tx.cache.hash) { - tx.cache.hash = keccakFunction(tx.serialize()) - } - return tx.cache.hash - } - - return keccakFunction(tx.serialize()) -} - -/** - * 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: LegacyTxInterface): void { - const { s } = tx - if (tx.common.gteHardfork('homestead') && s !== undefined && s > SECP256K1_ORDER_DIV_2) { - const msg = errorMsg( - tx, - 'Invalid Signature: s-values greater than secp256k1n/2 are considered invalid', - ) - throw new Error(msg) - } -} - -export function getSenderPublicKey(tx: LegacyTxInterface): Uint8Array { - if (tx.cache.senderPubKey !== undefined) { - return tx.cache.senderPubKey - } - - const msgHash = tx.getMessageToVerifySignature() - - const { v, r, s } = tx - - validateHighS(tx) - - try { - const ecrecoverFunction = tx.common.customCrypto.ecrecover ?? ecrecover - const sender = ecrecoverFunction( - msgHash, - v!, - bigIntToUnpaddedBytes(r!), - bigIntToUnpaddedBytes(s!), - tx.supports(Capability.EIP155ReplayProtection) ? tx.common.chainId() : undefined, - ) - if (Object.isFrozen(tx)) { - tx.cache.senderPubKey = sender - } - return sender - } catch (e: any) { - const msg = errorMsg(tx, 'Invalid Signature') - throw new Error(msg) - } -} - -export function getEffectivePriorityFee(gasPrice: bigint, baseFee: bigint | undefined): bigint { - if (baseFee !== undefined && baseFee > gasPrice) { - throw new Error('Tx cannot pay baseFee') - } - - if (baseFee === undefined) { - return gasPrice - } - - return gasPrice - baseFee -} - -/** - * Validates the transaction signature and minimum gas requirements. - * @returns {string[]} an array of error strings - */ -export function getValidationErrors(tx: LegacyTxInterface): string[] { - const errors = [] - - if (tx.isSigned() && !tx.verifySignature()) { - errors.push('Invalid Signature') - } - - if (tx.getIntrinsicGas() > tx.gasLimit) { - errors.push(`gasLimit is too low. given ${tx.gasLimit}, need at least ${tx.getIntrinsicGas()}`) - } - - return errors -} - -/** - * Validates the transaction signature and minimum gas requirements. - * @returns {boolean} true if the transaction is valid, false otherwise - */ -export function isValid(tx: LegacyTxInterface): boolean { - const errors = tx.getValidationErrors() - - return errors.length === 0 -} - -/** - * Determines if the signature is valid - */ -export function verifySignature(tx: LegacyTxInterface): boolean { - try { - // Main signature verification is done in `getSenderPublicKey()` - const publicKey = tx.getSenderPublicKey() - return unpadBytes(publicKey).length !== 0 - } catch (e: any) { - return false - } -} - -/** - * Returns the sender's address - */ -export function getSenderAddress(tx: LegacyTxInterface): Address { - return new Address(publicToAddress(tx.getSenderPublicKey())) -} - -/** - * Signs a transaction. - * - * Note that the signed tx is returned as a new object, - * use as follows: - * ```javascript - * const signedTx = tx.sign(privateKey) - * ``` - */ -export function sign(tx: LegacyTxInterface, privateKey: Uint8Array): Transaction[TransactionType] { - if (privateKey.length !== 32) { - // TODO figure out this errorMsg logic how this diverges on other txs - const msg = errorMsg(tx, 'Private key must be 32 bytes in length.') - throw new Error(msg) - } - - // TODO (Jochem, 05 nov 2024): figure out what this hack does and clean it up - - // Hack for the constellation that we have got a legacy tx after spuriousDragon with a non-EIP155 conforming signature - // and want to recreate a signature (where EIP155 should be applied) - // Leaving this hack lets the legacy.spec.ts -> sign(), verifySignature() test fail - // 2021-06-23 - let hackApplied = false - if ( - tx.type === TransactionType.Legacy && - tx.common.gteHardfork('spuriousDragon') && - !tx.supports(Capability.EIP155ReplayProtection) - ) { - // cast as any to edit the protected `activeCapabilities` - ;(tx as any).activeCapabilities.push(Capability.EIP155ReplayProtection) - hackApplied = true - } - - const msgHash = tx.getHashedMessageToSign() - const ecSignFunction = tx.common.customCrypto?.ecsign ?? ecsign - const { v, r, s } = ecSignFunction(msgHash, privateKey) - const signedTx = tx.addSignature(v, r, s, true) - - // Hack part 2 - if (hackApplied) { - // cast as any to edit the protected `activeCapabilities` - const index = (tx).activeCapabilities.indexOf(Capability.EIP155ReplayProtection) - if (index > -1) { - // cast as any to edit the protected `activeCapabilities` - ;(tx).activeCapabilities.splice(index, 1) - } - } - - return signedTx -} - -// TODO maybe move this to shared methods (util.ts in features) -export function getSharedErrorPostfix(tx: LegacyTxInterface) { - let hash = '' - try { - hash = tx.isSigned() ? bytesToHex(tx.hash()) : 'not available (unsigned)' - } catch (e: any) { - hash = 'error' - } - let isSigned = '' - try { - isSigned = tx.isSigned().toString() - } catch (e: any) { - hash = 'error' - } - let hf = '' - try { - hf = tx.common.hardfork() - } catch (e: any) { - hf = 'error' - } - - let postfix = `tx type=${tx.type} hash=${hash} nonce=${tx.nonce} value=${tx.value} ` - postfix += `signed=${isSigned} hf=${hf}` - - return postfix -} diff --git a/packages/tx/src/capabilities/legacyTransaction.ts b/packages/tx/src/capabilities/legacyTransaction.ts new file mode 100644 index 00000000000..14f96fd1b4b --- /dev/null +++ b/packages/tx/src/capabilities/legacyTransaction.ts @@ -0,0 +1,366 @@ +import { RLP } from '@ethereumjs/rlp' +import { + BIGINT_0, + BIGINT_2, + BIGINT_8, + bigIntToUnpaddedBytes, + bytesToBigInt, + bytesToHex, + ecsign, + toBytes, + unpadBytes, +} from '@ethereumjs/util' +import { keccak256 } from 'ethereum-cryptography/keccak' + +import { createLegacyTx } from '../legacy/constructors.js' +import { Capability, TransactionType, isLegacyTx } from '../types.js' + +import { + getSenderPublicKey as _getSenderPublicKey, + isSigned, + verifySignature, +} from './ecdsaSignable.js' +import { toCreationAddress } from './generic.js' + +import type { LegacyTxInterface, TxInterface, TxValuesArray } from '../types.js' + +// TODO: how to handle these tx-specific error msgs? +function errorMsg(tx: LegacyTxInterface, msg: string) { + return `${msg} (${errorStr(tx)})` +} + +/** + * The amount of gas paid for the data in this tx + */ +export function getDataGas(tx: LegacyTxInterface, extraCost?: bigint): bigint { + if (tx.cache.dataFee && tx.cache.dataFee.hardfork === tx.common.hardfork()) { + return tx.cache.dataFee.value + } + + const txDataZero = tx.common.param('txDataZeroGas') + const txDataNonZero = tx.common.param('txDataNonZeroGas') + + let cost = extraCost ?? 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) && tx.common.isActivatedEIP(3860)) { + const dataLength = BigInt(Math.ceil(tx.data.length / 32)) + const initCodeCost = tx.common.param('initCodeWordGas') * dataLength + cost += initCodeCost + } + + if (Object.isFrozen(tx)) { + tx.cache.dataFee = { + value: cost, + hardfork: tx.common.hardfork(), + } + } + + return cost +} + +/** + * The minimum gas limit which the tx to have to be valid. + * This covers costs as the standard fee (21000 gas), the data fee (paid for each calldata byte), + * the optional creation fee (if the transaction creates a contract), and if relevant the gas + * to be paid for access lists (EIP-2930) and authority lists (EIP-7702). + */ +export function getIntrinsicGas(tx: LegacyTxInterface): bigint { + const txFee = tx.common.param('txGas') + let fee = getDataGas(tx) + if (txFee) fee += txFee + if (tx.common.gteHardfork('homestead') && toCreationAddress(tx)) { + const txCreationFee = tx.common.param('txCreationGas') + if (txCreationFee) fee += txCreationFee + } + return fee +} + +// TODO maybe move this to shared methods (util.ts in features) +export function getSharedErrorPostfix(tx: LegacyTxInterface) { + let hashStr = '' + try { + hashStr = isSigned(tx) ? bytesToHex(hash(tx)) : 'not available (unsigned)' + } catch (e: any) { + hashStr = 'error' + } + let signStr = '' + try { + signStr = isSigned(tx).toString() + } catch (e: any) { + hashStr = 'error' + } + let hf = '' + try { + hf = tx.common.hardfork() + } catch (e: any) { + hf = 'error' + } + + let postfix = `tx type=${tx.type} hash=${hashStr} nonce=${tx.nonce} value=${tx.value} ` + postfix += `signed=${signStr} hf=${hf}` + + return postfix +} + +/** + * Return a compact error string representation of the object + */ +export function errorStr(tx: LegacyTxInterface) { + let errorStr = getSharedErrorPostfix(tx) + errorStr += ` gasPrice=${tx.gasPrice}` + return errorStr +} + +/** + * Returns the hashed serialized unsigned tx, which can be used + * to sign the transaction (e.g. for sending to a hardware wallet). + */ +export function getHashedMessageToSign(tx: LegacyTxInterface) { + if (!isLegacyTx(tx)) { + throw new Error('Transaction is not a legacy tx') + } + const message = getMessageToSign(tx) + const keccak = tx.common.customCrypto.keccak256 ?? keccak256 + return keccak(RLP.encode(message)) +} + +/** + * Computes a sha3-256 hash which can be used to verify the signature + */ +export function getMessageToVerifySignature(tx: LegacyTxInterface) { + if (!isLegacyTx(tx)) { + throw new Error('Transaction is not a legacy tx') + } + if (!isSigned(tx)) { + const msg = errorMsg(tx, 'This transaction is not signed') + throw new Error(msg) + } + return getHashedMessageToSign(tx) +} + +/** + * Returns the raw unsigned tx, which can be used + * to sign the transaction (e.g. for sending to a hardware wallet). + * + * Note: the raw message message format for the legacy tx is not RLP encoded + * and you might need to do yourself with: + * + * ```javascript + * import { RLP } from '@ethereumjs/rlp' + * const message = tx.getMessageToSign() + * const serializedMessage = RLP.encode(message)) // use this for the HW wallet input + * ``` + */ +export function getMessageToSign(tx: LegacyTxInterface): Uint8Array[] { + if (!isLegacyTx(tx)) { + throw new Error('Transaction is not a legacy tx') + } + const message = [ + bigIntToUnpaddedBytes(tx.nonce), + bigIntToUnpaddedBytes(tx.gasPrice), + bigIntToUnpaddedBytes(tx.gasLimit), + tx.to !== undefined ? tx.to.bytes : new Uint8Array(0), + bigIntToUnpaddedBytes(tx.value), + tx.data, + ] + + if (tx.activeCapabilities.includes(Capability.EIP155ReplayProtection)) { + message.push(bigIntToUnpaddedBytes(tx.common.chainId())) + message.push(unpadBytes(toBytes(0))) + message.push(unpadBytes(toBytes(0))) + } + + return message +} + +/** + * Returns a Uint8Array Array of the raw Bytes of the legacy transaction, in order. + * + * Format: `[nonce, gasPrice, gasLimit, to, value, data, v, r, s]` + * + * For legacy txs this is also the correct format to add transactions + * to a block with {@link createBlockFromBytesArray} (use the `serialize()` method + * for typed txs). + * + * For an unsigned tx this method returns the empty Bytes values + * for the signature parameters `v`, `r` and `s`. For an EIP-155 compliant + * representation have a look at {@link Transaction.getMessageToSign}. + */ +export function raw(tx: LegacyTxInterface): TxValuesArray[TransactionType.Legacy] { + if (!isLegacyTx(tx)) { + throw new Error('Transaction is not a legacy tx') + } + return [ + bigIntToUnpaddedBytes(tx.nonce), + bigIntToUnpaddedBytes(tx.gasPrice), + bigIntToUnpaddedBytes(tx.gasLimit), + tx.to !== undefined ? tx.to.bytes : new Uint8Array(0), + bigIntToUnpaddedBytes(tx.value), + tx.data, + tx.v !== undefined ? bigIntToUnpaddedBytes(tx.v) : new Uint8Array(0), + tx.r !== undefined ? bigIntToUnpaddedBytes(tx.r) : new Uint8Array(0), + tx.s !== undefined ? bigIntToUnpaddedBytes(tx.s) : new Uint8Array(0), + ] +} + +/** + * Returns the serialized encoding of the legacy transaction. + * + * Format: `rlp([nonce, gasPrice, gasLimit, to, value, data, v, r, s])` + * + * For an unsigned tx this method uses the empty Uint8Array values for the + * signature parameters `v`, `r` and `s` for encoding. For an EIP-155 compliant + * representation for external signing use {@link Transaction.getMessageToSign}. + */ +export function serialize(tx: LegacyTxInterface): Uint8Array { + if (!isLegacyTx(tx)) { + throw new Error('Transaction is not a legacy tx') + } + return RLP.encode(raw(tx)) +} + +/** + * Signs a transaction. + * + * Note that the signed tx is returned as a new object, + * use as follows: + * ```javascript + * const signedTx = tx.sign(privateKey) + * ``` + */ +export function sign(tx: LegacyTxInterface, privateKey: Uint8Array): LegacyTxInterface { + if (!isLegacyTx(tx)) { + throw new Error('Transaction is not a legacy tx') + } + if (privateKey.length !== 32) { + throw new Error('Private key must be 32 bytes in length.') + } + + // TODO (Jochem, 05 nov 2024): figure out what this hack does and clean it up + + // Hack for the constellation that we have got a legacy tx after spuriousDragon with a non-EIP155 conforming signature + // and want to recreate a signature (where EIP155 should be applied) + // Leaving this hack lets the legacy.spec.ts -> sign(), verifySignature() test fail + // 2021-06-23 + let hackApplied = false + if ( + tx.type === TransactionType.Legacy && + tx.common.gteHardfork('spuriousDragon') && + !tx.activeCapabilities.includes(Capability.EIP155ReplayProtection) + ) { + // cast as any to edit the protected `activeCapabilities` + ;(tx as any).activeCapabilities.push(Capability.EIP155ReplayProtection) + hackApplied = true + } + + const msgHash = getHashedMessageToSign(tx) + const ecSignFunction = tx.common.customCrypto?.ecsign ?? ecsign + const { v, r, s } = ecSignFunction(msgHash, privateKey) + const signedTx = addSignature(tx, v, r, s, true) + + // Hack part 2 + if (hackApplied) { + // cast as any to edit the protected `activeCapabilities` + const index = (tx).activeCapabilities.indexOf(Capability.EIP155ReplayProtection) + if (index > -1) { + // cast as any to edit the protected `activeCapabilities` + ;(tx).activeCapabilities.splice(index, 1) + } + } + + return signedTx +} + +// TODO: this should likely be added to `ecdsaSignable.ts` +// Then this would need an extra param, which would represent the constructor of the tx (here: `createLegacyTx`) +export function addSignature( + tx: LegacyTxInterface, + v: bigint, + r: Uint8Array | bigint, + s: Uint8Array | bigint, + convertV: boolean = false, +): LegacyTxInterface { + r = toBytes(r) + s = toBytes(s) + if (convertV && tx.activeCapabilities.includes(Capability.EIP155ReplayProtection)) { + v += tx.common.chainId() * BIGINT_2 + BIGINT_8 + } + + const opts = { ...tx.txOptions, common: tx.common } + + return createLegacyTx( + { + nonce: tx.nonce, + gasPrice: tx.gasPrice, + gasLimit: tx.gasLimit, + to: tx.to, + value: tx.value, + data: tx.data, + v, + r: bytesToBigInt(r), + s: bytesToBigInt(s), + }, + opts, + ) +} + +// TODO: move this to generic and replace the LegacyTxInterface with something better +export function hash(tx: LegacyTxInterface): Uint8Array { + if (!isSigned(tx)) { + const msg = errorMsg(tx, 'Cannot call hash method if transaction is not signed') + throw new Error(msg) + } + + const keccakFunction = tx.common.customCrypto.keccak256 ?? keccak256 + + if (Object.isFrozen(tx)) { + if (!tx.cache.hash) { + tx.cache.hash = keccakFunction(serialize(tx)) + } + return tx.cache.hash + } + + return keccakFunction(serialize(tx)) +} + +export function getSenderPublicKey(tx: LegacyTxInterface) { + return _getSenderPublicKey(tx, getHashedMessageToSign as (tx: TxInterface) => Uint8Array) +} + +/** + * Validates the transaction signature and minimum gas requirements. + * @returns {string[]} an array of error strings + */ +export function getValidationErrors(tx: LegacyTxInterface): string[] { + const errors = [] + + if ( + isSigned(tx) && + !verifySignature(tx, getHashedMessageToSign as (tx: TxInterface) => Uint8Array) + ) { + errors.push('Invalid Signature') + } + + if (getIntrinsicGas(tx) > tx.gasLimit) { + errors.push(`gasLimit is too low. given ${tx.gasLimit}, need at least ${getIntrinsicGas(tx)}`) + } + + return errors +} + +export function getUpfrontCost(tx: LegacyTxInterface): bigint { + return tx.gasLimit * tx.gasPrice + tx.value +} + +export function toJSON(tx: LegacyTxInterface): JSONTx { + // TODO this is just copied. Make this execution-api compliant + + const baseJSON = Generic.getBaseJSON(tx) as JSONTx + // TODO: fix type error below + baseJSON.gasPrice = bigIntToHex(tx.gasPrice) + + return baseJSON +} diff --git a/packages/tx/src/legacy/tx.ts b/packages/tx/src/legacy/tx.ts index a576f4f7ff7..92d9f9c2070 100644 --- a/packages/tx/src/legacy/tx.ts +++ b/packages/tx/src/legacy/tx.ts @@ -11,8 +11,8 @@ import { } from '@ethereumjs/util' import { keccak256 } from 'ethereum-cryptography/keccak.js' +import * as Generic from '../capabilities/generic.js' import * as Legacy from '../capabilities/legacy.js' -import { getBaseJSON, sharedConstructor, valueBoundaryCheck } from '../features/util.js' import { paramsTx } from '../index.js' import { Capability, TransactionType } from '../types.js' @@ -110,7 +110,7 @@ export class LegacyTx implements TransactionInterface { * e.g. 1559 (fee market) and 2930 (access lists) * for FeeMarket1559Tx objects */ - protected activeCapabilities: number[] = [] + public activeCapabilities: number[] = [] // TEMP: public, interface needs access to `activeCapabilities` /** * This constructor takes the values, validates them, assigns them and freezes the object. @@ -120,10 +120,10 @@ export class LegacyTx implements TransactionInterface { * varying data types. */ public constructor(txData: TxData, opts: TxOptions = {}) { - sharedConstructor(this, txData, opts) + Generic.sharedConstructor(this, txData, opts) this.gasPrice = bytesToBigInt(toBytes(txData.gasPrice)) - valueBoundaryCheck({ gasPrice: this.gasPrice }) + Generic.valueBoundaryCheck({ gasPrice: this.gasPrice }) // Everything from BaseTransaction done here this.common.updateParams(opts.params ?? paramsTx) // TODO should this move higher? @@ -205,17 +205,7 @@ export class LegacyTx implements TransactionInterface { * representation have a look at {@link Transaction.getMessageToSign}. */ raw(): TxValuesArray { - return [ - bigIntToUnpaddedBytes(this.nonce), - bigIntToUnpaddedBytes(this.gasPrice), - bigIntToUnpaddedBytes(this.gasLimit), - this.to !== undefined ? this.to.bytes : new Uint8Array(0), - bigIntToUnpaddedBytes(this.value), - this.data, - this.v !== undefined ? bigIntToUnpaddedBytes(this.v) : new Uint8Array(0), - this.r !== undefined ? bigIntToUnpaddedBytes(this.r) : new Uint8Array(0), - this.s !== undefined ? bigIntToUnpaddedBytes(this.s) : new Uint8Array(0), - ] + return Legacy.raw(this) } /** @@ -228,7 +218,7 @@ export class LegacyTx implements TransactionInterface { * representation for external signing use {@link Transaction.getMessageToSign}. */ serialize(): Uint8Array { - return RLP.encode(this.raw()) + return Legacy.serialize(this) } /** @@ -245,22 +235,7 @@ export class LegacyTx implements TransactionInterface { * ``` */ getMessageToSign(): Uint8Array[] { - const message = [ - bigIntToUnpaddedBytes(this.nonce), - bigIntToUnpaddedBytes(this.gasPrice), - bigIntToUnpaddedBytes(this.gasLimit), - this.to !== undefined ? this.to.bytes : new Uint8Array(0), - bigIntToUnpaddedBytes(this.value), - this.data, - ] - - if (this.supports(Capability.EIP155ReplayProtection)) { - message.push(bigIntToUnpaddedBytes(this.common.chainId())) - message.push(unpadBytes(toBytes(0))) - message.push(unpadBytes(toBytes(0))) - } - - return message + return Legacy.getMessageToSign(this) } /** @@ -268,8 +243,7 @@ export class LegacyTx implements TransactionInterface { * to sign the transaction (e.g. for sending to a hardware wallet). */ getHashedMessageToSign() { - const message = this.getMessageToSign() - return this.keccakFunction(RLP.encode(message)) + return Legacy.getHashedMessageToSign(this) } /** @@ -300,7 +274,7 @@ export class LegacyTx implements TransactionInterface { * The up front amount that an account must have for this transaction to be valid */ getUpfrontCost(): bigint { - return this.gasLimit * this.gasPrice + this.value + return Legacy.getUpfrontCost(this) } /** @@ -317,11 +291,7 @@ export class LegacyTx implements TransactionInterface { * Computes a sha3-256 hash which can be used to verify the signature */ getMessageToVerifySignature() { - if (!this.isSigned()) { - const msg = Legacy.errorMsg(this, 'This transaction is not signed') - throw new Error(msg) - } - return this.getHashedMessageToSign() + return Legacy.getMessageToVerifySignature(this) } /** @@ -337,40 +307,14 @@ export class LegacyTx implements TransactionInterface { s: Uint8Array | bigint, convertV: boolean = false, ): LegacyTx { - r = toBytes(r) - s = toBytes(s) - if (convertV && this.supports(Capability.EIP155ReplayProtection)) { - v += this.common.chainId() * BIGINT_2 + BIGINT_8 - } - - const opts = { ...this.txOptions, common: this.common } - - return createLegacyTx( - { - nonce: this.nonce, - gasPrice: this.gasPrice, - gasLimit: this.gasLimit, - to: this.to, - value: this.value, - data: this.data, - v, - r: bytesToBigInt(r), - s: bytesToBigInt(s), - }, - opts, - ) + return Legacy.addSignature(this, v, r, s, convertV) } /** * Returns an object with the JSON representation of the transaction. */ toJSON(): JSONTx { - // TODO this is just copied. Make this execution-api compliant - - const baseJSON = getBaseJSON(this) as JSONTx - baseJSON.gasPrice = bigIntToHex(this.gasPrice) - - return baseJSON + return Legacy.toJSON(this) } getValidationErrors(): string[] { @@ -397,8 +341,6 @@ export class LegacyTx implements TransactionInterface { * Return a compact error string representation of the object */ public errorStr() { - let errorStr = Legacy.getSharedErrorPostfix(this) - errorStr += ` gasPrice=${this.gasPrice}` - return errorStr + return Legacy.errorStr(this) } } diff --git a/packages/tx/src/types.ts b/packages/tx/src/types.ts index 16f442da16c..cf3d26599e7 100644 --- a/packages/tx/src/types.ts +++ b/packages/tx/src/types.ts @@ -1,4 +1,4 @@ -import { bytesToBigInt, toBytes } from '@ethereumjs/util' +import { BIGINT_0, BIGINT_1, bytesToBigInt, toBytes } from '@ethereumjs/util' import type { FeeMarket1559Tx } from './1559/tx.js' import type { AccessList2930Tx } from './2930/tx.js' @@ -24,6 +24,19 @@ export enum Capability { */ EIP155ReplayProtection = 155, + /** + * The tx supports the "legacy gas market": it has a `gasPrice` property + */ + LegacyGasMarket = 'LegacyGasMarket', + + /** + * The tx supports the "fee gas market": it has the `maxPriorityFeePerGas` and `maxFeePerGas` properties + */ + FeeGasMarket = 'FeeGasMarket', + + // Below here are tx-specfic Capabilities, used to distinguish transactions from other transactions + // These are used in methods such as `raw` + /** * Tx supports EIP-1559 gas fee market mechanism * See: [1559](https://eips.ethereum.org/EIPS/eip-1559) Fee Market EIP @@ -42,6 +55,12 @@ export enum Capability { */ EIP2930AccessLists = 2930, + /** + * Tx supports blobs generation as defined in EIP-4844 + * See: [4844](https://eips.ethereum.org/EIPS/eip-4844) Access Lists EIP + */ + EIP4844Blobs = 4844, + /** * Tx supports setting EOA code * See [EIP-7702](https://eips.ethereum.org/EIPS/eip-7702) @@ -165,26 +184,61 @@ export interface Transaction { export type TypedTransaction = Transaction[TransactionType] -export function isLegacyTx(tx: TypedTransaction): tx is LegacyTx { +export function isLegacyTx(tx: TxInterface): tx is LegacyTxInterface { return tx.type === TransactionType.Legacy } -export function isAccessList2930Tx(tx: TypedTransaction): tx is AccessList2930Tx { +export function isAccessList2930Tx(tx: TxInterface): tx is EIP2930CompatibleTx { return tx.type === TransactionType.AccessListEIP2930 } -export function isFeeMarket1559Tx(tx: TypedTransaction): tx is FeeMarket1559Tx { +export function isFeeMarket1559Tx(tx: TxInterface): tx is EIP1559CompatibleTx { return tx.type === TransactionType.FeeMarketEIP1559 } -export function isBlob4844Tx(tx: TypedTransaction): tx is Blob4844Tx { +export function isBlob4844Tx(tx: TxInterface): tx is EIP4844CompatibleTx { return tx.type === TransactionType.BlobEIP4844 } -export function isEOACode7702Tx(tx: TypedTransaction): tx is EOACode7702Tx { +export function isEOACode7702Tx(tx: TxInterface): tx is EIP7702CompatibleTx { return tx.type === TransactionType.EOACodeEIP7702 } +// Temp interface to replace TransactionInterface +export interface TxInterface { + readonly type: number + readonly cache: TransactionCache + readonly txOptions?: any // Placeholder for the saved "txOptions" when constructing a tx + readonly common: Common // TODO: remove Common from tx interfaces + // TODO: make this a Set. `supports()` method is removed in favour of just using a set. + readonly activeCapabilities: Capability[] // Necessary to determine the capabilities of the transaction in the respective methods +} + +export interface ECDSASignableInterface extends TxInterface { + readonly v?: bigint + readonly r?: bigint + readonly s?: bigint +} + +export interface LegacyGasMarketInterface extends TxInterface { + readonly gasLimit: bigint + readonly gasPrice: bigint + readonly value: bigint +} + +export interface FeeGasMarketInterface extends TxInterface { + readonly maxPriorityFeePerGas: bigint + readonly maxFeePerGas: bigint + readonly gasLimit: bigint + readonly value: bigint +} + +export interface FeeGasMarketInterface extends TxInterface { + readonly maxPriorityFeePerGas: bigint + readonly maxFeePerGas: bigint +} + +// TODO: this interface has to be removed or purged! export interface TransactionInterface { readonly common: Common readonly nonce: bigint @@ -227,42 +281,108 @@ export interface TransactionInterface - extends TransactionInterface {} +// Interface of a transaction which supports `to: undefined`, which is used to create a contract +export interface ContractCreationInterface extends TxInterface { + readonly to?: Address +} + +// Interface of a transaction which supports `to: Address`, which calls that address +// It is not possible to create a contract with this tx. +export interface ToInterface extends TxInterface { + readonly to: Address +} + +export interface LegacyTxInterface extends TxInterface { + readonly nonce: bigint + readonly gasLimit: bigint + readonly gasPrice: bigint + readonly to?: Address + readonly value: bigint + readonly data: Uint8Array + readonly v?: bigint + readonly r?: bigint + readonly s?: bigint +} +/* export interface EIP2718CompatibleTx extends TransactionInterface { readonly chainId: bigint getMessageToSign(): Uint8Array -} +}*/ -export interface EIP2930CompatibleTx - extends EIP2718CompatibleTx { +export interface EIP2930CompatibleTx // TODO (among the other types like `EIP1559CompatibleTx` below: rename this?) + extends TxInterface { + readonly nonce: bigint + readonly gasLimit: bigint + readonly gasPrice: bigint + readonly to?: Address + readonly value: bigint + readonly data: Uint8Array + readonly chainId: bigint readonly accessList: AccessListBytes - readonly AccessListJSON: AccessList + readonly v?: bigint + readonly r?: bigint + readonly s?: bigint } -export interface EIP1559CompatibleTx - extends EIP2930CompatibleTx { +export interface EIP1559CompatibleTx extends TxInterface { + readonly nonce: bigint + readonly gasLimit: bigint + readonly gasPrice: bigint + readonly to?: Address + readonly value: bigint + readonly data: Uint8Array + readonly chainId: bigint + readonly accessList: AccessListBytes readonly maxPriorityFeePerGas: bigint readonly maxFeePerGas: bigint + readonly v?: bigint + readonly r?: bigint + readonly s?: bigint } -export interface EIP4844CompatibleTx - extends EIP1559CompatibleTx { +export interface EIP4844CompatibleTx extends TxInterface { + readonly nonce: bigint + readonly gasLimit: bigint + readonly gasPrice: bigint + readonly to?: Address + readonly value: bigint + readonly data: Uint8Array + readonly chainId: bigint + readonly accessList: AccessListBytes + readonly maxPriorityFeePerGas: bigint + readonly maxFeePerGas: bigint readonly maxFeePerBlobGas: bigint + readonly v?: bigint + readonly r?: bigint + readonly s?: bigint + + // Network wrapper format blobVersionedHashes: Uint8Array[] blobs?: Uint8Array[] kzgCommitments?: Uint8Array[] kzgProofs?: Uint8Array[] - serializeNetworkWrapper(): Uint8Array - numBlobs(): number + //serializeNetworkWrapper(): Uint8Array + //numBlobs(): number } -export interface EIP7702CompatibleTx - extends EIP1559CompatibleTx { +export interface EIP7702CompatibleTx extends TxInterface { // ChainID, Address, [nonce], y_parity, r, s + readonly nonce: bigint + readonly gasLimit: bigint + readonly gasPrice: bigint + readonly to?: Address + readonly value: bigint + readonly data: Uint8Array + readonly chainId: bigint + readonly accessList: AccessListBytes readonly authorizationList: AuthorizationListBytes + readonly maxPriorityFeePerGas: bigint + readonly maxFeePerGas: bigint + readonly v?: bigint + readonly r?: bigint + readonly s?: bigint } export interface TxData {