Add blob sidecar gossip validation (#4785)

* Add blob gossip validation

* lint

* Add test for getBlobSidecarTopic

* Fix closure variable capture issue

* Update beacon_chain/nimbus_beacon_node.nim

Co-authored-by: tersec <tersec@users.noreply.github.com>

---------

Co-authored-by: tersec <tersec@users.noreply.github.com>
This commit is contained in:
henridf 2023-04-04 17:12:34 +02:00 committed by GitHub
parent 5167a373ab
commit 04302081b4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 170 additions and 0 deletions

View File

@ -41,6 +41,10 @@ declareCounter beacon_blocks_received,
"Number of valid blocks processed by this node"
declareCounter beacon_blocks_dropped,
"Number of invalid blocks dropped by this node", labels = ["reason"]
declareCounter blob_sidecars_received,
"Number of valid blobs processed by this node"
declareCounter blob_sidecars_dropped,
"Number of invalid blobs dropped by this node", labels = ["reason"]
declareCounter beacon_attester_slashings_received,
"Number of valid attester slashings processed by this node"
declareCounter beacon_attester_slashings_dropped,
@ -85,6 +89,9 @@ declareHistogram beacon_aggregate_delay,
declareHistogram beacon_block_delay,
"Time(s) between slot start and beacon block reception", buckets = delayBuckets
declareHistogram blob_sidecar_delay,
"Time(s) between slot start and blob sidecar reception", buckets = delayBuckets
type
DoppelgangerProtection = object
broadcastStartEpoch*: Epoch ##\
@ -246,6 +253,41 @@ proc processSignedBeaconBlock*(
v
proc processSignedBlobSidecar*(
self: var Eth2Processor, src: MsgSource,
signedBlobSidecar: deneb.SignedBlobSidecar, idx: BlobIndex): ValidationRes =
let
wallTime = self.getCurrentBeaconTime()
(afterGenesis, wallSlot) = wallTime.toSlot()
logScope:
blob = shortLog(signedBlobSidecar.message)
signature = shortLog(signedBlobSidecar.signature)
wallSlot
# Potential under/overflows are fine; would just create odd metrics and logs
let delay = wallTime - signedBlobSidecar.message.slot.start_beacon_time
debug "Blob received", delay
let v =
self.dag.validateBlobSidecar(self.quarantine, signedBlobSidecar, wallTime, idx)
if v.isOk():
trace "Blob validated"
# TODO
# hand blob off to blob quarantine
blob_sidecars_received.inc()
blob_sidecar_delay.observe(delay.toFloatSeconds())
else:
debug "Dropping blob", error = v.error()
blob_sidecars_dropped.inc(1, [$v.error[0]])
v
proc setupDoppelgangerDetection*(self: var Eth2Processor, slot: Slot) =
# When another client's already running, this is very likely to detect
# potential duplicate validators, which can trigger slashing.

View File

@ -220,6 +220,79 @@ template validateBeaconBlockBellatrix(
# `ACCEPTED` or `SYNCING` from the EL to get this far.
# https://github.com/ethereum/consensus-specs/blob/v1.3.0-rc.5/specs/deneb/p2p-interface.md#blob_sidecar_index
proc validateBlobSidecar*(
dag: ChainDAGRef, quarantine: ref Quarantine,
sbs: SignedBlobSidecar,
wallTime: BeaconTime, idx: BlobIndex): Result[void, ValidationError] =
# [REJECT] The sidecar is for the correct topic --
# i.e. sidecar.index matches the topic {index}.
if sbs.message.index != idx:
return errReject("SignedBlobSidecar: mismatched gossip topic index")
if dag.getBlockRef(sbs.message.block_root).isSome():
return errIgnore("SignedBlobSidecar: already have block")
# [IGNORE] The sidecar is not from a future slot (with a
# MAXIMUM_GOSSIP_CLOCK_DISPARITY allowance) -- i.e. validate that
# sidecar.slot <= current_slot (a client MAY queue future sidecars
# for processing at the appropriate slot).
if not (sbs.message.slot <=
(wallTime + MAXIMUM_GOSSIP_CLOCK_DISPARITY).slotOrZero):
return errIgnore("SignedBlobSidecar: slot too high")
# [IGNORE] The block is from a slot greater than the latest
# finalized slot -- i.e. validate that
# signed_beacon_block.message.slot >
# compute_start_slot_at_epoch(state.finalized_checkpoint.epoch)
if not (sbs.message.slot > dag.finalizedHead.slot):
return errIgnore("SignedBlobSidecar: slot already finalized")
# [REJECT] The sidecar's block's parent (defined by sidecar.block_parent_root)
# passes validation.
let parent = dag.getBlockRef(sbs.message.block_parent_root).valueOr:
return errReject("SignedBlobSidecar: parent not validated")
# [REJECT] The sidecar is from a higher slot than the sidecar's
# block's parent (defined by sidecar.block_parent_root).
if sbs.message.slot <= parent.bid.slot:
return errReject("SignedBlobSidecar: slot lower than parents'")
# [REJECT] The sidecar is proposed by the expected proposer_index
# for the block's slot in the context of the current shuffling
# (defined by block_parent_root/slot). If the proposer_index
# cannot immediately be verified against the expected shuffling,
# the sidecar MAY be queued for later processing while proposers
# for the block's branch are calculated -- in such a case do not
# REJECT, instead IGNORE this message.
let
proposer = getProposer(
dag, parent, sbs.message.slot).valueOr:
warn "cannot compute proposer for blob"
return errIgnore("SignedBlobSidecar: Cannot compute proposer")
if uint64(proposer) != sbs.message.proposer_index:
return errReject("SignedBlobSidecar: Unexpected proposer")
# [REJECT] The proposer signature, signed_blob_sidecar.signature,
# is valid as verified by verify_sidecar_signature.
if not verify_blob_signature(
dag.forkAtEpoch(sbs.message.slot.epoch),
getStateField(dag.headState, genesis_validators_root),
sbs.message.slot,
sbs.message,
dag.validatorKey(proposer).get(),
sbs.signature):
return errReject("SignedBlobSidecar: invalid blob signature")
# [IGNORE] The sidecar is the only sidecar with valid signature received for the tuple (sidecar.block_root, sidecar.index).
# TODO
# check that there isn't a conflicting sidecar in blob_quarantine
ok()
# https://github.com/ethereum/consensus-specs/blob/v1.1.9/specs/phase0/p2p-interface.md#beacon_block
# https://github.com/ethereum/consensus-specs/blob/v1.3.0-alpha.0/specs/bellatrix/p2p-interface.md#beacon_block
proc validateBeaconBlock*(

View File

@ -1478,6 +1478,15 @@ proc installMessageValidators(node: BeaconNode) =
toValidationResult(node.processor[].processSignedBeaconBlock(
MsgSource.gossip, signedBlock)))
for i in 0 ..< MAX_BLOBS_PER_BLOCK:
closureScope:
let idx = i
node.network.addValidator(
getBlobSidecarTopic(forkDigests.deneb, idx),
proc (signedBlobSidecar: deneb.SignedBlobSidecar): ValidationResult =
toValidationResult(node.processor[].processSignedBlobSidecar(
MsgSource.gossip, signedBlobSidecar, idx)))
template installSyncCommitteeeValidators(digest: auto) =
for subcommitteeIdx in SyncSubcommitteeIndex:
closureScope:

View File

@ -39,6 +39,9 @@ const
# https://github.com/ethereum/consensus-specs/blob/v1.3.0-rc.5/specs/capella/beacon-chain.md#domain-types
DOMAIN_BLS_TO_EXECUTION_CHANGE* = DomainType([byte 0x0a, 0x00, 0x00, 0x00])
# https://github.com/ethereum/consensus-specs/blob/v1.3.0-rc.5/specs/deneb/beacon-chain.md
DOMAIN_BLOB_SIDECAR* = DomainType([byte 0x0b, 0x00, 0x00, 0x00])
# https://github.com/ethereum/consensus-specs/blob/v1.3.0-rc.5/specs/bellatrix/beacon-chain.md#transition-settings
TERMINAL_BLOCK_HASH_ACTIVATION_EPOCH* = FAR_FUTURE_EPOCH

View File

@ -525,6 +525,22 @@ func shortLog*(v: SomeBeaconBlock): auto =
fee_recipient: to0xHex(v.body.execution_payload.fee_recipient.data),
)
func shortLog*(v: BlobSidecar): auto =
(
block_root: shortLog(v.block_root),
index: v.index,
slot: shortLog(v.slot),
block_parent_root: shortLog(v.block_parent_root),
proposer_index: v.proposer_index,
bloblen: v.blob.len(),
)
func shortLog*(v: SignedBlobSidecar): auto =
(
blob: shortLog(v.message),
signature: shortLog(v.signature)
)
func shortLog*(v: SomeSignedBeaconBlock): auto =
(
blck: shortLog(v.message),

View File

@ -97,6 +97,11 @@ func getSyncCommitteeContributionAndProofTopic*(forkDigest: ForkDigest): string
## For subscribing and unsubscribing to/from a subnet.
eth2Prefix(forkDigest) & "sync_committee_contribution_and_proof/ssz_snappy"
# https://github.com/ethereum/consensus-specs/blob/v1.3.0-rc.5/specs/deneb/p2p-interface.md#blob_sidecar_index
func getBlobSidecarTopic*(forkDigest: ForkDigest,
index: BlobIndex): string =
eth2Prefix(forkDigest) & "blob_sidecar_" & $index & "/ssz_snappy"
# https://github.com/ethereum/consensus-specs/blob/v1.3.0-rc.5/specs/altair/light-client/p2p-interface.md#light_client_finality_update
func getLightClientFinalityUpdateTopic*(forkDigest: ForkDigest): string =
## For broadcasting or obtaining the latest `LightClientFinalityUpdate`.

View File

@ -98,6 +98,15 @@ func compute_block_signing_root*(
fork, DOMAIN_BEACON_PROPOSER, epoch, genesis_validators_root)
compute_signing_root(blck, domain)
func compute_blob_signing_root*(
fork: Fork, genesis_validators_root: Eth2Digest, slot: Slot,
blob: BlobSidecar): Eth2Digest =
let
epoch = epoch(slot)
domain = get_domain(fork, DOMAIN_BLOB_SIDECAR, epoch,
genesis_validators_root)
compute_signing_root(blob, domain)
# https://github.com/ethereum/consensus-specs/blob/v1.3.0-rc.5/specs/phase0/validator.md#signature
func get_block_signature*(
fork: Fork, genesis_validators_root: Eth2Digest, slot: Slot,
@ -118,6 +127,17 @@ proc verify_block_signature*(
blsVerify(pubkey, signing_root.data, signature)
proc verify_blob_signature*(
fork: Fork, genesis_validators_root: Eth2Digest, slot: Slot,
blobSidecar: BlobSidecar,
pubkey: ValidatorPubKey | CookedPubKey, signature: SomeSig): bool =
withTrust(signature):
let
signing_root = compute_blob_signing_root(
fork, genesis_validators_root, slot, blobSidecar)
blsVerify(pubkey, signing_root.data, signature)
func compute_aggregate_and_proof_signing_root*(
fork: Fork, genesis_validators_root: Eth2Digest,
aggregate_and_proof: AggregateAndProof): Eth2Digest =

View File

@ -76,6 +76,8 @@ suite "Honest validator":
"/eth2/00000000/sync_committee_1/ssz_snappy"
getSyncCommitteeTopic(forkDigest, SyncSubcommitteeIndex(3)) ==
"/eth2/00000000/sync_committee_3/ssz_snappy"
getBlobSidecarTopic(forkDigest, BlobIndex(1)) ==
"/eth2/00000000/blob_sidecar_1/ssz_snappy"
test "is_aggregator":
check: