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

EIP-7685: Execution Layer Requests Implementation #3372

Merged
merged 34 commits into from
Apr 30, 2024
Merged
Show file tree
Hide file tree
Changes from 31 commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
3735c1a
Add request type to util
acolytec3 Apr 25, 2024
237e527
Add requests to block
acolytec3 Apr 25, 2024
352cd93
Add requests root validation
acolytec3 Apr 25, 2024
975ab38
Add ordering checks
acolytec3 Apr 25, 2024
0f06222
Add note on requestsRoot non-determinism
acolytec3 Apr 25, 2024
74369ba
Rework request structure to use interface and base class
acolytec3 Apr 25, 2024
c1b725e
Make requests optional
acolytec3 Apr 25, 2024
387af7c
Update ordering test
acolytec3 Apr 25, 2024
43ed67d
Improve tests and remove unnecessary rlp encoding
acolytec3 Apr 25, 2024
643e96e
Reorder requests order [no ci]
acolytec3 Apr 26, 2024
70ba18f
Add vm.runBlock
acolytec3 Apr 26, 2024
a5264d9
add tests with requests
acolytec3 Apr 26, 2024
0bf6387
Add buildblock changes and tests
acolytec3 Apr 26, 2024
b118e5d
lint
acolytec3 Apr 26, 2024
040f779
remove sorting function
acolytec3 Apr 26, 2024
fc191bb
Add order check for requests when generating trie
acolytec3 Apr 26, 2024
b203722
More fixes
acolytec3 Apr 26, 2024
6d7e676
remove generic
acolytec3 Apr 26, 2024
ad255da
Merge remote-tracking branch 'origin/master' into eip-7685 [no ci]
acolytec3 Apr 26, 2024
de4964c
in flight fromValuesArray changes [no ci]
acolytec3 Apr 27, 2024
39aa2e0
update min hardfork to cancun [no ci]
acolytec3 Apr 27, 2024
2781708
Throw on invalid requestsRoot [no ci]
acolytec3 Apr 27, 2024
52823c2
add scaffolding for pending requests in pendingBlock
acolytec3 Apr 27, 2024
d6f70ad
Merge remote-tracking branch 'origin/master' into eip-7685 [no ci]
acolytec3 Apr 27, 2024
a18cd5a
Update fromRPC constructors and toJSON methods
acolytec3 Apr 27, 2024
96ecc0a
Add requests to JsonRpcBlock
acolytec3 Apr 27, 2024
71be096
update runBlock/buildBlock and tests
acolytec3 Apr 29, 2024
9914dc8
Merge remote-tracking branch 'origin/master' into eip-7685
acolytec3 Apr 29, 2024
1aa3bb3
Remove obsolete references
acolytec3 Apr 29, 2024
cd23545
fix hex typing
acolytec3 Apr 29, 2024
0d6feaf
Merge remote-tracking branch 'origin/master' into eip-7685
acolytec3 Apr 29, 2024
6f07126
Check for 7685 before adding requests
acolytec3 Apr 29, 2024
9a8258f
address feedback
acolytec3 Apr 30, 2024
acd9c39
address feedback
acolytec3 Apr 30, 2024
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
98 changes: 94 additions & 4 deletions packages/block/src/block.ts
Original file line number Diff line number Diff line change
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())
acolytec3 marked this conversation as resolved.
Show resolved Hide resolved
}
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 @@ -139,11 +171,28 @@ export class Block {
}

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

// Requests should be sorted in monotonically ascending order based on type
// and whatever internal sorting logic is defined by each request type
if (clRequests !== undefined && clRequests.length > 1) {
for (let x = 1; x < clRequests.length; x++) {
if (clRequests[x].type < clRequests[x - 1].type)
throw new Error('requests are not sorted in ascending order')
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 check be moved into the constructor? It is possible to create a block which does not satisfy this condition by using fromValuesArray or fromRlpSerializedBlock.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fair point. Will move

}
}
// The witness data is planned to come in rlp serialized bytes so leave this
// 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 +226,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 +277,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 +298,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 @@ -417,6 +481,7 @@ export class Block {
uncleHeaders: BlockHeader[] = [],
withdrawals?: Withdrawal[],
opts: BlockOptions = {},
requests?: CLRequest[],
executionWitness?: VerkleExecutionWitness | null
) {
Copy link
Member

Choose a reason for hiding this comment

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

Just to note here that we need to be a bit careful with such parameter order switches (so: putting requests before executionWitness) for backwards compatibility reasons (we already have released a block library version with executionWitness included. Guess in this case it should be ok though and does make sense to have this in the "correct" order from the beginning.

this.header = header ?? BlockHeader.fromHeaderData({}, opts)
Expand All @@ -426,6 +491,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 +540,10 @@ 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`)
}

const freeze = opts?.freeze ?? true
if (freeze) {
Object.freeze(this)
Expand Down Expand Up @@ -549,6 +619,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 +908,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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
4 changes: 3 additions & 1 deletion packages/block/src/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,10 @@ export function valuesArrayToHeaderData(values: BlockHeaderBytes): HeaderData {
blobGasUsed,
excessBlobGas,
parentBeaconBlockRoot,
requestsRoot,
] = values

if (values.length > 20) {
if (values.length > 21) {
throw new Error(
`invalid header. More values than expected were received. Max: 20, got: ${values.length}`
)
Expand Down Expand Up @@ -78,6 +79,7 @@ export function valuesArrayToHeaderData(values: BlockHeaderBytes): HeaderData {
blobGasUsed,
excessBlobGas,
parentBeaconBlockRoot,
requestsRoot,
}
}

Expand Down
Loading
Loading