Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

tx: improve tx capability handling #3010

Merged
merged 18 commits into from
Sep 14, 2023
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
2992719
tx: TxCapability interfaces and rename generic capability to legacy
gabrocheleau Sep 5, 2023
7373569
Merge branch 'master' into tx/consolidation-improvements
gabrocheleau Sep 5, 2023
76160e3
tx: txTypeBytes helper
gabrocheleau Sep 5, 2023
cf53ddd
tx: add missing methods and props to tx interfaces
gabrocheleau Sep 5, 2023
eaf893d
tx: refactor and improve serialize helper
gabrocheleau Sep 5, 2023
0f708ea
tx: implement update serialize helper and txTypeBytes helper
gabrocheleau Sep 5, 2023
23dcf35
Merge branch 'master' into tx/consolidation-improvements
gabrocheleau Sep 5, 2023
ff58d43
Merge branch 'master' into tx/consolidation-improvements
gabrocheleau Sep 6, 2023
8411952
tx: refactor getDataFee for use in legacyTransaction
gabrocheleau Sep 6, 2023
77632bb
tx: remove redundant prop from EIP4844CompatibleTxInterface
gabrocheleau Sep 6, 2023
8b08c11
tx: shorten compatibletxInterface name
gabrocheleau Sep 11, 2023
32c7de8
tx: typedTransaction -> EIP2718CompatibleTx
gabrocheleau Sep 11, 2023
e410654
tx: remove implements interface
gabrocheleau Sep 11, 2023
903545d
tx: add legacytxinterface and move accesslists props to eip2930 inter…
gabrocheleau Sep 11, 2023
3c576cd
Merge branch 'master' into tx/consolidation-improvements
gabrocheleau Sep 11, 2023
189baf8
Merge branch 'master' into tx/consolidation-improvements
gabrocheleau Sep 14, 2023
22b7366
Merge branch 'master' into tx/consolidation-improvements
gabrocheleau Sep 14, 2023
ecd4a13
Merge branch 'master' into tx/consolidation-improvements
gabrocheleau Sep 14, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 { EIP1559CompatibleTxInterface } from '../types.js'

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

export function getUpfrontCost(tx: TypedTransactionEIP1559, baseFee: bigint): bigint {
export function getUpfrontCost(tx: EIP1559CompatibleTxInterface, 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 { EIP2718CompatibleTxInterface, TypedTransaction } from '../types'
import type { Input } from '@ethereumjs/rlp'

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

export function serialize(tx: EIP2718CompatibleTxInterface, base?: Input): Uint8Array {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we drop the Interface from these names to get it a bit shorter? 🤔 No super strong preference, but we also mostly (exception: these very important reused interfaces in Common) do not explicitly name interfaces with Interface or Ior the like in the monorepo.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(but the shortening would be the greater argument here, especially if there are more arguments to follow, then this likely often moves over unnecessarily into two lines)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shortened by removing the unnecessary Interface suffix.

return concatBytes(txTypeBytes(tx.type), RLP.encode(base ?? tx.raw()))
}

export function validateYParity(tx: TypedTransaction) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it not possible to type with EIP2718CompatibleTxInterface here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Forgot to adjust after refactoring, fixed.

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 { EIP2930CompatibleTxInterface } 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: EIP2930CompatibleTxInterface): 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 { TransactionInterface } from '../types.js'

export function errorMsg(tx: TransactionInterface, msg: string) {
return `${msg} (${tx.errorStr()})`
}

export function isSigned(tx: TransactionInterface): 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: TransactionInterface, 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: TransactionInterface): 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: TransactionInterface): 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: TransactionInterface): 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
40 changes: 20 additions & 20 deletions packages/tx/src/eip1559Transaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,25 +5,25 @@ 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,
AccessListBytes,
TxData as AllTypesTxData,
TxValuesArray as AllTypesTxValuesArray,
EIP1559CompatibleTxInterface,
JsonTx,
TxOptions,
} from './types.js'
Expand All @@ -32,17 +32,16 @@ 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> {
export class FeeMarketEIP1559Transaction
extends BaseTransaction<TransactionType.FeeMarketEIP1559>
implements EIP1559CompatibleTxInterface<TransactionType.FeeMarketEIP1559>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is not structurally correct to add this implements note respectively is both redundant and not complete at the same time. So I think we should remove.

If we would want to do this correctly/completely we would need to implement from several compatibility interfaces (here e.g. also from the EIP2930 interface), but multiple inheritance unfortunately doesn't exist in JavaScript (I thought a bit along these lines too some 1-2 years ago when thinking about how we could refactor this stuff).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I actually went back and forth on this. I agree about the point concerning multiple inheritance, but since all of the interfaces are nested within each other (eip1559 inherits from eip2930, and eip4844 inherits from eip1559), I figured this wasn't a problem. However I agree that this would not be "correct", especially if we want to treat capabilities as independent from one another.

There are no type errors even if we remove this ìmplements (typescript automatically figures out whether or not the class fits the necessary interface when we use the Capabilities, regardless of if we explicitly mention that it implements said interface. I'll go ahead and remove this.

{
public readonly chainId: bigint
public readonly accessList: AccessListBytes
public readonly AccessListJSON: AccessList
Expand Down Expand Up @@ -72,7 +71,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 +191,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 +256,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 +271,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 +282,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 +292,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 +306,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)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, but generally this reads super-great, to always already have the heritage of the feature read out of these calls, so that one can associate how the tx type is composed and where the functionality comes from! 🥳

}

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