ethereumjs / ethereumjs-monorepo

@@ -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%
1
coverage:
2
  status:
3
    project:
4
      default:
5
        target: auto
6
        threshold: 2%
7
    patch:
8
      default:
9
        target: auto
10
        threshold: 5%
11
comment:
12
  layout: 'reach, flags'
13
flags:
14
  rlp:
15
    carryforward: true
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.
Grid
Each block represents a single file in the project. The size and color of each block is represented by the number of statements and the coverage, respectively.
Loading