Skip to main content

molpha_verifier/
onchain.rs

1//! Signer resolution and high-level verification over already-parsed registry data.
2//!
3//! These helpers are framework-agnostic: the caller is responsible for reading its registry
4//! accounts (owner checks, deserialization) and passing the plain [`RegistryView`] and
5//! [`NodeEntry`] slice. All Anchor / framework coupling stays in the downstream program.
6
7use crate::verify::verify_attestation_parts;
8use crate::{
9    bitmap::{bitmap_load, for_each_set_bit_u256},
10    secp256k1_scalar_is_valid_nonzero, verify_aggregate_over_hash, AttestationError,
11    AttestationPayload, NodeEntry, RegistryTransitionType, RegistryView, SchnorrSignature,
12    SignerXy, VIRTUAL_INDEX,
13};
14
15#[inline(always)]
16pub fn expected_node_index(bit_pos: u32, registry: &RegistryView, apply_remove_remap: bool) -> u32 {
17    if !apply_remove_remap {
18        return bit_pos;
19    }
20    if bit_pos == registry.removed_old_index {
21        VIRTUAL_INDEX
22    } else if registry.last_transition_type == RegistryTransitionType::RemoveSwap
23        && bit_pos == registry.moved_old_index
24    {
25        registry.removed_old_index
26    } else {
27        bit_pos
28    }
29}
30
31#[inline(always)]
32pub fn validate_remove_transition_for_previous(
33    registry: &RegistryView,
34) -> Result<(), AttestationError> {
35    if registry.is_remove_transition() {
36        Ok(())
37    } else {
38        Err(AttestationError::InvalidTransitionAccount)
39    }
40}
41
42/// Pair each set bit of `signers_bitmap` with its registry entry (in `entries` order), validate the
43/// node indices (applying remove-transition remapping for a live previous version), and return the
44/// effective node count plus the signer pubkeys in ascending bitmap-bit order.
45///
46/// `entries` must contain exactly one entry per set bit, in the same order the caller iterated its
47/// signer accounts, and each entry must already be owner-checked against the program.
48pub fn resolve_ordered_signers(
49    entries: &[NodeEntry],
50    registry: &RegistryView,
51    registry_version: u32,
52    signers_bitmap: &[u8; 32],
53    now: i64,
54) -> Result<(u32, Vec<SignerXy>), AttestationError> {
55    let signers = bitmap_load(signers_bitmap);
56    let signer_count = signers.count_ones();
57
58    let is_current = registry_version == registry.current_version;
59    let is_previous_live =
60        registry_version == registry.previous_version && now <= registry.previous_expires_at;
61    if !is_current && !is_previous_live {
62        return Err(AttestationError::InvalidRegistryVersion);
63    }
64
65    let node_count = if is_current {
66        registry.current_node_count
67    } else {
68        registry.previous_node_count
69    };
70
71    if entries.len() != signer_count as usize {
72        return Err(AttestationError::MissingSignerAccount);
73    }
74
75    let apply_remove_remap =
76        !is_current && registry.last_transition_type != RegistryTransitionType::Add;
77    if apply_remove_remap {
78        validate_remove_transition_for_previous(registry)?;
79    }
80
81    let mut ordered = Vec::with_capacity(signer_count as usize);
82    let mut entry_cursor = 0usize;
83    for_each_set_bit_u256(signers, |bit_pos| {
84        let bit_pos = bit_pos as u32;
85        if bit_pos >= node_count {
86            return Err(AttestationError::InvalidSignersBitmap);
87        }
88
89        let entry = entries
90            .get(entry_cursor)
91            .ok_or(AttestationError::MissingSignerAccount)?;
92        entry_cursor = entry_cursor.saturating_add(1);
93
94        let expected_index = expected_node_index(bit_pos, registry, apply_remove_remap);
95        if entry.index != expected_index {
96            return Err(AttestationError::InvalidNodeIndex);
97        }
98        ordered.push((entry.x, entry.y));
99        Ok(())
100    })?;
101
102    if entry_cursor != entries.len() {
103        return Err(AttestationError::MissingSignerAccount);
104    }
105    Ok((node_count, ordered))
106}
107
108#[allow(clippy::too_many_arguments)]
109pub fn verify_attestation_resolved(
110    payload: &AttestationPayload,
111    signature: &SchnorrSignature,
112    registry: &RegistryView,
113    redundancy_buffer: u8,
114    now: i64,
115    entries: &[NodeEntry],
116) -> Result<(), AttestationError> {
117    let (node_count, ordered) = resolve_ordered_signers(
118        entries,
119        registry,
120        payload.registry_version,
121        &signature.signers_bitmap,
122        now,
123    )?;
124    verify_attestation_parts(payload, signature, node_count, redundancy_buffer, &ordered)
125}
126
127#[allow(clippy::too_many_arguments)]
128pub fn verify_aggregate_over_hash_resolved(
129    registry: &RegistryView,
130    registry_version: u32,
131    signers_bitmap: &[u8; 32],
132    agg_sig_s: &[u8; 32],
133    commitment_addr: &[u8; 20],
134    message_hash: &[u8; 32],
135    now: i64,
136    entries: &[NodeEntry],
137) -> Result<bool, AttestationError> {
138    if !secp256k1_scalar_is_valid_nonzero(agg_sig_s) {
139        return Ok(false);
140    }
141    let (_, ordered) =
142        resolve_ordered_signers(entries, registry, registry_version, signers_bitmap, now)?;
143    verify_aggregate_over_hash(&ordered, agg_sig_s, commitment_addr, message_hash)
144}
145
146#[cfg(test)]
147mod tests {
148    use super::*;
149    use crate::bitmap::{bitmap_set_bit, for_each_set_bit};
150    use crate::fixtures::{
151        CANONICAL_TIMESTAMP, COMMITMENT, PUBKEYS, REDUNDANCY_BUFFER, REGISTERED_NODE_COUNT,
152        REGISTRY_VERSION, S, SIGNATURES_REQUIRED, SIGNERS_BITMAP, SOURCE_ID, VALUE,
153    };
154    use crate::message::compute_message_hash;
155    use libsecp256k1::{PublicKey, PublicKeyFormat};
156
157    fn pubkey_to_entry(index: u32, compressed: &[u8; 33]) -> NodeEntry {
158        let pk = PublicKey::parse_slice(compressed, Some(PublicKeyFormat::Compressed))
159            .expect("fixture pubkey must be a valid curve point");
160        let full = pk.serialize();
161        NodeEntry {
162            index,
163            x: full[1..33].try_into().unwrap(),
164            y: full[33..65].try_into().unwrap(),
165        }
166    }
167
168    fn evm_fixture_registry() -> RegistryView {
169        RegistryView {
170            current_version: REGISTRY_VERSION,
171            previous_version: REGISTRY_VERSION - 1,
172            previous_expires_at: i64::MAX,
173            current_node_count: REGISTERED_NODE_COUNT,
174            previous_node_count: REGISTERED_NODE_COUNT - 1,
175            last_transition_type: RegistryTransitionType::Add,
176            removed_old_index: VIRTUAL_INDEX,
177            moved_old_index: VIRTUAL_INDEX,
178        }
179    }
180
181    fn evm_fixture_payload() -> AttestationPayload {
182        AttestationPayload {
183            value: VALUE,
184            source_id: SOURCE_ID,
185            registry_version: REGISTRY_VERSION,
186            canonical_timestamp: CANONICAL_TIMESTAMP,
187            signatures_required: SIGNATURES_REQUIRED,
188        }
189    }
190
191    fn evm_fixture_signature() -> SchnorrSignature {
192        SchnorrSignature {
193            agg_sig_s: S,
194            commitment_addr: COMMITMENT,
195            signers_bitmap: SIGNERS_BITMAP,
196        }
197    }
198
199    fn evm_fixture_entries_current() -> Vec<NodeEntry> {
200        let mut entries = Vec::new();
201        for_each_set_bit(&SIGNERS_BITMAP, |bit_pos| {
202            entries.push(pubkey_to_entry(bit_pos as u32, &PUBKEYS[bit_pos]));
203        });
204        entries
205    }
206
207    fn remove_swap_registry() -> RegistryView {
208        RegistryView {
209            current_version: 2,
210            previous_version: 1,
211            previous_expires_at: 9_999,
212            current_node_count: 7,
213            previous_node_count: 8,
214            last_transition_type: RegistryTransitionType::RemoveSwap,
215            removed_old_index: 1,
216            moved_old_index: 3,
217        }
218    }
219
220    fn remove_tail_registry() -> RegistryView {
221        let mut registry = remove_swap_registry();
222        registry.last_transition_type = RegistryTransitionType::RemoveTail;
223        registry.removed_old_index = 2;
224        registry.moved_old_index = VIRTUAL_INDEX;
225        registry
226    }
227
228    #[test]
229    fn expected_node_index_remove_swap_remaps_removed_and_moved() {
230        let registry = remove_swap_registry();
231        assert_eq!(expected_node_index(1, &registry, true), VIRTUAL_INDEX);
232        assert_eq!(expected_node_index(3, &registry, true), 1);
233        assert_eq!(expected_node_index(0, &registry, true), 0);
234    }
235
236    #[test]
237    fn expected_node_index_remove_tail_remaps_removed_only() {
238        let registry = remove_tail_registry();
239        assert_eq!(expected_node_index(2, &registry, true), VIRTUAL_INDEX);
240        assert_eq!(expected_node_index(0, &registry, true), 0);
241    }
242
243    #[test]
244    fn expected_node_index_without_remove_remap_is_identity() {
245        let registry = remove_swap_registry();
246        assert_eq!(expected_node_index(4, &registry, false), 4);
247    }
248
249    #[test]
250    fn resolve_ordered_signers_accepts_current_version_evm_fixture() {
251        let registry = evm_fixture_registry();
252        let entries = evm_fixture_entries_current();
253        let (node_count, ordered) =
254            resolve_ordered_signers(&entries, &registry, REGISTRY_VERSION, &SIGNERS_BITMAP, 0)
255                .expect("current-version fixture must resolve");
256        assert_eq!(node_count, REGISTERED_NODE_COUNT);
257        assert_eq!(ordered.len(), entries.len());
258    }
259
260    #[test]
261    fn resolve_ordered_signers_accepts_previous_version_add_transition() {
262        let registry = RegistryView {
263            current_version: 2,
264            previous_version: 1,
265            previous_expires_at: 9_999,
266            current_node_count: 6,
267            previous_node_count: 5,
268            last_transition_type: RegistryTransitionType::Add,
269            removed_old_index: VIRTUAL_INDEX,
270            moved_old_index: VIRTUAL_INDEX,
271        };
272        let mut signers_bitmap = [0u8; 32];
273        bitmap_set_bit(&mut signers_bitmap, 3);
274
275        let entries = [NodeEntry {
276            index: 3,
277            x: [1u8; 32],
278            y: [2u8; 32],
279        }];
280
281        let (node_count, ordered) = resolve_ordered_signers(
282            &entries,
283            &registry,
284            registry.previous_version,
285            &signers_bitmap,
286            0,
287        )
288        .expect("add-transition previous version must resolve without remap");
289        assert_eq!(node_count, registry.previous_node_count);
290        assert_eq!(ordered.len(), 1);
291    }
292
293    #[test]
294    fn resolve_ordered_signers_accepts_previous_version_remove_swap() {
295        let registry = remove_swap_registry();
296        let mut signers_bitmap = [0u8; 32];
297        bitmap_set_bit(&mut signers_bitmap, 0);
298        bitmap_set_bit(&mut signers_bitmap, 3);
299
300        let entries = [
301            NodeEntry {
302                index: 0,
303                x: [1u8; 32],
304                y: [2u8; 32],
305            },
306            NodeEntry {
307                index: 1,
308                x: [3u8; 32],
309                y: [4u8; 32],
310            },
311        ];
312
313        let (node_count, ordered) = resolve_ordered_signers(
314            &entries,
315            &registry,
316            registry.previous_version,
317            &signers_bitmap,
318            0,
319        )
320        .expect("remove-swap previous version must remap moved index");
321        assert_eq!(node_count, registry.previous_node_count);
322        assert_eq!(ordered.len(), 2);
323        assert_eq!(ordered[1], (entries[1].x, entries[1].y));
324    }
325
326    #[test]
327    fn resolve_ordered_signers_accepts_previous_version_remove_tail() {
328        let registry = remove_tail_registry();
329        let mut signers_bitmap = [0u8; 32];
330        bitmap_set_bit(&mut signers_bitmap, 0);
331        bitmap_set_bit(&mut signers_bitmap, 4);
332
333        let entries = [
334            NodeEntry {
335                index: 0,
336                x: [1u8; 32],
337                y: [2u8; 32],
338            },
339            NodeEntry {
340                index: 4,
341                x: [3u8; 32],
342                y: [4u8; 32],
343            },
344        ];
345
346        let (node_count, ordered) = resolve_ordered_signers(
347            &entries,
348            &registry,
349            registry.previous_version,
350            &signers_bitmap,
351            0,
352        )
353        .expect("remove-tail previous version must resolve unaffected indices");
354        assert_eq!(node_count, registry.previous_node_count);
355        assert_eq!(ordered.len(), 2);
356    }
357
358    #[test]
359    fn resolve_ordered_signers_rejects_out_of_range_bit_on_add_previous() {
360        let registry = RegistryView {
361            current_version: 2,
362            previous_version: 1,
363            previous_expires_at: 9_999,
364            current_node_count: 6,
365            previous_node_count: 5,
366            last_transition_type: RegistryTransitionType::Add,
367            removed_old_index: VIRTUAL_INDEX,
368            moved_old_index: VIRTUAL_INDEX,
369        };
370        let mut signers_bitmap = [0u8; 32];
371        signers_bitmap[0] = 1 << 5;
372
373        let entries = [NodeEntry {
374            index: 5,
375            x: [1u8; 32],
376            y: [2u8; 32],
377        }];
378
379        let err = resolve_ordered_signers(
380            &entries,
381            &registry,
382            registry.previous_version,
383            &signers_bitmap,
384            0,
385        )
386        .unwrap_err();
387        assert_eq!(err, AttestationError::InvalidSignersBitmap);
388    }
389
390    #[test]
391    fn resolve_ordered_signers_rejects_expired_previous_version() {
392        let registry = RegistryView {
393            current_version: 2,
394            previous_version: 1,
395            previous_expires_at: 100,
396            current_node_count: 6,
397            previous_node_count: 5,
398            last_transition_type: RegistryTransitionType::Add,
399            removed_old_index: VIRTUAL_INDEX,
400            moved_old_index: VIRTUAL_INDEX,
401        };
402        let mut signers_bitmap = [0u8; 32];
403        bitmap_set_bit(&mut signers_bitmap, 2);
404
405        let entries = [NodeEntry {
406            index: 2,
407            x: [1u8; 32],
408            y: [2u8; 32],
409        }];
410
411        let err = resolve_ordered_signers(
412            &entries,
413            &registry,
414            registry.previous_version,
415            &signers_bitmap,
416            101,
417        )
418        .unwrap_err();
419        assert_eq!(err, AttestationError::InvalidRegistryVersion);
420    }
421
422    #[test]
423    fn verify_attestation_resolved_accepts_evm_fixture() {
424        let registry = evm_fixture_registry();
425        let payload = evm_fixture_payload();
426        let signature = evm_fixture_signature();
427        let entries = evm_fixture_entries_current();
428
429        verify_attestation_resolved(
430            &payload,
431            &signature,
432            &registry,
433            REDUNDANCY_BUFFER,
434            0,
435            &entries,
436        )
437        .expect("resolved-path EVM fixture must verify");
438    }
439
440    #[test]
441    fn verify_aggregate_over_hash_resolved_roundtrip() {
442        let registry = evm_fixture_registry();
443        let payload = evm_fixture_payload();
444        let signature = evm_fixture_signature();
445        let entries = evm_fixture_entries_current();
446        let message_hash = compute_message_hash(&payload, signature.signers_bitmap);
447
448        assert!(verify_aggregate_over_hash_resolved(
449            &registry,
450            REGISTRY_VERSION,
451            &signature.signers_bitmap,
452            &signature.agg_sig_s,
453            &signature.commitment_addr,
454            &message_hash,
455            0,
456            &entries,
457        )
458        .unwrap());
459    }
460
461    #[test]
462    fn verify_aggregate_over_hash_resolved_rejects_tampered_hash() {
463        let registry = evm_fixture_registry();
464        let payload = evm_fixture_payload();
465        let signature = evm_fixture_signature();
466        let entries = evm_fixture_entries_current();
467        let mut message_hash = compute_message_hash(&payload, signature.signers_bitmap);
468        message_hash[0] ^= 0xff;
469
470        assert!(!verify_aggregate_over_hash_resolved(
471            &registry,
472            REGISTRY_VERSION,
473            &signature.signers_bitmap,
474            &signature.agg_sig_s,
475            &signature.commitment_addr,
476            &message_hash,
477            0,
478            &entries,
479        )
480        .unwrap());
481    }
482}