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: consolidate generic tx capabilities #2993

Merged
merged 6 commits into from
Aug 30, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
23 changes: 0 additions & 23 deletions packages/tx/src/baseTransaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import {
Address,
MAX_INTEGER,
MAX_UINT64,
SECP256K1_ORDER_DIV_2,
bigIntToHex,
bytesToBigInt,
bytesToHex,
Expand Down Expand Up @@ -179,28 +178,6 @@ export abstract class BaseTransaction<T extends TransactionType>
return errors.length === 0
}

protected _validateYParity() {
const { v } = this
if (v !== undefined && v !== BigInt(0) && v !== BigInt(1)) {
const msg = this._errorMsg('The y-parity of the transaction should either be 0 or 1')
throw new Error(msg)
}
}

/**
* EIP-2: All transaction signatures whose s-value is greater than secp256k1n/2are considered invalid.
* Reasoning: https://ethereum.stackexchange.com/a/55728
*/
protected _validateHighS() {
const { s } = this
if (this.common.gteHardfork('homestead') && s !== undefined && s > SECP256K1_ORDER_DIV_2) {
const msg = this._errorMsg(
'Invalid Signature: s-values greater than secp256k1n/2 are considered invalid'
)
throw new Error(msg)
}
}

/**
* The minimum amount of gas the tx must have (DataFee + TxFee + Creation Fee)
*/
Expand Down
13 changes: 13 additions & 0 deletions packages/tx/src/capabilities/eip1559.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import type { Transaction, TransactionType } from '../types.js'

