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
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ docker-build: ## 🐳 Build the Docker image
@echo

# 2026-04-29
LEAN_SPEC_COMMIT_HASH:=495c29d49f2b12b7cc240c4028e15d4253a7d54e
LEAN_SPEC_COMMIT_HASH:=18fe71fee49f8865a5c8a4cb8b1787b0cbc9e25b

leanSpec:
git clone https://github.com/leanEthereum/leanSpec.git --single-branch
Expand Down
133 changes: 133 additions & 0 deletions crates/common/types/src/attestation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,22 @@ pub fn validator_indices(bits: &AggregationBits) -> impl Iterator<Item = u64> +
})
}

/// Returns `true` iff every bit set in `a` is also set in `b` (i.e., `a` is a subset of `b`).
pub fn bits_is_subset(a: &AggregationBits, b: &AggregationBits) -> bool {
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.

it would be very nice to have tests for this function

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Added in ab80e2e

let a_bytes = a.as_bytes();
let b_bytes = b.as_bytes();
for (i, &a_byte) in a_bytes.iter().enumerate() {
if a_byte == 0 {
continue;
}
let b_byte = b_bytes.get(i).copied().unwrap_or(0);
if a_byte & !b_byte != 0 {
return false;
}
}
true
}

/// Aggregated attestation with its signature proof, used for gossip on the aggregation topic.
#[derive(Debug, Clone, SszEncode, SszDecode, HashTreeRoot)]
pub struct SignedAggregatedAttestation {
Expand Down Expand Up @@ -130,3 +146,120 @@ impl From<AttestationData> for HashedAttestationData {
Self::new(data)
}
}

#[cfg(test)]
mod tests {
use super::*;

/// Build an `AggregationBits` of `len` bits with the indices in `set` flipped on.
fn bits(len: usize, set: &[usize]) -> AggregationBits {
let mut b = AggregationBits::with_length(len).unwrap();
for &i in set {
b.set(i, true).unwrap();
}
b
}

#[test]
fn subset_empty_bitlists() {
let empty = AggregationBits::new();
assert!(bits_is_subset(&empty, &empty));
}

#[test]
fn subset_empty_a_is_subset_of_anything() {
let empty = AggregationBits::new();
let b = bits(8, &[0, 3, 7]);
assert!(bits_is_subset(&empty, &b));
}

#[test]
fn subset_zero_bits_a_is_subset_of_empty() {
// a has length but no set bits: byte iteration skips zero bytes, so it's a subset of empty.
let a = bits(8, &[]);
let empty = AggregationBits::new();
assert!(bits_is_subset(&a, &empty));
}

#[test]
fn subset_nonempty_a_is_not_subset_of_empty() {
let a = bits(8, &[2]);
let empty = AggregationBits::new();
assert!(!bits_is_subset(&a, &empty));
}

#[test]
fn subset_reflexive_equal_bitlists() {
let a = bits(16, &[0, 1, 5, 9, 15]);
let b = bits(16, &[0, 1, 5, 9, 15]);
assert!(bits_is_subset(&a, &b));
assert!(bits_is_subset(&b, &a));
}

#[test]
fn subset_strict_subset_returns_true() {
let a = bits(8, &[1, 4]);
let b = bits(8, &[1, 4, 6]);
assert!(bits_is_subset(&a, &b));
assert!(!bits_is_subset(&b, &a));
}

#[test]
fn subset_disjoint_bits_returns_false() {
let a = bits(8, &[0, 2]);
let b = bits(8, &[1, 3]);
assert!(!bits_is_subset(&a, &b));
assert!(!bits_is_subset(&b, &a));
}

#[test]
fn subset_partial_overlap_returns_false() {
// a shares bit 1 with b but also has bit 5 that b lacks.
let a = bits(8, &[1, 5]);
let b = bits(8, &[0, 1, 2]);
assert!(!bits_is_subset(&a, &b));
}

#[test]
fn subset_a_shorter_than_b_with_bits_in_b() {
// a has 8 bits, b has 16 bits. a's set bits are all present in b.
let a = bits(8, &[1, 4]);
let b = bits(16, &[1, 4, 11]);
assert!(bits_is_subset(&a, &b));
}

#[test]
fn subset_a_longer_than_b_with_zero_tail_is_subset() {
// a has 16 bits but only sets bit 2; b has 8 bits with the same bit set.
// a's tail byte is zero, so the loop skips it.
let a = bits(16, &[2]);
let b = bits(8, &[2]);
assert!(bits_is_subset(&a, &b));
}

#[test]
fn subset_a_longer_than_b_with_set_bit_past_b_returns_false() {
// a sets bit 9 (byte 1) but b only has 8 bits (1 byte). Missing bytes in b are
// treated as zero, so any bit in a's tail breaks the subset relation.
let a = bits(16, &[9]);
let b = bits(8, &[]);
assert!(!bits_is_subset(&a, &b));
}

#[test]
fn subset_multi_byte_bitlists() {
// Spans multiple bytes (24 bits = 3 bytes) to exercise the byte-by-byte loop.
let a = bits(24, &[0, 8, 16]);
let b = bits(24, &[0, 1, 8, 9, 16, 17]);
assert!(bits_is_subset(&a, &b));
assert!(!bits_is_subset(&b, &a));
}

#[test]
fn subset_violation_in_later_byte_returns_false() {
// a's first byte matches b, but bit 17 (in byte 2) is set in a only.
let a = bits(24, &[0, 1, 17]);
let b = bits(24, &[0, 1, 16]);
assert!(!bits_is_subset(&a, &b));
}
}
Loading