Skip to content
Merged
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
54 changes: 54 additions & 0 deletions crates/eth2api/src/versioned.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,22 @@ use crate::{
v1,
};

/// Graffiti string used to mark synthetic blocks that must never be submitted.
pub const SYNTHETIC_BLOCK_GRAFFITI: &str = "SYNTHETIC BLOCK: DO NOT SUBMIT";

/// 32-byte graffiti used to mark synthetic blocks, left-aligned with zero
/// padding.
pub const SYNTHETIC_GRAFFITI: phase0::Root = {
let mut graffiti = [0u8; 32];
let src = SYNTHETIC_BLOCK_GRAFFITI.as_bytes();
let mut i = 0;
while i < src.len() {
graffiti[i] = src[i];
i += 1;
}
graffiti
};

/// Signed proposal wrapper across all supported forks.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct VersionedSignedProposal {
Expand Down Expand Up @@ -87,6 +103,24 @@ impl SignedProposalBlock {
}
}

/// Returns the graffiti embedded in this proposal's block body.
pub fn graffiti(&self) -> phase0::Root {
match self {
Self::Phase0(block) => block.message.body.graffiti,
Self::Altair(block) => block.message.body.graffiti,
Self::Bellatrix(block) => block.message.body.graffiti,
Self::BellatrixBlinded(block) => block.message.body.graffiti,
Self::Capella(block) => block.message.body.graffiti,
Self::CapellaBlinded(block) => block.message.body.graffiti,
Self::Deneb(block) => block.signed_block.message.body.graffiti,
Self::DenebBlinded(block) => block.message.body.graffiti,
Self::Electra(block) => block.signed_block.message.body.graffiti,
Self::ElectraBlinded(block) => block.message.body.graffiti,
Self::Fulu(block) => block.signed_block.message.body.graffiti,
Self::FuluBlinded(block) => block.message.body.graffiti,
}
}

