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

Enhance typing of CL requests / beacon/execution payload handling / Dedicated Util Withdrawal + Deposit Request Classes #3398

Merged
merged 7 commits into from
May 4, 2024
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
62 changes: 53 additions & 9 deletions packages/block/src/block.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,13 @@ import { Trie } from '@ethereumjs/trie'
import { BlobEIP4844Transaction, Capability, TransactionFactory } from '@ethereumjs/tx'
import {
BIGINT_0,
CLRequest,
CLRequestFactory,
CLRequestType,
DepositRequest,
KECCAK256_RLP,
KECCAK256_RLP_ARRAY,
Withdrawal,
WithdrawalRequest,
bigIntToHex,
bytesToHex,
bytesToUtf8,
Expand Down Expand Up @@ -46,7 +49,7 @@ import type {
TypedTransaction,
} from '@ethereumjs/tx'
import type {
CLRequestType,
CLRequest,
EthersProvider,
PrefixedHexString,
RequestBytes,
Expand All @@ -61,7 +64,7 @@ export class Block {
public readonly transactions: TypedTransaction[] = []
public readonly uncleHeaders: BlockHeader[] = []
public readonly withdrawals?: Withdrawal[]
public readonly requests?: CLRequestType[]
public readonly requests?: CLRequest<CLRequestType>[]
public readonly common: Common
protected keccakFunction: (msg: Uint8Array) => Uint8Array

Expand Down Expand Up @@ -110,7 +113,7 @@ export class Block {
* @param emptyTrie optional empty trie used to generate the root
* @returns a 32 byte Uint8Array representing the requests trie root
*/
public static async genRequestsTrieRoot(requests: CLRequest[], emptyTrie?: Trie) {
public static async genRequestsTrieRoot(requests: CLRequest<CLRequestType>[], emptyTrie?: Trie) {
// Requests should be sorted in monotonically ascending order based on type
// and whatever internal sorting logic is defined by each request type
if (requests.length > 1) {
Expand Down Expand Up @@ -300,8 +303,8 @@ export class Block {

let requests
if (header.common.isActivatedEIP(7685)) {
requests = (requestBytes as RequestBytes[]).map(
(bytes) => new CLRequest(bytes[0], bytes.slice(1))
requests = (requestBytes as RequestBytes[]).map((bytes) =>
CLRequestFactory.fromSerializedRequest(bytes)
)
}
// executionWitness are not part of the EL fetched blocks via eth_ bodies method
Expand Down Expand Up @@ -420,6 +423,8 @@ export class Block {
transactions,
withdrawals: withdrawalsData,
requestsRoot,
depositRequests,
withdrawalRequests,
executionWitness,
} = payload

Expand Down Expand Up @@ -459,9 +464,25 @@ export class Block {
requestsRoot: reqRoot,
}

const hasDepositRequests = depositRequests !== undefined && depositRequests !== null
const hasWithdrawalRequests = withdrawalRequests !== undefined && withdrawalRequests !== null
const requests =
hasDepositRequests || hasWithdrawalRequests ? ([] as CLRequest<CLRequestType>[]) : undefined
Copy link
Member

Choose a reason for hiding this comment

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

Shouldn't this just check if there is a requestsRoot?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

well it can check that as well but I thought this check to be more thorough. though i have no strong opinions and can do requestroot check


if (depositRequests !== undefined && depositRequests !== null) {
for (const dJson of depositRequests) {
requests!.push(DepositRequest.fromJSON(dJson))
}
}
if (withdrawalRequests !== undefined && withdrawalRequests !== null) {
for (const wJson of withdrawalRequests) {
requests!.push(WithdrawalRequest.fromJSON(wJson))
}
}

// we are not setting setHardfork as common is already set to the correct hf
const block = Block.fromBlockData(
{ header, transactions: txs, withdrawals, executionWitness },
{ header, transactions: txs, withdrawals, executionWitness, requests },
opts
)
if (
Expand Down Expand Up @@ -505,7 +526,7 @@ export class Block {
uncleHeaders: BlockHeader[] = [],
withdrawals?: Withdrawal[],
opts: BlockOptions = {},
requests?: CLRequest[],
requests?: CLRequest<CLRequestType>[],
executionWitness?: VerkleExecutionWitness | null
) {
this.header = header ?? BlockHeader.fromHeaderData({}, opts)
Expand Down Expand Up @@ -657,7 +678,7 @@ export class Block {
return result
}

async requestsTrieIsValid(requestsInput?: CLRequest[]): Promise<boolean> {
async requestsTrieIsValid(requestsInput?: CLRequest<CLRequestType>[]): Promise<boolean> {
if (!this.common.isActivatedEIP(7685)) {
throw new Error('EIP 7685 is not activated')
}
Expand Down Expand Up @@ -978,6 +999,29 @@ export class Block {
...withdrawalsArr,
parentBeaconBlockRoot: header.parentBeaconBlockRoot,
executionWitness: this.executionWitness,

// lets add the request fields first and then iterate over requests to fill them up
depositRequests: this.common.isActivatedEIP(6110) ? [] : undefined,
withdrawalRequests: this.common.isActivatedEIP(7002) ? [] : undefined,
}

if (this.requests !== undefined) {
for (const request of this.requests) {
Copy link
Member

Choose a reason for hiding this comment

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

These requests should be ordered. I'm not sure if a switch / case structure is the "nicest" way to handle this 🤔

Copy link
Contributor Author

Choose a reason for hiding this comment

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

the sorting is verified as part of block validations so we don't need to worry and keep things simple here

switch (request.type) {
case CLRequestType.Deposit:
executionPayload.depositRequests!.push((request as DepositRequest).toJSON())
continue

case CLRequestType.Withdrawal:
executionPayload.withdrawalRequests!.push((request as WithdrawalRequest).toJSON())
continue
}
}
} else if (
executionPayload.depositRequests !== undefined ||
executionPayload.withdrawalRequests !== undefined
) {
throw Error(`Undefined requests for activated deposit or withdrawal requests`)
}

return executionPayload
Expand Down
37 changes: 37 additions & 0 deletions packages/block/src/from-beacon-payload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,20 @@ type BeaconWithdrawal = {
amount: PrefixedHexString
}

type BeaconDepositRequest = {
pubkey: PrefixedHexString
withdrawal_credentials: PrefixedHexString
amount: PrefixedHexString
signature: PrefixedHexString
index: PrefixedHexString
}

type BeaconWithdrawalRequest = {
source_address: PrefixedHexString
validator_public_key: PrefixedHexString
amount: PrefixedHexString
}

// Payload json that one gets using the beacon apis
// curl localhost:5052/eth/v2/beacon/blocks/56610 | jq .data.message.body.execution_payload
export type BeaconPayloadJson = {
Expand All @@ -31,6 +45,10 @@ export type BeaconPayloadJson = {
blob_gas_used?: PrefixedHexString
excess_blob_gas?: PrefixedHexString
parent_beacon_block_root?: PrefixedHexString
// requests data
deposit_requests?: BeaconDepositRequest[]
withdrawal_requests?: BeaconWithdrawalRequest[]

// the casing of VerkleExecutionWitness remains same camel case for now
execution_witness?: VerkleExecutionWitness
}
Expand Down Expand Up @@ -128,6 +146,25 @@ export function executionPayloadFromBeaconPayload(payload: BeaconPayloadJson): E
if (payload.parent_beacon_block_root !== undefined && payload.parent_beacon_block_root !== null) {
executionPayload.parentBeaconBlockRoot = payload.parent_beacon_block_root
}

// requests
if (payload.deposit_requests !== undefined && payload.deposit_requests !== null) {
executionPayload.depositRequests = payload.deposit_requests.map((breq) => ({
pubkey: breq.pubkey,
withdrawalCredentials: breq.withdrawal_credentials,
amount: breq.amount,
signature: breq.signature,
index: breq.index,
}))
}
if (payload.withdrawal_requests !== undefined && payload.withdrawal_requests !== null) {
executionPayload.withdrawalRequests = payload.withdrawal_requests.map((breq) => ({
sourceAddress: breq.source_address,
validatorPublicKey: breq.validator_public_key,
amount: breq.amount,
}))
}

if (payload.execution_witness !== undefined && payload.execution_witness !== null) {
// the casing structure in payload could be camel case or snake depending upon the CL
executionPayload.executionWitness =
Expand Down
11 changes: 9 additions & 2 deletions packages/block/src/from-rpc.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
import { TransactionFactory } from '@ethereumjs/tx'
import { CLRequest, TypeOutput, hexToBytes, setLengthLeft, toBytes, toType } from '@ethereumjs/util'
import {
CLRequestFactory,
TypeOutput,
hexToBytes,
setLengthLeft,
toBytes,
toType,
} from '@ethereumjs/util'

import { blockHeaderFromRpc } from './header-from-rpc.js'

Expand Down Expand Up @@ -57,7 +64,7 @@ export function blockFromRpc(

const requests = blockParams.requests?.map((req) => {
const bytes = hexToBytes(req as PrefixedHexString)
return new CLRequest(bytes[0], bytes.slice(1))
return CLRequestFactory.fromSerializedRequest(bytes)
})
return Block.fromBlockData(
{ header, transactions, uncleHeaders, withdrawals: blockParams.withdrawals, requests },
Expand Down
7 changes: 6 additions & 1 deletion packages/block/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,14 @@ import type {
BigIntLike,
BytesLike,
CLRequest,
CLRequestType,
DepositRequestV1,
JsonRpcWithdrawal,
PrefixedHexString,
RequestBytes,
WithdrawalBytes,
WithdrawalData,
WithdrawalRequestV1,
} from '@ethereumjs/util'

/**
Expand Down Expand Up @@ -153,7 +156,7 @@ export interface BlockData {
transactions?: Array<TxData[TransactionType]>
uncleHeaders?: Array<HeaderData>
withdrawals?: Array<WithdrawalData>
requests?: Array<CLRequest>
requests?: Array<CLRequest<CLRequestType>>
/**
* EIP-6800: Verkle Proof Data (experimental)
*/
Expand Down Expand Up @@ -303,4 +306,6 @@ export type ExecutionPayload = {
// VerkleExecutionWitness is already a hex serialized object
executionWitness?: VerkleExecutionWitness | null // QUANTITY, 64 Bits, null implies not available
requestsRoot?: PrefixedHexString | string | null // DATA, 32 bytes, null implies EIP 7685 not active yet
depositRequests?: DepositRequestV1[] // Array of 6110 deposit requests
withdrawalRequests?: WithdrawalRequestV1[] // Array of 7002 withdrawal requests
}
56 changes: 34 additions & 22 deletions packages/block/test/eip7685block.spec.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,35 @@
import { Chain, Common, Hardfork } from '@ethereumjs/common'
import { CLRequest, KECCAK256_RLP, concatBytes, hexToBytes, randomBytes } from '@ethereumjs/util'
import {
DepositRequest,
KECCAK256_RLP,
WithdrawalRequest,
bytesToBigInt,
randomBytes,
} from '@ethereumjs/util'
import { assert, describe, expect, it } from 'vitest'

import { Block, BlockHeader } from '../src/index.js'

import type { CLRequestType } from '@ethereumjs/util'
import type { CLRequest, CLRequestType } from '@ethereumjs/util'

class NumberRequest extends CLRequest implements CLRequestType {
constructor(type: number, bytes: Uint8Array) {
super(type, bytes)
}

public static fromRequestData(bytes: Uint8Array): CLRequestType {
return new NumberRequest(0x1, bytes)
function getRandomDepositRequest(): CLRequest<CLRequestType> {
const depositRequestData = {
pubkey: randomBytes(48),
withdrawalCredentials: randomBytes(32),
amount: bytesToBigInt(randomBytes(8)),
signature: randomBytes(96),
index: bytesToBigInt(randomBytes(8)),
}
return DepositRequest.fromRequestData(depositRequestData) as CLRequest<CLRequestType>
}

serialize() {
return concatBytes(Uint8Array.from([this.type]), this.bytes)
function getRandomWithdrawalRequest(): CLRequest<CLRequestType> {
const withdrawalRequestData = {
sourceAddress: randomBytes(20),
validatorPublicKey: randomBytes(48),
amount: bytesToBigInt(randomBytes(8)),
}
return WithdrawalRequest.fromRequestData(withdrawalRequestData) as CLRequest<CLRequestType>
}

const common = new Common({
Expand All @@ -34,7 +46,7 @@ describe('7685 tests', () => {
assert.equal(block2.requests?.length, 0)
})
it('should instantiate a block with requests', async () => {
const request = new NumberRequest(0x1, randomBytes(32))
const request = getRandomDepositRequest()
const requestsRoot = await Block.genRequestsTrieRoot([request])
const block = Block.fromBlockData(
{
Expand All @@ -47,7 +59,7 @@ describe('7685 tests', () => {
assert.deepEqual(block.header.requestsRoot, requestsRoot)
})
it('RequestsRootIsValid should return false when requestsRoot is invalid', async () => {
const request = new NumberRequest(0x1, randomBytes(32))
const request = getRandomDepositRequest()
const block = Block.fromBlockData(
{
requests: [request],
Expand All @@ -59,9 +71,9 @@ describe('7685 tests', () => {
assert.equal(await block.requestsTrieIsValid(), false)
})
it('should validate requests order', async () => {
const request1 = new NumberRequest(0x1, hexToBytes('0x1234'))
const request2 = new NumberRequest(0x1, hexToBytes('0x2345'))
const request3 = new NumberRequest(0x2, hexToBytes('0x2345'))
const request1 = getRandomDepositRequest()
const request2 = getRandomDepositRequest()
const request3 = getRandomWithdrawalRequest()
const requests = [request1, request2, request3]
const requestsRoot = await Block.genRequestsTrieRoot(requests)

Expand Down Expand Up @@ -101,9 +113,9 @@ describe('fromValuesArray tests', () => {
assert.deepEqual(block.header.requestsRoot, KECCAK256_RLP)
})
it('should construct a block with a valid requests array', async () => {
const request1 = new NumberRequest(0x1, hexToBytes('0x1234'))
const request2 = new NumberRequest(0x1, hexToBytes('0x2345'))
const request3 = new NumberRequest(0x2, hexToBytes('0x2345'))
const request1 = getRandomDepositRequest()
const request2 = getRandomWithdrawalRequest()
const request3 = getRandomWithdrawalRequest()
const requests = [request1, request2, request3]
const requestsRoot = await Block.genRequestsTrieRoot(requests)
const serializedRequests = [request1.serialize(), request2.serialize(), request3.serialize()]
Expand All @@ -127,9 +139,9 @@ describe('fromValuesArray tests', () => {

describe('fromRPC tests', () => {
it('should construct a block from a JSON object', async () => {
const request1 = new NumberRequest(0x1, hexToBytes('0x1234'))
const request2 = new NumberRequest(0x1, hexToBytes('0x2345'))
const request3 = new NumberRequest(0x2, hexToBytes('0x2345'))
const request1 = getRandomDepositRequest()
const request2 = getRandomDepositRequest()
const request3 = getRandomWithdrawalRequest()
const requests = [request1, request2, request3]
const requestsRoot = await Block.genRequestsTrieRoot(requests)
const serializedRequests = [request1.serialize(), request2.serialize(), request3.serialize()]
Expand Down
4 changes: 4 additions & 0 deletions packages/util/src/bytes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -539,3 +539,7 @@ export function bigInt64ToBytes(value: bigint, littleEndian: boolean = false): U

// eslint-disable-next-line no-restricted-imports
export { bytesToUtf8, equalsBytes, utf8ToBytes } from 'ethereum-cryptography/utils.js'

export function hexToBigInt(input: PrefixedHexString): bigint {
return bytesToBigInt(hexToBytes(input))
}
Loading
Loading