Skip to content

Commit

Permalink
tx: improve tx capability handling (#3010)
Browse files Browse the repository at this point in the history
* tx: TxCapability interfaces and rename generic capability to legacy

* tx: txTypeBytes helper

* tx: add missing methods and props to tx interfaces

* tx: refactor and improve serialize helper

* tx: implement update serialize helper and txTypeBytes helper

* tx: refactor getDataFee for use in legacyTransaction

* tx: remove redundant prop from EIP4844CompatibleTxInterface

* tx: shorten compatibletxInterface name

* tx: typedTransaction -> EIP2718CompatibleTx

* tx: remove implements interface

* tx: add legacytxinterface and move accesslists props to eip2930 interface
  • Loading branch information
gabrocheleau authored Sep 14, 2023
1 parent 04aa4da commit 67a20de
Show file tree
Hide file tree
Showing 11 changed files with 189 additions and 158 deletions.
13 changes: 2 additions & 11 deletions packages/tx/src/baseTransaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,23 +18,14 @@ import { checkMaxInitCodeSize } from './util.js'
import type {
JsonTx,
Transaction,
TransactionCache,
TransactionInterface,
TxData,
TxOptions,
TxValuesArray,
} from './types.js'
import type { Hardfork } from '@ethereumjs/common'
import type { BigIntLike } from '@ethereumjs/util'

interface TransactionCache {
hash?: Uint8Array
dataFee?: {
value: bigint
hardfork: string | Hardfork
}
senderPubKey?: Uint8Array
}

/**
* This base class will likely be subject to further
* refactoring along the introduction of additional tx types
Expand All @@ -59,7 +50,7 @@ export abstract class BaseTransaction<T extends TransactionType>

public readonly common!: Common

protected cache: TransactionCache = {
public cache: TransactionCache = {
hash: undefined,
dataFee: undefined,
senderPubKey: undefined,
Expand Down
8 changes: 2 additions & 6 deletions packages/tx/src/capabilities/eip1559.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,6 @@
import type { Transaction, TransactionType } from '../types.js'
import type { EIP1559CompatibleTx } from '../types.js'

type TypedTransactionEIP1559 =
| Transaction[TransactionType.BlobEIP4844]
| Transaction[TransactionType.FeeMarketEIP1559]

export function getUpfrontCost(tx: TypedTransactionEIP1559, baseFee: bigint): bigint {
export function getUpfrontCost(tx: EIP1559CompatibleTx, baseFee: bigint): bigint {
const prio = tx.maxPriorityFeePerGas
const maxBase = tx.maxFeePerGas - baseFee
const inclusionFeePerGas = prio < maxBase ? prio : maxBase
Expand Down
26 changes: 26 additions & 0 deletions packages/tx/src/capabilities/eip2718.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { RLP } from '@ethereumjs/rlp'
import { concatBytes } from '@ethereumjs/util'
import { keccak256 } from 'ethereum-cryptography/keccak.js'

import { txTypeBytes } from '../util.js'

import { errorMsg } from './legacy.js'

import type { EIP2718CompatibleTx } from '../types'
import type { Input } from '@ethereumjs/rlp'

export function getHashedMessageToSign(tx: EIP2718CompatibleTx): Uint8Array {
return keccak256(tx.getMessageToSign())
}

export function serialize(tx: EIP2718CompatibleTx, base?: Input): Uint8Array {
return concatBytes(txTypeBytes(tx.type), RLP.encode(base ?? tx.raw()))
}

export function validateYParity(tx: EIP2718CompatibleTx) {
const { v } = tx
if (v !== undefined && v !== BigInt(0) && v !== BigInt(1)) {
const msg = errorMsg(tx, 'The y-parity of the transaction should either be 0 or 1')
throw new Error(msg)
}
}
38 changes: 4 additions & 34 deletions packages/tx/src/capabilities/eip2930.ts
Original file line number Diff line number Diff line change
@@ -1,42 +1,12 @@
import { RLP } from '@ethereumjs/rlp'
import { concatBytes, hexToBytes } from '@ethereumjs/util'
import { keccak256 } from 'ethereum-cryptography/keccak.js'

import { BaseTransaction } from '../baseTransaction.js'
import { type TypedTransaction } from '../types.js'
import { AccessLists } from '../util.js'

import type { Transaction, TransactionType } from '../types.js'
import * as Legacy from './legacy.js'

type TypedTransactionEIP2930 = Exclude<TypedTransaction, Transaction[TransactionType.Legacy]>
import type { EIP2930CompatibleTx } from '../types.js'

/**
* The amount of gas paid for the data in this tx
*/
export function getDataFee(tx: TypedTransactionEIP2930): bigint {
if (tx['cache'].dataFee && tx['cache'].dataFee.hardfork === tx.common.hardfork()) {
return tx['cache'].dataFee.value
}

let cost = BaseTransaction.prototype.getDataFee.bind(tx)()
cost += BigInt(AccessLists.getDataFeeEIP2930(tx.accessList, tx.common))

if (Object.isFrozen(tx)) {
tx['cache'].dataFee = {
value: cost,
hardfork: tx.common.hardfork(),
}
}

return cost
}

export function getHashedMessageToSign(tx: TypedTransactionEIP2930): Uint8Array {
return keccak256(tx.getMessageToSign())
}

export function serialize(tx: TypedTransactionEIP2930): Uint8Array {
const base = tx.raw()
const txTypeBytes = hexToBytes('0x' + tx.type.toString(16).padStart(2, '0'))
return concatBytes(txTypeBytes, RLP.encode(base))
export function getDataFee(tx: EIP2930CompatibleTx): bigint {
return Legacy.getDataFee(tx, BigInt(AccessLists.getDataFeeEIP2930(tx.accessList, tx.common)))
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,16 @@
import { SECP256K1_ORDER_DIV_2, bigIntToUnpaddedBytes, ecrecover } from '@ethereumjs/util'
import { keccak256 } from 'ethereum-cryptography/keccak.js'

import { Capability, type TypedTransaction } from '../types.js'
import { BaseTransaction } from '../baseTransaction.js'
import { Capability } from '../types.js'

export function isSigned(tx: TypedTransaction): boolean {
import type { LegacyTxInterface } 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
Expand All @@ -12,21 +19,37 @@ export function isSigned(tx: TypedTransaction): boolean {
}
}

export function errorMsg(tx: TypedTransaction, msg: string) {
return `${msg} (${tx.errorStr()})`
/**
* The amount of gas paid for the data in this tx
*/
export function getDataFee(tx: LegacyTxInterface, extraCost?: bigint): bigint {
if (tx.cache.dataFee && tx.cache.dataFee.hardfork === tx.common.hardfork()) {
return tx.cache.dataFee.value
}

const cost = BaseTransaction.prototype.getDataFee.bind(tx)() + (extraCost ?? 0n)

if (Object.isFrozen(tx)) {
tx.cache.dataFee = {
value: cost,
hardfork: tx.common.hardfork(),
}
}

return cost
}

export function hash(tx: TypedTransaction): Uint8Array {
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)
}

if (Object.isFrozen(tx)) {
if (!tx['cache'].hash) {
tx['cache'].hash = keccak256(tx.serialize())
if (!tx.cache.hash) {
tx.cache.hash = keccak256(tx.serialize())
}
return tx['cache'].hash
return tx.cache.hash
}

return keccak256(tx.serialize())
Expand All @@ -36,7 +59,7 @@ export function hash(tx: TypedTransaction): Uint8Array {
* 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: TypedTransaction): void {
export function validateHighS(tx: LegacyTxInterface): void {
const { s } = tx
if (tx.common.gteHardfork('homestead') && s !== undefined && s > SECP256K1_ORDER_DIV_2) {
const msg = errorMsg(
Expand All @@ -47,17 +70,9 @@ export function validateHighS(tx: TypedTransaction): void {
}
}

export function validateYParity(tx: TypedTransaction) {
const { v } = tx
if (v !== undefined && v !== BigInt(0) && v !== BigInt(1)) {
const msg = errorMsg(tx, 'The y-parity of the transaction should either be 0 or 1')
throw new Error(msg)
}
}

export function getSenderPublicKey(tx: TypedTransaction): Uint8Array {
if (tx['cache'].senderPubKey !== undefined) {
return tx['cache'].senderPubKey
export function getSenderPublicKey(tx: LegacyTxInterface): Uint8Array {
if (tx.cache.senderPubKey !== undefined) {
return tx.cache.senderPubKey
}

const msgHash = tx.getMessageToVerifySignature()
Expand All @@ -75,7 +90,7 @@ export function getSenderPublicKey(tx: TypedTransaction): Uint8Array {
tx.supports(Capability.EIP155ReplayProtection) ? tx.common.chainId() : undefined
)
if (Object.isFrozen(tx)) {
tx['cache'].senderPubKey = sender
tx.cache.senderPubKey = sender
}
return sender
} catch (e: any) {
Expand Down
35 changes: 16 additions & 19 deletions packages/tx/src/eip1559Transaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,18 @@ import {
bigIntToUnpaddedBytes,
bytesToBigInt,
bytesToHex,
concatBytes,
equalsBytes,
hexToBytes,
toBytes,
validateNoLeadingZeroes,
} from '@ethereumjs/util'

import { BaseTransaction } from './baseTransaction.js'
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 { TransactionType } from './types.js'
import { AccessLists } from './util.js'
import { AccessLists, txTypeBytes } from './util.js'

import type {
AccessList,
Expand All @@ -32,17 +31,14 @@ import type { Common } from '@ethereumjs/common'
type TxData = AllTypesTxData[TransactionType.FeeMarketEIP1559]
type TxValuesArray = AllTypesTxValuesArray[TransactionType.FeeMarketEIP1559]

const TRANSACTION_TYPE_BYTES = hexToBytes(
'0x' + TransactionType.FeeMarketEIP1559.toString(16).padStart(2, '0')
)

/**
* Typed transaction with a new gas fee market mechanism
*
* - TransactionType: 2
* - EIP: [EIP-1559](https://eips.ethereum.org/EIPS/eip-1559)
*/
export class FeeMarketEIP1559Transaction extends BaseTransaction<TransactionType.FeeMarketEIP1559> {
// implements EIP1559CompatibleTx<TransactionType.FeeMarketEIP1559>
public readonly chainId: bigint
public readonly accessList: AccessListBytes
public readonly AccessListJSON: AccessList
Expand Down Expand Up @@ -72,7 +68,10 @@ export class FeeMarketEIP1559Transaction extends BaseTransaction<TransactionType
* accessList, signatureYParity, signatureR, signatureS])`
*/
public static fromSerializedTx(serialized: Uint8Array, opts: TxOptions = {}) {
if (equalsBytes(serialized.subarray(0, 1), TRANSACTION_TYPE_BYTES) === false) {
if (
equalsBytes(serialized.subarray(0, 1), txTypeBytes(TransactionType.FeeMarketEIP1559)) ===
false
) {
throw new Error(
`Invalid serialized tx input: not an EIP-1559 transaction (wrong tx type, expected: ${
TransactionType.FeeMarketEIP1559
Expand Down Expand Up @@ -189,8 +188,8 @@ export class FeeMarketEIP1559Transaction extends BaseTransaction<TransactionType
throw new Error(msg)
}

Generic.validateYParity(this)
Generic.validateHighS(this)
EIP2718.validateYParity(this)
Legacy.validateHighS(this)

const freeze = opts?.freeze ?? true
if (freeze) {
Expand Down Expand Up @@ -254,7 +253,7 @@ export class FeeMarketEIP1559Transaction extends BaseTransaction<TransactionType
* the RLP encoding of the values.
*/
serialize(): Uint8Array {
return EIP2930.serialize(this)
return EIP2718.serialize(this)
}

/**
Expand All @@ -269,9 +268,7 @@ export class FeeMarketEIP1559Transaction extends BaseTransaction<TransactionType
* ```
*/
getMessageToSign(): Uint8Array {
const base = this.raw().slice(0, 9)
const message = concatBytes(TRANSACTION_TYPE_BYTES, RLP.encode(base))
return message
return EIP2718.serialize(this, this.raw().slice(0, 9))
}

/**
Expand All @@ -282,7 +279,7 @@ export class FeeMarketEIP1559Transaction extends BaseTransaction<TransactionType
* serialized and doesn't need to be RLP encoded any more.
*/
getHashedMessageToSign(): Uint8Array {
return EIP2930.getHashedMessageToSign(this)
return EIP2718.getHashedMessageToSign(this)
}

/**
Expand All @@ -292,7 +289,7 @@ export class FeeMarketEIP1559Transaction extends BaseTransaction<TransactionType
* Use {@link FeeMarketEIP1559Transaction.getMessageToSign} to get a tx hash for the purpose of signing.
*/
public hash(): Uint8Array {
return Generic.hash(this)
return Legacy.hash(this)
}

/**
Expand All @@ -306,7 +303,7 @@ export class FeeMarketEIP1559Transaction extends BaseTransaction<TransactionType
* Returns the public key of the sender
*/
public getSenderPublicKey(): Uint8Array {
return Generic.getSenderPublicKey(this)
return Legacy.getSenderPublicKey(this)
}

protected _processSignature(v: bigint, r: Uint8Array, s: Uint8Array) {
Expand Down Expand Up @@ -363,6 +360,6 @@ export class FeeMarketEIP1559Transaction extends BaseTransaction<TransactionType
* @hidden
*/
protected _errorMsg(msg: string) {
return Generic.errorMsg(this, msg)
return Legacy.errorMsg(this, msg)
}
}
Loading

0 comments on commit 67a20de

Please sign in to comment.