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 9 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
68 changes: 65 additions & 3 deletions packages/block/src/block.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ import type {
TxOptions,
TypedTransaction,
} from '@ethereumjs/tx'
import type { EthersProvider, WithdrawalBytes } from '@ethereumjs/util'
import type { CLRequest, CLRequestType, EthersProvider, WithdrawalBytes } from '@ethereumjs/util'

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

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

/**
Expand Down Expand Up @@ -92,6 +94,24 @@ 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) {
const sortedRequests = requests?.sort((a, b) => {
if (a.type !== b.type) return a.type - b.type
return a.greaterThan(b) === true ? 1 : -1
})
const trie = emptyTrie ?? new Trie()
for (const [i, req] of sortedRequests.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 +125,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 +164,23 @@ export class Block {
// stub till that time
const executionWitness = executionWitnessData

return new Block(header, transactions, uncleHeaders, withdrawals, opts, executionWitness)
// Requests are sorted in ascending order based on type and then the internal
// ordering logic defined by the request type

const requests = clRequests?.sort((a, b) => {
if (a.type !== b.type) return a.type - b.type
return a.greaterThan(b) === true ? 1 : -1
})

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

/**
Expand Down Expand Up @@ -414,7 +451,8 @@ export class Block {
uncleHeaders: BlockHeader[] = [],
withdrawals?: Withdrawal[],
opts: BlockOptions = {},
executionWitness?: VerkleExecutionWitness | null
executionWitness?: VerkleExecutionWitness | null,
requests?: CLRequest[]
acolytec3 marked this conversation as resolved.
Show resolved Hide resolved
) {
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)
this.common = this.header.common
Expand All @@ -423,6 +461,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 @@ -471,6 +510,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 @@ -546,6 +589,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
24 changes: 20 additions & 4 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 @@ -119,15 +120,15 @@ export class BlockHeader {
const headerData = valuesArrayToHeaderData(values)
const { number, baseFeePerGas, excessBlobGas, blobGasUsed, parentBeaconBlockRoot } = 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 Down Expand Up @@ -222,6 +223,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 +237,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 +266,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 +290,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 +416,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
7 changes: 7 additions & 0 deletions packages/block/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@ import type {
AddressLike,
BigIntLike,
BytesLike,
CLRequest,
JsonRpcWithdrawal,
PrefixedHexString,
RequestBytes,
WithdrawalBytes,
WithdrawalData,
} from '@ethereumjs/util'
Expand Down Expand Up @@ -136,6 +138,7 @@ export interface HeaderData {
blobGasUsed?: BigIntLike
excessBlobGas?: BigIntLike
parentBeaconBlockRoot?: BytesLike
requestsRoot?: BytesLike
}

/**
Expand All @@ -149,6 +152,7 @@ export interface BlockData {
transactions?: Array<TxData[TransactionType]>
uncleHeaders?: Array<HeaderData>
withdrawals?: Array<WithdrawalData>
requests?: Array<CLRequest>
/**
* EIP-6800: Verkle Proof Data (experimental)
*/
Expand Down Expand Up @@ -218,6 +222,7 @@ export interface JsonHeader {
blobGasUsed?: PrefixedHexString
excessBlobGas?: PrefixedHexString
parentBeaconBlockRoot?: PrefixedHexString
requestsRoot?: PrefixedHexString
}

