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
Showing 12 of 24 files from the diff.
packages/vm/src/buildBlock.ts
changed.
packages/vm/src/runBlock.ts
changed.
packages/block/src/block.ts
changed.
Newly tracked file
packages/util/src/withdrawal.ts
created.
packages/block/src/header.ts
changed.
Other files ignored by Codecov
packages/util/src/index.ts
has changed.
packages/block/src/types.ts
has changed.
packages/block/test/eip4895block.spec.ts
has changed.
packages/common/src/utils.ts
has changed.
packages/vm/src/types.ts
has changed.
packages/common/src/hardforks/shanghai.json
has changed.
packages/client/test/rpc/helpers.ts
has changed.
@@ -45,7 +45,10 @@
Loading
45 | 45 | ||
46 | 46 | if ( |
|
47 | 47 | isGenesis || |
|
48 | - | (blockBody instanceof Block && (blockBody.transactions.length || blockBody.uncleHeaders.length)) |
|
48 | + | (blockBody instanceof Block && |
|
49 | + | (blockBody.transactions.length || |
|
50 | + | (blockBody.withdrawals?.length ?? 0) || |
|
51 | + | blockBody.uncleHeaders.length)) |
|
49 | 52 | ) { |
|
50 | 53 | const bodyValue = Buffer.from(RLP.encode(bufArrToArr(blockBody.raw()).slice(1))) |
|
51 | 54 | dbOps.push( |
@@ -1,6 +1,6 @@
Loading
1 | 1 | import { Block, BlockHeader } from '@ethereumjs/block' |
|
2 | 2 | import { RLP } from '@ethereumjs/rlp' |
|
3 | - | import { arrToBufArr, bufferToBigInt } from '@ethereumjs/util' |
|
3 | + | import { KECCAK256_RLP, arrToBufArr, bufferToBigInt } from '@ethereumjs/util' |
|
4 | 4 | ||
5 | 5 | import { Cache } from './cache' |
|
6 | 6 | import { DBOp, DBTarget } from './operation' |
@@ -113,6 +113,10 @@
Loading
113 | 113 | if (error.code !== 'LEVEL_NOT_FOUND') { |
|
114 | 114 | throw error |
|
115 | 115 | } |
|
116 | + | // If this block had empty withdrawals push an empty array in body |
|
117 | + | if (header.withdrawalsRoot !== undefined && header.withdrawalsRoot.equals(KECCAK256_RLP)) { |
|
118 | + | body.push([]) |
|
119 | + | } |
|
116 | 120 | } |
|
117 | 121 | const blockData = [header.raw(), ...body] as BlockBuffer |
|
118 | 122 | const opts: BlockOptions = { common: this._common } |
@@ -2,7 +2,7 @@
Loading
2 | 2 | import { ConsensusType } from '@ethereumjs/common' |
|
3 | 3 | import { RLP } from '@ethereumjs/rlp' |
|
4 | 4 | import { Trie } from '@ethereumjs/trie' |
|
5 | - | import { Address, TypeOutput, toBuffer, toType } from '@ethereumjs/util' |
|
5 | + | import { Address, TypeOutput, Withdrawal, toBuffer, toType } from '@ethereumjs/util' |
|
6 | 6 | ||
7 | 7 | import { Bloom } from './bloom' |
|
8 | 8 | import { calculateMinerReward, encodeReceipt, rewardAccount } from './runBlock' |
@@ -23,6 +23,7 @@
Loading
23 | 23 | private headerData: HeaderData |
|
24 | 24 | private transactions: TypedTransaction[] = [] |
|
25 | 25 | private transactionResults: RunTxResult[] = [] |
|
26 | + | private withdrawals?: Withdrawal[] |
|
26 | 27 | private checkpointed = false |
|
27 | 28 | private reverted = false |
|
28 | 29 | private built = false |
@@ -41,6 +42,7 @@
Loading
41 | 42 | number: opts.headerData?.number ?? opts.parentBlock.header.number + BigInt(1), |
|
42 | 43 | gasLimit: opts.headerData?.gasLimit ?? opts.parentBlock.header.gasLimit, |
|
43 | 44 | } |
|
45 | + | this.withdrawals = opts.withdrawals?.map(Withdrawal.fromWithdrawalData) |
|
44 | 46 | ||
45 | 47 | if ( |
|
46 | 48 | this.vm._common.isActivatedEIP(1559) === true && |
@@ -66,11 +68,7 @@
Loading
66 | 68 | * Calculates and returns the transactionsTrie for the block. |
|
67 | 69 | */ |
|
68 | 70 | public async transactionsTrie() { |
|
69 | - | const trie = new Trie() |
|
70 | - | for (const [i, tx] of this.transactions.entries()) { |
|
71 | - | await trie.put(Buffer.from(RLP.encode(i)), tx.serialize()) |
|
72 | - | } |
|
73 | - | return trie.root() |
|
71 | + | return Block.genTransactionsTrieRoot(this.transactions) |
|
74 | 72 | } |
|
75 | 73 | ||
76 | 74 | /** |
@@ -111,6 +109,21 @@
Loading
111 | 109 | await rewardAccount(this.vm.eei, coinbase, reward) |
|
112 | 110 | } |
|
113 | 111 | ||
112 | + | /** |
|
113 | + | * Adds the withdrawal amount to the withdrawal address |
|
114 | + | */ |
|
115 | + | private async processWithdrawals() { |
|
116 | + | for (const withdrawal of this.withdrawals ?? []) { |
|
117 | + | const { address, amount } = withdrawal |
|
118 | + | // If there is no amount to add, skip touching the account |
|
119 | + | // as per the implementation of other clients geth/nethermind |
|
120 | + | // although this should never happen as no withdrawals with 0 |
|
121 | + | // amount should ever land up here. |
|
122 | + | if (amount === 0n) continue |
|
123 | + | await rewardAccount(this.vm.eei, address, amount) |
|
124 | + | } |
|
125 | + | } |
|
126 | + | ||
114 | 127 | /** |
|
115 | 128 | * Run and add a transaction to the block being built. |
|
116 | 129 | * Please note that this modifies the state of the VM. |
@@ -179,9 +192,13 @@
Loading
179 | 192 | if (consensusType === ConsensusType.ProofOfWork) { |
|
180 | 193 | await this.rewardMiner() |
|
181 | 194 | } |
|
195 | + | await this.processWithdrawals() |
|
182 | 196 | ||
183 | 197 | const stateRoot = await this.vm.stateManager.getStateRoot() |
|
184 | 198 | const transactionsTrie = await this.transactionsTrie() |
|
199 | + | const withdrawalsRoot = this.withdrawals |
|
200 | + | ? await Block.genWithdrawalsTrieRoot(this.withdrawals) |
|
201 | + | : undefined |
|
185 | 202 | const receiptTrie = await this.receiptTrie() |
|
186 | 203 | const logsBloom = this.logsBloom() |
|
187 | 204 | const gasUsed = this.gasUsed |
@@ -191,6 +208,7 @@
Loading
191 | 208 | ...this.headerData, |
|
192 | 209 | stateRoot, |
|
193 | 210 | transactionsTrie, |
|
211 | + | withdrawalsRoot, |
|
194 | 212 | receiptTrie, |
|
195 | 213 | logsBloom, |
|
196 | 214 | gasUsed, |
@@ -202,7 +220,11 @@
Loading
202 | 220 | headerData.mixHash = sealOpts?.mixHash ?? headerData.mixHash |
|
203 | 221 | } |
|
204 | 222 | ||
205 | - | const blockData = { header: headerData, transactions: this.transactions } |
|
223 | + | const blockData = { |
|
224 | + | header: headerData, |
|
225 | + | transactions: this.transactions, |
|
226 | + | withdrawals: this.withdrawals, |
|
227 | + | } |
|
206 | 228 | const block = Block.fromBlockData(blockData, blockOpts) |
|
207 | 229 | ||
208 | 230 | if (this.blockOpts.putBlockIntoBlockchain === true) { |
@@ -1,6 +1,6 @@
Loading
1 | 1 | import { Block, BlockHeader } from '@ethereumjs/block' |
|
2 | 2 | import { Chain, Common, ConsensusAlgorithm, ConsensusType, Hardfork } from '@ethereumjs/common' |
|
3 | - | import { Lock } from '@ethereumjs/util' |
|
3 | + | import { KECCAK256_RLP, Lock } from '@ethereumjs/util' |
|
4 | 4 | import { MemoryLevel } from 'memory-level' |
|
5 | 5 | ||
6 | 6 | import { CasperConsensus, CliqueConsensus, EthashConsensus } from './consensus' |
@@ -1246,6 +1246,7 @@
Loading
1246 | 1246 | ...common.genesis(), |
|
1247 | 1247 | number: 0, |
|
1248 | 1248 | stateRoot, |
|
1249 | + | withdrawalsRoot: common.isActivatedEIP(4895) ? KECCAK256_RLP : undefined, |
|
1249 | 1250 | } |
|
1250 | 1251 | if (common.consensusType() === 'poa') { |
|
1251 | 1252 | if (common.genesis().extraData) { |
@@ -1256,7 +1257,10 @@
Loading
1256 | 1257 | header.extraData = Buffer.concat([Buffer.alloc(32), Buffer.alloc(65).fill(0)]) |
|
1257 | 1258 | } |
|
1258 | 1259 | } |
|
1259 | - | return Block.fromBlockData({ header }, { common }) |
|
1260 | + | return Block.fromBlockData( |
|
1261 | + | { header, withdrawals: common.isActivatedEIP(4895) ? [] : undefined }, |
|
1262 | + | { common } |
|
1263 | + | ) |
|
1260 | 1264 | } |
|
1261 | 1265 | ||
1262 | 1266 | /** |
@@ -1,9 +1,7 @@
Loading
1 | 1 | import { Block } from '@ethereumjs/block' |
|
2 | 2 | import { Hardfork } from '@ethereumjs/common' |
|
3 | - | import { RLP } from '@ethereumjs/rlp' |
|
4 | - | import { Trie } from '@ethereumjs/trie' |
|
5 | 3 | import { TransactionFactory } from '@ethereumjs/tx' |
|
6 | - | import { bufferToHex, toBuffer, zeros } from '@ethereumjs/util' |
|
4 | + | import { Withdrawal, bufferToHex, toBuffer, zeros } from '@ethereumjs/util' |
|
7 | 5 | ||
8 | 6 | import { PendingBlock } from '../../miner' |
|
9 | 7 | import { short } from '../../util' |
@@ -17,7 +15,6 @@
Loading
17 | 15 | import type { VMExecution } from '../../execution' |
|
18 | 16 | import type { FullEthereumService } from '../../service' |
|
19 | 17 | import type { HeaderData } from '@ethereumjs/block' |
|
20 | - | import type { TypedTransaction } from '@ethereumjs/tx' |
|
21 | 18 | import type { VM } from '@ethereumjs/vm' |
|
22 | 19 | ||
23 | 20 | export enum Status { |
@@ -28,7 +25,14 @@
Loading
28 | 25 | VALID = 'VALID', |
|
29 | 26 | } |
|
30 | 27 | ||
31 | - | export type ExecutionPayloadV1 = { |
|
28 | + | export type WithdrawalV1 = { |
|
29 | + | index: string // Quantity, 8 Bytes |
|
30 | + | validatorIndex: string // Quantity, 8 bytes |
|
31 | + | address: string // DATA, 20 bytes |
|
32 | + | amount: string // Quantity, 32 bytes |
|
33 | + | } |
|
34 | + | ||
35 | + | export type ExecutionPayload = { |
|
32 | 36 | parentHash: string // DATA, 32 Bytes |
|
33 | 37 | feeRecipient: string // DATA, 20 Bytes |
|
34 | 38 | stateRoot: string // DATA, 32 Bytes |
@@ -42,11 +46,11 @@
Loading
42 | 46 | extraData: string // DATA, 0 to 32 Bytes |
|
43 | 47 | baseFeePerGas: string // QUANTITY, 256 Bits |
|
44 | 48 | blockHash: string // DATA, 32 Bytes |
|
45 | - | transactions: string[] // Array of DATA - Array of transaction objects, |
|
46 | - | // each object is a byte list (DATA) representing |
|
47 | - | // TransactionType || TransactionPayload or LegacyTransaction |
|
48 | - | // as defined in EIP-2718. |
|
49 | + | transactions: string[] // Array of DATA - Array of transaction rlp strings, |
|
50 | + | withdrawals?: WithdrawalV1[] // Array of withdrawal objects |
|
49 | 51 | } |
|
52 | + | export type ExecutionPayloadV1 = Omit<ExecutionPayload, 'withdrawals'> |
|
53 | + | export type ExecutionPayloadV2 = ExecutionPayload & { withdrawals: WithdrawalV1[] } |
|
50 | 54 | ||
51 | 55 | export type ForkchoiceStateV1 = { |
|
52 | 56 | headBlockHash: string |
@@ -54,11 +58,14 @@
Loading
54 | 58 | finalizedBlockHash: string |
|
55 | 59 | } |
|
56 | 60 | ||
57 | - | type PayloadAttributesV1 = { |
|
61 | + | type PayloadAttributes = { |
|
58 | 62 | timestamp: string |
|
59 | 63 | prevRandao: string |
|
60 | 64 | suggestedFeeRecipient: string |
|
65 | + | withdrawals?: WithdrawalV1[] |
|
61 | 66 | } |
|
67 | + | type PayloadAttributesV1 = Omit<PayloadAttributes, 'withdrawals'> |
|
68 | + | type PayloadAttributesV2 = PayloadAttributes & { withdrawals: WithdrawalV1[] } |
|
62 | 69 | ||
63 | 70 | export type PayloadStatusV1 = { |
|
64 | 71 | status: Status |
@@ -84,13 +91,52 @@
Loading
84 | 91 | }, |
|
85 | 92 | } |
|
86 | 93 | ||
94 | + | const executionPayloadV1FieldValidators = { |
|
95 | + | parentHash: validators.blockHash, |
|
96 | + | feeRecipient: validators.address, |
|
97 | + | stateRoot: validators.hex, |
|
98 | + | receiptsRoot: validators.hex, |
|
99 | + | logsBloom: validators.hex, |
|
100 | + | prevRandao: validators.hex, |
|
101 | + | blockNumber: validators.hex, |
|
102 | + | gasLimit: validators.hex, |
|
103 | + | gasUsed: validators.hex, |
|
104 | + | timestamp: validators.hex, |
|
105 | + | extraData: validators.hex, |
|
106 | + | baseFeePerGas: validators.hex, |
|
107 | + | blockHash: validators.blockHash, |
|
108 | + | transactions: validators.array(validators.hex), |
|
109 | + | } |
|
110 | + | const executionPayloadV2FieldValidators = { |
|
111 | + | ...executionPayloadV1FieldValidators, |
|
112 | + | withdrawals: validators.array(validators.withdrawal()), |
|
113 | + | } |
|
114 | + | ||
115 | + | const forkchoiceFieldValidators = { |
|
116 | + | headBlockHash: validators.blockHash, |
|
117 | + | safeBlockHash: validators.blockHash, |
|
118 | + | finalizedBlockHash: validators.blockHash, |
|
119 | + | } |
|
120 | + | ||
121 | + | const payloadAttributesFieldValidatorsV1 = { |
|
122 | + | timestamp: validators.hex, |
|
123 | + | prevRandao: validators.hex, |
|
124 | + | suggestedFeeRecipient: validators.address, |
|
125 | + | } |
|
126 | + | const payloadAttributesFieldValidatorsV2 = { |
|
127 | + | ...payloadAttributesFieldValidatorsV1, |
|
128 | + | withdrawals: validators.array(validators.withdrawal()), |
|
129 | + | } |
|
87 | 130 | /** |
|
88 | 131 | * Formats a block to {@link ExecutionPayloadV1}. |
|
89 | 132 | */ |
|
90 | 133 | const blockToExecutionPayload = (block: Block) => { |
|
91 | - | const header = block.toJSON().header! |
|
134 | + | const blockJson = block.toJSON() |
|
135 | + | const header = blockJson.header! |
|
92 | 136 | const transactions = block.transactions.map((tx) => bufferToHex(tx.serialize())) ?? [] |
|
93 | - | const payload: ExecutionPayloadV1 = { |
|
137 | + | const withdrawalsArr = blockJson.withdrawals ? { withdrawals: blockJson.withdrawals } : {} |
|
138 | + | ||
139 | + | const payload: ExecutionPayload = { |
|
94 | 140 | blockNumber: header.number!, |
|
95 | 141 | parentHash: header.parentHash!, |
|
96 | 142 | feeRecipient: header.coinbase!, |
@@ -105,6 +151,7 @@
Loading
105 | 151 | blockHash: bufferToHex(block.hash()), |
|
106 | 152 | prevRandao: header.mixHash!, |
|
107 | 153 | transactions, |
|
154 | + | ...withdrawalsArr, |
|
108 | 155 | } |
|
109 | 156 | return payload |
|
110 | 157 | } |
@@ -128,17 +175,6 @@
Loading
128 | 175 | return parentBlocks.reverse() |
|
129 | 176 | } |
|
130 | 177 | ||
131 | - | /** |
|
132 | - | * Returns the txs trie root for the block. |
|
133 | - | */ |
|
134 | - | const txsTrieRoot = async (txs: TypedTransaction[]) => { |
|
135 | - | const trie = new Trie() |
|
136 | - | for (const [i, tx] of txs.entries()) { |
|
137 | - | await trie.put(Buffer.from(RLP.encode(i)), tx.serialize()) |
|
138 | - | } |
|
139 | - | return trie.root() |
|
140 | - | } |
|
141 | - | ||
142 | 178 | /** |
|
143 | 179 | * Returns the block hash as a 0x-prefixed hex string if found valid in the blockchain, otherwise returns null. |
|
144 | 180 | */ |
@@ -183,7 +219,7 @@
Loading
183 | 219 | * If errors, returns {@link PayloadStatusV1} |
|
184 | 220 | */ |
|
185 | 221 | const assembleBlock = async ( |
|
186 | - | payload: ExecutionPayloadV1, |
|
222 | + | payload: ExecutionPayload, |
|
187 | 223 | chain: Chain |
|
188 | 224 | ): Promise<{ block?: Block; error?: PayloadStatusV1 }> => { |
|
189 | 225 | const { |
@@ -192,6 +228,7 @@
Loading
192 | 228 | prevRandao: mixHash, |
|
193 | 229 | feeRecipient: coinbase, |
|
194 | 230 | transactions, |
|
231 | + | withdrawals: withdrawalsData, |
|
195 | 232 | } = payload |
|
196 | 233 | const { config } = chain |
|
197 | 234 | const common = config.chainCommon.copy() |
@@ -214,12 +251,15 @@
Loading
214 | 251 | } |
|
215 | 252 | } |
|
216 | 253 | ||
217 | - | const transactionsTrie = await txsTrieRoot(txs) |
|
254 | + | const transactionsTrie = await Block.genTransactionsTrieRoot(txs) |
|
255 | + | const withdrawals = withdrawalsData?.map((wData) => Withdrawal.fromWithdrawalData(wData)) |
|
256 | + | const withdrawalsRoot = withdrawals ? await Block.genWithdrawalsTrieRoot(withdrawals) : undefined |
|
218 | 257 | const header: HeaderData = { |
|
219 | 258 | ...payload, |
|
220 | 259 | number, |
|
221 | 260 | receiptTrie, |
|
222 | 261 | transactionsTrie, |
|
262 | + | withdrawalsRoot, |
|
223 | 263 | mixHash, |
|
224 | 264 | coinbase, |
|
225 | 265 | } |
@@ -228,8 +268,7 @@
Loading
228 | 268 | try { |
|
229 | 269 | // we are not setting hardforkByBlockNumber or hardforkByTTD as common is already |
|
230 | 270 | // correctly set to the correct hf |
|
231 | - | block = Block.fromBlockData({ header, transactions: txs }, { common }) |
|
232 | - | ||
271 | + | block = Block.fromBlockData({ header, transactions: txs, withdrawals }, { common }) |
|
233 | 272 | // Verify blockHash matches payload |
|
234 | 273 | if (!block.hash().equals(toBuffer(payload.blockHash))) { |
|
235 | 274 | const validationError = `Invalid blockHash, expected: ${ |
@@ -286,57 +325,47 @@
Loading
286 | 325 | ||
287 | 326 | this.newPayloadV1 = cmMiddleware( |
|
288 | 327 | middleware(this.newPayloadV1.bind(this), 1, [ |
|
289 | - | [ |
|
290 | - | validators.object({ |
|
291 | - | parentHash: validators.blockHash, |
|
292 | - | feeRecipient: validators.address, |
|
293 | - | stateRoot: validators.hex, |
|
294 | - | receiptsRoot: validators.hex, |
|
295 | - | logsBloom: validators.hex, |
|
296 | - | prevRandao: validators.hex, |
|
297 | - | blockNumber: validators.hex, |
|
298 | - | gasLimit: validators.hex, |
|
299 | - | gasUsed: validators.hex, |
|
300 | - | timestamp: validators.hex, |
|
301 | - | extraData: validators.hex, |
|
302 | - | baseFeePerGas: validators.hex, |
|
303 | - | blockHash: validators.blockHash, |
|
304 | - | transactions: validators.array(validators.hex), |
|
305 | - | }), |
|
306 | - | ], |
|
328 | + | [validators.object(executionPayloadV1FieldValidators)], |
|
329 | + | ]), |
|
330 | + | ([payload], response) => this.connectionManager.lastNewPayload({ payload, response }) |
|
331 | + | ) |
|
332 | + | ||
333 | + | this.newPayloadV2 = cmMiddleware( |
|
334 | + | middleware(this.newPayloadV2.bind(this), 1, [ |
|
335 | + | [validators.object(executionPayloadV2FieldValidators)], |
|
307 | 336 | ]), |
|
308 | 337 | ([payload], response) => this.connectionManager.lastNewPayload({ payload, response }) |
|
309 | 338 | ) |
|
310 | 339 | ||
340 | + | const forkchoiceUpdatedResponseCMHandler = ( |
|
341 | + | [state]: ForkchoiceStateV1[], |
|
342 | + | response?: ForkchoiceResponseV1 & { headBlock?: Block }, |
|
343 | + | error?: string |
|
344 | + | ) => { |
|
345 | + | this.connectionManager.lastForkchoiceUpdate({ |
|
346 | + | state, |
|
347 | + | response, |
|
348 | + | headBlock: response?.headBlock, |
|
349 | + | error, |
|
350 | + | }) |
|
351 | + | // Remove the headBlock from the response object as headBlock is bundled only for connectionManager |
|
352 | + | delete response?.headBlock |
|
353 | + | } |
|
354 | + | ||
311 | 355 | this.forkchoiceUpdatedV1 = cmMiddleware( |
|
312 | 356 | middleware(this.forkchoiceUpdatedV1.bind(this), 1, [ |
|
313 | - | [ |
|
314 | - | validators.object({ |
|
315 | - | headBlockHash: validators.blockHash, |
|
316 | - | safeBlockHash: validators.blockHash, |
|
317 | - | finalizedBlockHash: validators.blockHash, |
|
318 | - | }), |
|
319 | - | ], |
|
320 | - | [ |
|
321 | - | validators.optional( |
|
322 | - | validators.object({ |
|
323 | - | timestamp: validators.hex, |
|
324 | - | prevRandao: validators.hex, |
|
325 | - | suggestedFeeRecipient: validators.address, |
|
326 | - | }) |
|
327 | - | ), |
|
328 | - | ], |
|
357 | + | [validators.object(forkchoiceFieldValidators)], |
|
358 | + | [validators.optional(validators.object(payloadAttributesFieldValidatorsV1))], |
|
329 | 359 | ]), |
|
330 | - | ([state], response, error) => { |
|
331 | - | this.connectionManager.lastForkchoiceUpdate({ |
|
332 | - | state, |
|
333 | - | response, |
|
334 | - | headBlock: response?.headBlock, |
|
335 | - | error, |
|
336 | - | }) |
|
337 | - | // Remove the headBlock from the response object as headBlock is bundled only for connectionManager |
|
338 | - | delete response?.headBlock |
|
339 | - | } |
|
360 | + | forkchoiceUpdatedResponseCMHandler |
|
361 | + | ) |
|
362 | + | ||
363 | + | this.forkchoiceUpdatedV2 = cmMiddleware( |
|
364 | + | middleware(this.forkchoiceUpdatedV2.bind(this), 1, [ |
|
365 | + | [validators.object(forkchoiceFieldValidators)], |
|
366 | + | [validators.optional(validators.object(payloadAttributesFieldValidatorsV2))], |
|
367 | + | ]), |
|
368 | + | forkchoiceUpdatedResponseCMHandler |
|
340 | 369 | ) |
|
341 | 370 | ||
342 | 371 | this.getPayloadV1 = cmMiddleware( |
@@ -344,6 +373,11 @@
Loading
344 | 373 | () => this.connectionManager.updateStatus() |
|
345 | 374 | ) |
|
346 | 375 | ||
376 | + | this.getPayloadV2 = cmMiddleware( |
|
377 | + | middleware(this.getPayloadV2.bind(this), 1, [[validators.hex]]), |
|
378 | + | () => this.connectionManager.updateStatus() |
|
379 | + | ) |
|
380 | + | ||
347 | 381 | this.exchangeTransitionConfigurationV1 = cmMiddleware( |
|
348 | 382 | middleware(this.exchangeTransitionConfigurationV1.bind(this), 1, [ |
|
349 | 383 | [ |
@@ -375,10 +409,9 @@
Loading
375 | 409 | * valid block in the branch defined by payload and its ancestors |
|
376 | 410 | * 3. validationError: String|null - validation error message |
|
377 | 411 | */ |
|
378 | - | async newPayloadV1(params: [ExecutionPayloadV1]): Promise<PayloadStatusV1> { |
|
412 | + | private async newPayload(params: [ExecutionPayload]): Promise<PayloadStatusV1> { |
|
379 | 413 | const [payload] = params |
|
380 | 414 | const { parentHash, blockHash } = payload |
|
381 | - | ||
382 | 415 | const { block, error } = await assembleBlock(payload, this.chain) |
|
383 | 416 | if (!block || error) { |
|
384 | 417 | let response = error |
@@ -469,6 +502,8 @@
Loading
469 | 502 | return response |
|
470 | 503 | } |
|
471 | 504 | ||
505 | + | this.remoteBlocks.set(block.hash().toString('hex'), block) |
|
506 | + | ||
472 | 507 | const response = { |
|
473 | 508 | status: Status.VALID, |
|
474 | 509 | latestValidHash: bufferToHex(block.hash()), |
@@ -477,6 +512,14 @@
Loading
477 | 512 | return response |
|
478 | 513 | } |
|
479 | 514 | ||
515 | + | async newPayloadV1(params: [ExecutionPayloadV1]): Promise<PayloadStatusV1> { |
|
516 | + | return this.newPayload(params) |
|
517 | + | } |
|
518 | + | ||
519 | + | async newPayloadV2(params: [ExecutionPayloadV2]): Promise<PayloadStatusV1> { |
|
520 | + | return this.newPayload(params) |
|
521 | + | } |
|
522 | + | ||
480 | 523 | /** |
|
481 | 524 | * Propagates the change in the fork choice to the execution client. |
|
482 | 525 | * |
@@ -495,8 +538,8 @@
Loading
495 | 538 | * 2. payloadId: DATA|null - 8 Bytes - identifier of the payload build process or `null` |
|
496 | 539 | * 3. headBlock: Block|undefined - Block corresponding to headBlockHash if found |
|
497 | 540 | */ |
|
498 | - | async forkchoiceUpdatedV1( |
|
499 | - | params: [forkchoiceState: ForkchoiceStateV1, payloadAttributes: PayloadAttributesV1 | undefined] |
|
541 | + | private async forkchoiceUpdated( |
|
542 | + | params: [forkchoiceState: ForkchoiceStateV1, payloadAttributes: PayloadAttributes | undefined] |
|
500 | 543 | ): Promise<ForkchoiceResponseV1 & { headBlock?: Block }> { |
|
501 | 544 | const { headBlockHash, finalizedBlockHash, safeBlockHash } = params[0] |
|
502 | 545 | const payloadAttributes = params[1] |
@@ -540,7 +583,6 @@
Loading
540 | 583 | )}` |
|
541 | 584 | ) |
|
542 | 585 | await this.service.beaconSync?.setHead(headBlock) |
|
543 | - | ||
544 | 586 | // Only validate this as terminal block if this block's difficulty is non-zero, |
|
545 | 587 | // else this is a PoS block but its hardfork could be indeterminable if the skeleton |
|
546 | 588 | // is not yet connected. |
@@ -609,7 +651,6 @@
Loading
609 | 651 | this.service.txPool.checkRunState() |
|
610 | 652 | } |
|
611 | 653 | } |
|
612 | - | ||
613 | 654 | /* |
|
614 | 655 | * Process safe and finalized block |
|
615 | 656 | * Allowed to have zero value while transition block is finalizing |
@@ -643,13 +684,18 @@
Loading
643 | 684 | * If payloadAttributes is present, start building block and return payloadId |
|
644 | 685 | */ |
|
645 | 686 | if (payloadAttributes) { |
|
646 | - | const { timestamp, prevRandao, suggestedFeeRecipient } = payloadAttributes |
|
687 | + | const { timestamp, prevRandao, suggestedFeeRecipient, withdrawals } = payloadAttributes |
|
647 | 688 | const parentBlock = this.chain.blocks.latest! |
|
648 | - | const payloadId = await this.pendingBlock.start(await this.vm.copy(), parentBlock, { |
|
649 | - | timestamp, |
|
650 | - | mixHash: prevRandao, |
|
651 | - | coinbase: suggestedFeeRecipient, |
|
652 | - | }) |
|
689 | + | const payloadId = await this.pendingBlock.start( |
|
690 | + | await this.vm.copy(), |
|
691 | + | parentBlock, |
|
692 | + | { |
|
693 | + | timestamp, |
|
694 | + | mixHash: prevRandao, |
|
695 | + | coinbase: suggestedFeeRecipient, |
|
696 | + | }, |
|
697 | + | withdrawals |
|
698 | + | ) |
|
653 | 699 | const latestValidHash = await validHash(headBlock.hash(), this.chain) |
|
654 | 700 | const payloadStatus = { status: Status.VALID, latestValidHash, validationError: null } |
|
655 | 701 | const response = { payloadStatus, payloadId: bufferToHex(payloadId), headBlock } |
@@ -662,6 +708,18 @@
Loading
662 | 708 | return response |
|
663 | 709 | } |
|
664 | 710 | ||
711 | + | private async forkchoiceUpdatedV1( |
|
712 | + | params: [forkchoiceState: ForkchoiceStateV1, payloadAttributes: PayloadAttributesV1 | undefined] |
|
713 | + | ): Promise<ForkchoiceResponseV1 & { headBlock?: Block }> { |
|
714 | + | return this.forkchoiceUpdated(params) |
|
715 | + | } |
|
716 | + | ||
717 | + | private async forkchoiceUpdatedV2( |
|
718 | + | params: [forkchoiceState: ForkchoiceStateV1, payloadAttributes: PayloadAttributesV2 | undefined] |
|
719 | + | ): Promise<ForkchoiceResponseV1 & { headBlock?: Block }> { |
|
720 | + | return this.forkchoiceUpdated(params) |
|
721 | + | } |
|
722 | + | ||
665 | 723 | /** |
|
666 | 724 | * Given payloadId, returns the most recent version of an execution payload |
|
667 | 725 | * that is available by the time of the call or responds with an error. |
@@ -670,7 +728,7 @@
Loading
670 | 728 | * 1. payloadId: DATA, 8 bytes - identifier of the payload building process |
|
671 | 729 | * @returns Instance of {@link ExecutionPayloadV1} or an error |
|
672 | 730 | */ |
|
673 | - | async getPayloadV1(params: [string]) { |
|
731 | + | private async getPayload(params: [string]) { |
|
674 | 732 | const payloadId = toBuffer(params[0]) |
|
675 | 733 | try { |
|
676 | 734 | const built = await this.pendingBlock.build(payloadId) |
@@ -689,6 +747,13 @@
Loading
689 | 747 | } |
|
690 | 748 | } |
|
691 | 749 | ||
750 | + | async getPayloadV1(params: [string]) { |
|
751 | + | return this.getPayload(params) |
|
752 | + | } |
|
753 | + | ||
754 | + | async getPayloadV2(params: [string]) { |
|
755 | + | return this.getPayload(params) |
|
756 | + | } |
|
692 | 757 | /** |
|
693 | 758 | * Compare transition configuration parameters. |
|
694 | 759 | * |
@@ -2,15 +2,7 @@
Loading
2 | 2 | import { ConsensusType, Hardfork } from '@ethereumjs/common' |
|
3 | 3 | import { RLP } from '@ethereumjs/rlp' |
|
4 | 4 | import { Trie } from '@ethereumjs/trie' |
|
5 | - | import { |
|
6 | - | Account, |
|
7 | - | Address, |
|
8 | - | bigIntToBuffer, |
|
9 | - | bufArrToArr, |
|
10 | - | intToBuffer, |
|
11 | - | short, |
|
12 | - | toBuffer, |
|
13 | - | } from '@ethereumjs/util' |
|
5 | + | import { Account, Address, bigIntToBuffer, bufArrToArr, intToBuffer, short } from '@ethereumjs/util' |
|
14 | 6 | import { debug as createDebugLogger } from 'debug' |
|
15 | 7 | ||
16 | 8 | import { Bloom } from './bloom' |
@@ -331,11 +323,9 @@
Loading
331 | 323 | const state = this.eei |
|
332 | 324 | const withdrawals = block.withdrawals! |
|
333 | 325 | for (const withdrawal of withdrawals) { |
|
334 | - | const { address: addressData, amount: amountData } = withdrawal |
|
335 | - | const address = new Address(toBuffer(addressData)) |
|
336 | - | const amount = Buffer.isBuffer(amountData) |
|
337 | - | ? BigInt('0x' + amountData.toString('hex')) |
|
338 | - | : BigInt(amountData) |
|
326 | + | const { address, amount } = withdrawal |
|
327 | + | // skip touching account if no amount update |
|
328 | + | if (amount === BigInt(0)) continue |
|
339 | 329 | await rewardAccount(state, address, amount) |
|
340 | 330 | } |
|
341 | 331 | } |
@@ -234,6 +234,56 @@
Loading
234 | 234 | } |
|
235 | 235 | }, |
|
236 | 236 | ||
237 | + | /** |
|
238 | + | * validator to ensure required withdawal fields are present, and checks for valid address and hex values |
|
239 | + | * for the other quantity based fields |
|
240 | + | * @param requiredFields array of required fields |
|
241 | + | * @returns validator function with params: |
|
242 | + | * - @param params parameters of method |
|
243 | + | * - @param index index of parameter |
|
244 | + | */ |
|
245 | + | get withdrawal() { |
|
246 | + | return (requiredFields: string[] = ['index', 'validatorIndex', 'address', 'amount']) => { |
|
247 | + | return (params: any[], index: number) => { |
|
248 | + | if (typeof params[index] !== 'object') { |
|
249 | + | return { |
|
250 | + | code: INVALID_PARAMS, |
|
251 | + | message: `invalid argument ${index}: argument must be an object`, |
|
252 | + | } |
|
253 | + | } |
|
254 | + | ||
255 | + | const wt = params[index] |
|
256 | + | ||
257 | + | for (const field of requiredFields) { |
|
258 | + | if (wt[field] === undefined) { |
|
259 | + | return { |
|
260 | + | code: INVALID_PARAMS, |
|
261 | + | message: `invalid argument ${index}: required field ${field}`, |
|
262 | + | } |
|
263 | + | } |
|
264 | + | } |
|
265 | + | ||
266 | + | const validate = (field: any, validator: Function) => { |
|
267 | + | if (field === undefined) return |
|
268 | + | const v = validator([field], 0) |
|
269 | + | if (v !== undefined) return v |
|
270 | + | } |
|
271 | + | ||
272 | + | // validate addresses |
|
273 | + | for (const field of [wt.address]) { |
|
274 | + | const v = validate(field, this.address) |
|
275 | + | if (v !== undefined) return v |
|
276 | + | } |
|
277 | + | ||
278 | + | // validate hex |
|
279 | + | for (const field of [wt.index, wt.validatorIndex, wt.amount]) { |
|
280 | + | const v = validate(field, this.hex) |
|
281 | + | if (v !== undefined) return v |
|
282 | + | } |
|
283 | + | } |
|
284 | + | } |
|
285 | + | }, |
|
286 | + | ||
237 | 287 | /** |
|
238 | 288 | * object validator to check if type is object with |
|
239 | 289 | * required keys and expected validation of values |
@@ -4,13 +4,13 @@
Loading
4 | 4 | import { Capability, TransactionFactory } from '@ethereumjs/tx' |
|
5 | 5 | import { |
|
6 | 6 | KECCAK256_RLP, |
|
7 | + | Withdrawal, |
|
7 | 8 | arrToBufArr, |
|
8 | 9 | bigIntToHex, |
|
9 | 10 | bufArrToArr, |
|
10 | 11 | bufferToHex, |
|
11 | 12 | intToHex, |
|
12 | 13 | isHexPrefixed, |
|
13 | - | toBuffer, |
|
14 | 14 | } from '@ethereumjs/util' |
|
15 | 15 | import { keccak256 } from 'ethereum-cryptography/keccak' |
|
16 | 16 | import { ethers } from 'ethers' |
@@ -18,14 +18,7 @@
Loading
18 | 18 | import { blockFromRpc } from './from-rpc' |
|
19 | 19 | import { BlockHeader } from './header' |
|
20 | 20 | ||
21 | - | import type { |
|
22 | - | BlockBuffer, |
|
23 | - | BlockData, |
|
24 | - | BlockOptions, |
|
25 | - | JsonBlock, |
|
26 | - | JsonRpcBlock, |
|
27 | - | Withdrawal, |
|
28 | - | } from './types' |
|
21 | + | import type { BlockBuffer, BlockData, BlockOptions, JsonBlock, JsonRpcBlock } from './types' |
|
29 | 22 | import type { Common } from '@ethereumjs/common' |
|
30 | 23 | import type { |
|
31 | 24 | FeeMarketEIP1559Transaction, |
@@ -33,7 +26,7 @@
Loading
33 | 26 | TxOptions, |
|
34 | 27 | TypedTransaction, |
|
35 | 28 | } from '@ethereumjs/tx' |
|
36 | - | import type { Address } from '@ethereumjs/util' |
|
29 | + | import type { WithdrawalBuffer } from '@ethereumjs/util' |
|
37 | 30 | ||
38 | 31 | /** |
|
39 | 32 | * An object that represents the block. |
@@ -46,6 +39,32 @@
Loading
46 | 39 | public readonly txTrie = new Trie() |
|
47 | 40 | public readonly _common: Common |
|
48 | 41 | ||
42 | + | /** |
|
43 | + | * Returns the withdrawals trie root for array of Withdrawal. |
|
44 | + | * @param wts array of Withdrawal to compute the root of |
|
45 | + | * @param optional emptyTrie to use to generate the root |
|
46 | + | */ |
|
47 | + | public static async genWithdrawalsTrieRoot(wts: Withdrawal[], emptyTrie?: Trie) { |
|
48 | + | const trie = emptyTrie ?? new Trie() |
|
49 | + | for (const [i, wt] of wts.entries()) { |
|
50 | + | await trie.put(Buffer.from(RLP.encode(i)), arrToBufArr(RLP.encode(wt.raw()))) |
|
51 | + | } |
|
52 | + | return trie.root() |
|
53 | + | } |
|
54 | + | ||
55 | + | /** |
|
56 | + | * Returns the txs trie root for array of TypedTransaction |
|
57 | + | * @param txs array of TypedTransaction to compute the root of |
|
58 | + | * @param optional emptyTrie to use to generate the root |
|
59 | + | */ |
|
60 | + | public static async genTransactionsTrieRoot(txs: TypedTransaction[], emptyTrie?: Trie) { |
|
61 | + | const trie = emptyTrie ?? new Trie() |
|
62 | + | for (const [i, tx] of txs.entries()) { |
|
63 | + | await trie.put(Buffer.from(RLP.encode(i)), tx.serialize()) |
|
64 | + | } |
|
65 | + | return trie.root() |
|
66 | + | } |
|
67 | + | ||
49 | 68 | /** |
|
50 | 69 | * Static constructor to create a block from a block data dictionary |
|
51 | 70 | * |
@@ -57,7 +76,7 @@
Loading
57 | 76 | header: headerData, |
|
58 | 77 | transactions: txsData, |
|
59 | 78 | uncleHeaders: uhsData, |
|
60 | - | withdrawals, |
|
79 | + | withdrawals: withdrawalsData, |
|
61 | 80 | } = blockData |
|
62 | 81 | const header = BlockHeader.fromHeaderData(headerData, opts) |
|
63 | 82 |
@@ -93,6 +112,8 @@
Loading
93 | 112 | uncleHeaders.push(uh) |
|
94 | 113 | } |
|
95 | 114 | ||
115 | + | const withdrawals = withdrawalsData?.map(Withdrawal.fromWithdrawalData) |
|
116 | + | ||
96 | 117 | return new Block(header, transactions, uncleHeaders, opts, withdrawals) |
|
97 | 118 | } |
|
98 | 119 |
@@ -122,8 +143,7 @@
Loading
122 | 143 | if (values.length > 4) { |
|
123 | 144 | throw new Error('invalid block. More values than expected were received') |
|
124 | 145 | } |
|
125 | - | ||
126 | - | const [headerData, txsData, uhsData, withdrawalsData] = values |
|
146 | + | const [headerData, txsData, uhsData, withdrawalsBuffer] = values |
|
127 | 147 | ||
128 | 148 | const header = BlockHeader.fromValuesArray(headerData, opts) |
|
129 | 149 |
@@ -159,14 +179,14 @@
Loading
159 | 179 | uncleHeaders.push(BlockHeader.fromValuesArray(uncleHeaderData, uncleOpts)) |
|
160 | 180 | } |
|
161 | 181 | ||
162 | - | let withdrawals |
|
163 | - | if (withdrawalsData) { |
|
164 | - | withdrawals = <Withdrawal[]>[] |
|
165 | - | for (const withdrawal of withdrawalsData) { |
|
166 | - | const [index, validatorIndex, address, amount] = withdrawal |
|
167 | - | withdrawals.push({ index, validatorIndex, address, amount }) |
|
168 | - | } |
|
169 | - | } |
|
182 | + | const withdrawals = (withdrawalsBuffer as WithdrawalBuffer[]) |
|
183 | + | ?.map(([index, validatorIndex, address, amount]) => ({ |
|
184 | + | index, |
|
185 | + | validatorIndex, |
|
186 | + | address, |
|
187 | + | amount, |
|
188 | + | })) |
|
189 | + | ?.map(Withdrawal.fromWithdrawalData) |
|
170 | 190 | ||
171 | 191 | return new Block(header, transactions, uncleHeaders, opts, withdrawals) |
|
172 | 192 | } |
@@ -241,6 +261,7 @@
Loading
241 | 261 | ) { |
|
242 | 262 | this.header = header ?? BlockHeader.fromHeaderData({}, opts) |
|
243 | 263 | this.transactions = transactions |
|
264 | + | this.withdrawals = withdrawals |
|
244 | 265 | this.uncleHeaders = uncleHeaders |
|
245 | 266 | this._common = this.header._common |
|
246 | 267 | if (uncleHeaders.length > 0) { |
@@ -265,32 +286,12 @@
Loading
265 | 286 | throw new Error('Cannot have a withdrawals field if EIP 4895 is not active') |
|
266 | 287 | } |
|
267 | 288 | ||
268 | - | this.withdrawals = withdrawals |
|
269 | - | ||
270 | 289 | const freeze = opts?.freeze ?? true |
|
271 | 290 | if (freeze) { |
|
272 | 291 | Object.freeze(this) |
|
273 | 292 | } |
|
274 | 293 | } |
|
275 | 294 | ||
276 | - | /** |
|
277 | - | * Convert a withdrawal to a buffer array |
|
278 | - | * @param withdrawal the withdrawal to convert |
|
279 | - | * @returns buffer array of the withdrawal |
|
280 | - | */ |
|
281 | - | private withdrawalToBufferArray(withdrawal: Withdrawal): [Buffer, Buffer, Buffer, Buffer] { |
|
282 | - | const { index, validatorIndex, address, amount } = withdrawal |
|
283 | - | let addressBuffer: Buffer |
|
284 | - | if (typeof address === 'string') { |
|
285 | - | addressBuffer = Buffer.from(address.slice(2)) |
|
286 | - | } else if (Buffer.isBuffer(address)) { |
|
287 | - | addressBuffer = address |
|
288 | - | } else { |
|
289 | - | addressBuffer = (<Address>address).buf |
|
290 | - | } |
|
291 | - | return [toBuffer(index), toBuffer(validatorIndex), addressBuffer, toBuffer(amount)] |
|
292 | - | } |
|
293 | - | ||
294 | 295 | /** |
|
295 | 296 | * Returns a Buffer Array of the raw Buffers of this block, in order. |
|
296 | 297 | */ |
@@ -302,10 +303,9 @@
Loading
302 | 303 | ) as Buffer[], |
|
303 | 304 | this.uncleHeaders.map((uh) => uh.raw()), |
|
304 | 305 | ] |
|
305 | - | if (this.withdrawals) { |
|
306 | - | for (const withdrawal of this.withdrawals) { |
|
307 | - | bufferArray.push(this.withdrawalToBufferArray(withdrawal)) |
|
308 | - | } |
|
306 | + | const withdrawalsRaw = this.withdrawals?.map((wt) => wt.raw()) |
|
307 | + | if (withdrawalsRaw) { |
|
308 | + | bufferArray.push(withdrawalsRaw) |
|
309 | 309 | } |
|
310 | 310 | return bufferArray |
|
311 | 311 | } |
@@ -336,12 +336,7 @@
Loading
336 | 336 | */ |
|
337 | 337 | async genTxTrie(): Promise<void> { |
|
338 | 338 | const { transactions, txTrie } = this |
|
339 | - | for (let i = 0; i < transactions.length; i++) { |
|
340 | - | const tx = transactions[i] |
|
341 | - | const key = Buffer.from(RLP.encode(i)) |
|
342 | - | const value = tx.serialize() |
|
343 | - | await txTrie.put(key, value) |
|
344 | - | } |
|
339 | + | await Block.genTransactionsTrieRoot(transactions, txTrie) |
|
345 | 340 | } |
|
346 | 341 | ||
347 | 342 | /** |
@@ -449,14 +444,8 @@
Loading
449 | 444 | if (!this._common.isActivatedEIP(4895)) { |
|
450 | 445 | throw new Error('EIP 4895 is not activated') |
|
451 | 446 | } |
|
452 | - | const trie = new Trie() |
|
453 | - | let index = 0 |
|
454 | - | for (const withdrawal of this.withdrawals!) { |
|
455 | - | const withdrawalRLP = RLP.encode(this.withdrawalToBufferArray(withdrawal)) |
|
456 | - | await trie.put(Buffer.from('0x' + index.toString(16)), arrToBufArr(withdrawalRLP)) |
|
457 | - | index++ |
|
458 | - | } |
|
459 | - | return trie.root().equals(this.header.withdrawalsRoot!) |
|
447 | + | const withdrawalsRoot = await Block.genWithdrawalsTrieRoot(this.withdrawals!) |
|
448 | + | return withdrawalsRoot.equals(this.header.withdrawalsRoot!) |
|
460 | 449 | } |
|
461 | 450 | ||
462 | 451 | /** |
@@ -510,10 +499,16 @@
Loading
510 | 499 | * Returns the block in JSON format. |
|
511 | 500 | */ |
|
512 | 501 | toJSON(): JsonBlock { |
|
502 | + | const withdrawalsAttr = this.withdrawals |
|
503 | + | ? { |
|
504 | + | withdrawals: this.withdrawals.map((wt) => wt.toJSON()), |
|
505 | + | } |
|
506 | + | : {} |
|
513 | 507 | return { |
|
514 | 508 | header: this.header.toJSON(), |
|
515 | 509 | transactions: this.transactions.map((tx) => tx.toJSON()), |
|
516 | 510 | uncleHeaders: this.uncleHeaders.map((uh) => uh.toJSON()), |
|
511 | + | ...withdrawalsAttr, |
|
517 | 512 | } |
|
518 | 513 | } |
|
519 | 514 |
@@ -0,0 +1,95 @@
Loading
1 | + | import { Address } from './address' |
|
2 | + | import { bigIntToHex } from './bytes' |
|
3 | + | import { TypeOutput, toType } from './types' |
|
4 | + | ||
5 | + | import type { AddressLike, BigIntLike } from './types' |
|
6 | + | ||
7 | + | export type WithdrawalData = { |
|
8 | + | index: BigIntLike |
|
9 | + | validatorIndex: BigIntLike |
|
10 | + | address: AddressLike |
|
11 | + | amount: BigIntLike |
|
12 | + | } |
|
13 | + | ||
14 | + | export interface JsonRpcWithdrawal { |
|
15 | + | index: string // QUANTITY - bigint 8 bytes |
|
16 | + | validatorIndex: string // QUANTITY - bigint 8 bytes |
|
17 | + | address: string // DATA, 20 Bytes address to withdraw to |
|
18 | + | amount: string // QUANTITY - bigint amount in wei 32 bytes |
|
19 | + | } |
|
20 | + | ||
21 | + | export type WithdrawalBuffer = [Buffer, Buffer, Buffer, Buffer] |
|
22 | + | ||
23 | + | export class Withdrawal { |
|
24 | + | constructor( |
|
25 | + | public readonly index: bigint, |
|
26 | + | public readonly validatorIndex: bigint, |
|
27 | + | public readonly address: Address, |
|
28 | + | public readonly amount: bigint |
|
29 | + | ) {} |
|
30 | + | ||
31 | + | public static fromWithdrawalData(withdrawalData: WithdrawalData) { |
|
32 | + | const { |
|
33 | + | index: indexData, |
|
34 | + | validatorIndex: validatorIndexData, |
|
35 | + | address: addressData, |
|
36 | + | amount: amountData, |
|
37 | + | } = withdrawalData |
|
38 | + | const index = toType(indexData, TypeOutput.BigInt) |
|
39 | + | const validatorIndex = toType(validatorIndexData, TypeOutput.BigInt) |
|
40 | + | const address = new Address(toType(addressData, TypeOutput.Buffer)) |
|
41 | + | const amount = toType(amountData, TypeOutput.BigInt) |
|
42 | + | ||
43 | + | return new Withdrawal(index, validatorIndex, address, amount) |
|
44 | + | } |
|
45 | + | ||
46 | + | public static fromValuesArray(withdrawalArray: WithdrawalBuffer) { |
|
47 | + | if (withdrawalArray.length !== 4) { |
|
48 | + | throw Error(`Invalid withdrawalArray length expected=4 actual=${withdrawalArray.length}`) |
|
49 | + | } |
|
50 | + | const [index, validatorIndex, address, amount] = withdrawalArray |
|
51 | + | return Withdrawal.fromWithdrawalData({ index, validatorIndex, address, amount }) |
|
52 | + | } |
|
53 | + | ||
54 | + | /** |
|
55 | + | * Convert a withdrawal to a buffer array |
|
56 | + | * @param withdrawal the withdrawal to convert |
|
57 | + | * @returns buffer array of the withdrawal |
|
58 | + | */ |
|
59 | + | public static toBufferArray(withdrawal: Withdrawal | WithdrawalData): WithdrawalBuffer { |
|
60 | + | const { index, validatorIndex, address, amount } = withdrawal |
|
61 | + | const indexBuffer = |
|
62 | + | toType(index, TypeOutput.BigInt) === BigInt(0) |
|
63 | + | ? Buffer.alloc(0) |
|
64 | + | : toType(index, TypeOutput.Buffer) |
|
65 | + | const validatorIndexBuffer = |
|
66 | + | toType(validatorIndex, TypeOutput.BigInt) === BigInt(0) |
|
67 | + | ? Buffer.alloc(0) |
|
68 | + | : toType(validatorIndex, TypeOutput.Buffer) |
|
69 | + | let addressBuffer |
|
70 | + | if (address instanceof Address) { |
|
71 | + | addressBuffer = (<Address>address).buf |
|
72 | + | } else { |
|
73 | + | addressBuffer = toType(address, TypeOutput.Buffer) |
|
74 | + | } |
|
75 | + | const amountBuffer = |
|
76 | + | toType(amount, TypeOutput.BigInt) === BigInt(0) |
|
77 | + | ? Buffer.alloc(0) |
|
78 | + | : toType(amount, TypeOutput.Buffer) |
|
79 | + | ||
80 | + | return [indexBuffer, validatorIndexBuffer, addressBuffer, amountBuffer] |
|
81 | + | } |
|
82 | + | ||
83 | + | raw() { |
|
84 | + | return Withdrawal.toBufferArray(this) |
|
85 | + | } |
|
86 | + | ||
87 | + | toJSON() { |
|
88 | + | return { |
|
89 | + | index: bigIntToHex(this.index), |
|
90 | + | validatorIndex: bigIntToHex(this.validatorIndex), |
|
91 | + | address: '0x' + this.address.buf.toString('hex'), |
|
92 | + | amount: bigIntToHex(this.amount), |
|
93 | + | } |
|
94 | + | } |
|
95 | + | } |
@@ -4,6 +4,7 @@
Loading
4 | 4 | import type { TxPool } from '../service/txpool' |
|
5 | 5 | import type { Block, HeaderData } from '@ethereumjs/block' |
|
6 | 6 | import type { TypedTransaction } from '@ethereumjs/tx' |
|
7 | + | import type { WithdrawalData } from '@ethereumjs/util' |
|
7 | 8 | import type { TxReceipt, VM } from '@ethereumjs/vm' |
|
8 | 9 | import type { BlockBuilder } from '@ethereumjs/vm/dist/buildBlock' |
|
9 | 10 |
@@ -36,7 +37,12 @@
Loading
36 | 37 | * Starts building a pending block with the given payload |
|
37 | 38 | * @returns an 8-byte payload identifier to call {@link BlockBuilder.build} with |
|
38 | 39 | */ |
|
39 | - | async start(vm: VM, parentBlock: Block, headerData: Partial<HeaderData> = {}) { |
|
40 | + | async start( |
|
41 | + | vm: VM, |
|
42 | + | parentBlock: Block, |
|
43 | + | headerData: Partial<HeaderData> = {}, |
|
44 | + | withdrawals?: WithdrawalData[] |
|
45 | + | ) { |
|
40 | 46 | const number = parentBlock.header.number + BigInt(1) |
|
41 | 47 | const { gasLimit } = parentBlock.header |
|
42 | 48 | const baseFeePerGas = |
@@ -60,6 +66,7 @@
Loading
60 | 66 | gasLimit, |
|
61 | 67 | baseFeePerGas, |
|
62 | 68 | }, |
|
69 | + | withdrawals, |
|
63 | 70 | blockOpts: { |
|
64 | 71 | putBlockIntoBlockchain: false, |
|
65 | 72 | }, |
@@ -156,10 +163,11 @@
Loading
156 | 163 | } |
|
157 | 164 | ||
158 | 165 | const block = await builder.build() |
|
166 | + | const withdrawalsStr = block.withdrawals ? ` withdrawals=${block.withdrawals.length}` : '' |
|
159 | 167 | this.config.logger.info( |
|
160 | 168 | `Pending: Built block number=${block.header.number} txs=${ |
|
161 | 169 | block.transactions.length |
|
162 | - | } hash=${block.hash().toString('hex')}` |
|
170 | + | }${withdrawalsStr} hash=${block.hash().toString('hex')}` |
|
163 | 171 | ) |
|
164 | 172 | ||
165 | 173 | // Remove from pendingPayloads |
@@ -86,6 +86,13 @@
Loading
86 | 86 | const transactions = block.transactions.map((tx, txIndex) => |
|
87 | 87 | includeTransactions ? jsonRpcTx(tx, block, txIndex) : bufferToHex(tx.hash()) |
|
88 | 88 | ) |
|
89 | + | const withdrawalsAttr = |
|
90 | + | header.withdrawalsRoot !== undefined |
|
91 | + | ? { |
|
92 | + | withdrawalsRoot: header.withdrawalsRoot!, |
|
93 | + | withdrawals: json.withdrawals, |
|
94 | + | } |
|
95 | + | : {} |
|
89 | 96 | const td = await chain.getTd(block.hash(), block.header.number) |
|
90 | 97 | return { |
|
91 | 98 | number: header.number!, |
@@ -109,6 +116,7 @@
Loading
109 | 116 | transactions, |
|
110 | 117 | uncles: block.uncleHeaders.map((uh) => bufferToHex(uh.hash())), |
|
111 | 118 | baseFeePerGas: header.baseFeePerGas, |
|
119 | + | ...withdrawalsAttr, |
|
112 | 120 | } |
|
113 | 121 | } |
|
114 | 122 |
@@ -203,7 +203,7 @@
Loading
203 | 203 | } |
|
204 | 204 | ||
205 | 205 | if (this._common.isActivatedEIP(4895)) { |
|
206 | - | if (withdrawalsRoot === defaults.withdrawalsRoot) { |
|
206 | + | if (withdrawalsRoot === undefined) { |
|
207 | 207 | throw new Error('invalid header. withdrawalsRoot should be provided') |
|
208 | 208 | } |
|
209 | 209 | } else { |
@@ -540,6 +540,10 @@
Loading
540 | 540 | rawItems.push(bigIntToUnpaddedBuffer(this.baseFeePerGas!)) |
|
541 | 541 | } |
|
542 | 542 | ||
543 | + | if (this._common.isActivatedEIP(4895) === true) { |
|
544 | + | rawItems.push(this.withdrawalsRoot!) |
|
545 | + | } |
|
546 | + | ||
543 | 547 | return rawItems |
|
544 | 548 | } |
|
545 | 549 |
@@ -778,12 +782,16 @@
Loading
778 | 782 | * Returns the block header in JSON format. |
|
779 | 783 | */ |
|
780 | 784 | toJSON(): JsonHeader { |
|
785 | + | const withdrawalAttr = this.withdrawalsRoot |
|
786 | + | ? { withdrawalsRoot: '0x' + this.withdrawalsRoot.toString('hex') } |
|
787 | + | : {} |
|
781 | 788 | const jsonDict: JsonHeader = { |
|
782 | 789 | parentHash: '0x' + this.parentHash.toString('hex'), |
|
783 | 790 | uncleHash: '0x' + this.uncleHash.toString('hex'), |
|
784 | 791 | coinbase: this.coinbase.toString(), |
|
785 | 792 | stateRoot: '0x' + this.stateRoot.toString('hex'), |
|
786 | 793 | transactionsTrie: '0x' + this.transactionsTrie.toString('hex'), |
|
794 | + | ...withdrawalAttr, |
|
787 | 795 | receiptTrie: '0x' + this.receiptTrie.toString('hex'), |
|
788 | 796 | logsBloom: '0x' + this.logsBloom.toString('hex'), |
|
789 | 797 | difficulty: bigIntToHex(this.difficulty), |
Files | Coverage |
---|---|
packages | 87.53% |
Project Totals (198 files) | 87.53% |
Sunburst
The inner-most circle is the entire project, moving away from the center are folders then, finally, a single file.
The size and color of each slice is representing the number of statements and the coverage, respectively.
Icicle
The top section represents the entire project. Proceeding with folders and finally individual files.
The size and color of each slice is representing the number of statements and the coverage, respectively.