Skip to content

Commit

Permalink
monorepo: work on eip7002 exits
Browse files Browse the repository at this point in the history
  • Loading branch information
jochem-brouwer committed Mar 26, 2024
1 parent f3feabc commit 06c3ad4
Show file tree
Hide file tree
Showing 6 changed files with 179 additions and 5 deletions.
43 changes: 39 additions & 4 deletions packages/block/src/block.ts
Expand Up @@ -4,6 +4,7 @@ import { Trie } from '@ethereumjs/trie'
import { BlobEIP4844Transaction, Capability, TransactionFactory } from '@ethereumjs/tx'
import {
BIGINT_0,
Exit,
KECCAK256_RLP,
Withdrawal,
bigIntToHex,
Expand Down Expand Up @@ -40,7 +41,7 @@ import type {
TxOptions,
TypedTransaction,
} from '@ethereumjs/tx'
import type { EthersProvider, WithdrawalBytes } from '@ethereumjs/util'
import type { EthersProvider, ExitBytes, ExitData, WithdrawalBytes } from '@ethereumjs/util'

/**
* An object that represents the block.
Expand All @@ -50,6 +51,7 @@ export class Block {
public readonly transactions: TypedTransaction[] = []
public readonly uncleHeaders: BlockHeader[] = []
public readonly withdrawals?: Withdrawal[]
public readonly exits?: Exit[]
public readonly common: Common
protected keccakFunction: (msg: Uint8Array) => Uint8Array

Expand Down Expand Up @@ -102,6 +104,7 @@ export class Block {
transactions: txsData,
uncleHeaders: uhsData,
withdrawals: withdrawalsData,
exitData,
executionWitness: executionWitnessData,
} = blockData

Expand Down Expand Up @@ -140,8 +143,9 @@ export class Block {
// The witness data is planned to come in rlp serialized bytes so leave this
// stub till that time
const executionWitness = executionWitnessData
const exits = exitData?.map(Exit.fromExitData)

return new Block(header, transactions, uncleHeaders, withdrawals, opts, executionWitness)
return new Block(header, transactions, uncleHeaders, withdrawals, exits, opts, executionWitness)
}

/**
Expand Down Expand Up @@ -175,7 +179,7 @@ export class Block {

// First try to load header so that we can use its common (in case of setHardfork being activated)
// to correctly make checks on the hardforks
const [headerData, txsData, uhsData, withdrawalBytes, executionWitnessBytes] = values
const [headerData, txsData, uhsData, withdrawalBytes, exitBytes, executionWitnessBytes] = values
const header = BlockHeader.fromValuesArray(headerData, opts)

if (
Expand All @@ -187,6 +191,15 @@ export class Block {
)
}

if (
(header.common.isActivatedEIP(7002) && exitBytes === undefined) ||
!Array.isArray(exitBytes)
) {
throw new Error(
'Invalid serialized block input: EIP-7002 is active, and no exits were provided as array'
)
}

// parse transactions
const transactions = []
for (const txData of txsData ?? []) {
Expand Down Expand Up @@ -225,6 +238,15 @@ export class Block {
}))
?.map(Withdrawal.fromWithdrawalData)

// TODO fix my type
// @ts-ignore
const exits = (exitBytes as ExitBytes[])
?.map(([address, validatorPubkey]) => ({
address,
validatorPubkey,
}))
?.map(Exit.fromExitData)

// executionWitness are not part of the EL fetched blocks via eth_ bodies method
// they are currently only available via the engine api constructed blocks
let executionWitness
Expand All @@ -240,7 +262,7 @@ export class Block {
}
}

return new Block(header, transactions, uncleHeaders, withdrawals, opts, executionWitness)
return new Block(header, transactions, uncleHeaders, withdrawals, exits, opts, executionWitness)
}

/**
Expand Down Expand Up @@ -411,6 +433,7 @@ export class Block {
transactions: TypedTransaction[] = [],
uncleHeaders: BlockHeader[] = [],
withdrawals?: Withdrawal[],
exits?: Exit[],
opts: BlockOptions = {},
executionWitness?: VerkleExecutionWitness | null
) {
Expand All @@ -420,6 +443,7 @@ export class Block {

this.transactions = transactions
this.withdrawals = withdrawals ?? (this.common.isActivatedEIP(4895) ? [] : undefined)
this.exits = exits ?? (this.common.isActivatedEIP(7002) ? [] : undefined)
this.executionWitness = executionWitness
// null indicates an intentional absence of value or unavailability
// undefined indicates that the executionWitness should be initialized with the default state
Expand Down Expand Up @@ -469,6 +493,10 @@ export class Block {
throw new Error(`Cannot have executionWitness field if EIP 6800 is not active `)
}

if (!this.common.isActivatedEIP(7002) && exits !== undefined && exits !== null) {
throw new Error(`Cannot have exits field if EIP 7002 is not active`)
}

const freeze = opts?.freeze ?? true
if (freeze) {
Object.freeze(this)
Expand All @@ -490,6 +518,7 @@ export class Block {
if (withdrawalsRaw) {
bytesArray.push(withdrawalsRaw)
}
const exits = this.exits?.map((ex) => ex.raw())
if (this.executionWitness !== undefined && this.executionWitness !== null) {
const executionWitnessBytes = RLP.encode(JSON.stringify(this.executionWitness))
bytesArray.push(executionWitnessBytes as any)
Expand Down Expand Up @@ -663,6 +692,12 @@ export class Block {
throw new Error(`Invalid block: ethereumjs stateless client needs executionWitness`)
}
}

if (this.common.isActivatedEIP(7002)) {
if (this.exits === undefined || this.exits === null) {
throw new Error('Invalid block: missing exits')
}
}
}

/**
Expand Down
34 changes: 33 additions & 1 deletion packages/block/src/header.ts
Expand Up @@ -63,6 +63,7 @@ export class BlockHeader {
public readonly blobGasUsed?: bigint
public readonly excessBlobGas?: bigint
public readonly parentBeaconBlockRoot?: Uint8Array
public readonly exitsRoot?: Uint8Array

public readonly common: Common

Expand Down Expand Up @@ -117,7 +118,8 @@ export class BlockHeader {
*/
public static fromValuesArray(values: BlockHeaderBytes, opts: BlockOptions = {}) {
const headerData = valuesArrayToHeaderData(values)
const { number, baseFeePerGas, excessBlobGas, blobGasUsed, parentBeaconBlockRoot } = headerData
const { number, baseFeePerGas, excessBlobGas, blobGasUsed, parentBeaconBlockRoot, exitsRoot } =
headerData
const header = BlockHeader.fromHeaderData(headerData, opts)
// eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
if (header.common.isActivatedEIP(1559) && baseFeePerGas === undefined) {
Expand All @@ -138,6 +140,9 @@ export class BlockHeader {
if (header.common.isActivatedEIP(4788) && parentBeaconBlockRoot === undefined) {
throw new Error('invalid header. parentBeaconBlockRoot should be provided')
}
if (header.common.isActivatedEIP(7002) && exitsRoot === undefined) {
throw new Error('invalid header. exitsRoot should be provided')
}
return header
}
/**
Expand Down Expand Up @@ -222,6 +227,7 @@ export class BlockHeader {
blobGasUsed: this.common.isActivatedEIP(4844) ? BIGINT_0 : undefined,
excessBlobGas: this.common.isActivatedEIP(4844) ? BIGINT_0 : undefined,
parentBeaconBlockRoot: this.common.isActivatedEIP(4788) ? zeros(32) : undefined,
exitsRoot: this.common.isActivatedEIP(7002) ? zeros(32) : undefined,
}

const baseFeePerGas =
Expand All @@ -235,6 +241,8 @@ export class BlockHeader {
const parentBeaconBlockRoot =
toType(headerData.parentBeaconBlockRoot, TypeOutput.Uint8Array) ??
hardforkDefaults.parentBeaconBlockRoot
const exitsRoot =
toType(headerData.exitsRoot, TypeOutput.Uint8Array) ?? hardforkDefaults.exitsRoot

if (!this.common.isActivatedEIP(1559) && baseFeePerGas !== undefined) {
throw new Error('A base fee for a block can only be set with EIP1559 being activated')
Expand Down Expand Up @@ -262,6 +270,10 @@ export class BlockHeader {
)
}

if (!this.common.isActivatedEIP(7200) && exitsRoot !== undefined) {
throw new Error('A exitsRoot for a header can only be provided with EIP7002 being activated')
}

this.parentHash = parentHash
this.uncleHash = uncleHash
this.coinbase = coinbase
Expand All @@ -282,6 +294,7 @@ export class BlockHeader {
this.blobGasUsed = blobGasUsed
this.excessBlobGas = excessBlobGas
this.parentBeaconBlockRoot = parentBeaconBlockRoot
this.exitsRoot = exitsRoot
this._genericFormatValidation()
this._validateDAOExtraData()

Expand Down Expand Up @@ -407,6 +420,19 @@ export class BlockHeader {
throw new Error(msg)
}
}

if (this.common.isActivatedEIP(7002) === true) {
if (this.exitsRoot === undefined) {
const msg = this._errorMsg('EIP7002 has no exitsRoot field')
throw new Error(msg)
}
if (this.exitsRoot.length !== 32) {
const msg = this._errorMsg(
`exitsRoot must be 32 bytes, received ${this.exitsRoot.length} bytes`
)
throw new Error(msg)
}
}
}

/**
Expand Down Expand Up @@ -693,6 +719,9 @@ export class BlockHeader {
if (this.common.isActivatedEIP(4788) === true) {
rawItems.push(this.parentBeaconBlockRoot!)
}
if (this.common.isActivatedEIP(7002)) {
rawItems.push(this.exitsRoot!)
}

return rawItems
}
Expand Down Expand Up @@ -960,6 +989,9 @@ export class BlockHeader {
if (this.common.isActivatedEIP(4788) === true) {
jsonDict.parentBeaconBlockRoot = bytesToHex(this.parentBeaconBlockRoot!)
}
if (this.common.isActivatedEIP(7002)) {
jsonDict.exitsRoot = bytesToHex(this.exitsRoot!)
}
return jsonDict
}

Expand Down
6 changes: 6 additions & 0 deletions packages/block/src/types.ts
Expand Up @@ -5,6 +5,8 @@ import type {
AddressLike,
BigIntLike,
BytesLike,
ExitBytes,
ExitData,
JsonRpcWithdrawal,
PrefixedHexString,
WithdrawalBytes,
Expand Down Expand Up @@ -136,6 +138,7 @@ export interface HeaderData {
blobGasUsed?: BigIntLike
excessBlobGas?: BigIntLike
parentBeaconBlockRoot?: BytesLike
exitsRoot?: BytesLike
}

/**
Expand All @@ -149,6 +152,7 @@ export interface BlockData {
transactions?: Array<TxData[TransactionType]>
uncleHeaders?: Array<HeaderData>
withdrawals?: Array<WithdrawalData>
exitData?: Array<ExitData>
/**
* EIP-6800: Verkle Proof Data (experimental)
*/
Expand All @@ -166,6 +170,7 @@ export type BlockBytes =
TransactionsBytes,
UncleHeadersBytes,
WithdrawalsBytes,
ExitBytes,
ExecutionWitnessBytes
]

Expand Down Expand Up @@ -218,6 +223,7 @@ export interface JsonHeader {
blobGasUsed?: string
excessBlobGas?: string
parentBeaconBlockRoot?: string
exitsRoot?: string
}

/*
Expand Down
13 changes: 13 additions & 0 deletions packages/common/src/eips.ts
Expand Up @@ -510,6 +510,19 @@ export const EIPs: EIPsDict = {
},
},
},
7002: {
comment: 'Execution layer triggerable exits (experimental)',
url: 'https://github.com/ethereum/EIPs/commit/35589d35c40576d932923542b31a4d8f7812c3e7',
status: Status.Draft,
minimumHardfork: Hardfork.Paris,
requiredEIPs: [],
gasPrices: {
validatorExcessAddress: {
v: BigInt('0x0f1ee3e66777F27a7703400644C6fCE41527E017'),
d: 'Address of the validator excess address',
},
},
},
7516: {
comment: 'BLOBBASEFEE opcode',
url: 'https://eips.ethereum.org/EIPS/eip-7516',
Expand Down
83 changes: 83 additions & 0 deletions packages/util/src/exit.ts
@@ -0,0 +1,83 @@
import { Address } from './address.js'
import { bigIntToHex, bytesToHex, toBytes } from './bytes.js'
import { BIGINT_0 } from './constants.js'
import { TypeOutput, toType } from './types.js'

import type { AddressLike, BigIntLike, BytesLike, PrefixedHexString } from './types.js'

/**
* Flexible input data type for EIP-4895 withdrawal data with amount in Gwei to
* match CL representation and for eventual ssz withdrawalsRoot
*/
export type ExitData = {
address: Uint8Array
validatorPubkey: Uint8Array
}

/**
* JSON RPC interface for EIP-7002 exit data
*/
export interface JsonRpcExit {
address: PrefixedHexString // 20 bytes
validatorPubkey: PrefixedHexString // 48 bytes
}

export type ExitBytes = [Uint8Array, Uint8Array]

/**
* Representation of EIP-7002 exit data
*/
export class Exit {
/**
* This constructor assigns and validates the values.
* Use the static factory methods to assist in creating a Withdrawal object from varying data types.
* Its amount is in Gwei to match CL representation and for eventual ssz withdrawalsRoot
*/
constructor(public readonly address: Uint8Array, public readonly validatorPubkey: Uint8Array) {}

public static fromExitData(exitData: ExitData) {
const { address, validatorPubkey } = exitData

const vPubKey = toType(validatorPubkey, TypeOutput.Uint8Array)
const addr = toType(address, TypeOutput.Uint8Array)

return new Exit(addr, vPubKey)
}

public static fromValuesArray(exitArray: ExitBytes) {
if (exitArray.length !== 2) {
throw Error(`Invalid exitArray length expected=2 actual=${exitArray.length}`)
}
const [address, validatorPubkey] = exitArray
return Exit.fromExitData({ validatorPubkey, address })
}

/**
* Convert a withdrawal to a buffer array
* @param withdrawal the withdrawal to convert
* @returns buffer array of the withdrawal
*/
public static toBytesArray(exit: Exit | ExitData): ExitBytes {
const { validatorPubkey, address } = exit

return [address, validatorPubkey]
}

raw() {
return Exit.toBytesArray(this)
}

toValue() {
return {
address: this.address,
validatorPubkey: this.validatorPubkey,
}
}

toJSON() {
return {
address: this.address.toString(),
validatorPubkey: bytesToHex(this.validatorPubkey),
}
}
}

0 comments on commit 06c3ad4

Please sign in to comment.