/*
Expand Down Expand Up @@ -251,6 +256,7 @@ export interface JsonRpcBlock {
excessBlobGas?: PrefixedHexString // If EIP-4844 is enabled for this block, returns the excess blob gas for the block
parentBeaconBlockRoot?: PrefixedHexString // If EIP-4788 is enabled for this block, returns parent beacon block root
executionWitness?: VerkleExecutionWitness | null // If Verkle is enabled for this block
requestsRoot?: PrefixedHexString // If EIP-7685 is enabled for this block, returns the requests root
}

export type WithdrawalV1 = {
Expand Down Expand Up @@ -282,4 +288,5 @@ export type ExecutionPayload = {
parentBeaconBlockRoot?: PrefixedHexString // QUANTITY, 64 Bits
// VerkleExecutionWitness is already a hex serialized object
executionWitness?: VerkleExecutionWitness | null // QUANTITY, 64 Bits, null implies not available
requests?: RequestBytes[]
}
103 changes: 103 additions & 0 deletions packages/block/test/eip7685block.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import { Chain, Common, Hardfork } from '@ethereumjs/common'
import {
CLRequest,
KECCAK256_RLP,
bytesToBigInt,
bytesToHex,
concatBytes,
hexToBytes,
randomBytes,
} from '@ethereumjs/util'
import { assert, describe, it } from 'vitest'

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

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

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

public static fromRequestData(bytes: Uint8Array): CLRequestType<NumberRequest> {
return new NumberRequest(0x1, bytes)
}
public greaterThan(a: NumberRequest): boolean {
return bytesToBigInt(a.bytes) < bytesToBigInt(this.bytes)
}

serialize() {
return concatBytes(Uint8Array.from([this.type]), this.bytes)
}
}

const common = new Common({ chain: Chain.Mainnet, hardfork: Hardfork.Shanghai, eips: [7685] })
describe('7685 tests', () => {
it('should instantiate block with defaults', () => {
const block = Block.fromBlockData({}, { common })
assert.deepEqual(block.header.requestsRoot, KECCAK256_RLP)
const block2 = new Block(undefined, undefined, undefined, undefined, { common })
assert.deepEqual(block.header.requestsRoot, KECCAK256_RLP)
assert.equal(block2.requests?.length, 0)
})
it('should instantiate a block with requests', async () => {
const request = new NumberRequest(0x1, randomBytes(32))
const requestsRoot = await Block.genRequestsTrieRoot([request])
const block = Block.fromBlockData(
{
requests: [request],
header: { requestsRoot },
},
{ common }
)
assert.equal(block.requests?.length, 1)
assert.deepEqual(block.header.requestsRoot, requestsRoot)
})
it('RequestsRootIsValid should return false when requestsRoot is invalid', async () => {
const request = new NumberRequest(0x1, randomBytes(32))
const block = Block.fromBlockData(
{
requests: [request],
header: { requestsRoot: randomBytes(32) },
},
{ common }
)

assert.equal(await block.requestsTrieIsValid(), false)
})
it('should order requests correctly in block and produce correct requestsRoot', async () => {
const request1 = new NumberRequest(0x1, hexToBytes('0x1234'))
const request2 = new NumberRequest(0x1, hexToBytes('0x2345'))
const request3 = new NumberRequest(0x2, hexToBytes('0x2345'))
const requests = [request1, request2, request3]
const requestsRoot = await Block.genRequestsTrieRoot(requests)

// Construct 2 blocks with differently ordered requests and verify requestsRoot is valid for both

const block = Block.fromBlockData(
{
requests: [request2, request1, request3],
header: { requestsRoot },
},
{ common }
)

assert.ok(await block.requestsTrieIsValid())

const block2 = Block.fromBlockData(
{
requests: [request1, request3, request2],
header: { requestsRoot },
},
{ common }
)

assert.ok(await block2.requestsTrieIsValid())

// Verifies that requests are in same sort order
assert.deepEqual(block.requests!, block2.requests!)
assert.equal(bytesToHex(block.requests![1].bytes), '0x2345')
assert.equal(block.requests![2].type, 0x2)
assert.equal(block.requests![2].type, block2.requests![2].type)
})
})
9 changes: 9 additions & 0 deletions packages/common/src/eips.ts
Original file line number Diff line number Diff line change
Expand Up @@ -542,4 +542,13 @@ export const EIPs: EIPsDict = {
},
},
},
7685: {
comment: 'General purpose execution layer requests',
url: 'https://eips.ethereum.org/EIPS/eip-7685',
status: Status.Draft,
// TODO: Set correct minimum hardfork
holgerd77 marked this conversation as resolved.
Show resolved Hide resolved
minimumHardfork: Hardfork.Shanghai,
acolytec3 marked this conversation as resolved.
Show resolved Hide resolved
requiredEIPs: [],
Copy link
Member

Choose a reason for hiding this comment

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

Guess we can (minimally) add 3675 (consensus to PoS Upgrade) here if we want to.

Yeah, but also not so relevant eventually.

gasPrices: {},
},
}
1 change: 1 addition & 0 deletions packages/util/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,3 +65,4 @@ export * from './kzg.js'
export * from './lock.js'
export * from './mapDB.js'
export * from './provider.js'
export * from './requests.js'
Loading
Loading