diff --git a/beacon_chain/gossip_processing/eth2_processor.nim b/beacon_chain/gossip_processing/eth2_processor.nim index 5cba760b6..19f756551 100644 --- a/beacon_chain/gossip_processing/eth2_processor.nim +++ b/beacon_chain/gossip_processing/eth2_processor.nim @@ -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. diff --git a/beacon_chain/gossip_processing/gossip_validation.nim b/beacon_chain/gossip_processing/gossip_validation.nim index 7f488cfed..fa26ba3c1 100644 --- a/beacon_chain/gossip_processing/gossip_validation.nim +++ b/beacon_chain/gossip_processing/gossip_validation.nim @@ -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*( diff --git a/beacon_chain/nimbus_beacon_node.nim b/beacon_chain/nimbus_beacon_node.nim index 4ca732ae3..8b481a68f 100644 --- a/beacon_chain/nimbus_beacon_node.nim +++ b/beacon_chain/nimbus_beacon_node.nim @@ -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: diff --git a/beacon_chain/spec/datatypes/constants.nim b/beacon_chain/spec/datatypes/constants.nim index 6b25b3586..22f9f7756 100644 --- a/beacon_chain/spec/datatypes/constants.nim +++ b/beacon_chain/spec/datatypes/constants.nim @@ -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 diff --git a/beacon_chain/spec/datatypes/deneb.nim b/beacon_chain/spec/datatypes/deneb.nim index 11d5190a6..85cab4d0b 100644 --- a/beacon_chain/spec/datatypes/deneb.nim +++ b/beacon_chain/spec/datatypes/deneb.nim @@ -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), diff --git a/beacon_chain/spec/network.nim b/beacon_chain/spec/network.nim index ca32adaf0..998a4fd56 100644 --- a/beacon_chain/spec/network.nim +++ b/beacon_chain/spec/network.nim @@ -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`. diff --git a/beacon_chain/spec/signatures.nim b/beacon_chain/spec/signatures.nim index a1f1490fd..49769e36e 100644 --- a/beacon_chain/spec/signatures.nim +++ b/beacon_chain/spec/signatures.nim @@ -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 = diff --git a/tests/test_honest_validator.nim b/tests/test_honest_validator.nim index c4467d46f..79ae2abd5 100644 --- a/tests/test_honest_validator.nim +++ b/tests/test_honest_validator.nim @@ -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: