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
150    fn remove_swap_registry() -> RegistryView {
151        RegistryView {
152            current_version: 2,
153            previous_version: 1,
154            previous_expires_at: 9_999,
155            current_node_count: 7,
156            previous_node_count: 8,
157            last_transition_type: RegistryTransitionType::RemoveSwap,
158            removed_old_index: 1,
159            moved_old_index: 3,
160        }
161    }
162
163    #[test]
164    fn expected_node_index_remove_swap_remaps_removed_and_moved() {
165        let registry = remove_swap_registry();
166        assert_eq!(expected_node_index(1, &registry, true), VIRTUAL_INDEX);
167        assert_eq!(expected_node_index(3, &registry, true), 1);
168        assert_eq!(expected_node_index(0, &registry, true), 0);
169    }
170
171    #[test]
172    fn expected_node_index_remove_tail_remaps_removed_only() {
173        let mut registry = remove_swap_registry();
174        registry.last_transition_type = RegistryTransitionType::RemoveTail;
175        registry.removed_old_index = 2;
176        registry.moved_old_index = VIRTUAL_INDEX;
177        assert_eq!(expected_node_index(2, &registry, true), VIRTUAL_INDEX);
178        assert_eq!(expected_node_index(0, &registry, true), 0);
179    }
180
181    #[test]
182    fn expected_node_index_without_remove_remap_is_identity() {
183        let registry = remove_swap_registry();
184        assert_eq!(expected_node_index(4, &registry, false), 4);
185    }
186
187    #[test]
188    fn resolve_ordered_signers_rejects_out_of_range_bit_on_add_previous() {
189        let registry = RegistryView {
190            current_version: 2,
191            previous_version: 1,
192            previous_expires_at: 9_999,
193            current_node_count: 6,
194            previous_node_count: 5,
195            last_transition_type: RegistryTransitionType::Add,
196            removed_old_index: VIRTUAL_INDEX,
197            moved_old_index: VIRTUAL_INDEX,
198        };
199        let mut signers_bitmap = [0u8; 32];
200        signers_bitmap[0] = 1 << 5;
201
202        let entries = [NodeEntry {
203            index: 5,
204            x: [1u8; 32],
205            y: [2u8; 32],
206        }];
207
208        let err = resolve_ordered_signers(
209            &entries,
210            &registry,
211            registry.previous_version,
212            &signers_bitmap,
213            0,
214        )
215        .unwrap_err();
216        assert_eq!(err, AttestationError::InvalidSignersBitmap);
217    }
218}