Skip to content

Commit

Permalink
tx: implement tx containers and tx workers [WIP]
Browse files Browse the repository at this point in the history
  • Loading branch information
jochem-brouwer committed Nov 10, 2024
1 parent 03b7caf commit 0657313
Show file tree
Hide file tree
Showing 6 changed files with 403 additions and 0 deletions.
207 changes: 207 additions & 0 deletions packages/tx/src/dataContainerTypes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
import type {
AccessList,
AccessListBytes,
AuthorizationList,
AuthorizationListBytes,
JSONTx,
TransactionType,
} from './types.js'
import type {
Address,
AddressLike,
BigIntLike,
BytesLike,
PrefixedHexString,
} from '@ethereumjs/util'

// TODO
// Make a very simple "Features" class which handles supports/activate/deactivate (?)

export enum Feature {
ReplayProtection = 'ReplayProtection', // For EIP-155 replay protection
ECDSASignable = 'ECDSASignable', // For unsigned/signed ECDSA containers
ECDSASigned = 'ECDSASigned', // For signed ECDSA containers

LegacyGasMarket = 'LegacyGasMarket', // Txs with legacy gas market (pre-1559)
FeeMarket = 'FeeMarket', // Txs with EIP1559 gas market

TypedTransaction = 'TypedTransaction',

AccessLists = 'AccessLists',
EOACode = 'EOACode',
}

export type NestedUint8Array = (Uint8Array | NestedUint8Array)[]

export interface TxDataContainer {
supports(capability: Feature): boolean
type: TransactionType

// Raw list of Uint8Arrays (can be nested)
raw(): NestedUint8Array // TODO make more tx-specific

// The serialized version of the raw() one
// (current: RLP.encode)
serialize(): Uint8Array

// Utility to convert to a JSON object
toJSON(): JSONTx

/** Signature related stuff, TODO */
/*
isSigned(): boolean
isValid(): boolean
verifySignature(): boolean
getSenderAddress(): Address
getSenderPublicKey(): Uint8Array
sign(privateKey: Uint8Array): Transaction[T]
errorStr(): string
addSignature(
v: bigint,
r: Uint8Array | bigint,
s: Uint8Array | bigint,
convertV?: boolean,
): Transaction[T]
// Get the non-hashed message to sign (this is input, but then hashed, is input to methods like ecsign)
getMessageToSign(): Uint8Array | Uint8Array[]
// Get the hashed message to sign (allows for flexibility over the hash method, now: keccak256)
getHashedMessageToSign(): Uint8Array
// The hash of the transaction (note: hash currently has to do with signed txs but on L2 likely can also be of non-signed txs (?))
hash(): Uint8Array
*/
}

// Container "fields" and container "interface" below
// Fields: used for the CONSTRUCTOR of the containers
// Interface: used for the resulting constructor, so each param of the field is converted to that type before resulting in the container

export type DefaultContainerDataFields = {
nonce?: BigIntLike
gasLimit?: BigIntLike
to?: AddressLike
value?: BigIntLike
data?: BytesLike | '' // Note: '' is for empty data (TODO look if we want to keep this)
}

export interface DefaultContainerInterface {
readonly gasPrice: bigint
readonly nonce: bigint
readonly gasLimit: bigint
readonly value: bigint
readonly data: Uint8Array
readonly to: Address | null // TODO: figure out how to handle this on txs which do not allow to:null (7702/4844)
}

export type ECDSASignedContainerFields = {
v?: BigIntLike
r?: BigIntLike
s?: BigIntLike
}

export interface ECDSAContainerInterface {
readonly v?: bigint
readonly r?: bigint
readonly s?: bigint
}

// The container / tx data fields if the tx can create contracts (to `null`)
/*export type ContractCreationDataFields = {
to?: DefaultContainerDataFields['to'] | null | ''
}*/

/*
export interface ContractCreationContainerInterface {
to: DefaultContainerInterface['to'] | null
}
*/

export type LegacyGasMarketFields = {
gasPrice: BigIntLike
}

export interface LegacyGasMarketInterface {
readonly gasPrice: bigint
}

interface L1DefaultContainer
extends TxDataContainer,
DefaultContainerInterface,
ECDSAContainerInterface {}

export interface LegacyContainerInterface extends L1DefaultContainer, LegacyGasMarketInterface {
// to: DefaultContainerInterface['to'] | null
}

export type ChainIdFields = {
chainId?: BigIntLike
}

export interface ChainIdInterface {
chainId: bigint
}

export type AccessListFields = {
accessList?: AccessListBytes | AccessList | null
}

export interface AccessListInterface {
accessList: AccessListBytes
}

interface L1_2930Interface extends L1DefaultContainer, ChainIdInterface, AccessListInterface {}

export interface AccessList2930ContainerInterface
extends L1_2930Interface,
LegacyGasMarketInterface {}

// interface AccessList2930Interface: L1DefaultFields, ContractCreationDataFields, LegacyGasMarket, ChainId, AccessList

// EIP1559 txs
export type FeeMarketFields = {
maxPriorityFeePerGas?: BigIntLike
maxFeePerGas?: BigIntLike
}

export interface FeeMarketInterface {
readonly maxPriorityFeePerGas: bigint
readonly maxFeePerGas: bigint
}

