Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 10 additions & 9 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

23 changes: 18 additions & 5 deletions crates/beacon_state/tile/src/bls.rs
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,10 @@ pub fn fork_version_at_epoch(
/// available (e.g. `verify_deposit_signature`), use `verify_one_compressed`.
pub(crate) fn verify_one(pk: &PublicKey, sig: &[u8; 96], message: &B256) -> bool {
let Ok(sig) = Signature::from_bytes(sig) else { return false };
verify_one_parsed(pk, &sig, message)
}

fn verify_one_parsed(pk: &PublicKey, sig: &Signature, message: &B256) -> bool {
sig.verify(true, message, DST, &[], pk, false) == BLST_ERROR::BLST_SUCCESS
}

Expand Down Expand Up @@ -412,23 +416,32 @@ pub fn verify_deposit_signature(pubkey: &BLSPubkey, sig: &[u8; 96], signing_root
verify_one_compressed(pubkey, sig, signing_root)
}

/// Signature is subgroup-checked by the verify, so downstream aggregation
/// may add it without a second group check.
pub struct VerifiedSingleAttestation {
pub data_root: B256,
pub signature: Signature,
}

/// Verify a single-attester `SingleAttestation` (gossip subnet form). Used
/// on the gossip hot path; the body-included aggregate path goes through
/// `stf::validate_attestations` + `SigBatch`.
#[timed]
pub fn verify_single_attestation(
att: &[u8; SINGLE_ATT_SIZE],
attester_pubkey: &PublicKey,
fork_version: [u8; 4],
genesis_validators_root: &B256,
) -> bool {
) -> Option<VerifiedSingleAttestation> {
let data = SingleAttestationView::data(att);
let sig = SingleAttestationView::signature(att);
let signature = Signature::from_bytes(SingleAttestationView::signature(att)).ok()?;

let object_root = hash_attestation_data(data.as_bytes());
let data_root = hash_attestation_data(data.as_bytes());
let domain = compute_domain(DOMAIN_BEACON_ATTESTER, fork_version, genesis_validators_root);
let signing_root = compute_signing_root(&object_root, &domain);
let signing_root = compute_signing_root(&data_root, &domain);

verify_one(attester_pubkey, sig, &signing_root)
verify_one_parsed(attester_pubkey, &signature, &signing_root)
.then_some(VerifiedSingleAttestation { data_root, signature })
}

#[cfg(test)]
Expand Down
14 changes: 14 additions & 0 deletions crates/beacon_state/tile/src/counters.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
//! Per-tile counters exposed via `silver_common`'s shmem-mapped
//! atomic-counter primitive.
//!
//! Counters are interpreted by position — append before `_Count`, do
//! not reorder.

silver_common::declare_counters! {
pub BeaconStateCounters => "beacon_state" {
// gossip-admission structures at capacity (attestations still
// accepted and relayed; aggregation/shedding coverage degrades)
AttestationPoolFull,
SeenAggregatesFull,
}
}
2 changes: 2 additions & 0 deletions crates/beacon_state/tile/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#![allow(clippy::result_large_err)]

pub mod bls;
pub mod counters;
pub mod error;
mod fork_choice;
pub mod shuffling;
Expand All @@ -16,6 +17,7 @@ pub(crate) mod test_signing;
#[cfg(test)]
pub(crate) mod test_state;

pub use counters::BeaconStateCounters;
pub use error::{Error, PrecheckError, Result};
// Fork-choice store types, exposed only for the EF vector harnesses' read
// access via `BeaconStateTile::ef_fork_choice`.
Expand Down
168 changes: 100 additions & 68 deletions crates/beacon_state/tile/src/test_signing.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,10 @@ use silver_beacon_state_data::{
B256, BLSPubkey, BeaconBlockHeader, Fork, Immutable, SLOTS_PER_EPOCH,
};
use silver_common::ssz_view::{
AttestationDataView, IndexedAttestationView, PROPOSER_SLASHING_SIZE, ProposerSlashingView,
SIGNED_AGG_PROOF_MIN, SIGNED_BLS_CHANGE_SIZE, SIGNED_VOLUNTARY_EXIT_SIZE, SINGLE_ATT_SIZE,
SignedAggregateAndProofView, SignedBlsToExecutionChangeView, SignedVoluntaryExitView,
SingleAttestationView,
ATTESTATION_FIXED, AttestationView, IndexedAttestationView, PROPOSER_SLASHING_SIZE,
ProposerSlashingView, SIGNED_AGG_PROOF_MIN, SIGNED_BLS_CHANGE_SIZE, SIGNED_VOLUNTARY_EXIT_SIZE,
SINGLE_ATT_SIZE, SignedAggregateAndProofView, SignedBlsToExecutionChangeView,
SignedVoluntaryExitView, SingleAttestationView,
};

use crate::{
Expand Down Expand Up @@ -248,110 +248,142 @@ pub fn sign_single_attestation(
buf[104..112].copy_from_slice(&target_epoch.to_le_bytes());
buf[112..144].copy_from_slice(&target_root);

let fv = test_fork_version(target_epoch);
let data: &[u8; 128] = buf[16..144].try_into().unwrap();
let object_root = ssz_hash::hash_attestation_data(data);
resign_single_attestation(sk_idx, &mut buf, imm);
buf
}

/// Recompute the signature over the buffer's current `AttestationData`, for
/// tests that mutate data fields after `sign_single_attestation`.
pub fn resign_single_attestation(sk_idx: usize, buf: &mut [u8; SINGLE_ATT_SIZE], imm: &Immutable) {
let data: [u8; 128] = buf[16..144].try_into().unwrap();
let fv = test_fork_version(SingleAttestationView::target_epoch(buf));
let domain = bls::compute_domain(bls::DOMAIN_BEACON_ATTESTER, fv, &imm.genesis_validators_root);
let signing_root = bls::compute_signing_root(&object_root, &domain);
let signing_root = bls::compute_signing_root(&ssz_hash::hash_attestation_data(&data), &domain);
let sig = sign(sk_idx, &signing_root);
buf[144..240].copy_from_slice(&sig);
debug_assert_eq!(SingleAttestationView::signature(&buf), &sig);
buf
debug_assert_eq!(SingleAttestationView::signature(buf), &sig);
}

/// Build a fully signed `SignedAggregateAndProof` from a single-signer
/// committee. `committee_index` is the (single) bit in `committee_bits`;
/// the bit at position `aggregator_pos_in_committee` is the only set bit in
/// `aggregation_bits` — that participant signs the inner aggregate, the same
/// validator is the aggregator.
#[allow(clippy::too_many_arguments)]
pub fn sign_aggregate_and_proof(
/// Build a single-signer `Attestation`: `committee_index` is the (single)
/// bit in `committee_bits`; the bit at `participant_pos` is the only set bit
/// in `aggregation_bits` — that participant signs the aggregate.
pub fn build_attestation(
sk_idx: usize,
aggregator_index: u64,
slot: u64,
target_epoch: u64,
beacon_block_root: B256,
target_root: B256,
committee_index: usize,
aggregator_pos_in_committee: usize,
participant_pos: usize,
committee_size: usize,
imm: &Immutable,
) -> Vec<u8> {
// Bitlist[committee_size] with a single set bit at pos
// `aggregator_pos_in_committee`, plus terminator bit at position
// `committee_size`. Length = ceil((committee_size+1)/8).
// Trailing Bitlist[committee_size] with terminator bit at position
// `committee_size` → length = ceil((committee_size+1)/8).
let bl_len = (committee_size + 1).div_ceil(8);
let mut agg_bits = vec![0u8; bl_len];
let pos = aggregator_pos_in_committee;
agg_bits[pos / 8] |= 1 << (pos % 8);
// Terminator.
let term = committee_size;
agg_bits[term / 8] |= 1 << (term % 8);
let mut buf = vec![0u8; ATTESTATION_FIXED + bl_len];

// SignedAggregateAndProof fixed part = 444B; aggregation_bits trail.
let mut buf = vec![0u8; SIGNED_AGG_PROOF_MIN + bl_len];
buf[0..4].copy_from_slice(&(ATTESTATION_FIXED as u32).to_le_bytes());

// Outer fixed: offset to message (4) = 100, signature (96) = [4..100).
buf[0..4].copy_from_slice(&100u32.to_le_bytes());

// AggregateAndProof: aggregator_index(8) at 100, offset to aggregate(4)
// = 108 (relative-to-message = 108), selection_proof(96) at 112.
buf[100..108].copy_from_slice(&aggregator_index.to_le_bytes());
buf[108..112].copy_from_slice(&108u32.to_le_bytes());

// Attestation (variable, starts at 208):
// offset to aggregation_bits(4) = 236 (rel-to-attestation = 236, absolute 444).
buf[208..212].copy_from_slice(&236u32.to_le_bytes());

// AttestationData[212..340]: slot, index(=0), beacon_block_root, source,
// AttestationData[4..132]: slot, index(=0), beacon_block_root, source,
// target.
buf[212..220].copy_from_slice(&slot.to_le_bytes());
buf[228..260].copy_from_slice(&beacon_block_root);
buf[300..308].copy_from_slice(&target_epoch.to_le_bytes());
buf[308..340].copy_from_slice(&target_root);
buf[4..12].copy_from_slice(&slot.to_le_bytes());
buf[20..52].copy_from_slice(&beacon_block_root);
buf[92..100].copy_from_slice(&target_epoch.to_le_bytes());
buf[100..132].copy_from_slice(&target_root);

// committee_bits[436..444]: single bit at `committee_index`.
buf[436 + committee_index / 8] |= 1 << (committee_index % 8);
// committee_bits[228..236]: single bit at `committee_index`.
buf[228 + committee_index / 8] |= 1 << (committee_index % 8);

// aggregation_bits at [444..444+bl_len].
buf[444..444 + bl_len].copy_from_slice(&agg_bits);
// aggregation_bits: participant bit, then terminator.
buf[ATTESTATION_FIXED + participant_pos / 8] |= 1 << (participant_pos % 8);
buf[ATTESTATION_FIXED + committee_size / 8] |= 1 << (committee_size % 8);

// Sign the inner aggregate (single signer = aggregator).
let fv = test_fork_version(target_epoch);
let data: &[u8; 128] = buf[212..340].try_into().unwrap();
let data: &[u8; 128] = buf[4..132].try_into().unwrap();
let data_root = ssz_hash::hash_attestation_data(data);
let domain_att =
bls::compute_domain(bls::DOMAIN_BEACON_ATTESTER, fv, &imm.genesis_validators_root);
let sr_att = bls::compute_signing_root(&data_root, &domain_att);
let inner_sig = sign(sk_idx, &sr_att);
buf[340..436].copy_from_slice(&inner_sig);
let sig = sign(sk_idx, &bls::compute_signing_root(&data_root, &domain_att));
buf[132..228].copy_from_slice(&sig);

debug_assert_eq!(AttestationView::signature(&buf), &sig);
debug_assert_eq!(AttestationView::data(&buf).target_epoch(), target_epoch);
buf
}

/// Wrap an existing inner `Attestation` SSZ buffer in a fully signed
/// `SignedAggregateAndProof`: `sk_idx` (the aggregator's key) signs the
/// selection proof over the attestation's slot and the outer signature;
/// the inner aggregate keeps whatever signature it carries.
pub fn wrap_aggregate_and_proof(
sk_idx: usize,
aggregator_index: u64,
attestation: &[u8],
imm: &Immutable,
) -> Vec<u8> {
const PREFIX: usize = SIGNED_AGG_PROOF_MIN - ATTESTATION_FIXED;

// Outer fixed: offset to message(4) = 100, signature(96) = [4..100).
// AggregateAndProof: aggregator_index(8) at 100, offset to aggregate(4)
// = 108 (relative-to-message = 108), selection_proof(96) at 112.
let mut buf = vec![0u8; PREFIX + attestation.len()];
buf[0..4].copy_from_slice(&100u32.to_le_bytes());
buf[100..108].copy_from_slice(&aggregator_index.to_le_bytes());
buf[108..112].copy_from_slice(&108u32.to_le_bytes());
buf[PREFIX..].copy_from_slice(attestation);

let data = AttestationView::data(attestation);
let fv = test_fork_version(data.target_epoch());

// Selection proof: signer = aggregator, msg = htr(uint64(slot)).
let slot_root = merkle::uint64_chunk(slot);
let slot_root = merkle::uint64_chunk(data.slot());
let domain_sp =
bls::compute_domain(bls::DOMAIN_SELECTION_PROOF, fv, &imm.genesis_validators_root);
let sr_sp = bls::compute_signing_root(&slot_root, &domain_sp);
let selection_proof = sign(sk_idx, &sr_sp);
let selection_proof = sign(sk_idx, &bls::compute_signing_root(&slot_root, &domain_sp));
buf[112..208].copy_from_slice(&selection_proof);

// Outer AggregateAndProof sig.
let aggregate_bytes = SignedAggregateAndProofView::aggregate(&buf);
let agg_proof_root = ssz_hash::hash_tree_root_aggregate_and_proof(
aggregator_index,
aggregate_bytes,
&buf[PREFIX..],
&selection_proof,
fv == imm.gloas_fork_version,
);
let domain_aap =
bls::compute_domain(bls::DOMAIN_AGGREGATE_AND_PROOF, fv, &imm.genesis_validators_root);
let sr_aap = bls::compute_signing_root(&agg_proof_root, &domain_aap);
let outer_sig = sign(sk_idx, &sr_aap);
let outer_sig = sign(sk_idx, &bls::compute_signing_root(&agg_proof_root, &domain_aap));
buf[4..100].copy_from_slice(&outer_sig);

debug_assert_eq!(SignedAggregateAndProofView::signature(&buf), &outer_sig);
debug_assert_eq!(
AttestationDataView::new(buf[212..340].try_into().unwrap()).target_epoch(),
target_epoch
);
buf
}

/// Build a fully signed `SignedAggregateAndProof` from a single-signer
/// committee — the participant at `aggregator_pos_in_committee` signs the
/// inner aggregate, and the same validator is the aggregator.
#[allow(clippy::too_many_arguments)]
pub fn sign_aggregate_and_proof(
sk_idx: usize,
aggregator_index: u64,
slot: u64,
target_epoch: u64,
beacon_block_root: B256,
target_root: B256,
committee_index: usize,
aggregator_pos_in_committee: usize,
committee_size: usize,
imm: &Immutable,
) -> Vec<u8> {
let att = build_attestation(
sk_idx,
slot,
target_epoch,
beacon_block_root,
target_root,
committee_index,
aggregator_pos_in_committee,
committee_size,
imm,
);
wrap_aggregate_and_proof(sk_idx, aggregator_index, &att, imm)
}
Loading
Loading