type TypedTransactionEIP1559 =
| Transaction[TransactionType.BlobEIP4844]
| Transaction[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.

Is it generally possible to type in a way that these methods can also be called by custom transaction type implementations? 🤔 So without the need to predefine all the existing tx types which come into question atm here?

Also coming to surface here again: we really really need to be super careful to make the distinction clear between "EIP-1559 -> the specific transaction" and "Capabilities derived from EIP-1559".

If we do not make this very clear things will get blurry over time and mixed up all the time I fear. In this context the naming above "TypedTransactionEIP1559" is too close to the wrong association thing and we should rename these throughout the files for clarity.

Just need to think right now myself: hmm. I would find EIP1559CompatibleTx already better. 🤔 Not sure if it is the last word of wisdom here though.

(maybe this whole discussion interplays with an eventually possible re-typing though)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'll think about this a bit more about how to architect this. You're right that these "capabilities" are currently tightly paired with "transaction types", whereas they shouldn't. I'm currently wondering if we make use of the existing tx Capability enum. Will reflect a bit more and follow up...

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Here's what I feel is a more sound approach: #3010, curious to hear your thoughts!


export function getUpfrontCost(tx: TypedTransactionEIP1559, baseFee: bigint): bigint {
const prio = tx.maxPriorityFeePerGas
const maxBase = tx.maxFeePerGas - baseFee
const inclusionFeePerGas = prio < maxBase ? prio : maxBase
const gasPrice = inclusionFeePerGas + baseFee
return tx.gasLimit * gasPrice + tx.value
}
Copy link
Member

Choose a reason for hiding this comment

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

Ok, I will just go through the methods one-by-one. Think this initial structure is really crucial so that things can be build cleanly on top of this work.

So: getUpfrontCost() should be ok here.

42 changes: 42 additions & 0 deletions packages/tx/src/capabilities/eip2930.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
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'

type TypedTransactionEIP2930 = Exclude<TypedTransaction, Transaction[TransactionType.Legacy]>

/**
* 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
}
Copy link
Member

Choose a reason for hiding this comment

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

I would cautiously say this one is actually well placed in a eip2930.ts capability file (?).

Copy link
Member

Choose a reason for hiding this comment

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

And I would - also cautiously - say that the "other" version of getDataFee() would actually be a candidate for a legacy.ts capability file?

Also to re-iterate here: so legacy.ts also would include capabilities which were introduced by the legacy transaction and would therefore be wider than the specific legacy transaction itself.

Since also typed transactions have just inherited specific functionality from the legacy transaction, it would very much be ok if typed transactions call into the legacy.ts capability file (same logic as an EIP4844 tx calls into the EIP1559 capability file).

Distinction between generic.ts and legacy.ts would just be that legacy.ts only includes functionality (like getUpfrontCost()) which then changed along the introduction of a new transaction type.

When thinking about it: when staying in this logic it might be even more consistent to skip generic.ts alltogether and just do legacy.ts and move everything being now in generic.ts over there. These is even more consistent with this capabilities-thinking.


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

Choose a reason for hiding this comment

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

-> EIP-2718.

(just to mention: there is also a legacy version of this)


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))
}
Copy link
Member

Choose a reason for hiding this comment

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

Yeah, so this is definitely an EIP-2718 thing.

85 changes: 85 additions & 0 deletions packages/tx/src/capabilities/generic.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import { SECP256K1_ORDER_DIV_2, bigIntToUnpaddedBytes, ecrecover } from '@ethereumjs/util'
import { keccak256 } from 'ethereum-cryptography/keccak.js'

import { Capability, type TypedTransaction } from '../types.js'
Copy link
Contributor

Choose a reason for hiding this comment

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

Ah, I didn't know you could import a type inside a general import statement like this. Neat!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Found that out recently too!


export function isSigned(tx: TypedTransaction): boolean {
const { v, r, s } = tx
if (v === undefined || r === undefined || s === undefined) {
return false
} else {
return true
}
}
Copy link
Member

Choose a reason for hiding this comment

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

Yes, guess ok, respectively: -> legacy.ts


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

export function hash(tx: TypedTransaction): 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())
}
return tx['cache'].hash
}

return keccak256(tx.serialize())
}
Copy link
Member

Choose a reason for hiding this comment

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

If I see correctly this is actually also an EIP-2718 thing, so this specific implementation (sorry: I suggested this placing wrongly initially)


/**
* 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 {
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 new Error(msg)
}
}
Copy link
Member

Choose a reason for hiding this comment

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

-> legacy.ts

(I think I am getting more and more a fan of the idea to remove generic.ts while going through here)


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)
}
}
Copy link
Member

Choose a reason for hiding this comment

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

This is actually an EIP-2718 thing and definitely not generic.


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

const msgHash = tx.getMessageToVerifySignature()

const { v, r, s } = tx

validateHighS(tx)

try {
const sender = ecrecover(
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 (e: any) {
const msg = errorMsg(tx, 'Invalid Signature')
throw new Error(msg)
}
}
Copy link
Member

Choose a reason for hiding this comment

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

-> legacy.ts

82 changes: 12 additions & 70 deletions packages/tx/src/eip1559Transaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,16 @@ import {
bytesToBigInt,
bytesToHex,
concatBytes,
ecrecover,
equalsBytes,
hexToBytes,
toBytes,
validateNoLeadingZeroes,
} from '@ethereumjs/util'
import { keccak256 } from 'ethereum-cryptography/keccak.js'

import { BaseTransaction } from './baseTransaction.js'
import * as EIP1559 from './capabilities/eip1559.js'
import * as EIP2930 from './capabilities/eip2930.js'
import * as Generic from './capabilities/generic.js'
import { TransactionType } from './types.js'
import { AccessLists } from './util.js'

Expand Down Expand Up @@ -188,8 +189,8 @@ export class FeeMarketEIP1559Transaction extends BaseTransaction<TransactionType
throw new Error(msg)
}

this._validateYParity()
this._validateHighS()
Generic.validateYParity(this)
Generic.validateHighS(this)

const freeze = opts?.freeze ?? true
if (freeze) {
Expand All @@ -201,33 +202,15 @@ export class FeeMarketEIP1559Transaction extends BaseTransaction<TransactionType
* The amount of gas paid for the data in this tx
*/
getDataFee(): bigint {
if (this.cache.dataFee && this.cache.dataFee.hardfork === this.common.hardfork()) {
return this.cache.dataFee.value
}

let cost = super.getDataFee()
cost += BigInt(AccessLists.getDataFeeEIP2930(this.accessList, this.common))

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

return cost
return EIP2930.getDataFee(this)
}

/**
* The up front amount that an account must have for this transaction to be valid
* @param baseFee The base fee of the block (will be set to 0 if not provided)
*/
getUpfrontCost(baseFee: bigint = BigInt(0)): bigint {
const prio = this.maxPriorityFeePerGas
const maxBase = this.maxFeePerGas - baseFee
const inclusionFeePerGas = prio < maxBase ? prio : maxBase
const gasPrice = inclusionFeePerGas + baseFee
return this.gasLimit * gasPrice + this.value
return EIP1559.getUpfrontCost(this, baseFee)
}

/**
Expand Down Expand Up @@ -271,8 +254,7 @@ export class FeeMarketEIP1559Transaction extends BaseTransaction<TransactionType
* the RLP encoding of the values.
*/
serialize(): Uint8Array {
const base = this.raw()
return concatBytes(TRANSACTION_TYPE_BYTES, RLP.encode(base))
return EIP2930.serialize(this)
}

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

/**
Expand All @@ -310,19 +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 {
if (!this.isSigned()) {
const msg = this._errorMsg('Cannot call hash method if transaction is not signed')
throw new Error(msg)
}

if (Object.isFrozen(this)) {
if (!this.cache.hash) {
this.cache.hash = keccak256(this.serialize())
}
return this.cache.hash
}

return keccak256(this.serialize())
return Generic.hash(this)
}

/**
Expand All @@ -336,35 +306,7 @@ export class FeeMarketEIP1559Transaction extends BaseTransaction<TransactionType
* Returns the public key of the sender
*/
public getSenderPublicKey(): Uint8Array {
if (this.cache.senderPubKey !== undefined) {
return this.cache.senderPubKey
}

if (!this.isSigned()) {
const msg = this._errorMsg('Cannot call this method if transaction is not signed')
throw new Error(msg)
}

const msgHash = this.getMessageToVerifySignature()
const { v, r, s } = this

this._validateHighS()

try {
const sender = ecrecover(
msgHash,
v! + BigInt(27), // Recover the 27 which was stripped from ecsign
bigIntToUnpaddedBytes(r!),
bigIntToUnpaddedBytes(s!)
)
if (Object.isFrozen(this)) {
this.cache.senderPubKey = sender
}
return sender
} catch (e: any) {
const msg = this._errorMsg('Invalid Signature')
throw new Error(msg)
}
return Generic.getSenderPublicKey(this)
}

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