@ethereumjs/tx
Version:
Implementation of the various Ethereum Transaction Types
314 lines • 11.4 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.errorMsg = errorMsg;
exports.isSigned = isSigned;
exports.getDataGas = getDataGas;
exports.getIntrinsicGas = getIntrinsicGas;
exports.toCreationAddress = toCreationAddress;
exports.hash = hash;
exports.validateHighS = validateHighS;
exports.getSenderPublicKey = getSenderPublicKey;
exports.getEffectivePriorityFee = getEffectivePriorityFee;
exports.getValidationErrors = getValidationErrors;
exports.isValid = isValid;
exports.verifySignature = verifySignature;
exports.getSenderAddress = getSenderAddress;
exports.sign = sign;
exports.getSharedErrorPostfix = getSharedErrorPostfix;
const util_1 = require("@ethereumjs/util");
const secp256k1_js_1 = require("@noble/curves/secp256k1.js");
const sha3_js_1 = require("@noble/hashes/sha3.js");
const types_ts_1 = require("../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
*/
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
*/
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
*/
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 = util_1.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).
*/
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
*/
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
*/
function hash(tx) {
var _a;
if (!tx.isSigned()) {
const msg = errorMsg(tx, 'Cannot call hash method if transaction is not signed');
throw (0, util_1.EthereumJSErrorWithoutCode)(msg);
}
const keccakFunction = tx.common.customCrypto.keccak256 ?? sha3_js_1.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
*/
function validateHighS(tx) {
const { s } = tx;
if (tx.common.gteHardfork('homestead') && s !== undefined && s > util_1.SECP256K1_ORDER_DIV_2) {
const msg = errorMsg(tx, 'Invalid Signature: s-values greater than secp256k1n/2 are considered invalid');
throw (0, util_1.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
*/
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 ?? util_1.ecrecover;
const sender = ecrecoverFunction(msgHash, v, (0, util_1.bigIntToUnpaddedBytes)(r), (0, util_1.bigIntToUnpaddedBytes)(s), tx.supports(types_ts_1.Capability.EIP155ReplayProtection) ? tx.common.chainId() : undefined);
if (Object.isFrozen(tx)) {
tx.cache.senderPubKey = sender;
}
return sender;
}
catch {
const msg = errorMsg(tx, 'Invalid Signature');
throw (0, util_1.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
*/
function getEffectivePriorityFee(gasPrice, baseFee) {
if (baseFee !== undefined && baseFee > gasPrice) {
throw (0, util_1.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
*/
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 = (0, util_1.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
*/
function isValid(tx) {
const errors = tx.getValidationErrors();
return errors.length === 0;
}
/**
* Determines if the signature is valid
*/
function verifySignature(tx) {
try {
// Main signature verification is done in `getSenderPublicKey()`
const publicKey = tx.getSenderPublicKey();
return (0, util_1.unpadBytes)(publicKey).length !== 0;
}
catch {
return false;
}
}
/**
* Returns the sender's address
*/
function getSenderAddress(tx) {
return new util_1.Address((0, util_1.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)
* ```
*/
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 (0, util_1.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 === types_ts_1.TransactionType.Legacy &&
tx.common.gteHardfork('spuriousDragon') &&
!tx.supports(types_ts_1.Capability.EIP155ReplayProtection)) {
;
tx['activeCapabilities'].push(types_ts_1.Capability.EIP155ReplayProtection);
hackApplied = true;
}
const msgHash = tx.getHashedMessageToSign();
const ecSignFunction = tx.common.customCrypto?.ecsign ?? secp256k1_js_1.secp256k1.sign;
const signatureBytes = ecSignFunction(msgHash, privateKey, {
extraEntropy,
format: 'recovered',
prehash: false,
});
const { recovery, r, s } = secp256k1_js_1.secp256k1.Signature.fromBytes(signatureBytes, 'recovered');
if (recovery === undefined) {
throw (0, util_1.EthereumJSErrorWithoutCode)('Invalid signature recovery');
}
const signedTx = tx.addSignature(BigInt(recovery), r, s, true);
// Hack part 2
if (hackApplied) {
const index = tx['activeCapabilities'].indexOf(types_ts_1.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
*/
function getSharedErrorPostfix(tx) {
let hash = '';
try {
hash = tx.isSigned() ? (0, util_1.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