Skip to content

Commit 27b8e9f

Browse files
authored
client: add getBlobsV1 to the client to support CL blob import (#3711)
* client: add getBlobsV1 to the client to support CL blob import * add the getBlobV1 spec and debug/fix the api * debug and fix pending block spec and add assertions there as well * apply feedback * fix the lint issues * spell checl * refac the cache length param to fix build * fix breasking tests * improvs * apply params fix for 4844 custom common * fix spec * 128 len check * verify cache pruning in the test spec * lint/spell * fix typecheck
1 parent 2f70c44 commit 27b8e9f

File tree

8 files changed

+160
-18
lines changed

8 files changed

+160
-18
lines changed

packages/client/src/config.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -339,6 +339,11 @@ export interface ConfigOptions {
339339
startExecution?: boolean
340340
ignoreStatelessInvalidExecs?: boolean
341341

342+
/**
343+
* The cache for blobs and proofs to support CL import blocks
344+
*/
345+
blobsAndProofsCacheBlocks?: number
346+
342347
/**
343348
* Enables Prometheus Metrics that can be collected for monitoring client health
344349
*/
@@ -393,6 +398,9 @@ export class Config {
393398
// randomly kept it at 5 for fast testing purposes but ideally should be >=32 slots
394399
public static readonly SNAP_TRANSITION_SAFE_DEPTH = BigInt(5)
395400

401+
// support blobs and proofs cache for CL getBlobs for upto 1 epoch of data
402+
public static readonly BLOBS_AND_PROOFS_CACHE_BLOCKS = 32
403+
396404
public readonly logger: Logger
397405
public readonly syncmode: SyncMode
398406
public readonly vm?: VM
@@ -451,6 +459,8 @@ export class Config {
451459
public readonly startExecution: boolean
452460
public readonly ignoreStatelessInvalidExecs: boolean
453461

462+
public readonly blobsAndProofsCacheBlocks: number
463+
454464
public synchronized: boolean
455465
public lastSynchronized?: boolean
456466
/** lastSyncDate in ms */
@@ -553,6 +563,9 @@ export class Config {
553563
this.chainCommon = common.copy()
554564
this.execCommon = common.copy()
555565

566+
this.blobsAndProofsCacheBlocks =
567+
options.blobsAndProofsCacheBlocks ?? Config.BLOBS_AND_PROOFS_CACHE_BLOCKS
568+
556569
this.discDns = this.getDnsDiscovery(options.discDns)
557570
this.discV4 = options.discV4 ?? true
558571

packages/client/src/rpc/modules/engine/engine.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ import type { Config } from '../../../config.js'
5252
import type { VMExecution } from '../../../execution/index.js'
5353
import type { FullEthereumService, Skeleton } from '../../../service/index.js'
5454
import type {
55+
BlobAndProofV1,
5556
Bytes32,
5657
Bytes8,
5758
ExecutionPayloadBodyV1,
@@ -316,6 +317,13 @@ export class Engine {
316317
]),
317318
() => this.connectionManager.updateStatus(),
318319
)
320+
321+
this.getBlobsV1 = cmMiddleware(
322+
middleware(callWithStackTrace(this.getBlobsV1.bind(this), this._rpcDebug), 1, [
323+
[validators.array(validators.bytes32)],
324+
]),
325+
() => this.connectionManager.updateStatus(),
326+
)
319327
}
320328

321329
/**
@@ -1513,4 +1521,20 @@ export class Engine {
15131521
}
15141522
return payloads
15151523
}
1524+
1525+
private async getBlobsV1(params: [[Bytes32]]): Promise<(BlobAndProofV1 | null)[]> {
1526+
if (params[0].length > 128) {
1527+
throw {
1528+
code: TOO_LARGE_REQUEST,
1529+
message: `More than 128 hashes queried`,
1530+
}
1531+
}
1532+
1533+
const blobsAndProof: (BlobAndProofV1 | null)[] = []
1534+
for (const versionedHashHex of params[0]) {
1535+
blobsAndProof.push(this.service.txPool.blobsAndProofsByHash.get(versionedHashHex) ?? null)
1536+
}
1537+
1538+
return blobsAndProof
1539+
}
15161540
}

packages/client/src/rpc/modules/engine/types.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,7 @@ export enum Status {
2020
export type Bytes8 = PrefixedHexString
2121
export type Bytes20 = PrefixedHexString
2222
export type Bytes32 = PrefixedHexString
23-
// type Root = Bytes32
24-
export type Blob = Bytes32
23+
export type Blob = PrefixedHexString
2524
export type Bytes48 = PrefixedHexString
2625
export type Uint64 = PrefixedHexString
2726
export type Uint256 = PrefixedHexString
@@ -81,6 +80,11 @@ export type ExecutionPayloadBodyV1 = {
8180
withdrawals: WithdrawalV1[] | null
8281
}
8382

83+
export type BlobAndProofV1 = {
84+
blob: PrefixedHexString
85+
proof: PrefixedHexString
86+
}
87+
8488
export type ChainCache = {
8589
remoteBlocks: Map<String, Block>
8690
executedBlocks: Map<String, Block>

packages/client/src/service/txpool.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import type { PeerPool } from '../net/peerpool.js'
2626
import type { FullEthereumService } from './fullethereumservice.js'
2727
import type { Block } from '@ethereumjs/block'
2828
import type { FeeMarket1559Tx, LegacyTx, TypedTransaction } from '@ethereumjs/tx'
29+
import type { PrefixedHexString } from '@ethereumjs/util'
2930
import type { VM } from '@ethereumjs/vm'
3031

3132
// Configuration constants
@@ -102,6 +103,10 @@ export class TxPool {
102103
* Maps an address to a `TxPoolObject`
103104
*/
104105
public pool: Map<UnprefixedAddress, TxPoolObject[]>
106+
public blobsAndProofsByHash: Map<
107+
PrefixedHexString,
108+
{ blob: PrefixedHexString; proof: PrefixedHexString }
109+
>
105110

106111
/**
107112
* The number of txs currently in the pool
@@ -167,6 +172,10 @@ export class TxPool {
167172
this.service = options.service
168173

169174
this.pool = new Map<UnprefixedAddress, TxPoolObject[]>()
175+
this.blobsAndProofsByHash = new Map<
176+
PrefixedHexString,
177+
{ blob: PrefixedHexString; proof: PrefixedHexString }
178+
>()
170179
this.txsInPool = 0
171180
this.handled = new Map<UnprefixedHash, HandledObject>()
172181
this.knownByPeer = new Map<PeerId, SentObject[]>()
@@ -371,6 +380,16 @@ export class TxPool {
371380
this.config.metrics?.feeMarketEIP1559TxGauge?.inc()
372381
}
373382
if (isBlob4844Tx(tx)) {
383+
// add to blobs and proofs cache
384+
if (tx.blobs !== undefined && tx.kzgProofs !== undefined) {
385+
for (const [i, versionedHash] of tx.blobVersionedHashes.entries()) {
386+
const blob = tx.blobs![i]
387+
const proof = tx.kzgProofs![i]
388+
this.blobsAndProofsByHash.set(versionedHash, { blob, proof })
389+
}
390+
this.pruneBlobsAndProofsCache()
391+
}
392+
374393
this.config.metrics?.blobEIP4844TxGauge?.inc()
375394
}
376395
} catch (e) {
@@ -379,6 +398,24 @@ export class TxPool {
379398
}
380399
}
381400

401+
pruneBlobsAndProofsCache() {
402+
const blobGasLimit = this.config.chainCommon.param('maxblobGasPerBlock')
403+
const blobGasPerBlob = this.config.chainCommon.param('blobGasPerBlob')
404+
const allowedBlobsPerBlock = Number(blobGasLimit / blobGasPerBlob)
405+
406+
const pruneLength =
407+
this.blobsAndProofsByHash.size - allowedBlobsPerBlock * this.config.blobsAndProofsCacheBlocks
408+
let pruned = 0
409+
// since keys() is sorted by insertion order this prunes the oldest data in cache
410+
for (const versionedHash of this.blobsAndProofsByHash.keys()) {
411+
if (pruned >= pruneLength) {
412+
break
413+
}
414+
this.blobsAndProofsByHash.delete(versionedHash)
415+
pruned++
416+
}
417+
}
418+
382419
/**
383420
* Returns the available txs from the pool
384421
* @param txHashes

packages/client/test/miner/pendingBlock.spec.ts

Lines changed: 65 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
commitmentsToVersionedHashes,
1313
getBlobs,
1414
hexToBytes,
15+
intToHex,
1516
randomBytes,
1617
} from '@ethereumjs/util'
1718
import { createVM } from '@ethereumjs/vm'
@@ -28,7 +29,9 @@ import { mockBlockchain } from '../rpc/mockBlockchain.js'
2829

2930
import type { Blockchain } from '@ethereumjs/blockchain'
3031
import type { TypedTransaction } from '@ethereumjs/tx'
32+
import type { PrefixedHexString } from '@ethereumjs/util'
3133
import type { VM } from '@ethereumjs/vm'
34+
3235
const kzg = new microEthKZG(trustedSetup)
3336

3437
const A = {
@@ -353,24 +356,48 @@ describe('[PendingBlock]', async () => {
353356
})
354357

355358
const { txPool } = setup()
359+
txPool['config'].chainCommon.setHardfork(Hardfork.Cancun)
360+
361+
// fill up the blobsAndProofsByHash and proofs cache before adding a blob tx
362+
// for cache pruning check
363+
const fillBlobs = getBlobs('hello world')
364+
const fillCommitments = blobsToCommitments(kzg, fillBlobs)
365+
const fillProofs = blobsToProofs(kzg, fillBlobs, fillCommitments)
366+
const fillBlobAndProof = { blob: fillBlobs[0], proof: fillProofs[0] }
367+
368+
const blobGasLimit = txPool['config'].chainCommon.param('maxblobGasPerBlock')
369+
const blobGasPerBlob = txPool['config'].chainCommon.param('blobGasPerBlob')
370+
const allowedBlobsPerBlock = Number(blobGasLimit / blobGasPerBlob)
371+
const allowedLength = allowedBlobsPerBlock * txPool['config'].blobsAndProofsCacheBlocks
372+
373+
for (let i = 0; i < allowedLength; i++) {
374+
// this is space efficient as same object is inserted in dummy positions
375+
txPool.blobsAndProofsByHash.set(intToHex(i), fillBlobAndProof)
376+
}
377+
assert.equal(txPool.blobsAndProofsByHash.size, allowedLength, 'fill the cache to capacity')
356378

357-
const blobs = getBlobs('hello world')
358-
const commitments = blobsToCommitments(kzg, blobs)
359-
const blobVersionedHashes = commitmentsToVersionedHashes(commitments)
360-
const proofs = blobsToProofs(kzg, blobs, commitments)
361-
362-
// Create 3 txs with 2 blobs each so that only 2 of them can be included in a build
379+
// Create 2 txs with 3 blobs each so that only 2 of them can be included in a build
380+
let blobs: PrefixedHexString[] = [],
381+
proofs: PrefixedHexString[] = [],
382+
versionedHashes: PrefixedHexString[] = []
363383
for (let x = 0; x <= 2; x++) {
384+
// generate unique blobs different from fillBlobs
385+
const txBlobs = [
386+
...getBlobs(`hello world-${x}1`),
387+
...getBlobs(`hello world-${x}2`),
388+
...getBlobs(`hello world-${x}3`),
389+
]
390+
assert.equal(txBlobs.length, 3, '3 blobs should be created')
391+
const txCommitments = blobsToCommitments(kzg, txBlobs)
392+
const txBlobVersionedHashes = commitmentsToVersionedHashes(txCommitments)
393+
const txProofs = blobsToProofs(kzg, txBlobs, txCommitments)
394+
364395
const txA01 = createBlob4844Tx(
365396
{
366-
blobVersionedHashes: [
367-
...blobVersionedHashes,
368-
...blobVersionedHashes,
369-
...blobVersionedHashes,
370-
],
371-
blobs: [...blobs, ...blobs, ...blobs],
372-
kzgCommitments: [...commitments, ...commitments, ...commitments],
373-
kzgProofs: [...proofs, ...proofs, ...proofs],
397+
blobVersionedHashes: txBlobVersionedHashes,
398+
blobs: txBlobs,
399+
kzgCommitments: txCommitments,
400+
kzgProofs: txProofs,
374401
maxFeePerBlobGas: 100000000n,
375402
gasLimit: 0xffffffn,
376403
maxFeePerGas: 1000000000n,
@@ -381,6 +408,30 @@ describe('[PendingBlock]', async () => {
381408
{ common },
382409
).sign(A.privateKey)
383410
await txPool.add(txA01)
411+
412+
// accumulate for verification
413+
blobs = [...blobs, ...txBlobs]
414+
proofs = [...proofs, ...txProofs]
415+
versionedHashes = [...versionedHashes, ...txBlobVersionedHashes]
416+
}
417+
418+
assert.equal(
419+
txPool.blobsAndProofsByHash.size,
420+
allowedLength,
421+
'cache should be prune and stay at same size',
422+
)
423+
// check if blobs and proofs are added in txpool by versioned hashes
424+
for (let i = 0; i < versionedHashes.length; i++) {
425+
const versionedHash = versionedHashes[i]
426+
const blob = blobs[i]
427+
const proof = proofs[i]
428+
429+
const blobAndProof = txPool.blobsAndProofsByHash.get(versionedHash) ?? {
430+
blob: '0x0',
431+
proof: '0x0',
432+
}
433+
assert.equal(blob, blobAndProof.blob, 'blob should match')
434+
assert.equal(proof, blobAndProof.proof, 'proof should match')
384435
}
385436

386437
// Add one other normal tx for nonce 3 which should also be not included in the build

packages/client/test/rpc/engine/getPayloadV3.spec.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,16 @@ describe(method, () => {
110110
).sign(pkey)
111111

112112
await service.txPool.add(tx, true)
113+
114+
// check the blob and proof is available via getBlobsV1
115+
res = await rpc.request('engine_getBlobsV1', [txVersionedHashes])
116+
const blobsAndProofs = res.result
117+
for (let i = 0; i < txVersionedHashes.length; i++) {
118+
const { blob, proof } = blobsAndProofs[i]
119+
assert.equal(blob, txBlobs[i])
120+
assert.equal(proof, txProofs[i])
121+
}
122+
113123
res = await rpc.request('engine_getPayloadV3', [payloadId])
114124

115125
const { executionPayload, blobsBundle } = res.result

packages/client/test/rpc/eth/sendRawTransaction.spec.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { BlockHeader } from '@ethereumjs/block'
1+
import { BlockHeader, paramsBlock } from '@ethereumjs/block'
22
import { Common, Hardfork, Mainnet, createCommonFromGethGenesis } from '@ethereumjs/common'
33
import { MerkleStateManager } from '@ethereumjs/statemanager'
44
import { createBlob4844Tx, createFeeMarket1559TxFromRLP, createLegacyTx } from '@ethereumjs/tx'
@@ -228,7 +228,9 @@ describe(method, () => {
228228
chain: 'customChain',
229229
hardfork: Hardfork.Cancun,
230230
customCrypto: { kzg },
231+
params: paramsBlock,
231232
})
233+
232234
common.setHardfork(Hardfork.Cancun)
233235
const { rpc, client } = await baseSetup({
234236
commonChain: common,

packages/client/test/rpc/helpers.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { createBlockHeader } from '@ethereumjs/block'
1+
import { createBlockHeader, paramsBlock } from '@ethereumjs/block'
22
import { createBlockchain } from '@ethereumjs/blockchain'
33
import {
44
Common,
@@ -235,6 +235,7 @@ export async function setupChain(genesisFile: any, chainName = 'dev', clientOpts
235235
const common = createCommonFromGethGenesis(genesisFile, {
236236
chain: chainName,
237237
customCrypto: clientOpts.customCrypto,
238+
params: paramsBlock,
238239
})
239240
common.setHardforkBy({
240241
blockNumber: 0,

0 commit comments

Comments
 (0)