UNPKG

@ethereumjs/tx

Version:
300 lines (257 loc) 8.58 kB
import { Address, BIGINT_0, EthereumJSErrorWithoutCode, SECP256K1_ORDER_DIV_2, bigIntMax, bigIntToUnpaddedBytes, bytesToHex, ecrecover, publicToAddress, unpadBytes, } from '@ethereumjs/util' import { keccak256 } from 'ethereum-cryptography/keccak.js' import { Capability, TransactionType } from '../types.ts' import { secp256k1 } from 'ethereum-cryptography/secp256k1' import type { LegacyTx } from '../legacy/tx.ts' import type { LegacyTxInterface, Transaction } from '../types.ts' 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): 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 = 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 EthereumJSErrorWithoutCode(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 EthereumJSErrorWithoutCode(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 { const msg = errorMsg(tx, 'Invalid Signature') throw EthereumJSErrorWithoutCode(msg) } } export function getEffectivePriorityFee(gasPrice: bigint, baseFee: bigint | undefined): bigint { if (baseFee !== undefined && baseFee > gasPrice) { throw EthereumJSErrorWithoutCode('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') } let intrinsicGas = tx.getIntrinsicGas() if (tx.common.isActivatedEIP(7623)) { let tokens = 0 for (let i = 0; i < tx.data.length; i++) { tokens += tx.data[i] === 0 ? 1 : 4 } const floorCost = tx.common.param('txGas') + tx.common.param('totalCostFloorPerToken') * BigInt(tokens) intrinsicGas = bigIntMax(intrinsicGas, floorCost) } if (intrinsicGas > tx.gasLimit) { errors.push( `gasLimit is too low. The gasLimit is lower than the minimum gas limit of ${tx.getIntrinsicGas()}, the gas limit is: ${tx.gasLimit}`, ) } 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 { 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, extraEntropy: Uint8Array | boolean = true, ): 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 EthereumJSErrorWithoutCode(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) ) { ;(tx as LegacyTx)['activeCapabilities'].push(Capability.EIP155ReplayProtection) hackApplied = true } const msgHash = tx.getHashedMessageToSign() const ecSignFunction = tx.common.customCrypto?.ecsign ?? secp256k1.sign const { recovery, r, s } = ecSignFunction(msgHash, privateKey, { extraEntropy }) const signedTx = tx.addSignature(BigInt(recovery), r, s, true) // Hack part 2 if (hackApplied) { const index = (tx as LegacyTx)['activeCapabilities'].indexOf(Capability.EIP155ReplayProtection) if (index > -1) { ;(tx as LegacyTx)['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 { hash = 'error' } let isSigned = '' try { isSigned = tx.isSigned().toString() } catch { hash = 'error' } let hf = '' try { hf = tx.common.hardfork() } catch { hf = 'error' } let postfix = `tx type=${tx.type} hash=${hash} nonce=${tx.nonce} value=${tx.value} ` postfix += `signed=${isSigned} hf=${hf}` return postfix }