export interface FeeMarket1559Interface extends L1_2930Interface, FeeMarketInterface {}

// EIP4844 txs
export type BlobFields = {
blobVersionedHashes?: BytesLike[]
maxFeePerBlobGas?: BigIntLike
blobs?: BytesLike[]
kzgCommitments?: BytesLike[]
kzgProofs?: BytesLike[]
blobsData?: string[]
}

export interface BlobInterface {
readonly blobVersionedHashes: PrefixedHexString[] // TODO why is this a string and not uint8array?
readonly blobs?: PrefixedHexString[]
readonly kzgCommitments?: PrefixedHexString[]
readonly kzgProofs?: PrefixedHexString[]
readonly maxFeePerBlobGas: bigint
}

export interface Blob4844Interface extends FeeMarket1559Interface, BlobInterface {}

// EIP7702 txs

export type AuthorizationListFields = {
authorizationList?: AuthorizationListBytes | AuthorizationList | never
}

export interface AuthorizationListInterface {
readonly authorizationList: AuthorizationListBytes
}

export interface EOA7702Interface extends FeeMarket1559Interface, AuthorizationListInterface {}
13 changes: 13 additions & 0 deletions packages/tx/src/dataContainers/AccessList2930Container.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { Address, bytesToBigInt, toBytes } from '@ethereumjs/util'

import { Feature } from '../dataContainerTypes.js'

import type { AccessListInterface } from '../dataContainerTypes.js'

const accessListFeatures = new Set<Feature>([
Feature.ECDSASignable,
Feature.LegacyGasMarket,
Feature.AccessLists,
])

export class AccessList2930Container implements AccessListInterface {}
3 changes: 3 additions & 0 deletions packages/tx/src/dataContainers/EOA7702DataContainer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import type { EOA7702Interface } from '../dataContainerTypes.js'

export class EOA7702DataContainer implements EOA7702Interface {}
84 changes: 84 additions & 0 deletions packages/tx/src/dataContainers/legacyContainer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import { RLP } from '@ethereumjs/rlp'
import { Address, bytesToBigInt, toBytes } from '@ethereumjs/util'

import { Feature } from '../dataContainerTypes.js'
import { TransactionType } from '../types.js'

import type {
ContractCreationContainerInterface,
DefaultContainerInterface,
ECDSAContainerInterface,
LegacyContainerDataFields,
LegacyGasMarketInterface,
TxDataContainer,
} from '../dataContainerTypes.js'
import type { TxOptions } from '../types.js'

const legacyFeatures = new Set<Feature>([Feature.ECDSASignable, Feature.LegacyGasMarket])

export class LegacyDataContainer
implements
TxDataContainer,
DefaultContainerInterface,
ContractCreationContainerInterface,
ECDSAContainerInterface,
LegacyGasMarketInterface
{
public type: number = TransactionType.Legacy // Legacy tx type

// Tx data part (part of the RLP)
public readonly gasPrice: bigint
public readonly nonce: bigint
public readonly gasLimit: bigint
public readonly value: bigint
public readonly data: Uint8Array
// TODO fix type (how to do this? need to somehow override the interface)
public readonly to: Address | null

// Props only for signed txs
public readonly v?: bigint
public readonly r?: bigint
public readonly s?: bigint

// TODO: verify if txOptions is necessary
// TODO (optimizing): for reach tx we auto-convert the input values to the target values (mostly bigints)
// Is this necessary? What if we need the unconverted values? Convert it on the fly?
constructor(txData: LegacyContainerDataFields, txOptions: TxOptions) {
const { nonce, gasLimit, to, value, data, v, r, s } = txData

// Set the tx properties
const toB = toBytes(to === '' ? '0x' : to)
this.to = toB.length > 0 ? new Address(toB) : null

this.nonce = bytesToBigInt(toBytes(nonce))
this.gasLimit = bytesToBigInt(toBytes(gasLimit))
this.value = bytesToBigInt(toBytes(value))
this.data = toBytes(data === '' ? '0x' : data)
this.gasPrice = bytesToBigInt(toBytes(txData.gasPrice))

// Set signature values (if the tx is signed)

const vB = toBytes(v)
const rB = toBytes(r)
const sB = toBytes(s)
this.v = vB.length > 0 ? bytesToBigInt(vB) : undefined
this.r = rB.length > 0 ? bytesToBigInt(rB) : undefined
this.s = sB.length > 0 ? bytesToBigInt(sB) : undefined
}

raw() {
// TODO
return []
}
serialize() {
return RLP.encode(this.raw())
}

supports(feature: Feature) {
return legacyFeatures.has(feature)
}

toJSON() {
return {}
}
}
22 changes: 22 additions & 0 deletions packages/tx/src/dataContainers/template.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { RLP } from '@ethereumjs/rlp'

import type { TxDataContainer } from '../dataContainerTypes.js'
import type { NestedUint8Array } from '@ethereumjs/rlp'

export abstract class TemplateDataContainer implements TxDataContainer {
type = -1

abstract raw(): NestedUint8Array
serialize() {
// Defaults to use RLP.encode
return RLP.encode(this.raw())
}

supports(/*feature: Feature*/) {
return false
}

toJSON() {
return {}
}
}
Loading

0 comments on commit 0657313

Please sign in to comment.