@ethereumjs/tx
Version:
Implementation of the various Ethereum Transaction Types
237 lines • 8.42 kB
JavaScript
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.js";
import { secp256k1 } from 'ethereum-cryptography/secp256k1';
export function errorMsg(tx, msg) {
return `${msg} (${tx.errorStr()})`;
}
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;
if (tx.common.gteHardfork('homestead') && tx.toCreationAddress()) {
const txCreationFee = tx.common.param('txCreationGas');
if (txCreationFee)
fee += txCreationFee;
}
return fee;
}
export function toCreationAddress(tx) {
return tx.to === undefined || tx.to.bytes.length === 0;
}
export function hash(tx) {
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) {
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) {
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, 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;
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 { recovery, r, s } = ecSignFunction(msgHash, privateKey, { extraEntropy });
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)
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