Skip to main content

molpha_verifier/
bitmap.rs

1//! EVM-compatible `uint256` bitmap operations and deterministic selection-group derivation.
2//!
3//! Pure, anchor-free. Moved verbatim from the Molpha program's `utils/bitmap.rs`
4//! (`MolphaError` → [`DataUpdateError`]).
5
6use ethnum::U256;
7use solana_keccak_hasher::hashv;
8
9use crate::error::DataUpdateError;
10
11/// `keccak256("MOLPHA_SELECTION_DERIVE")` — EVM `NodeGroupBitmapLib.SELECTION_DOMAIN`.
12const SELECTION_DOMAIN: [u8; 32] = [
13    0x49, 0x28, 0x48, 0xfe, 0x5e, 0x85, 0xd4, 0xce, 0x22, 0x31, 0xd6, 0x93, 0xa5, 0x8f, 0x08, 0x20,
14    0xa4, 0x05, 0x6e, 0x28, 0x22, 0xfe, 0x5d, 0xca, 0xd7, 0xc7, 0x56, 0xaf, 0xe0, 0x44, 0xb7, 0x0b,
15];
16
17/// Max Keccak-256 rounds (bounded compute); Solidity loop is unbounded.
18const DERIVE_GROUP_BITMAP_MAX_ROUNDS: u64 = 65_536;
19
20/// EVM `uint256` bitmap loaded from Solidity `bytes32` (big-endian).
21#[inline]
22pub fn bitmap_load(bytes: &[u8; 32]) -> U256 {
23    U256::from_be_bytes(*bytes)
24}
25
26/// Serialize an EVM `uint256` bitmap to `bytes32` for storage / hashing.
27#[inline]
28pub fn bitmap_store(value: U256) -> [u8; 32] {
29    value.to_be_bytes()
30}
31
32/// Solidity `uint256` layout: bit `pos` has weight `1 << pos`, `pos == 0` is the integer LSB.
33#[inline]
34pub fn bitmap_bit_set(bitmap: &[u8; 32], pos: usize) -> bool {
35    debug_assert!(pos < 256);
36    (bitmap_load(bitmap) >> pos) & U256::from(1u8) != U256::ZERO
37}
38
39#[inline]
40pub fn bitmap_set_bit(bitmap: &mut [u8; 32], pos: usize) {
41    debug_assert!(pos < 256);
42    let mut v = bitmap_load(bitmap);
43    v |= U256::from(1u8) << pos;
44    *bitmap = bitmap_store(v);
45}
46
47#[inline]
48pub fn bitmap_clear_bit(bitmap: &mut [u8; 32], pos: usize) {
49    debug_assert!(pos < 256);
50    let mut v = bitmap_load(bitmap);
51    v &= !(U256::from(1u8) << pos);
52    *bitmap = bitmap_store(v);
53}
54
55pub fn bitmap_popcount_evm(bitmap: &[u8; 32]) -> u32 {
56    bitmap_load(bitmap).count_ones()
57}
58
59/// `sub ⊆ sup` when both are already loaded as `U256`.
60#[inline]
61pub fn bitmap_is_subset_u256(sub: U256, sup: U256) -> bool {
62    (sub & !sup) == U256::ZERO
63}
64
65/// Reject signers with bits outside `[0, node_count)`.
66pub fn validate_bitmap_upper_bits_clear_u256(
67    bitmap: U256,
68    node_count: u32,
69) -> Result<(), DataUpdateError> {
70    if node_count > 256 {
71        return Err(DataUpdateError::InvalidSignersBitmap);
72    }
73    let mask = if node_count == 256 {
74        U256::MAX
75    } else {
76        (U256::from(1u8) << node_count) - U256::from(1u8)
77    };
78    if (bitmap & !mask) != U256::ZERO {
79        return Err(DataUpdateError::InvalidSignersBitmap);
80    }
81    Ok(())
82}
83
84/// Visit set bits in ascending order; returns the peeled bitmap (zero when fully consumed).
85pub fn for_each_set_bit_u256<F, E>(mut bm: U256, mut f: F) -> Result<U256, E>
86where
87    F: FnMut(usize) -> Result<(), E>,
88{
89    while bm != U256::ZERO {
90        let bit_pos = bm.trailing_zeros() as usize;
91        bm &= bm - U256::from(1u8);
92        f(bit_pos)?;
93    }
94    Ok(bm)
95}
96
97/// Bitmap **bit index** of the signer at 0-based rank `pos` when signers are ordered by ascending
98/// bit index (same order as EVM `Validator.verify` combines pubkeys).
99pub fn get_index(bitmap: &[u8; 32], pos: usize) -> Option<usize> {
100    let mut bm = bitmap_load(bitmap);
101    let mut rank = 0usize;
102    while bm != U256::ZERO {
103        let bit_pos = bm.trailing_zeros() as usize;
104        if rank == pos {
105            return Some(bit_pos);
106        }
107        bm &= bm - U256::from(1u8);
108        rank += 1;
109    }
110    None
111}
112
113/// Iterate set bit positions in ascending order (EVM signer order).
114pub fn for_each_set_bit<F>(bitmap: &[u8; 32], mut f: F)
115where
116    F: FnMut(usize),
117{
118    let mut bm = bitmap_load(bitmap);
119    while bm != U256::ZERO {
120        let bit_pos = bm.trailing_zeros() as usize;
121        f(bit_pos);
122        bm &= bm - U256::from(1u8);
123    }
124}
125
126fn u256_be_from_u64(counter: u64) -> [u8; 32] {
127    let mut out = [0u8; 32];
128    out[24..32].copy_from_slice(&counter.to_be_bytes());
129    out
130}
131
132/// `keccak256(seed || SELECTION_DOMAIN || counter_be)` — uses `sol_keccak256` on BPF.
133fn selection_hash_round(seed: &[u8; 32], counter: u64) -> [u8; 32] {
134    let counter_word = u256_be_from_u64(counter);
135    hashv(&[seed.as_ref(), &SELECTION_DOMAIN, &counter_word]).to_bytes()
136}
137
138#[inline]
139fn full_mask_u256(node_count: u32) -> U256 {
140    if node_count == 256 {
141        U256::MAX
142    } else {
143        (U256::from(1u8) << node_count) - U256::from(1u8)
144    }
145}
146
147/// Without-replacement sampling — port of EVM `_sampleWithoutReplacement`.
148fn sample_without_replacement(
149    seed: &[u8; 32],
150    node_count: u32,
151    group_size: u32,
152) -> Result<U256, DataUpdateError> {
153    let limit = (u64::from(u32::MAX) / u64::from(node_count)) * u64::from(node_count);
154    let mut bitmap = U256::ZERO;
155    let mut selected = 0u32;
156    let mut counter = 0u64;
157
158    while selected < group_size {
159        if counter >= DERIVE_GROUP_BITMAP_MAX_ROUNDS {
160            return Err(DataUpdateError::GroupBitmapDerivationFailed);
161        }
162
163        let digest = selection_hash_round(seed, counter);
164        counter += 1;
165
166        let mut word = bitmap_load(&digest);
167        for _ in 0..8 {
168            if selected >= group_size {
169                break;
170            }
171
172            let limb = u32::try_from(word >> 224).unwrap_or(u32::MAX);
173            word <<= 32;
174
175            if u64::from(limb) < limit {
176                let pos = (limb % node_count) as usize;
177                let bit = U256::from(1u8) << pos;
178                if (bitmap & bit) == U256::ZERO {
179                    bitmap |= bit;
180                    selected += 1;
181                }
182            }
183        }
184    }
185
186    Ok(bitmap)
187}
188
189/// Port of `NodeGroupBitmapLib.derive` from the EVM reference.
190pub fn derive_group_bitmap(
191    seed: &[u8; 32],
192    node_count: u32,
193    group_size: u32,
194) -> Result<[u8; 32], DataUpdateError> {
195    if node_count == 0 {
196        return Err(DataUpdateError::GroupBitmapDerivationFailed);
197    }
198    if node_count > 256 {
199        return Err(DataUpdateError::GroupBitmapDerivationFailed);
200    }
201    if group_size > node_count {
202        return Err(DataUpdateError::GroupBitmapDerivationFailed);
203    }
204    if group_size == 0 {
205        return Ok([0u8; 32]);
206    }
207    if group_size == node_count {
208        return Ok(bitmap_store(full_mask_u256(node_count)));
209    }
210
211    let bitmap = if group_size > node_count / 2 {
212        let excluded = sample_without_replacement(seed, node_count, node_count - group_size)?;
213        full_mask_u256(node_count) ^ excluded
214    } else {
215        sample_without_replacement(seed, node_count, group_size)?
216    };
217
218    Ok(bitmap_store(bitmap))
219}
220
221pub fn bitmap_is_subset(sub: &[u8; 32], sup: &[u8; 32]) -> bool {
222    let sub = bitmap_load(sub);
223    let sup = bitmap_load(sup);
224    (sub & !sup) == U256::ZERO
225}
226
227/// Selection slot count: `min(node_count, signatures_required + redundancy_buffer)`.
228#[inline]
229pub fn effective_selection_size(
230    signatures_required: u8,
231    redundancy_buffer: u8,
232    node_count: u32,
233) -> u32 {
234    (signatures_required as u32)
235        .saturating_add(redundancy_buffer as u32)
236        .min(node_count)
237}
238
239pub fn validate_bitmap_upper_bits_clear(
240    bitmap: &[u8; 32],
241    node_count: u32,
242) -> Result<(), DataUpdateError> {
243    validate_bitmap_upper_bits_clear_u256(bitmap_load(bitmap), node_count)
244}
245
246#[cfg(test)]
247mod tests {
248    use super::*;
249
250    #[test]
251    fn get_index_orders_signers_by_ascending_bit() {
252        // EVM uint256 `7` => bits 0,1,2 set.
253        let bm: [u8; 32] = [
254            0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
255            0, 0, 0x07,
256        ];
257        assert_eq!(get_index(&bm, 0), Some(0));
258        assert_eq!(get_index(&bm, 1), Some(1));
259        assert_eq!(get_index(&bm, 2), Some(2));
260        assert_eq!(get_index(&bm, 3), None);
261    }
262
263    #[test]
264    fn get_index_skips_gaps() {
265        let mut bm = [0u8; 32];
266        bitmap_set_bit(&mut bm, 5);
267        bitmap_set_bit(&mut bm, 10);
268        assert_eq!(get_index(&bm, 0), Some(5));
269        assert_eq!(get_index(&bm, 1), Some(10));
270        assert_eq!(get_index(&bm, 2), None);
271    }
272
273    #[test]
274    fn validate_bitmap_upper_bits_clear_rejects_high_bits() {
275        let mut bm = [0u8; 32];
276        bitmap_set_bit(&mut bm, 10);
277        assert!(validate_bitmap_upper_bits_clear(&bm, 8).is_err());
278        assert!(validate_bitmap_upper_bits_clear(&bm, 11).is_ok());
279    }
280
281    #[test]
282    fn u256_bit_ops_match_byte_layout() {
283        let mut bm = [0u8; 32];
284        bitmap_set_bit(&mut bm, 0);
285        bitmap_set_bit(&mut bm, 7);
286        bitmap_set_bit(&mut bm, 255);
287        assert!(bitmap_bit_set(&bm, 0));
288        assert!(bitmap_bit_set(&bm, 7));
289        assert!(bitmap_bit_set(&bm, 255));
290        assert!(!bitmap_bit_set(&bm, 1));
291        assert_eq!(bitmap_popcount_evm(&bm), 3);
292    }
293
294    #[test]
295    fn effective_selection_size_caps_at_node_count() {
296        assert_eq!(effective_selection_size(8, 2, 8), 8);
297        assert_eq!(effective_selection_size(5, 2, 8), 7);
298        assert_eq!(effective_selection_size(10, 2, 8), 8);
299    }
300
301    #[test]
302    fn derive_group_bitmap_matches_evm_reference_vectors() {
303        let seed = [0x11u8; 32];
304        let cases: &[(u32, u32, &str)] = &[
305            (
306                8,
307                3,
308                "0000000000000000000000000000000000000000000000000000000000000038",
309            ),
310            (
311                10,
312                7,
313                "00000000000000000000000000000000000000000000000000000000000003d3",
314            ),
315            (
316                16,
317                5,
318                "0000000000000000000000000000000000000000000000000000000000002c30",
319            ),
320            (
321                32,
322                20,
323                "00000000000000000000000000000000000000000000000000000000d7ddb3a1",
324            ),
325        ];
326        for (node_count, group_size, want_hex) in cases {
327            let got = derive_group_bitmap(&seed, *node_count, *group_size).unwrap();
328            let want = hex_to_bytes32(want_hex);
329            assert_eq!(got, want, "n={node_count} g={group_size}");
330        }
331    }
332
333    #[test]
334    fn derive_group_bitmap_complement_path_matches_direct_sample() {
335        let seed = [0x22u8; 32];
336        let node_count = 10u32;
337        let group_size = 7u32;
338        let direct = derive_group_bitmap(&seed, node_count, group_size).unwrap();
339        let excluded = derive_group_bitmap(&seed, node_count, node_count - group_size).unwrap();
340        let mut full = [0u8; 32];
341        for pos in 0..node_count as usize {
342            bitmap_set_bit(&mut full, pos);
343        }
344        let excluded_bm = bitmap_load(&excluded);
345        let complement = bitmap_store(bitmap_load(&full) ^ excluded_bm);
346        assert_eq!(direct, complement);
347    }
348
349    fn hex_to_bytes32(hex: &str) -> [u8; 32] {
350        let hex = hex.strip_prefix("0x").unwrap_or(hex);
351        let mut out = [0u8; 32];
352        for (i, chunk) in hex.as_bytes().chunks(2).enumerate() {
353            let s = std::str::from_utf8(chunk).unwrap();
354            out[i] = u8::from_str_radix(s, 16).unwrap();
355        }
356        out
357    }
358}