Attestation superstruct changes for EIP 7549 (#5644)

* update

* experiment

* superstruct changes

* revert

* superstruct changes

* fix tests

* indexed attestation

* indexed attestation superstruct

* updated TODOs
This commit is contained in:
Eitan Seri-Levi
2024-04-30 19:49:08 +03:00
committed by GitHub
parent 4a48d7b546
commit 3b7132bc0d
56 changed files with 943 additions and 429 deletions

View File

@@ -53,7 +53,7 @@ impl<E: EthSpec> AggregateAndProof<E> {
let selection_proof = selection_proof
.unwrap_or_else(|| {
SelectionProof::new::<E>(
aggregate.data.slot,
aggregate.data().slot,
secret_key,
fork,
genesis_validators_root,
@@ -77,14 +77,14 @@ impl<E: EthSpec> AggregateAndProof<E> {
genesis_validators_root: Hash256,
spec: &ChainSpec,
) -> bool {
let target_epoch = self.aggregate.data.slot.epoch(E::slots_per_epoch());
let target_epoch = self.aggregate.data().slot.epoch(E::slots_per_epoch());
let domain = spec.get_domain(
target_epoch,
Domain::SelectionProof,
fork,
genesis_validators_root,
);
let message = self.aggregate.data.slot.signing_root(domain);
let message = self.aggregate.data().slot.signing_root(domain);
self.selection_proof.verify(validator_pubkey, message)
}
}

View File

@@ -1,12 +1,16 @@
use derivative::Derivative;
use safe_arith::ArithError;
use serde::{Deserialize, Serialize};
use ssz_derive::{Decode, Encode};
use test_random_derive::TestRandom;
use tree_hash_derive::TreeHash;
use crate::slot_data::SlotData;
use crate::{test_utils::TestRandom, Hash256, Slot};
use derivative::Derivative;
use rand::RngCore;
use safe_arith::ArithError;
use serde::{Deserialize, Serialize};
use ssz::Decode;
use ssz_derive::{Decode, Encode};
use ssz_types::BitVector;
use std::hash::{Hash, Hasher};
use superstruct::superstruct;
use test_random_derive::TestRandom;
use tree_hash_derive::TreeHash;
use super::{
AggregateSignature, AttestationData, BitList, ChainSpec, Domain, EthSpec, Fork, SecretKey,
@@ -20,31 +24,273 @@ pub enum Error {
SubnetCountIsZero(ArithError),
}
/// Details an attestation that can be slashable.
///
/// Spec v0.12.1
#[superstruct(
variants(Base, Electra),
variant_attributes(
derive(
Debug,
Clone,
Serialize,
Deserialize,
Decode,
Encode,
TestRandom,
Derivative,
arbitrary::Arbitrary,
TreeHash,
),
derivative(PartialEq, Hash(bound = "E: EthSpec")),
serde(bound = "E: EthSpec", deny_unknown_fields),
arbitrary(bound = "E: EthSpec"),
)
)]
#[derive(
arbitrary::Arbitrary,
Debug,
Clone,
Serialize,
Deserialize,
Encode,
Decode,
TreeHash,
TestRandom,
Encode,
Derivative,
Deserialize,
arbitrary::Arbitrary,
PartialEq,
)]
#[derivative(PartialEq, Hash(bound = "E: EthSpec"))]
#[serde(bound = "E: EthSpec")]
#[serde(untagged)]
#[tree_hash(enum_behaviour = "transparent")]
#[ssz(enum_behaviour = "transparent")]
#[serde(bound = "E: EthSpec", deny_unknown_fields)]
#[arbitrary(bound = "E: EthSpec")]
pub struct Attestation<E: EthSpec> {
#[superstruct(only(Base), partial_getter(rename = "aggregation_bits_base"))]
pub aggregation_bits: BitList<E::MaxValidatorsPerCommittee>,
#[superstruct(only(Electra), partial_getter(rename = "aggregation_bits_electra"))]
pub aggregation_bits: BitList<E::MaxValidatorsPerCommitteePerSlot>,
pub data: AttestationData,
pub signature: AggregateSignature,
#[superstruct(only(Electra))]
pub committee_bits: BitVector<E::MaxCommitteesPerSlot>,
}
impl<E: EthSpec> Decode for Attestation<E> {
fn is_ssz_fixed_len() -> bool {
false
}
fn from_ssz_bytes(bytes: &[u8]) -> Result<Self, ssz::DecodeError> {
if let Ok(result) = AttestationBase::from_ssz_bytes(bytes) {
return Ok(Attestation::Base(result));
}
if let Ok(result) = AttestationElectra::from_ssz_bytes(bytes) {
return Ok(Attestation::Electra(result));
}
Err(ssz::DecodeError::BytesInvalid(String::from(
"bytes not valid for any fork variant",
)))
}
}
impl<E: EthSpec> TestRandom for Attestation<E> {
fn random_for_test(rng: &mut impl RngCore) -> Self {
let aggregation_bits: BitList<E::MaxValidatorsPerCommittee> = BitList::random_for_test(rng);
// let committee_bits: BitList<E::MaxCommitteesPerSlot> = BitList::random_for_test(rng);
let data = AttestationData::random_for_test(rng);
let signature = AggregateSignature::random_for_test(rng);
Self::Base(AttestationBase {
aggregation_bits,
// committee_bits,
data,
signature,
})
}
}
impl<E: EthSpec> Hash for Attestation<E> {
fn hash<H>(&self, state: &mut H)
where
H: Hasher,
{
match self {
Attestation::Base(att) => att.hash(state),
Attestation::Electra(att) => att.hash(state),
}
}
}
impl<E: EthSpec> Attestation<E> {
/// Aggregate another Attestation into this one.
///
/// The aggregation bitfields must be disjoint, and the data must be the same.
pub fn aggregate(&mut self, other: &Self) {
match self {
Attestation::Base(att) => {
debug_assert!(other.as_base().is_ok());
if let Ok(other) = other.as_base() {
att.aggregate(other)
}
}
Attestation::Electra(att) => {
debug_assert!(other.as_electra().is_ok());
if let Ok(other) = other.as_electra() {
att.aggregate(other)
}
}
}
}
/// Signs `self`, setting the `committee_position`'th bit of `aggregation_bits` to `true`.
///
/// Returns an `AlreadySigned` error if the `committee_position`'th bit is already `true`.
pub fn sign(
&mut self,
secret_key: &SecretKey,
committee_position: usize,
fork: &Fork,
genesis_validators_root: Hash256,
spec: &ChainSpec,
) -> Result<(), Error> {
match self {
Attestation::Base(att) => att.sign(
secret_key,
committee_position,
fork,
genesis_validators_root,
spec,
),
Attestation::Electra(att) => att.sign(
secret_key,
committee_position,
fork,
genesis_validators_root,
spec,
),
}
}
/// Returns an `AlreadySigned` error if the `committee_position`'th bit is already `true`.
pub fn add_signature(
&mut self,
signature: &Signature,
committee_position: usize,
) -> Result<(), Error> {
match self {
Attestation::Base(att) => att.add_signature(signature, committee_position),
Attestation::Electra(att) => att.add_signature(signature, committee_position),
}
}
pub fn committee_index(&self) -> u64 {
match self {
Attestation::Base(att) => att.data.index,
Attestation::Electra(att) => att.committee_index(),
}
}
pub fn is_aggregation_bits_zero(&self) -> bool {
match self {
Attestation::Base(att) => att.aggregation_bits.is_zero(),
Attestation::Electra(att) => att.aggregation_bits.is_zero(),
}
}
pub fn num_set_aggregation_bits(&self) -> usize {
match self {
Attestation::Base(att) => att.aggregation_bits.num_set_bits(),
Attestation::Electra(att) => att.aggregation_bits.num_set_bits(),
}
}
pub fn get_aggregation_bit(&self, index: usize) -> Result<bool, ssz_types::Error> {
match self {
Attestation::Base(att) => att.aggregation_bits.get(index),
Attestation::Electra(att) => att.aggregation_bits.get(index),
}
}
}
impl<E: EthSpec> AttestationElectra<E> {
/// Are the aggregation bitfields of these attestations disjoint?
pub fn signers_disjoint_from(&self, other: &Self) -> bool {
self.aggregation_bits
.intersection(&other.aggregation_bits)
.is_zero()
}
pub fn committee_index(&self) -> u64 {
*self.get_committee_indices().first().unwrap_or(&0u64)
}
pub fn get_committee_indices(&self) -> Vec<u64> {
self.committee_bits
.iter()
.enumerate()
.filter_map(|(index, bit)| if bit { Some(index as u64) } else { None })
.collect()
}
/// Aggregate another Attestation into this one.
///
/// The aggregation bitfields must be disjoint, and the data must be the same.
pub fn aggregate(&mut self, other: &Self) {
debug_assert_eq!(self.data, other.data);
debug_assert!(self.signers_disjoint_from(other));
self.aggregation_bits = self.aggregation_bits.union(&other.aggregation_bits);
self.signature.add_assign_aggregate(&other.signature);
}
/// Signs `self`, setting the `committee_position`'th bit of `aggregation_bits` to `true`.
///
/// Returns an `AlreadySigned` error if the `committee_position`'th bit is already `true`.
pub fn sign(
&mut self,
secret_key: &SecretKey,
committee_position: usize,
fork: &Fork,
genesis_validators_root: Hash256,
spec: &ChainSpec,
) -> Result<(), Error> {
let domain = spec.get_domain(
self.data.target.epoch,
Domain::BeaconAttester,
fork,
genesis_validators_root,
);
let message = self.data.signing_root(domain);
self.add_signature(&secret_key.sign(message), committee_position)
}
/// Adds `signature` to `self` and sets the `committee_position`'th bit of `aggregation_bits` to `true`.
///
/// Returns an `AlreadySigned` error if the `committee_position`'th bit is already `true`.
pub fn add_signature(
&mut self,
signature: &Signature,
committee_position: usize,
) -> Result<(), Error> {
if self
.aggregation_bits
.get(committee_position)
.map_err(Error::SszTypesError)?
{
Err(Error::AlreadySigned(committee_position))
} else {
self.aggregation_bits
.set(committee_position, true)
.map_err(Error::SszTypesError)?;
self.signature.add_assign(signature);
Ok(())
}
}
}
impl<E: EthSpec> AttestationBase<E> {
/// Are the aggregation bitfields of these attestations disjoint?
pub fn signers_disjoint_from(&self, other: &Self) -> bool {
self.aggregation_bits
@@ -113,7 +359,7 @@ impl<E: EthSpec> Attestation<E> {
impl<E: EthSpec> SlotData for Attestation<E> {
fn get_slot(&self) -> Slot {
self.data.slot
self.data().slot
}
}

View File

@@ -1,3 +1,4 @@
use crate::attestation::AttestationBase;
use crate::test_utils::TestRandom;
use crate::*;
use derivative::Derivative;
@@ -10,6 +11,8 @@ use test_random_derive::TestRandom;
use tree_hash::TreeHash;
use tree_hash_derive::TreeHash;
use self::indexed_attestation::IndexedAttestationBase;
/// A block of the `BeaconChain`.
#[superstruct(
variants(Base, Altair, Merge, Capella, Deneb, Electra),
@@ -324,15 +327,16 @@ impl<E: EthSpec, Payload: AbstractExecPayload<E>> BeaconBlockBase<E, Payload> {
message: header,
signature: Signature::empty(),
};
let indexed_attestation: IndexedAttestation<E> = IndexedAttestation {
attesting_indices: VariableList::new(vec![
0_u64;
E::MaxValidatorsPerCommittee::to_usize()
])
.unwrap(),
data: AttestationData::default(),
signature: AggregateSignature::empty(),
};
let indexed_attestation: IndexedAttestation<E> =
IndexedAttestation::Base(IndexedAttestationBase {
attesting_indices: VariableList::new(vec![
0_u64;
E::MaxValidatorsPerCommittee::to_usize()
])
.unwrap(),
data: AttestationData::default(),
signature: AggregateSignature::empty(),
});
let deposit_data = DepositData {
pubkey: PublicKeyBytes::empty(),
@@ -350,12 +354,12 @@ impl<E: EthSpec, Payload: AbstractExecPayload<E>> BeaconBlockBase<E, Payload> {
attestation_2: indexed_attestation,
};
let attestation: Attestation<E> = Attestation {
let attestation: Attestation<E> = Attestation::Base(AttestationBase {
aggregation_bits: BitList::with_capacity(E::MaxValidatorsPerCommittee::to_usize())
.unwrap(),
data: AttestationData::default(),
signature: AggregateSignature::empty(),
};
});
let deposit = Deposit {
proof: FixedVector::from_elem(Hash256::zero()),

View File

@@ -63,6 +63,8 @@ pub trait EthSpec:
* Misc
*/
type MaxValidatorsPerCommittee: Unsigned + Clone + Sync + Send + Debug + PartialEq + Eq;
type MaxValidatorsPerCommitteePerSlot: Unsigned + Clone + Sync + Send + Debug + PartialEq + Eq;
type MaxCommitteesPerSlot: Unsigned + Clone + Sync + Send + Debug + PartialEq + Eq;
/*
* Time parameters
*/
@@ -349,6 +351,8 @@ impl EthSpec for MainnetEthSpec {
type JustificationBitsLength = U4;
type SubnetBitfieldLength = U64;
type MaxValidatorsPerCommittee = U2048;
type MaxCommitteesPerSlot = U64;
type MaxValidatorsPerCommitteePerSlot = U131072;
type GenesisEpoch = U0;
type SlotsPerEpoch = U32;
type EpochsPerEth1VotingPeriod = U64;
@@ -424,6 +428,8 @@ impl EthSpec for MinimalEthSpec {
SubnetBitfieldLength,
SyncCommitteeSubnetCount,
MaxValidatorsPerCommittee,
MaxCommitteesPerSlot,
MaxValidatorsPerCommitteePerSlot,
GenesisEpoch,
HistoricalRootsLimit,
ValidatorRegistryLimit,
@@ -468,6 +474,8 @@ impl EthSpec for GnosisEthSpec {
type JustificationBitsLength = U4;
type SubnetBitfieldLength = U64;
type MaxValidatorsPerCommittee = U2048;
type MaxCommitteesPerSlot = U64;
type MaxValidatorsPerCommitteePerSlot = U131072;
type GenesisEpoch = U0;
type SlotsPerEpoch = U16;
type EpochsPerEth1VotingPeriod = U64;

View File

@@ -1,9 +1,13 @@
use crate::{test_utils::TestRandom, AggregateSignature, AttestationData, EthSpec, VariableList};
use core::slice::Iter;
use derivative::Derivative;
use rand::RngCore;
use serde::{Deserialize, Serialize};
use ssz::Decode;
use ssz::Encode;
use ssz_derive::{Decode, Encode};
use std::hash::{Hash, Hasher};
use superstruct::superstruct;
use test_random_derive::TestRandom;
use tree_hash_derive::TreeHash;
@@ -12,25 +16,50 @@ use tree_hash_derive::TreeHash;
/// To be included in an `AttesterSlashing`.
///
/// Spec v0.12.1
#[superstruct(
variants(Base, Electra),
variant_attributes(
derive(
Debug,
Clone,
Serialize,
Deserialize,
Decode,
Encode,
TestRandom,
Derivative,
arbitrary::Arbitrary,
TreeHash,
),
derivative(PartialEq, Hash(bound = "E: EthSpec")),
serde(bound = "E: EthSpec", deny_unknown_fields),
arbitrary(bound = "E: EthSpec"),
)
)]
#[derive(
Derivative,
Debug,
Clone,
Serialize,
Deserialize,
Encode,
Decode,
TreeHash,
TestRandom,
Encode,
Derivative,
Deserialize,
arbitrary::Arbitrary,
PartialEq,
)]
#[derivative(PartialEq, Eq)] // to satisfy Clippy's lint about `Hash`
#[serde(bound = "E: EthSpec")]
#[serde(untagged)]
#[tree_hash(enum_behaviour = "transparent")]
#[ssz(enum_behaviour = "transparent")]
#[serde(bound = "E: EthSpec", deny_unknown_fields)]
#[arbitrary(bound = "E: EthSpec")]
pub struct IndexedAttestation<E: EthSpec> {
/// Lists validator registry indices, not committee indices.
#[superstruct(only(Base), partial_getter(rename = "attesting_indices_base"))]
#[serde(with = "quoted_variable_list_u64")]
pub attesting_indices: VariableList<u64, E::MaxValidatorsPerCommittee>,
#[superstruct(only(Electra), partial_getter(rename = "attesting_indices_electra"))]
#[serde(with = "quoted_variable_list_u64")]
pub attesting_indices: VariableList<u64, E::MaxValidatorsPerCommitteePerSlot>,
pub data: AttestationData,
pub signature: AggregateSignature,
}
@@ -40,15 +69,84 @@ impl<E: EthSpec> IndexedAttestation<E> {
///
/// Spec v0.12.1
pub fn is_double_vote(&self, other: &Self) -> bool {
self.data.target.epoch == other.data.target.epoch && self.data != other.data
self.data().target.epoch == other.data().target.epoch && self.data() != other.data()
}
/// Check if ``attestation_data_1`` surrounds ``attestation_data_2``.
///
/// Spec v0.12.1
pub fn is_surround_vote(&self, other: &Self) -> bool {
self.data.source.epoch < other.data.source.epoch
&& other.data.target.epoch < self.data.target.epoch
self.data().source.epoch < other.data().source.epoch
&& other.data().target.epoch < self.data().target.epoch
}
pub fn attesting_indices_len(&self) -> usize {
match self {
IndexedAttestation::Base(att) => att.attesting_indices.len(),
IndexedAttestation::Electra(att) => att.attesting_indices.len(),
}
}
pub fn attesting_indices_to_vec(&self) -> Vec<u64> {
match self {
IndexedAttestation::Base(att) => att.attesting_indices.to_vec(),
IndexedAttestation::Electra(att) => att.attesting_indices.to_vec(),
}
}
pub fn attesting_indices_is_empty(&self) -> bool {
match self {
IndexedAttestation::Base(att) => att.attesting_indices.is_empty(),
IndexedAttestation::Electra(att) => att.attesting_indices.is_empty(),
}
}
pub fn attesting_indices_iter(&self) -> Iter<'_, u64> {
match self {
IndexedAttestation::Base(att) => att.attesting_indices.iter(),
IndexedAttestation::Electra(att) => att.attesting_indices.iter(),
}
}
pub fn attesting_indices_first(&self) -> Option<&u64> {
match self {
IndexedAttestation::Base(att) => att.attesting_indices.first(),
IndexedAttestation::Electra(att) => att.attesting_indices.first(),
}
}
}
impl<E: EthSpec> Decode for IndexedAttestation<E> {
fn is_ssz_fixed_len() -> bool {
false
}
fn from_ssz_bytes(bytes: &[u8]) -> Result<Self, ssz::DecodeError> {
if let Ok(result) = IndexedAttestationBase::from_ssz_bytes(bytes) {
return Ok(IndexedAttestation::Base(result));
}
if let Ok(result) = IndexedAttestationElectra::from_ssz_bytes(bytes) {
return Ok(IndexedAttestation::Electra(result));
}
Err(ssz::DecodeError::BytesInvalid(String::from(
"bytes not valid for any fork variant",
)))
}
}
impl<E: EthSpec> TestRandom for IndexedAttestation<E> {
fn random_for_test(rng: &mut impl RngCore) -> Self {
let attesting_indices = VariableList::random_for_test(rng);
let data = AttestationData::random_for_test(rng);
let signature = AggregateSignature::random_for_test(rng);
Self::Base(IndexedAttestationBase {
attesting_indices,
data,
signature,
})
}
}
@@ -59,9 +157,12 @@ impl<E: EthSpec> IndexedAttestation<E> {
/// Used in the operation pool.
impl<E: EthSpec> Hash for IndexedAttestation<E> {
fn hash<H: Hasher>(&self, state: &mut H) {
self.attesting_indices.hash(state);
self.data.hash(state);
self.signature.as_ssz_bytes().hash(state);
match self {
IndexedAttestation::Base(att) => att.attesting_indices.hash(state),
IndexedAttestation::Electra(att) => att.attesting_indices.hash(state),
};
self.data().hash(state);
self.signature().as_ssz_bytes().hash(state);
}
}
@@ -166,8 +267,8 @@ mod tests {
let mut rng = XorShiftRng::from_seed([42; 16]);
let mut indexed_vote = IndexedAttestation::random_for_test(&mut rng);
indexed_vote.data.source.epoch = Epoch::new(source_epoch);
indexed_vote.data.target.epoch = Epoch::new(target_epoch);
indexed_vote.data_mut().source.epoch = Epoch::new(source_epoch);
indexed_vote.data_mut().target.epoch = Epoch::new(target_epoch);
indexed_vote
}
}

View File

@@ -57,7 +57,7 @@ impl<E: EthSpec> SignedAggregateAndProof<E> {
spec,
);
let target_epoch = message.aggregate.data.slot.epoch(E::slots_per_epoch());
let target_epoch = message.aggregate.data().slot.epoch(E::slots_per_epoch());
let domain = spec.get_domain(
target_epoch,
Domain::AggregateAndProof,