Skip to content

Commit

Permalink
Implement EIP 7685 (#3372)
Browse files Browse the repository at this point in the history
* Add request type to util

* Add requests to block

* Add requests root validation

* Add ordering checks

* Add note on requestsRoot non-determinism

* Rework request structure to use interface and base class

* Make requests optional

* Update ordering test

* Improve tests and remove unnecessary rlp encoding

* Reorder requests order [no ci]

* Add vm.runBlock

* add tests with requests

* Add buildblock changes and tests

* lint

* remove sorting function

* Add order check for requests when generating trie

* More fixes

* remove generic

* in flight fromValuesArray changes [no ci]

* update min hardfork to cancun [no ci]

* Throw on invalid requestsRoot [no ci]

* add scaffolding for pending requests in pendingBlock

* Update fromRPC constructors and toJSON methods

* Add requests to JsonRpcBlock

* update runBlock/buildBlock and tests

* Remove obsolete references

* fix hex typing

* Check for 7685 before adding requests

* address feedback

* address feedback
  • Loading branch information
acolytec3 committed Apr 30, 2024
1 parent 5103318 commit 55a8e66
Show file tree
Hide file tree
Showing 22 changed files with 717 additions and 68 deletions.
100 changes: 96 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,
CLRequest,
KECCAK256_RLP,
KECCAK256_RLP_ARRAY,
Withdrawal,
Expand Down Expand Up @@ -41,7 +42,13 @@ import type {
TxOptions,
TypedTransaction,
} from '@ethereumjs/tx'
import type { EthersProvider, PrefixedHexString, WithdrawalBytes } from '@ethereumjs/util'
import type {
CLRequestType,
EthersProvider,
PrefixedHexString,
RequestBytes,
WithdrawalBytes,
} from '@ethereumjs/util'

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

Expand All @@ -64,6 +72,7 @@ export class Block {
protected cache: {
txTrieRoot?: Uint8Array
withdrawalsTrieRoot?: Uint8Array
requestsRoot?: Uint8Array
} = {}

/**
Expand Down Expand Up @@ -92,6 +101,28 @@ export class Block {
return trie.root()
}

/**
* Returns the requests trie root for an array of CLRequests
* @param requests - an array of CLRequests
* @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) {
// 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) {
for (let x = 1; x < requests.length; x++) {
if (requests[x].type < requests[x - 1].type)
throw new Error('requests are not sorted in ascending order')
}
}
const trie = emptyTrie ?? new Trie()
for (const [i, req] of requests.entries()) {
await trie.put(RLP.encode(i), req.serialize())
}
return trie.root()
}

/**
* Static constructor to create a block from a block data dictionary
*
Expand All @@ -105,6 +136,7 @@ export class Block {
uncleHeaders: uhsData,
withdrawals: withdrawalsData,
executionWitness: executionWitnessData,
requests: clRequests,
} = blockData

const header = BlockHeader.fromHeaderData(headerData, opts)
Expand Down Expand Up @@ -143,7 +175,15 @@ export class Block {
// stub till that time
const executionWitness = executionWitnessData

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

/**
Expand Down Expand Up @@ -177,7 +217,8 @@ 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, requestBytes, executionWitnessBytes] =
values
const header = BlockHeader.fromValuesArray(headerData, opts)

if (
Expand Down Expand Up @@ -227,6 +268,12 @@ export class Block {
}))
?.map(Withdrawal.fromWithdrawalData)

let requests
if (header.common.isActivatedEIP(7685)) {
requests = (requestBytes as RequestBytes[]).map(
(bytes) => new CLRequest(bytes[0], bytes.slice(1))
)
}
// 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 @@ -242,7 +289,15 @@ export class Block {
}
}

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

/**
Expand Down Expand Up @@ -334,6 +389,7 @@ export class Block {
feeRecipient: coinbase,
transactions,
withdrawals: withdrawalsData,
requestsRoot,
executionWitness,
} = payload

Expand All @@ -353,6 +409,7 @@ export class Block {
}
}

const reqRoot = requestsRoot === null ? undefined : requestsRoot
const transactionsTrie = await Block.genTransactionsTrieRoot(
txs,
new Trie({ common: opts?.common })
Expand All @@ -369,6 +426,7 @@ export class Block {
withdrawalsRoot,
mixHash,
coinbase,
requestsRoot: reqRoot,
}

// we are not setting setHardfork as common is already set to the correct hf
Expand Down Expand Up @@ -417,6 +475,7 @@ export class Block {
uncleHeaders: BlockHeader[] = [],
withdrawals?: Withdrawal[],
opts: BlockOptions = {},
requests?: CLRequest[],
executionWitness?: VerkleExecutionWitness | null
) {
this.header = header ?? BlockHeader.fromHeaderData({}, opts)
Expand All @@ -426,6 +485,7 @@ export class Block {
this.transactions = transactions
this.withdrawals = withdrawals ?? (this.common.isActivatedEIP(4895) ? [] : undefined)
this.executionWitness = executionWitness
this.requests = requests ?? (this.common.isActivatedEIP(7685) ? [] : undefined)
// null indicates an intentional absence of value or unavailability
// undefined indicates that the executionWitness should be initialized with the default state
if (this.common.isActivatedEIP(6800) && this.executionWitness === undefined) {
Expand Down Expand Up @@ -474,6 +534,18 @@ export class Block {
throw new Error(`Cannot have executionWitness field if EIP 6800 is not active `)
}

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

// Requests should be sorted in monotonically ascending order based on type
// and whatever internal sorting logic is defined by each request type
if (requests !== undefined && requests.length > 1) {
for (let x = 1; x < requests.length; x++) {
if (requests[x].type < requests[x - 1].type)
throw new Error('requests are not sorted in ascending order')
}
}
const freeze = opts?.freeze ?? true
if (freeze) {
Object.freeze(this)
Expand Down Expand Up @@ -549,6 +621,25 @@ export class Block {
return result
}

async requestsTrieIsValid(): Promise<boolean> {
if (!this.common.isActivatedEIP(7685)) {
throw new Error('EIP 7685 is not activated')
}

let result
if (this.requests!.length === 0) {
result = equalsBytes(this.header.requestsRoot!, KECCAK256_RLP)
return result
}

if (this.cache.requestsRoot === undefined) {
this.cache.requestsRoot = await Block.genRequestsTrieRoot(this.requests!)
}

result = equalsBytes(this.cache.requestsRoot, this.header.requestsRoot!)

return result
}
/**
* Validates transaction signatures and minimum gas requirements.
* @returns {string[]} an array of error strings
Expand Down Expand Up @@ -819,6 +910,7 @@ export class Block {
transactions: this.transactions.map((tx) => tx.toJSON()),
uncleHeaders: this.uncleHeaders.map((uh) => uh.toJSON()),
...withdrawalsAttr,
requests: this.requests?.map((req) => bytesToHex(req.serialize())),
}
}

Expand Down
9 changes: 7 additions & 2 deletions packages/block/src/from-rpc.ts
@@ -1,12 +1,13 @@
import { TransactionFactory } from '@ethereumjs/tx'
import { TypeOutput, setLengthLeft, toBytes, toType } from '@ethereumjs/util'
import { CLRequest, TypeOutput, hexToBytes, setLengthLeft, toBytes, toType } from '@ethereumjs/util'

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

import { Block } from './index.js'

import type { BlockOptions, JsonRpcBlock } from './index.js'
import type { TypedTransaction } from '@ethereumjs/tx'
import type { PrefixedHexString } from '@ethereumjs/util'

function normalizeTxParams(_txParams: any) {
const txParams = Object.assign({}, _txParams)
Expand Down Expand Up @@ -54,8 +55,12 @@ export function blockFromRpc(

const uncleHeaders = uncles.map((uh) => blockHeaderFromRpc(uh, options))

const requests = blockParams.requests?.map((req) => {
const bytes = hexToBytes(req as PrefixedHexString)
return new CLRequest(bytes[0], bytes.slice(1))
})
return Block.fromBlockData(
{ header, transactions, uncleHeaders, withdrawals: blockParams.withdrawals },
{ header, transactions, uncleHeaders, withdrawals: blockParams.withdrawals, requests },
options
)
}
2 changes: 2 additions & 0 deletions packages/block/src/header-from-rpc.ts
Expand Up @@ -31,6 +31,7 @@ export function blockHeaderFromRpc(blockParams: JsonRpcBlock, options?: BlockOpt
blobGasUsed,
excessBlobGas,
parentBeaconBlockRoot,
requestsRoot,
} = blockParams

const blockHeader = BlockHeader.fromHeaderData(
Expand All @@ -55,6 +56,7 @@ export function blockHeaderFromRpc(blockParams: JsonRpcBlock, options?: BlockOpt
blobGasUsed,
excessBlobGas,
parentBeaconBlockRoot,
requestsRoot,
},
options
)
Expand Down
43 changes: 38 additions & 5 deletions 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 requestsRoot?: Uint8Array

public readonly common: Common

Expand Down Expand Up @@ -117,17 +118,24 @@ 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,
requestsRoot,
} = headerData
const header = BlockHeader.fromHeaderData(headerData, opts)
// eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
if (header.common.isActivatedEIP(1559) && baseFeePerGas === undefined) {
const eip1559ActivationBlock = bigIntToBytes(header.common.eipBlock(1559)!)
// eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
if (eip1559ActivationBlock && equalsBytes(eip1559ActivationBlock, number as Uint8Array)) {
if (
eip1559ActivationBlock !== undefined &&
equalsBytes(eip1559ActivationBlock, number as Uint8Array)
) {
throw new Error('invalid header. baseFeePerGas should be provided')
}
}
// eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
if (header.common.isActivatedEIP(4844)) {
if (excessBlobGas === undefined) {
throw new Error('invalid header. excessBlobGas should be provided')
Expand All @@ -138,6 +146,10 @@ export class BlockHeader {
if (header.common.isActivatedEIP(4788) && parentBeaconBlockRoot === undefined) {
throw new Error('invalid header. parentBeaconBlockRoot should be provided')
}

if (header.common.isActivatedEIP(7685) && requestsRoot === undefined) {
throw new Error('invalid header. requestsRoot should be provided')
}
return header
}
/**
Expand Down Expand Up @@ -222,6 +234,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,
requestsRoot: this.common.isActivatedEIP(7685) ? KECCAK256_RLP : undefined,
}

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

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 +277,10 @@ export class BlockHeader {
)
}

if (!this.common.isActivatedEIP(7685) && requestsRoot !== undefined) {
throw new Error('requestsRoot can only be provided with EIP 7685 activated')
}

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

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

if (this.common.isActivatedEIP(7685) === true) {
if (this.requestsRoot === undefined) {
const msg = this._errorMsg('EIP7685 block has no requestsRoot field')
throw new Error(msg)
}
}
}

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

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

Expand Down

0 comments on commit 55a8e66

Please sign in to comment.