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