/// Converts blinded payload variants into blinded-wrapper payloads.
pub fn into_blinded(self) -> Option<SignedBlindedProposalBlock> {
match self {
Expand All @@ -106,6 +140,18 @@ impl SignedProposalBlock {
}
}

impl VersionedSignedProposal {
/// Returns `true` if this is a synthetic proposal, i.e. its block body
/// graffiti matches [`SYNTHETIC_GRAFFITI`].
///
/// Unifies Go's separate blinded/full checks: the payload enum already
/// carries both blinded and full variants, so a single graffiti comparison
/// covers every case.
pub fn is_synthetic(&self) -> bool {
Comment thread
mskrzypkows marked this conversation as resolved.
self.block.graffiti() == SYNTHETIC_GRAFFITI
}
}

/// Signed blinded proposal wrapper across all supported forks.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct VersionedSignedBlindedProposal {
Expand Down Expand Up @@ -401,6 +447,14 @@ mod tests {
use super::*;
use crate::test_fixtures;

#[test]
fn synthetic_graffiti_layout() {
let marker = SYNTHETIC_BLOCK_GRAFFITI.as_bytes();
assert_eq!(&SYNTHETIC_GRAFFITI[..marker.len()], marker);
// Remaining bytes are zero-padded.
assert!(SYNTHETIC_GRAFFITI[marker.len()..].iter().all(|&b| b == 0));
}

#[test]
fn versioned_signed_aggregate_and_proof_message_root_delegates_to_payload() {
let signed = electra::SignedAggregateAndProof {
Expand Down
2 changes: 1 addition & 1 deletion crates/ssz/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ pub use helpers::{
from_0x_hex_str, left_pad, put_byte_list, put_bytes_n, put_hex_bytes_n, to_0x_hex,
};
/// Generic SSZ list, vector, and bitfield wrappers.
pub use types::{BitList, BitVector, SszList, SszVector};
pub use types::{BitList, BitVector, BitfieldError, SszList, SszVector};

/// Error type for SSZ binary encode/decode operations.
#[derive(Debug, thiserror::Error)]
Expand Down
167 changes: 167 additions & 0 deletions crates/ssz/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,15 @@ impl<T: TreeHash, const SIZE: usize> TreeHash for SszVector<T, SIZE> {

const BIT_MASK: [u8; 8] = [0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80];

/// Error returned by bitfield combinators that require operands of equal
/// length.
#[derive(Debug, thiserror::Error, PartialEq, Eq)]
pub enum BitfieldError {
/// The two bitlists have different bit lengths and cannot be combined.
#[error("bitlists are different lengths")]
DifferentLength,
}

/// SSZ variable-length bitfield with maximum capacity.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct BitList<const MAX: usize> {
Expand Down Expand Up @@ -344,6 +353,66 @@ impl<const MAX: usize> BitList<MAX> {
len: capacity,
}
}

/// Returns the bit at index `i`, or `false` if `i` is out of range.
pub fn bit_at(&self, i: usize) -> bool {
if i >= self.len {
return false;
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this better return Result / Option for this function?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

}
self.bytes[i / 8] & BIT_MASK[i % 8] != 0
}

/// Sets the bit at index `i` to `value`; out-of-range indices are ignored.
pub fn set_bit_at(&mut self, i: usize, value: bool) {
if i >= self.len {
return;
Comment thread
mskrzypkows marked this conversation as resolved.
}
if value {
self.bytes[i / 8] |= BIT_MASK[i % 8];
} else {
self.bytes[i / 8] &= !BIT_MASK[i % 8];
}
}

/// Returns the indices of all set bits in ascending order.
pub fn bit_indices(&self) -> Vec<usize> {
(0..self.len).filter(|&i| self.bit_at(i)).collect()
}

/// Returns `true` if every bit set in `other` is also set in `self`.
///
/// Errors with [`BitfieldError::DifferentLength`] if the two bitlists do
/// not have the same bit length.
pub fn contains(&self, other: &Self) -> Result<bool, BitfieldError> {
if self.len != other.len {
return Err(BitfieldError::DifferentLength);
}
Ok(other
.bytes
.iter()
.zip(&self.bytes)
.all(|(o, s)| o & s == *o))
}

/// Returns the bitwise OR (union) of `self` and `other`.
///
/// Errors with [`BitfieldError::DifferentLength`] if the two bitlists do
/// not have the same bit length.
pub fn or(&self, other: &Self) -> Result<Self, BitfieldError> {
if self.len != other.len {
return Err(BitfieldError::DifferentLength);
}
let bytes = self
.bytes
.iter()
.zip(&other.bytes)
.map(|(a, b)| a | b)
.collect();
Ok(Self {
bytes,
len: self.len,
})
}
}

impl<const MAX: usize> Serialize for BitList<MAX> {
Expand Down Expand Up @@ -433,6 +502,31 @@ impl<const SIZE: usize> BitVector<SIZE> {
}
v
}

/// Returns the bit at index `i`, or `false` if `i` is out of range.
pub fn bit_at(&self, i: usize) -> bool {
if i >= SIZE {
return false;
}
self.bytes[i / 8] & BIT_MASK[i % 8] != 0
}

/// Sets the bit at index `i` to `value`; out-of-range indices are ignored.
pub fn set_bit_at(&mut self, i: usize, value: bool) {
if i >= SIZE {
return;
}
if value {
self.bytes[i / 8] |= BIT_MASK[i % 8];
} else {
self.bytes[i / 8] &= !BIT_MASK[i % 8];
}
}

/// Returns the indices of all set bits in ascending order.
pub fn bit_indices(&self) -> Vec<usize> {
(0..SIZE).filter(|&i| self.bit_at(i)).collect()
}
}

impl<const SIZE: usize> Serialize for BitVector<SIZE> {
Expand Down Expand Up @@ -560,4 +654,77 @@ mod tests {
let vec: SszVector<u8, 3> = vec![1, 2, 3].into();
assert_eq!(vec.as_ref(), &[1, 2, 3]);
}

#[test]
fn bitlist_bit_at_and_indices() {
let bl = BitList::<2048>::with_bits(3, &[0, 2]);
assert!(bl.bit_at(0));
assert!(!bl.bit_at(1));
assert!(bl.bit_at(2));
// Out-of-range index reads as unset.
assert!(!bl.bit_at(3));
assert!(!bl.bit_at(9001));
assert_eq!(bl.bit_indices(), vec![0, 2]);
}

#[test]
fn bitlist_bit_at_matches_ssz_round_trip() {
// SSZ byte 0x0D = sentinel at bit 3 ⇒ 3 data bits with bits 0 and 2 set,
// matching the bytes returned by `aggregation_bits()`.
let bl = BitList::<2048>::from_ssz_bytes(vec![0x0D]);
assert_eq!(bl.len(), 3);
assert_eq!(bl.bit_indices(), vec![0, 2]);
}

#[test]
fn bitlist_set_bit_at() {
let mut bl = BitList::<2048>::with_bits(8, &[0]);
bl.set_bit_at(3, true);
assert_eq!(bl.bit_indices(), vec![0, 3]);
bl.set_bit_at(0, false);
assert_eq!(bl.bit_indices(), vec![3]);
// Out-of-range set is a no-op.
bl.set_bit_at(8, true);
assert_eq!(bl.bit_indices(), vec![3]);
}

#[test]
fn bitlist_contains() {
let superset = BitList::<2048>::with_bits(4, &[0, 1, 2]);
let subset = BitList::<2048>::with_bits(4, &[0, 2]);
assert_eq!(superset.contains(&subset), Ok(true));
assert_eq!(subset.contains(&superset), Ok(false));

let other_len = BitList::<2048>::with_bits(8, &[0]);
assert_eq!(
superset.contains(&other_len),
Err(BitfieldError::DifferentLength)
);
}

#[test]
fn bitlist_or() {
let a = BitList::<2048>::with_bits(4, &[0]);
let b = BitList::<2048>::with_bits(4, &[1, 3]);
assert_eq!(a.or(&b).unwrap().bit_indices(), vec![0, 1, 3]);

let other_len = BitList::<2048>::with_bits(8, &[0]);
assert_eq!(a.or(&other_len), Err(BitfieldError::DifferentLength));
}

#[test]
fn bitvector_bit_ops() {
let mut bv = BitVector::<64>::with_bits(&[0, 2]);
assert!(bv.bit_at(0));
assert!(!bv.bit_at(1));
assert_eq!(bv.bit_indices(), vec![0, 2]);

bv.set_bit_at(1, true);
assert_eq!(bv.bit_indices(), vec![0, 1, 2]);

// Out-of-range access is a no-op / reads as unset.
bv.set_bit_at(64, true);
assert!(!bv.bit_at(64));
assert_eq!(bv.bit_indices(), vec![0, 1, 2]);
}
}
Loading