Skip to content

Commit e102cbe

Browse files
authored
Unify block proposer verification (#7675)
* verify proposer index/signature the same way across blocks/blobs/columns/quarantine * rename block clearance helpers to differentiate backfill vs light forward syncing and signature versus proposer checking (the latter includes checking the proposer index against the state) * avoid redundant parent lookup when re-queueing quarantine blocks * delay `Forked...` block init in quarantine until the block is actually being added
1 parent c33fda1 commit e102cbe

File tree

8 files changed

+143
-176
lines changed

8 files changed

+143
-176
lines changed

beacon_chain/consensus_object_pools/block_clearance.nim

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,39 @@ export results, signatures_batch, block_dag, blockchain_dag
2828
logScope:
2929
topics = "clearance"
3030

31+
proc verifyBlockProposer*(
32+
dag: ChainDAGRef,
33+
parent: BlockRef,
34+
slot: Slot,
35+
proposer_index: uint64,
36+
blockRoot: Eth2Digest,
37+
signature: ValidatorSig,
38+
): Result[void, tuple[msg: cstring, invalid: bool]] =
39+
## Verify block proposer and signature, making sure to check that the proposer
40+
## was indeed elected for the given slot and that the signature checks out.
41+
##
42+
## Because the signature only covers the block body (via its root), it's
43+
## possible that the block itself is valid while the signature is not: in
44+
## this case false is returned for "invalid".
45+
let proposer = dag.getProposer(parent, slot).valueOr:
46+
warn "cannot compute proposer for block", parent, slot, proposer_index, blockRoot
47+
return err(("verifyBlockProposer: cannot compute proposer", false)) # internal issue
48+
49+
# `getProposer` returns a trusted proposer index, while `proposer_index` may
50+
# be invalid -> convert the former to the latter
51+
if uint64(proposer) != proposer_index:
52+
return err(("verifyBlockProposer: unexpected proposer", true))
53+
54+
let
55+
proposerKey = dag.validatorKey(proposer).expect("valid after getProposer")
56+
fork = dag.forkAtEpoch(slot.epoch)
57+
if not verify_block_signature(
58+
fork, dag.genesis_validators_root, slot, blockRoot, proposerKey, signature
59+
):
60+
return err(("verifyBlockProposer: invalid signature", false))
61+
62+
ok()
63+
3164
proc addResolvedHeadBlock(
3265
dag: ChainDAGRef,
3366
state: var ForkedHashedBeaconState,
@@ -435,7 +468,7 @@ proc addBackfillBlock*(
435468

436469
ok()
437470

438-
proc verifyBlockProposer*(
471+
proc verifyBlockSignatures*(
439472
verifier: var BatchVerifier,
440473
fork: Fork,
441474
genesis_validators_root: Eth2Digest,
@@ -452,7 +485,7 @@ proc verifyBlockProposer*(
452485
else:
453486
ok()
454487

455-
proc addBackfillBlockData*(
488+
proc addLightForwardBlock*(
456489
dag: ChainDAGRef,
457490
consensusFork: static ConsensusFork,
458491
bdata: BlockData,

beacon_chain/consensus_object_pools/block_quarantine.nim

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -301,17 +301,17 @@ func pruneAfterFinalization*(
301301
proc addOrphan*(
302302
quarantine: var Quarantine,
303303
finalizedSlot: Slot,
304-
signedBlock: ForkedSignedBeaconBlock
304+
signedBlock: ForkySignedBeaconBlock
305305
): Result[void, cstring] =
306306
## Adds block to quarantine's `orphans` and `missing` lists.
307307

308-
if not isViable(finalizedSlot, getForkedBlockField(signedBlock, slot)):
308+
if not isViable(finalizedSlot, signedBlock.message.slot):
309309
quarantine.addUnviable(signedBlock.root) # will remove from missing
310310
return err("block unviable")
311311

312312
quarantine.cleanupOrphans(finalizedSlot)
313313

314-
let parent_root = getForkedBlockField(signedBlock, parent_root)
314+
let parent_root = signedBlock.message.parent_root
315315

316316
if parent_root in quarantine.unviable:
317317
quarantine.addUnviable(signedBlock.root)
@@ -335,7 +335,8 @@ proc addOrphan*(
335335
quarantine.orphans.del oldest_orphan_key
336336
quarantine.sidecarless.del oldest_orphan_key[0]
337337

338-
quarantine.orphans[(signedBlock.root, signedBlock.signature)] = signedBlock
338+
quarantine.orphans[(signedBlock.root, signedBlock.signature)] =
339+
ForkedSignedBeaconBlock.init(signedBlock)
339340
quarantine.orphansEvent.fire()
340341

341342
ok()

beacon_chain/consensus_object_pools/blockchain_dag.nim

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -999,6 +999,9 @@ proc applyBlock(
999999

10001000
ok()
10011001

1002+
proc genesis_validators_root*(dag: ChainDAGRef): Eth2Digest =
1003+
getStateField(dag.headState, genesis_validators_root)
1004+
10021005
proc init*(T: type ChainDAGRef, cfg: RuntimeConfig, db: BeaconChainDB,
10031006
validatorMonitor: ref ValidatorMonitor, updateFlags: UpdateFlags,
10041007
eraPath = ".",
@@ -1183,8 +1186,7 @@ proc init*(T: type ChainDAGRef, cfg: RuntimeConfig, db: BeaconChainDB,
11831186
quit 1
11841187

11851188
# Need to load state to find genesis validators root, before loading era db
1186-
dag.era = EraDB.new(
1187-
cfg, eraPath, getStateField(dag.headState, genesis_validators_root))
1189+
dag.era = EraDB.new(cfg, eraPath, dag.genesis_validators_root)
11881190

11891191
# We used an interim finalizedHead while loading the head state above - now
11901192
# that we have loaded the dag up to the finalized slot, we can also set
@@ -1255,8 +1257,7 @@ proc init*(T: type ChainDAGRef, cfg: RuntimeConfig, db: BeaconChainDB,
12551257
slot: dag.tail.slot + 1,
12561258
parent_root: dag.tail.root)
12571259

1258-
dag.forkDigests = newClone ForkDigests.init(
1259-
cfg, getStateField(dag.headState, genesis_validators_root))
1260+
dag.forkDigests = newClone ForkDigests.init(cfg, dag.genesis_validators_root)
12601261

12611262
withState(dag.headState):
12621263
dag.validatorMonitor[].registerState(forkyState.data)
@@ -1344,9 +1345,6 @@ proc init*(T: type ChainDAGRef, cfg: RuntimeConfig, db: BeaconChainDB,
13441345

13451346
dag
13461347

1347-
template genesis_validators_root*(dag: ChainDAGRef): Eth2Digest =
1348-
getStateField(dag.headState, genesis_validators_root)
1349-
13501348
proc genesisBlockRoot*(dag: ChainDAGRef): Eth2Digest =
13511349
dag.db.getGenesisBlock().expect("DB must be initialized with genesis block")
13521350

beacon_chain/consensus_object_pools/blockchain_list.nim

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -167,7 +167,7 @@ proc checkBlobs(signedBlock: ForkedSignedBeaconBlock,
167167
return err(VerifierError.Invalid)
168168
ok()
169169

170-
proc addBackfillBlockData*(
170+
proc addLightForwardBlock*(
171171
clist: ChainListRef, signedBlock: ForkedSignedBeaconBlock,
172172
blobsOpt: Opt[BlobSidecars]): Result[void, VerifierError] =
173173
doAssert(not(isNil(clist)))
@@ -243,5 +243,5 @@ proc untrustedBackfillVerifier*(
243243
): Future[Result[void, VerifierError]] {.
244244
async: (raises: [CancelledError], raw: true).} =
245245
let retFuture = newFuture[Result[void, VerifierError]]()
246-
retFuture.complete(clist.addBackfillBlockData(signedBlock, blobs))
246+
retFuture.complete(clist.addLightForwardBlock(signedBlock, blobs))
247247
retFuture

beacon_chain/gossip_processing/block_processor.nim

Lines changed: 37 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,7 @@ proc dumpInvalidBlock*(
157157
proc dumpBlock(
158158
self: BlockProcessor,
159159
signedBlock: ForkySignedBeaconBlock,
160-
res: Result[void, VerifierError]) =
160+
res: Result[BlockRef, VerifierError]) =
161161
if self.dumpEnabled and res.isErr:
162162
case res.error
163163
of VerifierError.Invalid:
@@ -168,7 +168,7 @@ proc dumpBlock(
168168
discard
169169

170170
from ../consensus_object_pools/block_clearance import
171-
addBackfillBlock, addHeadBlockWithParent, checkHeadBlock
171+
addBackfillBlock, addHeadBlockWithParent, checkHeadBlock, verifyBlockProposer
172172

173173
proc verifySidecars(
174174
signedBlock: ForkySignedBeaconBlock,
@@ -179,10 +179,7 @@ proc verifySidecars(
179179
when consensusFork == ConsensusFork.Gloas:
180180
# For Gloas, we still need to store the columns if they're provided
181181
# but skip validation since we don't have kzg_commitments in the block
182-
if sidecarsOpt.isSome:
183-
debugGloasComment "potentially validate against payload envelope"
184-
let columns = sidecarsOpt.get()
185-
discard
182+
debugGloasComment "potentially validate against payload envelope"
186183
elif consensusFork == ConsensusFork.Fulu:
187184
if sidecarsOpt.isSome:
188185
let columns = sidecarsOpt.get()
@@ -332,29 +329,6 @@ proc getExecutionValidity(
332329

333330
Opt.some(optimisticStatus)
334331

335-
proc checkBlobOrColumnlessSignature(
336-
self: BlockProcessor,
337-
signed_beacon_block: deneb.SignedBeaconBlock | electra.SignedBeaconBlock |
338-
fulu.SignedBeaconBlock):
339-
Result[void, cstring] =
340-
let dag = self.consensusManager.dag
341-
let parent = dag.getBlockRef(signed_beacon_block.message.parent_root).valueOr:
342-
return err("checkBlobOrColumnlessSignature called with orphan block")
343-
let proposer = getProposer(
344-
dag, parent, signed_beacon_block.message.slot).valueOr:
345-
return err("checkBlobOrColumnlessSignature: Cannot compute proposer")
346-
if distinctBase(proposer) != signed_beacon_block.message.proposer_index:
347-
return err("checkBlobOrColumnlessSignature: Incorrect proposer")
348-
if not verify_block_signature(
349-
dag.forkAtEpoch(signed_beacon_block.message.slot.epoch),
350-
getStateField(dag.headState, genesis_validators_root),
351-
signed_beacon_block.message.slot,
352-
signed_beacon_block.root,
353-
dag.validatorKey(proposer).get(),
354-
signed_beacon_block.signature):
355-
return err("checkBlobOrColumnlessSignature: Invalid proposer signature")
356-
ok()
357-
358332
proc addBlock*(
359333
self: ref BlockProcessor,
360334
src: MsgSource,
@@ -387,55 +361,46 @@ proc enqueueBlock*(
387361
# `addBlock` should be used where managing backpressure is appropriate.
388362
discard self.addBlock(src, blck, sidecarsOpt, maybeFinalized, validationDur)
389363

390-
proc enqueueQuarantine(self: ref BlockProcessor, root: Eth2Digest) =
364+
proc enqueueQuarantine(self: ref BlockProcessor, parent: BlockRef) =
391365
## Enqueue blocks whose parent is `root` - ie when `root` has been added to
392366
## the blockchain dag, its direct descendants are now candidates for
393367
## processing
394-
for quarantined in self.consensusManager.quarantine[].pop(root):
368+
let
369+
dag = self.consensusManager[].dag
370+
quarantine = self.consensusManager[].quarantine
371+
for quarantined in quarantine[].pop(parent.root):
395372
# Process the blocks that had the newly accepted block as parent
396-
debug "Block from quarantine",
397-
blockRoot = shortLog(root), quarantined = shortLog(quarantined.root)
373+
debug "Block from quarantine", parent, quarantined = shortLog(quarantined.root)
398374

399375
withBlck(quarantined):
400376
when consensusFork == ConsensusFork.Gloas:
401377
debugGloasComment ""
402-
self.enqueueBlock(
403-
MsgSource.gossip, forkyBlck, Opt.none(gloas.DataColumnSidecars))
378+
const sidecarsOpt = noSidecars
404379
elif consensusFork == ConsensusFork.Fulu:
405-
if len(forkyBlck.message.body.blob_kzg_commitments) == 0:
406-
self.enqueueBlock(
407-
MsgSource.gossip, forkyBlck, Opt.some(fulu.DataColumnSidecars @[])
408-
)
409-
else:
410-
if (let res = checkBlobOrColumnlessSignature(self[], forkyBlck); res.isErr):
411-
warn "Failed to verify signature of unorphaned blobless block",
412-
blck = shortLog(forkyBlck), error = res.error()
413-
continue
414-
let cres = self.dataColumnQuarantine[].popSidecars(forkyBlck.root, forkyBlck)
415-
if cres.isSome:
416-
self.enqueueBlock(MsgSource.gossip, forkyBlck, cres)
417-
else:
418-
discard self.consensusManager.quarantine[].addSidecarless(
419-
self.consensusManager[].dag.finalizedHead.slot, forkyBlck
420-
)
380+
let sidecarsOpt =
381+
self.dataColumnQuarantine[].popSidecars(forkyBlck.root, forkyBlck)
421382
elif consensusFork in ConsensusFork.Deneb .. ConsensusFork.Electra:
422-
if len(forkyBlck.message.body.blob_kzg_commitments) == 0:
423-
self.enqueueBlock(MsgSource.gossip, forkyBlck, Opt.some(BlobSidecars @[]))
424-
else:
425-
if (let res = checkBlobOrColumnlessSignature(self[], forkyBlck); res.isErr):
426-
warn "Failed to verify signature of unorphaned columnless block",
427-
blck = shortLog(forkyBlck), error = res.error()
428-
continue
429-
let bres = self.blobQuarantine[].popSidecars(forkyBlck.root, forkyBlck)
430-
if bres.isSome():
431-
self.enqueueBlock(MsgSource.gossip, forkyBlck, bres)
432-
else:
433-
self.consensusManager.quarantine[].addSidecarless(forkyBlck)
383+
let sidecarsOpt = self.blobQuarantine[].popSidecars(forkyBlck.root, forkyBlck)
434384
elif consensusFork in ConsensusFork.Phase0 .. ConsensusFork.Capella:
435-
self.enqueueBlock(MsgSource.gossip, forkyBlck, noSidecars)
385+
const sidecarsOpt = noSidecars
436386
else:
437387
{.error: "Unknown consensus fork " & $consensusFork.}
438388

389+
when consensusFork in ConsensusFork.Deneb .. ConsensusFork.Fulu:
390+
if not sidecarsOpt.isSome():
391+
dag.verifyBlockProposer(
392+
parent, forkyBlck.message.slot, forkyBlck.message.proposer_index,
393+
forkyBlck.root, forkyBlck.signature,
394+
).isOkOr:
395+
warn "Failed to verify signature of unorphaned blobless block",
396+
blck = shortLog(forkyBlck), error = error.msg
397+
continue
398+
399+
discard quarantine[].addSidecarless(dag.finalizedHead.slot, forkyBlck)
400+
continue
401+
402+
self.enqueueBlock(MsgSource.gossip, forkyBlck, sidecarsOpt)
403+
439404
proc onBlockAdded*(
440405
dag: ChainDAGRef,
441406
consensusFork: static ConsensusFork,
@@ -571,7 +536,7 @@ proc storeBlock(
571536
maybeFinalized: bool,
572537
queueTick: Moment,
573538
validationDur: Duration,
574-
): Future[Result[void, VerifierError]] {.async: (raises: [CancelledError]).} =
539+
): Future[Result[BlockRef, VerifierError]] {.async: (raises: [CancelledError]).} =
575540
## storeBlock is the main entry point for unvalidated blocks - all untrusted
576541
## blocks, regardless of origin, pass through here. When storing a block,
577542
## we will add it to the dag and pass it to all block consumers that need
@@ -586,9 +551,9 @@ proc storeBlock(
586551
deadlineTime =
587552
block:
588553
let slotTime =
589-
(wallSlot + 1).start_beacon_time(dag.timeParams) - 1.seconds
554+
(wallSlot + 1).start_beacon_time(dag.timeParams) - chronos.seconds(1)
590555
if slotTime <= wallTime:
591-
0.seconds
556+
chronos.seconds(0)
592557
else:
593558
chronos.nanoseconds((slotTime - wallTime).nanoseconds)
594559
deadline = sleepAsync(deadlineTime)
@@ -725,7 +690,7 @@ proc storeBlock(
725690
blck = shortLog(blck),
726691
validationDur, queueDur, newPayloadDur, addHeadBlockDur, updateHeadDur
727692

728-
ok()
693+
ok(blck)
729694

730695
proc addBlock*(
731696
self: ref BlockProcessor,
@@ -776,7 +741,7 @@ proc addBlock*(
776741
# taking up all CPU - we don't want to _completely_ stop processing blocks
777742
# in this case - doing so also allows us to benefit from more batching /
778743
# larger network reads when under load.
779-
idleTimeout = 10.milliseconds
744+
idleTimeout = chronos.milliseconds(10)
780745

781746
discard await idleAsync().withTimeout(idleTimeout)
782747

@@ -799,15 +764,13 @@ proc addBlock*(
799764

800765
if res.isOk():
801766
# Once a block is successfully stored, enqueue the direct descendants
802-
self.enqueueQuarantine(blockRoot)
767+
self.enqueueQuarantine(res[])
803768
else:
804769
case res.error()
805770
of VerifierError.MissingParent:
806771
let finalizedSlot = self.consensusManager.dag.finalizedHead.slot
807772
if (
808-
let r = self.consensusManager.quarantine[].addOrphan(
809-
finalizedSlot, ForkedSignedBeaconBlock.init(blck)
810-
)
773+
let r = self.consensusManager.quarantine[].addOrphan(finalizedSlot, blck)
811774
r.isErr()
812775
):
813776
debug "Could not add orphan",
@@ -838,4 +801,4 @@ proc addBlock*(
838801
else:
839802
discard
840803

841-
res
804+
res.mapConvert(void)

0 commit comments

Comments
 (0)