1use solana_secp256k1_recover::secp256k1_recover;
8
9use crate::bitmap::{bitmap_is_subset_u256, bitmap_load};
10use crate::coalition::CoalitionAccumulator;
11use crate::error::AttestationError;
12use crate::message::compute_message_hash;
13use crate::payload::{Attestation, AttestationPayload, SchnorrSignature};
14use crate::scalar::{
15 eth_address_from_uncompressed_pubkey, evm_schnorr_ecdsa_inputs,
16 secp256k1_scalar_is_valid_nonzero,
17};
18use crate::selection::derive_selection_bitmap;
19
20pub type SignerXy = ([u8; 32], [u8; 32]);
22
23pub fn verify_attestation(
36 attestation: &Attestation,
37 node_count: u32,
38 redundancy_buffer: u8,
39 ordered_signers: &[SignerXy],
40) -> Result<(), AttestationError> {
41 verify_attestation_parts(
42 &attestation.payload,
43 &attestation.signature,
44 node_count,
45 redundancy_buffer,
46 ordered_signers,
47 )
48}
49
50pub fn verify_attestation_compressed(
52 attestation: &Attestation,
53 node_count: u32,
54 redundancy_buffer: u8,
55 ordered_signers_compressed: &[[u8; 33]],
56) -> Result<(), AttestationError> {
57 let xy = decompress_all(ordered_signers_compressed)?;
58 verify_attestation_parts(
59 &attestation.payload,
60 &attestation.signature,
61 node_count,
62 redundancy_buffer,
63 &xy,
64 )
65}
66
67pub(crate) fn verify_attestation_parts(
68 payload: &AttestationPayload,
69 signature: &SchnorrSignature,
70 node_count: u32,
71 redundancy_buffer: u8,
72 ordered_signers: &[SignerXy],
73) -> Result<(), AttestationError> {
74 if signature.agg_sig_s == [0u8; 32] || !secp256k1_scalar_is_valid_nonzero(&signature.agg_sig_s)
75 {
76 return Err(AttestationError::InvalidAggregateSignature);
77 }
78
79 let signers = bitmap_load(&signature.signers_bitmap);
80 let signer_count = signers.count_ones();
81 if signer_count < u32::from(payload.signatures_required) {
82 return Err(AttestationError::InsufficientSigners);
83 }
84
85 let expected_selection = derive_selection_bitmap(
86 &payload.source_id,
87 payload.registry_version,
88 payload.canonical_timestamp,
89 node_count,
90 payload.signatures_required,
91 redundancy_buffer,
92 )?;
93 if !bitmap_is_subset_u256(signers, bitmap_load(&expected_selection)) {
94 return Err(AttestationError::SignersNotSubsetOfSelection);
95 }
96
97 if ordered_signers.len() != signer_count as usize {
98 return Err(AttestationError::SignerCountMismatch);
99 }
100
101 let x_coalition = reconstruct_coalition_key(ordered_signers)?;
102 let message_hash = compute_message_hash(payload, signature.signers_bitmap);
103
104 if recover_and_match(
105 &x_coalition,
106 &message_hash,
107 &signature.agg_sig_s,
108 &signature.commitment_addr,
109 ) {
110 Ok(())
111 } else {
112 Err(AttestationError::InvalidAggregateSignature)
113 }
114}
115
116pub fn reconstruct_coalition_key(
120 ordered_signers: &[SignerXy],
121) -> Result<[u8; 33], AttestationError> {
122 if ordered_signers.is_empty() {
123 return Err(AttestationError::InvalidSignersBitmap);
124 }
125 let mut coalition = CoalitionAccumulator::default();
126 for (x, y) in ordered_signers {
127 coalition.add_stored_xy(x, y)?;
128 }
129 coalition.compressed_pubkey()
130}
131
132pub fn reconstruct_coalition_key_compressed(
134 ordered_signers_compressed: &[[u8; 33]],
135) -> Result<[u8; 33], AttestationError> {
136 let xy = decompress_all(ordered_signers_compressed)?;
137 reconstruct_coalition_key(&xy)
138}
139
140pub fn verify_aggregate_over_hash(
147 ordered_signers: &[SignerXy],
148 agg_sig_s: &[u8; 32],
149 commitment_addr: &[u8; 20],
150 message_hash: &[u8; 32],
151) -> Result<bool, AttestationError> {
152 if !secp256k1_scalar_is_valid_nonzero(agg_sig_s) {
153 return Ok(false);
154 }
155 let x_coalition = reconstruct_coalition_key(ordered_signers)?;
156 Ok(recover_and_match(
157 &x_coalition,
158 message_hash,
159 agg_sig_s,
160 commitment_addr,
161 ))
162}
163
164fn recover_and_match(
166 x_coalition: &[u8; 33],
167 message_hash: &[u8; 32],
168 agg_sig_s: &[u8; 32],
169 commitment_addr: &[u8; 20],
170) -> bool {
171 let (recovery_id, ecdsa_signature, ecdsa_hash) =
172 match evm_schnorr_ecdsa_inputs(x_coalition, message_hash, agg_sig_s, commitment_addr) {
173 Ok(v) => v,
174 Err(_) => return false,
175 };
176 let recovered = match secp256k1_recover(&ecdsa_hash, recovery_id, &ecdsa_signature) {
177 Ok(r) => r,
178 Err(_) => return false,
179 };
180 eth_address_from_uncompressed_pubkey(recovered.to_bytes()) == *commitment_addr
181}
182
183fn decompress_all(compressed: &[[u8; 33]]) -> Result<Vec<SignerXy>, AttestationError> {
184 use libsecp256k1::{PublicKey, PublicKeyFormat};
185 compressed
186 .iter()
187 .map(|c| {
188 let pk = PublicKey::parse_slice(c, Some(PublicKeyFormat::Compressed))
189 .map_err(|_| AttestationError::InvalidAggregateSignature)?;
190 let full = pk.serialize(); let x: [u8; 32] = full[1..33].try_into().unwrap();
192 let y: [u8; 32] = full[33..65].try_into().unwrap();
193 Ok((x, y))
194 })
195 .collect()
196}
197
198#[cfg(test)]
199mod tests {
200 use super::*;
201 use crate::fixtures::{
202 CANONICAL_TIMESTAMP, COMMITMENT, PUBKEYS, REDUNDANCY_BUFFER, REGISTERED_NODE_COUNT,
203 REGISTRY_VERSION, S, SIGNATURES_REQUIRED, SIGNERS_BITMAP, SIGNER_COUNT, SOURCE_ID, VALUE,
204 };
205 use crate::message::MESSAGE_PREFIX;
206 use libsecp256k1::{PublicKey, PublicKeyFormat};
207
208 fn fixture_payload() -> AttestationPayload {
209 AttestationPayload {
210 value: VALUE,
211 source_id: SOURCE_ID,
212 registry_version: REGISTRY_VERSION,
213 canonical_timestamp: CANONICAL_TIMESTAMP,
214 signatures_required: SIGNATURES_REQUIRED,
215 }
216 }
217
218 fn fixture_signature() -> SchnorrSignature {
219 SchnorrSignature {
220 agg_sig_s: S,
221 commitment_addr: COMMITMENT,
222 signers_bitmap: SIGNERS_BITMAP,
223 }
224 }
225
226 fn fixture_signers_xy() -> Vec<SignerXy> {
227 use crate::bitmap::for_each_set_bit;
228 let mut signers = Vec::new();
229 for_each_set_bit(&SIGNERS_BITMAP, |i| {
230 let c = &PUBKEYS[i];
231 let pk = PublicKey::parse_slice(c, Some(PublicKeyFormat::Compressed))
232 .expect("fixture pubkey must be a valid curve point");
233 let full = pk.serialize();
234 let x: [u8; 32] = full[1..33].try_into().unwrap();
235 let y: [u8; 32] = full[33..65].try_into().unwrap();
236 signers.push((x, y));
237 });
238 signers
239 }
240
241 fn fixture_signer_pubkeys_compressed() -> Vec<[u8; 33]> {
242 use crate::bitmap::for_each_set_bit;
243 let mut signers = Vec::new();
244 for_each_set_bit(&SIGNERS_BITMAP, |i| {
245 signers.push(PUBKEYS[i]);
246 });
247 signers
248 }
249
250 #[test]
251 fn fixture_pubkeys_are_valid_curve_points() {
252 for (i, pk) in PUBKEYS.iter().enumerate() {
253 PublicKey::parse_slice(pk, Some(PublicKeyFormat::Compressed))
254 .unwrap_or_else(|_| panic!("fixture pubkey {i} is not a valid curve point"));
255 }
256 }
257
258 #[test]
259 fn fixture_signers_bitmap_popcount_meets_threshold() {
260 use crate::bitmap::bitmap_popcount_evm;
261 let popcount = bitmap_popcount_evm(&SIGNERS_BITMAP);
262 assert_eq!(popcount, SIGNER_COUNT);
263 assert!(popcount >= u32::from(SIGNATURES_REQUIRED));
264 }
265
266 #[test]
268 fn reconstruct_coalition_key_matches_combine() {
269 let signer_pubkeys = fixture_signer_pubkeys_compressed();
270 let pks: Vec<PublicKey> = signer_pubkeys
271 .iter()
272 .map(|c| PublicKey::parse_slice(c, Some(PublicKeyFormat::Compressed)).unwrap())
273 .collect();
274 let combined = PublicKey::combine(&pks).unwrap().serialize_compressed();
275 let got = reconstruct_coalition_key(&fixture_signers_xy()).unwrap();
276 assert_eq!(got, combined);
277 let got_c = reconstruct_coalition_key_compressed(&signer_pubkeys).unwrap();
278 assert_eq!(got_c, combined);
279 }
280
281 fn fixture_attestation() -> Attestation {
282 Attestation {
283 payload: fixture_payload(),
284 signature: fixture_signature(),
285 }
286 }
287
288 #[test]
290 fn verify_attestation_accepts_evm_fixture() {
291 let attestation = fixture_attestation();
292 let signer_pubkeys = fixture_signer_pubkeys_compressed();
293 verify_attestation(
294 &attestation,
295 REGISTERED_NODE_COUNT,
296 REDUNDANCY_BUFFER,
297 &fixture_signers_xy(),
298 )
299 .expect("fixture attestation must verify");
300 verify_attestation_compressed(
301 &attestation,
302 REGISTERED_NODE_COUNT,
303 REDUNDANCY_BUFFER,
304 &signer_pubkeys,
305 )
306 .expect("compressed variant must verify");
307 }
308
309 #[test]
310 fn tampered_s_fails_verification() {
311 let mut attestation = fixture_attestation();
312 attestation.signature.agg_sig_s[31] ^= 0x01;
313 let res = verify_attestation(
314 &attestation,
315 REGISTERED_NODE_COUNT,
316 REDUNDANCY_BUFFER,
317 &fixture_signers_xy(),
318 );
319 assert_eq!(res, Err(AttestationError::InvalidAggregateSignature));
320 }
321
322 #[test]
323 fn wrong_signer_count_is_rejected() {
324 let attestation = fixture_attestation();
325 let mut signers = fixture_signers_xy();
326 signers.pop();
327 assert_eq!(
328 verify_attestation(
329 &attestation,
330 REGISTERED_NODE_COUNT,
331 REDUNDANCY_BUFFER,
332 &signers,
333 ),
334 Err(AttestationError::SignerCountMismatch)
335 );
336 }
337
338 #[test]
339 fn verify_aggregate_over_hash_roundtrip() {
340 let payload = fixture_payload();
341 let signature = fixture_signature();
342 let signers = fixture_signers_xy();
343 let message_hash = compute_message_hash(&payload, signature.signers_bitmap);
344 assert!(verify_aggregate_over_hash(
345 &signers,
346 &signature.agg_sig_s,
347 &signature.commitment_addr,
348 &message_hash,
349 )
350 .unwrap());
351
352 let mut bad_hash = message_hash;
354 bad_hash[0] ^= 0xff;
355 assert!(!verify_aggregate_over_hash(
356 &signers,
357 &signature.agg_sig_s,
358 &signature.commitment_addr,
359 &bad_hash,
360 )
361 .unwrap());
362 }
363
364 #[test]
365 fn message_prefix_matches_known_constant() {
366 assert_eq!(MESSAGE_PREFIX[0], 0xa7);
368 }
369}