Skip to content

Commit

Permalink
client: Implement withdrawals via engine api (#2401)
Browse files Browse the repository at this point in the history
* client: Implement v2 versions for execution api supporting withdrawals

* create v2 endpoints and proxy them to main handlers

* refac withdrawals to have a correct withdrawal object

* fix lint

* add v2 versions and withdrawal validator

* extract out withdrawals as separate class

* use withdrawal in newpayload

* fix the v2 binding for fcu

* add withdrawals to block building

* add withdrawals to shanghai

* fully working withdrawals feature

* add withdrawals data in eth getBlock response

* check genesis annoucement

* fix and test empty withdrawals

* add static helpers for trie roots

* clean up trie roots

* fix withdrawals root to match with other clients

* skeleton improv + withdrawal root check

* add the failing withdrawal root mismatch testcase

* fix the stateroot mismatch

* skip withdrawal reward if 0 on runblock too

* fix spec

* restore the buildblock's trieroot method

* rename gen root methods

* improve the jsdocs

* genesis handling at skeleton sethead

* cleanup skeleton

* cleanup bigint literal

* remove extra typecasting

* add comments for spec vec source

* withdrawal spec vector in test

* improve var name

* refactor withdrawal and enhance spec test

* add zero amount withdrawal test case for vm block run

* add spec test for buildblock with withdrawals
  • Loading branch information
g11tech authored Nov 18, 2022
1 parent 5274b49 commit a00251d
Show file tree
Hide file tree
Showing 24 changed files with 1,011 additions and 317 deletions.
113 changes: 54 additions & 59 deletions packages/block/src/block.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,36 +4,29 @@ import { Trie } from '@ethereumjs/trie'
import { Capability, TransactionFactory } from '@ethereumjs/tx'
import {
KECCAK256_RLP,
Withdrawal,
arrToBufArr,
bigIntToHex,
bufArrToArr,
bufferToHex,
intToHex,
isHexPrefixed,
toBuffer,
} from '@ethereumjs/util'
import { keccak256 } from 'ethereum-cryptography/keccak'
import { ethers } from 'ethers'

import { blockFromRpc } from './from-rpc'
import { BlockHeader } from './header'

import type {
BlockBuffer,
BlockData,
BlockOptions,
JsonBlock,
JsonRpcBlock,
Withdrawal,
} from './types'
import type { BlockBuffer, BlockData, BlockOptions, JsonBlock, JsonRpcBlock } from './types'
import type { Common } from '@ethereumjs/common'
import type {
FeeMarketEIP1559Transaction,
Transaction,
TxOptions,
TypedTransaction,
} from '@ethereumjs/tx'
import type { Address } from '@ethereumjs/util'
import type { WithdrawalBuffer } from '@ethereumjs/util'

/**
* An object that represents the block.
Expand All @@ -46,6 +39,32 @@ export class Block {
public readonly txTrie = new Trie()
public readonly _common: Common

/**
* Returns the withdrawals trie root for array of Withdrawal.
* @param wts array of Withdrawal to compute the root of
* @param optional emptyTrie to use to generate the root
*/
public static async genWithdrawalsTrieRoot(wts: Withdrawal[], emptyTrie?: Trie) {
const trie = emptyTrie ?? new Trie()
for (const [i, wt] of wts.entries()) {
await trie.put(Buffer.from(RLP.encode(i)), arrToBufArr(RLP.encode(wt.raw())))
}
return trie.root()
}

/**
* Returns the txs trie root for array of TypedTransaction
* @param txs array of TypedTransaction to compute the root of
* @param optional emptyTrie to use to generate the root
*/
public static async genTransactionsTrieRoot(txs: TypedTransaction[], emptyTrie?: Trie) {
const trie = emptyTrie ?? new Trie()
for (const [i, tx] of txs.entries()) {
await trie.put(Buffer.from(RLP.encode(i)), tx.serialize())
}
return trie.root()
}

/**
* Static constructor to create a block from a block data dictionary
*
Expand All @@ -57,7 +76,7 @@ export class Block {
header: headerData,
transactions: txsData,
uncleHeaders: uhsData,
withdrawals,
withdrawals: withdrawalsData,
} = blockData
const header = BlockHeader.fromHeaderData(headerData, opts)

Expand Down Expand Up @@ -93,6 +112,8 @@ export class Block {
uncleHeaders.push(uh)
}

const withdrawals = withdrawalsData?.map(Withdrawal.fromWithdrawalData)

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

Expand Down Expand Up @@ -122,8 +143,7 @@ export class Block {
if (values.length > 4) {
throw new Error('invalid block. More values than expected were received')
}

const [headerData, txsData, uhsData, withdrawalsData] = values
const [headerData, txsData, uhsData, withdrawalsBuffer] = values

const header = BlockHeader.fromValuesArray(headerData, opts)

Expand Down Expand Up @@ -159,14 +179,14 @@ export class Block {
uncleHeaders.push(BlockHeader.fromValuesArray(uncleHeaderData, uncleOpts))
}

let withdrawals
if (withdrawalsData) {
withdrawals = <Withdrawal[]>[]
for (const withdrawal of withdrawalsData) {
const [index, validatorIndex, address, amount] = withdrawal
withdrawals.push({ index, validatorIndex, address, amount })
}
}
const withdrawals = (withdrawalsBuffer as WithdrawalBuffer[])
?.map(([index, validatorIndex, address, amount]) => ({
index,
validatorIndex,
address,
amount,
}))
?.map(Withdrawal.fromWithdrawalData)

return new Block(header, transactions, uncleHeaders, opts, withdrawals)
}
Expand Down Expand Up @@ -241,6 +261,7 @@ export class Block {
) {
this.header = header ?? BlockHeader.fromHeaderData({}, opts)
this.transactions = transactions
this.withdrawals = withdrawals
this.uncleHeaders = uncleHeaders
this._common = this.header._common
if (uncleHeaders.length > 0) {
Expand All @@ -265,32 +286,12 @@ export class Block {
throw new Error('Cannot have a withdrawals field if EIP 4895 is not active')
}

this.withdrawals = withdrawals

const freeze = opts?.freeze ?? true
if (freeze) {
Object.freeze(this)
}
}

/**
* Convert a withdrawal to a buffer array
* @param withdrawal the withdrawal to convert
* @returns buffer array of the withdrawal
*/
private withdrawalToBufferArray(withdrawal: Withdrawal): [Buffer, Buffer, Buffer, Buffer] {
const { index, validatorIndex, address, amount } = withdrawal
let addressBuffer: Buffer
if (typeof address === 'string') {
addressBuffer = Buffer.from(address.slice(2))
} else if (Buffer.isBuffer(address)) {
addressBuffer = address
} else {
addressBuffer = (<Address>address).buf
}
return [toBuffer(index), toBuffer(validatorIndex), addressBuffer, toBuffer(amount)]
}

/**
* Returns a Buffer Array of the raw Buffers of this block, in order.
*/
Expand All @@ -302,10 +303,9 @@ export class Block {
) as Buffer[],
this.uncleHeaders.map((uh) => uh.raw()),
]
if (this.withdrawals) {
for (const withdrawal of this.withdrawals) {
bufferArray.push(this.withdrawalToBufferArray(withdrawal))
}
const withdrawalsRaw = this.withdrawals?.map((wt) => wt.raw())
if (withdrawalsRaw) {
bufferArray.push(withdrawalsRaw)
}
return bufferArray
}
Expand Down Expand Up @@ -336,12 +336,7 @@ export class Block {
*/
async genTxTrie(): Promise<void> {
const { transactions, txTrie } = this
for (let i = 0; i < transactions.length; i++) {
const tx = transactions[i]
const key = Buffer.from(RLP.encode(i))
const value = tx.serialize()
await txTrie.put(key, value)
}
await Block.genTransactionsTrieRoot(transactions, txTrie)
}

/**
Expand Down Expand Up @@ -449,14 +444,8 @@ export class Block {
if (!this._common.isActivatedEIP(4895)) {
throw new Error('EIP 4895 is not activated')
}
const trie = new Trie()
let index = 0
for (const withdrawal of this.withdrawals!) {
const withdrawalRLP = RLP.encode(this.withdrawalToBufferArray(withdrawal))
await trie.put(Buffer.from('0x' + index.toString(16)), arrToBufArr(withdrawalRLP))
index++
}
return trie.root().equals(this.header.withdrawalsRoot!)
const withdrawalsRoot = await Block.genWithdrawalsTrieRoot(this.withdrawals!)
return withdrawalsRoot.equals(this.header.withdrawalsRoot!)
}

/**
Expand Down Expand Up @@ -510,10 +499,16 @@ export class Block {
* Returns the block in JSON format.
*/
toJSON(): JsonBlock {
const withdrawalsAttr = this.withdrawals
? {
withdrawals: this.withdrawals.map((wt) => wt.toJSON()),
}
: {}
return {
header: this.header.toJSON(),
transactions: this.transactions.map((tx) => tx.toJSON()),
uncleHeaders: this.uncleHeaders.map((uh) => uh.toJSON()),
...withdrawalsAttr,
}
}

Expand Down
10 changes: 9 additions & 1 deletion packages/block/src/header.ts
Original file line number Diff line number Diff line change
Expand Up @@ -203,7 +203,7 @@ export class BlockHeader {
}

if (this._common.isActivatedEIP(4895)) {
if (withdrawalsRoot === defaults.withdrawalsRoot) {
if (withdrawalsRoot === undefined) {
throw new Error('invalid header. withdrawalsRoot should be provided')
}
} else {
Expand Down Expand Up @@ -540,6 +540,10 @@ export class BlockHeader {
rawItems.push(bigIntToUnpaddedBuffer(this.baseFeePerGas!))
}

if (this._common.isActivatedEIP(4895) === true) {
rawItems.push(this.withdrawalsRoot!)
}

return rawItems
}

Expand Down Expand Up @@ -778,12 +782,16 @@ export class BlockHeader {
* Returns the block header in JSON format.
*/
toJSON(): JsonHeader {
const withdrawalAttr = this.withdrawalsRoot
? { withdrawalsRoot: '0x' + this.withdrawalsRoot.toString('hex') }
: {}
const jsonDict: JsonHeader = {
parentHash: '0x' + this.parentHash.toString('hex'),
uncleHash: '0x' + this.uncleHash.toString('hex'),
coinbase: this.coinbase.toString(),
stateRoot: '0x' + this.stateRoot.toString('hex'),
transactionsTrie: '0x' + this.transactionsTrie.toString('hex'),
...withdrawalAttr,
receiptTrie: '0x' + this.receiptTrie.toString('hex'),
logsBloom: '0x' + this.logsBloom.toString('hex'),
difficulty: bigIntToHex(this.difficulty),
Expand Down
33 changes: 14 additions & 19 deletions packages/block/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,15 @@ import type {
JsonTx,
TxData,
} from '@ethereumjs/tx'
import type { AddressLike, BigIntLike, BufferLike } from '@ethereumjs/util'
import type {
AddressLike,
BigIntLike,
BufferLike,
JsonRpcWithdrawal,
WithdrawalBuffer,
WithdrawalData,
} from '@ethereumjs/util'

/**
* An object to set to which blockchain the blocks and their headers belong. This could be specified
* using a {@link Common} object, or `chain` and `hardfork`. Defaults to mainnet without specifying a
Expand Down Expand Up @@ -98,13 +106,6 @@ export interface HeaderData {
withdrawalsRoot?: BufferLike
}

export type Withdrawal = {
index: BigIntLike
validatorIndex: BigIntLike
address: AddressLike
amount: BigIntLike
}

/**
* A block's data.
*/
Expand All @@ -115,20 +116,21 @@ export interface BlockData {
header?: HeaderData
transactions?: Array<TxData | AccessListEIP2930TxData | FeeMarketEIP1559TxData>
uncleHeaders?: Array<HeaderData>
withdrawals?: Array<Withdrawal>
withdrawals?: Array<WithdrawalData>
}

export type WithdrawalsBuffer = WithdrawalBuffer[]

export type BlockBuffer =
| [BlockHeaderBuffer, TransactionsBuffer, UncleHeadersBuffer]
| [BlockHeaderBuffer, TransactionsBuffer, UncleHeadersBuffer, WithdrawalBuffer]
| [BlockHeaderBuffer, TransactionsBuffer, UncleHeadersBuffer, WithdrawalsBuffer]
export type BlockHeaderBuffer = Buffer[]
export type BlockBodyBuffer = [TransactionsBuffer, UncleHeadersBuffer, WithdrawalBuffer?]
export type BlockBodyBuffer = [TransactionsBuffer, UncleHeadersBuffer, WithdrawalsBuffer?]
/**
* TransactionsBuffer can be an array of serialized txs for Typed Transactions or an array of Buffer Arrays for legacy transactions.
*/
export type TransactionsBuffer = Buffer[][] | Buffer[]
export type UncleHeadersBuffer = Buffer[][]
export type WithdrawalBuffer = Buffer[][]

/**
* An object with the block's data represented as strings.
Expand Down Expand Up @@ -166,13 +168,6 @@ export interface JsonHeader {
withdrawalsRoot?: string
}

export interface JsonRpcWithdrawal {
index: string // QUANTITY - bigint 8 bytes
validatorIndex: string // QUANTITY - bigint 8 bytes
address: string // DATA, 20 Bytes address to withdraw to
amount: string // QUANTITY - bigint amount in wei 32 bytes
}

/*
* Based on https://ethereum.org/en/developers/docs/apis/json-rpc/
*/
Expand Down
Loading

0 comments on commit a00251d

Please sign in to comment.