UNPKG

@ethereumjs/tx

Version:
297 lines 10.7 kB
import { Address, BIGINT_0, EthereumJSErrorWithoutCode, SECP256K1_ORDER_DIV_2, bigIntMax, bigIntToUnpaddedBytes, bytesToHex, ecrecover, publicToAddress, unpadBytes, } from '@ethereumjs/util'; import { secp256k1 } from '@noble/curves/secp256k1.js'; import { keccak_256 } from '@noble/hashes/sha3.js'; import { Capability, TransactionType } from "../types.js"; /** * Creates an error message with transaction context * @param tx - The transaction interface * @param msg - The error message * @returns Formatted error message with transaction context */ export function errorMsg(tx, msg) { return `${msg} (${tx.errorStr()})`; } /** * Checks if a transaction is signed * @param tx - The transaction interface * @returns true if the transaction is signed */ export function isSigned(tx) { 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) { 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) { const txFee = tx.common.param('txGas'); let fee = tx.getDataGas(); if (txFee) fee += txFee; let isContractCreation = false; try { isContractCreation = tx.toCreationAddress(); } catch { isContractCreation = false; } if (tx.common.gteHardfork('homestead') && isContractCreation) { const txCreationFee = tx.common.param('txCreationGas'); if (txCreationFee) fee += txCreationFee; } return fee; } /** * Checks if the transaction targets the creation address (deploys a contract). * @param tx - Transaction interface to inspect * @returns true if the transaction's `to` is undefined or empty */ export function toCreationAddress(tx) { return tx.to === undefined || tx.to.bytes.length === 0; } /** * Computes the keccak_256 hash of a signed legacy transaction. * @param tx - Transaction to hash * @returns Hash of the serialized transaction * @throws EthereumJSErrorWithoutCode if the transaction is unsigned */ export function hash(tx) { var _a; 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 ?? keccak_256; if (Object.isFrozen(tx)) { (_a = tx.cache).hash ?? (_a.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) { 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); } } /** * Recovers the sender's public key from the transaction signature. * @param tx - Transaction from which the public key should be derived * @returns The uncompressed sender public key * @throws EthereumJSErrorWithoutCode if the signature is invalid */ export function getSenderPublicKey(tx) { 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); } } /** * Calculates the effective priority fee for a legacy-style transaction. * @param gasPrice - Gas price specified on the transaction * @param baseFee - Optional base fee from the block when operating on L2s that mimic 1559 behavior * @returns The priority fee portion that can be paid to the block producer * @throws EthereumJSErrorWithoutCode if the base fee exceeds the gas price */ export function getEffectivePriorityFee(gasPrice, baseFee) { 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) { const errors = []; if (tx.isSigned() && !tx.verifySignature()) { errors.push('Invalid Signature'); } let intrinsicGas = tx.getIntrinsicGas(); if (tx.common.isActivatedEIP(7623)) { let tokens = 0; if (tx.common.isActivatedEIP(7976)) { // EIP-7976: uniform 4 tokens per byte regardless of zero/non-zero tokens = tx.data.length * 4; } else { 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) { const errors = tx.getValidationErrors(); return errors.length === 0; } /** * Determines if the signature is valid */ export function verifySignature(tx) { 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) { 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, privateKey, extraEntropy = true) { 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['activeCapabilities'].push(Capability.EIP155ReplayProtection); hackApplied = true; } const msgHash = tx.getHashedMessageToSign(); const ecSignFunction = tx.common.customCrypto?.ecsign ?? secp256k1.sign; const signatureBytes = ecSignFunction(msgHash, privateKey, { extraEntropy, format: 'recovered', prehash: false, }); const { recovery, r, s } = secp256k1.Signature.fromBytes(signatureBytes, 'recovered'); if (recovery === undefined) { throw EthereumJSErrorWithoutCode('Invalid signature recovery'); } const signedTx = tx.addSignature(BigInt(recovery), r, s, true); // Hack part 2 if (hackApplied) { const index = tx['activeCapabilities'].indexOf(Capability.EIP155ReplayProtection); if (index > -1) { ; tx['activeCapabilities'].splice(index, 1); } } return signedTx; } // TODO maybe move this to shared methods (util.ts in features) /** * Builds a compact string that summarizes common transaction fields for error messages. * @param tx - Transaction used to assemble the postfix * @returns A formatted string containing tx type, hash, nonce, value, signature status, and hardfork */ export function getSharedErrorPostfix(tx) { 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; } //# sourceMappingURL=legacy.js.map