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::{
8    bitmap::{bitmap_load, for_each_set_bit_u256},
9    secp256k1_scalar_is_valid_nonzero, verify_aggregate_over_hash, verify_data_update, DataUpdate,
10    DataUpdateError, NodeEntry, RegistryTransitionType, RegistryView, SchnorrSignature, SignerXy,
11    VIRTUAL_INDEX,
12};
13
14#[inline(always)]
15pub fn expected_node_index(bit_pos: u32, registry: &RegistryView, apply_remove_remap: bool) -> u32 {
16    if !apply_remove_remap {
17        return bit_pos;
18    }
19    if bit_pos == registry.removed_old_index {
20        VIRTUAL_INDEX
21    } else if registry.last_transition_type == RegistryTransitionType::RemoveSwap
22        && bit_pos == registry.moved_old_index
23    {
24        registry.removed_old_index
25    } else {
26        bit_pos
27    }
28}
29
30#[inline(always)]
31pub fn validate_remove_transition_for_previous(
32    registry: &RegistryView,
33) -> Result<(), DataUpdateError> {
34    if registry.is_remove_transition() {
35        Ok(())
36    } else {
37        Err(DataUpdateError::InvalidTransitionAccount)
38    }
39}
40
41/// Pair each set bit of `signers_bitmap` with its registry entry (in `entries` order), validate the
42/// node indices (applying remove-transition remapping for a live previous version), and return the
43/// effective node count plus the signer pubkeys in ascending bitmap-bit order.
44///
45/// `entries` must contain exactly one entry per set bit, in the same order the caller iterated its
46/// signer accounts, and each entry must already be owner-checked against the program.
47pub fn resolve_ordered_signers(
48    entries: &[NodeEntry],
49    registry: &RegistryView,
50    registry_version: u32,
51    signers_bitmap: &[u8; 32],
52    now: i64,
53) -> Result<(u32, Vec<SignerXy>), DataUpdateError> {
54    let signers = bitmap_load(signers_bitmap);
55    let signer_count = signers.count_ones();
56
57    let is_current = registry_version == registry.current_version;
58    let is_previous_live =
59        registry_version == registry.previous_version && now <= registry.previous_expires_at;
60    if !is_current && !is_previous_live {
61        return Err(DataUpdateError::InvalidRegistryVersion);
62    }
63
64    let node_count = if is_current {
65        registry.current_node_count
66    } else {
67        registry.previous_node_count
68    };
69
70    if entries.len() != signer_count as usize {
71        return Err(DataUpdateError::MissingSignerAccount);
72    }
73
74    let apply_remove_remap =
75        !is_current && registry.last_transition_type != RegistryTransitionType::Add;
76    if apply_remove_remap {
77        validate_remove_transition_for_previous(registry)?;
78    }
79
80    let mut ordered = Vec::with_capacity(signer_count as usize);
81    let mut entry_cursor = 0usize;
82    for_each_set_bit_u256(signers, |bit_pos| {
83        let bit_pos = bit_pos as u32;
84        if bit_pos >= node_count {
85            return Err(DataUpdateError::InvalidSignersBitmap);
86        }
87
88        let entry = entries
89            .get(entry_cursor)
90            .ok_or(DataUpdateError::MissingSignerAccount)?;
91        entry_cursor = entry_cursor.saturating_add(1);
92
93        let expected_index = expected_node_index(bit_pos, registry, apply_remove_remap);
94        if entry.index != expected_index {
95            return Err(DataUpdateError::InvalidNodeIndex);
96        }
97        ordered.push((entry.x, entry.y));
98        Ok(())
99    })?;
100
101    if entry_cursor != entries.len() {
102        return Err(DataUpdateError::MissingSignerAccount);
103    }
104    Ok((node_count, ordered))
105}
106
107#[allow(clippy::too_many_arguments)]
108pub fn verify_data_update_resolved(
109    payload: &DataUpdate,
110    signature: &SchnorrSignature,
111    registry: &RegistryView,
112    redundancy_buffer: u8,
113    now: i64,
114    entries: &[NodeEntry],
115) -> Result<(), DataUpdateError> {
116    let (node_count, ordered) = resolve_ordered_signers(
117        entries,
118        registry,
119        payload.registry_version,
120        &signature.signers_bitmap,
121        now,
122    )?;
123    verify_data_update(payload, signature, node_count, redundancy_buffer, &ordered)
124}
125
126#[allow(clippy::too_many_arguments)]
127pub fn verify_aggregate_over_hash_resolved(
128    registry: &RegistryView,
129    registry_version: u32,
130    signers_bitmap: &[u8; 32],
131    agg_sig_s: &[u8; 32],
132    commitment_addr: &[u8; 20],
133    message_hash: &[u8; 32],
134    now: i64,
135    entries: &[NodeEntry],
136) -> Result<bool, DataUpdateError> {
137    if !secp256k1_scalar_is_valid_nonzero(agg_sig_s) {
138        return Ok(false);
139    }
140    let (_, ordered) =
141        resolve_ordered_signers(entries, registry, registry_version, signers_bitmap, now)?;
142    verify_aggregate_over_hash(&ordered, agg_sig_s, commitment_addr, message_hash)
143}
144
145#[cfg(test)]
146mod tests {
147    use super::*;
148
149    fn remove_swap_registry() -> RegistryView {
150        RegistryView {
151            current_version: 2,
152            previous_version: 1,
153            previous_expires_at: 9_999,
154            current_node_count: 7,
155            previous_node_count: 8,
156            last_transition_type: RegistryTransitionType::RemoveSwap,
157            removed_old_index: 1,
158            moved_old_index: 3,
159        }
160    }
161
162    #[test]
163    fn expected_node_index_remove_swap_remaps_removed_and_moved() {
164        let registry = remove_swap_registry();
165        assert_eq!(expected_node_index(1, &registry, true), VIRTUAL_INDEX);
166        assert_eq!(expected_node_index(3, &registry, true), 1);
167        assert_eq!(expected_node_index(0, &registry, true), 0);
168    }
169
170    #[test]
171    fn expected_node_index_remove_tail_remaps_removed_only() {
172        let mut registry = remove_swap_registry();
173        registry.last_transition_type = RegistryTransitionType::RemoveTail;
174        registry.removed_old_index = 2;
175        registry.moved_old_index = VIRTUAL_INDEX;
176        assert_eq!(expected_node_index(2, &registry, true), VIRTUAL_INDEX);
177        assert_eq!(expected_node_index(0, &registry, true), 0);
178    }
179
180    #[test]
181    fn expected_node_index_without_remove_remap_is_identity() {
182        let registry = remove_swap_registry();
183        assert_eq!(expected_node_index(4, &registry, false), 4);
184    }
185
186    #[test]
187    fn resolve_ordered_signers_rejects_out_of_range_bit_on_add_previous() {
188        let registry = RegistryView {
189            current_version: 2,
190            previous_version: 1,
191            previous_expires_at: 9_999,
192            current_node_count: 6,
193            previous_node_count: 5,
194            last_transition_type: RegistryTransitionType::Add,
195            removed_old_index: VIRTUAL_INDEX,
196            moved_old_index: VIRTUAL_INDEX,
197        };
198        let mut signers_bitmap = [0u8; 32];
199        signers_bitmap[0] = 1 << 5;
200
201        let entries = [NodeEntry {
202            index: 5,
203            x: [1u8; 32],
204            y: [2u8; 32],
205        }];
206
207        let err = resolve_ordered_signers(
208            &entries,
209            &registry,
210            registry.previous_version,
211            &signers_bitmap,
212            0,
213        )
214        .unwrap_err();
215        assert_eq!(err, DataUpdateError::InvalidSignersBitmap);
216    }
217}