Sidecar inclusion proof (#4900)

* Refactor BlobSidecar to new type

* Fix some compile errors

* Gossip verification compiles

* Fix http api types take 1

* Fix another round of compile errors

* Beacon node crate compiles

* EF tests compile

* Remove all blob signing from VC

* fmt

* Tests compile

* Fix some tests

* Fix more http tests

* get compiling

* Fix gossip conditions and tests

* Add basic proof generation and verification

* remove unnecessary ssz decode

* add back build_sidecar

* remove default at fork for blobs

* fix beacon chain tests

* get relase tests compiling

* fix lints

* fix existing spec tests

* add new ef tests

* fix gossip duplicate rule

* lints

* add back sidecar signature check in gossip

* add finalized descendant check to blob sidecar gossip

* fix error conversion

* fix release tests

* sidecar inclusion self review cleanup

* Add proof verification and computation metrics

* Remove accidentally committed file

* Unify some block and blob errors; add slashing conditions for sidecars

* Address review comment

* Clean up re-org tests (#4957)

* Address more review comments

* Add Comments & Eliminate Unnecessary Clones

* update names

* Update beacon_node/beacon_chain/src/metrics.rs

Co-authored-by: Jimmy Chen <jchen.tc@gmail.com>

* Update beacon_node/network/src/network_beacon_processor/tests.rs

Co-authored-by: Jimmy Chen <jchen.tc@gmail.com>

* pr feedback

* fix test compile

* Sidecar Inclusion proof small refactor and updates (#4967)

* Update some comments, variables and small cosmetic fixes.

* Couple blobs and proofs into a tuple in `PayloadAndBlobs` for simplicity and safety.

* Update function comment.

* Update testing/ef_tests/src/cases/merkle_proof_validity.rs

Co-authored-by: Jimmy Chen <jchen.tc@gmail.com>

* Rename the block and blob wrapper types used in the beacon API interfaces.

* make sure gossip invalid blobs are passed to the slasher (#4970)

* Add blob headers to slasher before adding to DA checker

* Replace Vec with HashSet in BlockQueue

* fmt

* Rename gindex -> index

* Simplify gossip condition

---------

Co-authored-by: realbigsean <seananderson33@gmail.com>
Co-authored-by: realbigsean <sean@sigmaprime.io>
Co-authored-by: Michael Sproul <michael@sigmaprime.io>
Co-authored-by: Mark Mackey <mark@sigmaprime.io>
Co-authored-by: Jimmy Chen <jchen.tc@gmail.com>
This commit is contained in:
Pawan Dhananjay
2023-12-05 08:19:59 -08:00
committed by GitHub
parent ec8edfb89a
commit 31044402ee
74 changed files with 1950 additions and 2270 deletions

View File

@@ -1,6 +1,11 @@
use super::*;
use crate::decode::{ssz_decode_file, ssz_decode_file_with, ssz_decode_state, yaml_decode_file};
use ::fork_choice::PayloadVerificationStatus;
use ::fork_choice::{PayloadVerificationStatus, ProposerHeadError};
use beacon_chain::beacon_proposer_cache::compute_proposer_duties_from_head;
use beacon_chain::blob_verification::GossipBlobError;
use beacon_chain::chain_config::{
DisallowedReOrgOffsets, DEFAULT_RE_ORG_MAX_EPOCHS_SINCE_FINALIZATION, DEFAULT_RE_ORG_THRESHOLD,
};
use beacon_chain::slot_clock::SlotClock;
use beacon_chain::{
attestation_verification::{
@@ -20,7 +25,7 @@ use std::time::Duration;
use types::{
Attestation, AttesterSlashing, BeaconBlock, BeaconState, BlobSidecar, BlobsList, Checkpoint,
EthSpec, ExecutionBlockHash, ForkName, Hash256, IndexedAttestation, KzgProof,
ProgressiveBalancesMode, Signature, SignedBeaconBlock, SignedBlobSidecar, Slot, Uint256,
ProgressiveBalancesMode, ProposerPreparationData, SignedBeaconBlock, Slot, Uint256,
};
#[derive(Default, Debug, PartialEq, Clone, Deserialize, Decode)]
@@ -38,6 +43,13 @@ pub struct Head {
root: Hash256,
}
#[derive(Debug, Clone, Copy, PartialEq, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ShouldOverrideFcu {
validator_is_connected: bool,
result: bool,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Checks {
@@ -50,6 +62,8 @@ pub struct Checks {
u_justified_checkpoint: Option<Checkpoint>,
u_finalized_checkpoint: Option<Checkpoint>,
proposer_boost_root: Option<Hash256>,
get_proposer_head: Option<Hash256>,
should_override_forkchoice_update: Option<ShouldOverrideFcu>,
}
#[derive(Debug, Clone, Deserialize)]
@@ -256,6 +270,8 @@ impl<E: EthSpec> Case for ForkChoiceTest<E> {
u_justified_checkpoint,
u_finalized_checkpoint,
proposer_boost_root,
get_proposer_head,
should_override_forkchoice_update: should_override_fcu,
} = checks.as_ref();
if let Some(expected_head) = head {
@@ -294,6 +310,14 @@ impl<E: EthSpec> Case for ForkChoiceTest<E> {
if let Some(expected_proposer_boost_root) = proposer_boost_root {
tester.check_expected_proposer_boost_root(*expected_proposer_boost_root)?;
}
if let Some(should_override_fcu) = should_override_fcu {
tester.check_should_override_fcu(*should_override_fcu)?;
}
if let Some(expected_proposer_head) = get_proposer_head {
tester.check_expected_proposer_head(*expected_proposer_head)?;
}
}
}
}
@@ -325,6 +349,7 @@ impl<E: EthSpec> Tester<E> {
}
let harness = BeaconChainHarness::<EphemeralHarnessType<E>>::builder(E::default())
.logger(logging::test_logger())
.spec(spec.clone())
.keypairs(vec![])
.chain_config(ChainConfig {
@@ -413,6 +438,8 @@ impl<E: EthSpec> Tester<E> {
) -> Result<(), Error> {
let block_root = block.canonical_root();
let mut blob_success = true;
// Convert blobs and kzg_proofs into sidecars, then plumb them into the availability tracker
if let Some(blobs) = blobs.clone() {
let proofs = kzg_proofs.unwrap();
@@ -432,25 +459,32 @@ impl<E: EthSpec> Tester<E> {
.zip(commitments.into_iter())
.enumerate()
{
let signed_sidecar = SignedBlobSidecar {
message: Arc::new(BlobSidecar {
block_root,
index: i as u64,
slot: block.slot(),
block_parent_root: block.parent_root(),
proposer_index: block.message().proposer_index(),
blob,
kzg_commitment,
kzg_proof,
}),
signature: Signature::empty(),
_phantom: Default::default(),
};
let result = self.block_on_dangerous(
self.harness
.chain
.process_gossip_blob(GossipVerifiedBlob::__assumed_valid(signed_sidecar)),
)?;
let blob_sidecar = Arc::new(BlobSidecar {
index: i as u64,
blob,
kzg_commitment,
kzg_proof,
signed_block_header: block.signed_block_header(),
kzg_commitment_inclusion_proof: block
.message()
.body()
.kzg_commitment_merkle_proof(i)
.unwrap(),
});
let chain = self.harness.chain.clone();
let blob =
match GossipVerifiedBlob::new(blob_sidecar.clone(), blob_sidecar.index, &chain)
{
Ok(gossip_verified_blob) => gossip_verified_blob,
Err(GossipBlobError::KzgError(_)) => {
blob_success = false;
GossipVerifiedBlob::__assumed_valid(blob_sidecar)
}
Err(_) => GossipVerifiedBlob::__assumed_valid(blob_sidecar),
};
let result =
self.block_on_dangerous(self.harness.chain.process_gossip_blob(blob))?;
if valid {
assert!(result.is_ok());
}
@@ -466,7 +500,7 @@ impl<E: EthSpec> Tester<E> {
|| Ok(()),
))?
.map(|avail: AvailabilityProcessingStatus| avail.try_into());
let success = result.as_ref().map_or(false, |inner| inner.is_ok());
let success = blob_success && result.as_ref().map_or(false, |inner| inner.is_ok());
if success != valid {
return Err(Error::DidntFail(format!(
"block with root {} was valid={} whilst test expects valid={}. result: {:?}",
@@ -703,6 +737,82 @@ impl<E: EthSpec> Tester<E> {
expected_proposer_boost_root,
)
}
pub fn check_expected_proposer_head(
&self,
expected_proposer_head: Hash256,
) -> Result<(), Error> {
let mut fc = self.harness.chain.canonical_head.fork_choice_write_lock();
let slot = self.harness.chain.slot().unwrap();
let canonical_head = fc.get_head(slot, &self.harness.spec).unwrap();
let proposer_head_result = fc.get_proposer_head(
slot,
canonical_head,
DEFAULT_RE_ORG_THRESHOLD,
&DisallowedReOrgOffsets::default(),
DEFAULT_RE_ORG_MAX_EPOCHS_SINCE_FINALIZATION,
);
let proposer_head = match proposer_head_result {
Ok(head) => head.parent_node.root,
Err(ProposerHeadError::DoNotReOrg(_)) => canonical_head,
_ => panic!("Unexpected error in get proposer head"),
};
check_equal("proposer_head", proposer_head, expected_proposer_head)
}
pub fn check_should_override_fcu(
&self,
expected_should_override_fcu: ShouldOverrideFcu,
) -> Result<(), Error> {
// Determine proposer.
let cached_head = self.harness.chain.canonical_head.cached_head();
let next_slot = cached_head.snapshot.beacon_block.slot() + 1;
let next_slot_epoch = next_slot.epoch(E::slots_per_epoch());
let (proposer_indices, decision_root, _, fork) =
compute_proposer_duties_from_head(next_slot_epoch, &self.harness.chain).unwrap();
let proposer_index = proposer_indices[next_slot.as_usize() % E::slots_per_epoch() as usize];
// Ensure the proposer index cache is primed.
self.harness
.chain
.beacon_proposer_cache
.lock()
.insert(next_slot_epoch, decision_root, proposer_indices, fork)
.unwrap();
// Update the execution layer proposer preparation to match the test config.
let el = self.harness.chain.execution_layer.clone().unwrap();
self.block_on_dangerous(async {
if expected_should_override_fcu.validator_is_connected {
el.update_proposer_preparation(
next_slot_epoch,
&[ProposerPreparationData {
validator_index: dbg!(proposer_index) as u64,
fee_recipient: Default::default(),
}],
)
.await;
} else {
el.clear_proposer_preparation(proposer_index as u64).await;
}
})
.unwrap();
// Check forkchoice override.
let canonical_fcu_params = cached_head.forkchoice_update_parameters();
let fcu_params = self
.harness
.chain
.overridden_forkchoice_update_params(canonical_fcu_params)
.unwrap();
check_equal(
"should_override_forkchoice_update",
fcu_params != canonical_fcu_params,
expected_should_override_fcu.result,
)
}
}
/// Checks that the `head` checkpoint from the beacon chain head matches the `fc` checkpoint gleaned

View File

@@ -2,7 +2,7 @@ use super::*;
use crate::case_result::compare_result;
use beacon_chain::kzg_utils::validate_blob;
use eth2_network_config::TRUSTED_SETUP_BYTES;
use kzg::{Kzg, KzgCommitment, KzgProof, TrustedSetup};
use kzg::{Error as KzgError, Kzg, KzgCommitment, KzgProof, TrustedSetup};
use serde::Deserialize;
use std::convert::TryInto;
use std::marker::PhantomData;
@@ -91,8 +91,14 @@ impl<E: EthSpec> Case for KZGVerifyBlobKZGProof<E> {
let kzg = get_kzg()?;
let result = parse_input(&self.input).and_then(|(blob, commitment, proof)| {
validate_blob::<E>(&kzg, &blob, commitment, proof)
.map_err(|e| Error::InternalError(format!("Failed to validate blob: {:?}", e)))
match validate_blob::<E>(&kzg, &blob, commitment, proof) {
Ok(_) => Ok(true),
Err(KzgError::KzgVerificationFailed) => Ok(false),
Err(e) => Err(Error::InternalError(format!(
"Failed to validate blob: {:?}",
e
))),
}
});
compare_result::<bool, _>(&result, &self.output)

View File

@@ -1,6 +1,7 @@
use super::*;
use crate::case_result::compare_result;
use beacon_chain::kzg_utils::validate_blobs;
use kzg::Error as KzgError;
use serde::Deserialize;
use std::marker::PhantomData;
@@ -53,10 +54,23 @@ impl<E: EthSpec> Case for KZGVerifyBlobKZGProofBatch<E> {
};
let kzg = get_kzg()?;
let result = parse_input(&self.input).and_then(|(commitments, blobs, proofs)| {
validate_blobs::<E>(&kzg, &commitments, blobs.iter().collect(), &proofs)
.map_err(|e| Error::InternalError(format!("Failed to validate blobs: {:?}", e)))
});
let result =
parse_input(&self.input).and_then(
|(commitments, blobs, proofs)| match validate_blobs::<E>(
&kzg,
&commitments,
blobs.iter().collect(),
&proofs,
) {
Ok(_) => Ok(true),
Err(KzgError::KzgVerificationFailed) => Ok(false),
Err(e) => Err(Error::InternalError(format!(
"Failed to validate blobs: {:?}",
e
))),
},
);
compare_result::<bool, _>(&result, &self.output)
}

View File

@@ -1,9 +1,9 @@
use super::*;
use crate::decode::{ssz_decode_state, yaml_decode_file};
use crate::decode::{ssz_decode_file, ssz_decode_state, yaml_decode_file};
use serde::Deserialize;
use std::path::Path;
use tree_hash::Hash256;
use types::{BeaconState, EthSpec, ForkName};
use types::{BeaconBlockBody, BeaconBlockBodyDeneb, BeaconState, EthSpec, ForkName};
#[derive(Debug, Clone, Deserialize)]
pub struct Metadata {
@@ -82,3 +82,72 @@ impl<E: EthSpec> Case for MerkleProofValidity<E> {
Ok(())
}
}
#[derive(Debug, Clone, Deserialize)]
#[serde(bound = "E: EthSpec")]
pub struct KzgInclusionMerkleProofValidity<E: EthSpec> {
pub metadata: Option<Metadata>,
pub block: BeaconBlockBody<E>,
pub merkle_proof: MerkleProof,
}
impl<E: EthSpec> LoadCase for KzgInclusionMerkleProofValidity<E> {
fn load_from_dir(path: &Path, fork_name: ForkName) -> Result<Self, Error> {
let block = match fork_name {
ForkName::Base | ForkName::Altair | ForkName::Merge | ForkName::Capella => {
return Err(Error::InternalError(format!(
"KZG inclusion merkle proof validity test skipped for {:?}",
fork_name
)))
}
ForkName::Deneb => {
ssz_decode_file::<BeaconBlockBodyDeneb<E>>(&path.join("object.ssz_snappy"))?
}
};
let merkle_proof = yaml_decode_file(&path.join("proof.yaml"))?;
// Metadata does not exist in these tests but it is left like this just in case.
let meta_path = path.join("meta.yaml");
let metadata = if meta_path.exists() {
Some(yaml_decode_file(&meta_path)?)
} else {
None
};
Ok(Self {
metadata,
block: block.into(),
merkle_proof,
})
}
}
impl<E: EthSpec> Case for KzgInclusionMerkleProofValidity<E> {
fn result(&self, _case_index: usize, _fork_name: ForkName) -> Result<(), Error> {
let Ok(proof) = self.block.to_ref().kzg_commitment_merkle_proof(0) else {
return Err(Error::FailedToParseTest(
"Could not retrieve merkle proof".to_string(),
));
};
let proof_len = proof.len();
let branch_len = self.merkle_proof.branch.len();
if proof_len != branch_len {
return Err(Error::NotEqual(format!(
"Branches not equal in length computed: {}, expected {}",
proof_len, branch_len
)));
}
for (i, proof_leaf) in proof.iter().enumerate().take(proof_len) {
let expected_leaf = self.merkle_proof.branch[i];
if *proof_leaf != expected_leaf {
return Err(Error::NotEqual(format!(
"Leaves not equal in merkle proof computed: {}, expected: {}",
hex::encode(proof_leaf),
hex::encode(expected_leaf)
)));
}
}
Ok(())
}
}

View File

@@ -560,6 +560,13 @@ impl<E: EthSpec + TypeName> Handler for ForkChoiceHandler<E> {
return false;
}
// No FCU override tests prior to bellatrix.
if self.handler_name == "should_override_forkchoice_update"
&& (fork_name == ForkName::Base || fork_name == ForkName::Altair)
{
return false;
}
// These tests check block validity (which may include signatures) and there is no need to
// run them with fake crypto.
cfg!(not(feature = "fake_crypto"))
@@ -786,6 +793,34 @@ impl<E: EthSpec + TypeName> Handler for MerkleProofValidityHandler<E> {
}
}
#[derive(Derivative)]
#[derivative(Default(bound = ""))]
pub struct KzgInclusionMerkleProofValidityHandler<E>(PhantomData<E>);
impl<E: EthSpec + TypeName> Handler for KzgInclusionMerkleProofValidityHandler<E> {
type Case = cases::KzgInclusionMerkleProofValidity<E>;
fn config_name() -> &'static str {
E::name()
}
fn runner_name() -> &'static str {
"merkle_proof"
}
fn handler_name(&self) -> String {
"single_merkle_proof".into()
}
fn is_enabled_for_fork(&self, fork_name: ForkName) -> bool {
// Enabled in Deneb
fork_name != ForkName::Base
&& fork_name != ForkName::Altair
&& fork_name != ForkName::Merge
&& fork_name != ForkName::Capella
}
}
#[derive(Derivative)]
#[derivative(Default(bound = ""))]
pub struct OperationsHandler<E, O>(PhantomData<(E, O)>);

View File

@@ -78,7 +78,6 @@ type_name!(ProposerSlashing);
type_name_generic!(SignedAggregateAndProof);
type_name_generic!(SignedBeaconBlock);
type_name!(SignedBeaconBlockHeader);
type_name_generic!(SignedBlobSidecar);
type_name_generic!(SignedContributionAndProof);
type_name!(SignedVoluntaryExit);
type_name!(SigningData);