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 < 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(
103 payload,
104 signature.signers_bitmap,
105 payload.signatures_required,
106 );
107
108 if recover_and_match(
109 &x_coalition,
110 &message_hash,
111 &signature.agg_sig_s,
112 &signature.commitment_addr,
113 ) {
114 Ok(())
115 } else {
116 Err(AttestationError::InvalidAggregateSignature)
117 }
118}
119
120pub fn reconstruct_coalition_key(
124 ordered_signers: &[SignerXy],
125) -> Result<[u8; 33], AttestationError> {
126 if ordered_signers.is_empty() {
127 return Err(AttestationError::InvalidSignersBitmap);
128 }
129 let mut coalition = CoalitionAccumulator::default();
130 for (x, y) in ordered_signers {
131 coalition.add_stored_xy(x, y)?;
132 }
133 coalition.compressed_pubkey()
134}
135
136pub fn reconstruct_coalition_key_compressed(
138 ordered_signers_compressed: &[[u8; 33]],
139) -> Result<[u8; 33], AttestationError> {
140 let xy = decompress_all(ordered_signers_compressed)?;
141 reconstruct_coalition_key(&xy)
142}
143
144pub fn verify_aggregate_over_hash(
151 ordered_signers: &[SignerXy],
152 agg_sig_s: &[u8; 32],
153 commitment_addr: &[u8; 20],
154 message_hash: &[u8; 32],
155) -> Result<bool, AttestationError> {
156 if !secp256k1_scalar_is_valid_nonzero(agg_sig_s) {
157 return Ok(false);
158 }
159 let x_coalition = reconstruct_coalition_key(ordered_signers)?;
160 Ok(recover_and_match(
161 &x_coalition,
162 message_hash,
163 agg_sig_s,
164 commitment_addr,
165 ))
166}
167
168fn recover_and_match(
170 x_coalition: &[u8; 33],
171 message_hash: &[u8; 32],
172 agg_sig_s: &[u8; 32],
173 commitment_addr: &[u8; 20],
174) -> bool {
175 let (recovery_id, ecdsa_signature, ecdsa_hash) =
176 match evm_schnorr_ecdsa_inputs(x_coalition, message_hash, agg_sig_s, commitment_addr) {
177 Ok(v) => v,
178 Err(_) => return false,
179 };
180 let recovered = match secp256k1_recover(&ecdsa_hash, recovery_id, &ecdsa_signature) {
181 Ok(r) => r,
182 Err(_) => return false,
183 };
184 eth_address_from_uncompressed_pubkey(recovered.to_bytes()) == *commitment_addr
185}
186
187fn decompress_all(compressed: &[[u8; 33]]) -> Result<Vec<SignerXy>, AttestationError> {
188 use libsecp256k1::{PublicKey, PublicKeyFormat};
189 compressed
190 .iter()
191 .map(|c| {
192 let pk = PublicKey::parse_slice(c, Some(PublicKeyFormat::Compressed))
193 .map_err(|_| AttestationError::InvalidAggregateSignature)?;
194 let full = pk.serialize(); let x: [u8; 32] = full[1..33].try_into().unwrap();
196 let y: [u8; 32] = full[33..65].try_into().unwrap();
197 Ok((x, y))
198 })
199 .collect()
200}
201
202#[cfg(test)]
203mod tests {
204 use super::*;
205 use crate::message::MESSAGE_PREFIX;
206 use libsecp256k1::{PublicKey, PublicKeyFormat};
207
208 const FIXTURE_REGISTERED_NODE_COUNT: u32 = 12;
214 const FIXTURE_REGISTRY_VERSION: u32 = 12;
215 const FIXTURE_SIGNATURES_REQUIRED: u32 = 5;
216 const FIXTURE_REDUNDANCY_BUFFER: u8 = 2;
217 const FIXTURE_CANONICAL_TIMESTAMP: u64 = 1_708_525_180;
218 const FIXTURE_SIGNER_COUNT: u32 = 7;
219
220 const FIXTURE_SOURCE_ID: [u8; 32] = [
221 0x0b, 0x0c, 0x5c, 0x4a, 0x0e, 0x67, 0x58, 0x69, 0xda, 0xc2, 0x27, 0x2a, 0x40, 0x04, 0x63,
222 0x65, 0xa2, 0x9c, 0x8a, 0xe7, 0x63, 0x5e, 0x52, 0xc4, 0x94, 0xd8, 0x40, 0xda, 0x2e, 0xc8,
223 0x26, 0xcb,
224 ];
225
226 const FIXTURE_VALUE: [u8; 32] = [
227 0xe1, 0xcd, 0x5b, 0x4f, 0x67, 0xac, 0xdc, 0x78, 0x68, 0xc3, 0xb1, 0x5f, 0x7b, 0x6c, 0xc2,
228 0xdc, 0x27, 0x70, 0x54, 0x53, 0x71, 0x34, 0x2c, 0xab, 0x76, 0x62, 0x71, 0xbb, 0x3f, 0xd5,
229 0xe7, 0x34,
230 ];
231
232 const FIXTURE_SIGNERS_BITMAP: [u8; 32] = [
234 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
235 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
236 0x0e, 0x1d,
237 ];
238
239 const FIXTURE_S: [u8; 32] = [
241 0x1b, 0x8d, 0xd2, 0x78, 0xb3, 0x67, 0xb3, 0x4d, 0x4e, 0xce, 0x69, 0xb8, 0x8c, 0x28, 0xff,
242 0x13, 0x01, 0xb6, 0x72, 0x51, 0xfc, 0x3d, 0x79, 0x26, 0xac, 0xb5, 0x25, 0xd1, 0x1f, 0xd3,
243 0x17, 0x1d,
244 ];
245
246 const FIXTURE_COMMITMENT: [u8; 20] = [
248 0x51, 0xbe, 0x44, 0x69, 0x33, 0x1a, 0x9e, 0xb3, 0xed, 0x48, 0xb1, 0xd4, 0xe1, 0x1e, 0xc9,
249 0xa0, 0xa5, 0x95, 0x2d, 0xf4,
250 ];
251
252 const FIXTURE_PUBKEYS: [[u8; 33]; 12] = [
254 [
255 0x03, 0x04, 0xb2, 0x3a, 0xff, 0xb9, 0xae, 0xb2, 0x80, 0xd6, 0xa2, 0x75, 0xb8, 0x65,
256 0xe6, 0x3b, 0x1f, 0x27, 0xb0, 0xd5, 0x01, 0x6e, 0x35, 0x6d, 0xdb, 0xfe, 0x8b, 0xd2,
257 0x5b, 0x27, 0xd1, 0x7e, 0x5f,
258 ],
259 [
260 0x02, 0x1b, 0xdf, 0x3b, 0x69, 0xc5, 0x3c, 0x4e, 0xb2, 0xa9, 0x4c, 0x44, 0x3e, 0x68,
261 0x65, 0x02, 0x68, 0x0f, 0xe3, 0x69, 0xd8, 0xba, 0xe5, 0xef, 0x02, 0x2b, 0x6e, 0x07,
262 0xcc, 0xac, 0x05, 0xaa, 0x7d,
263 ],
264 [
265 0x02, 0xdc, 0x2d, 0x88, 0xad, 0x9d, 0x1c, 0x4f, 0xc7, 0x6b, 0xc5, 0xaf, 0x00, 0xc3,
266 0x90, 0x20, 0x08, 0xa0, 0xbe, 0x5f, 0x8f, 0x10, 0x48, 0xd1, 0xd5, 0xb3, 0xfb, 0xc7,
267 0x19, 0xfc, 0x7a, 0xd5, 0xec,
268 ],
269 [
270 0x02, 0xc2, 0x6e, 0xd5, 0xda, 0x51, 0x58, 0xfd, 0x27, 0xe5, 0xaf, 0xc0, 0x5f, 0x88,
271 0xeb, 0xe4, 0x4b, 0xcb, 0xf0, 0x90, 0xae, 0x9b, 0xc5, 0xe7, 0x02, 0x4d, 0xf0, 0xd5,
272 0x7e, 0xa4, 0xcd, 0x7a, 0x44,
273 ],
274 [
275 0x02, 0x85, 0x07, 0x3b, 0x91, 0x57, 0xfb, 0xd6, 0x77, 0x95, 0x9b, 0xf9, 0x12, 0xac,
276 0x07, 0x95, 0x8c, 0x4a, 0x62, 0x5d, 0xcc, 0xd7, 0x4f, 0xa1, 0x3c, 0x92, 0x9e, 0x3d,
277 0xbb, 0x8d, 0x3d, 0xbd, 0x41,
278 ],
279 [
280 0x02, 0x25, 0x50, 0xee, 0x49, 0x3c, 0x38, 0x43, 0x8a, 0xa7, 0x40, 0xc0, 0xa9, 0x97,
281 0x8b, 0x20, 0x84, 0xa3, 0x50, 0x86, 0xbf, 0xef, 0x28, 0x9f, 0x3b, 0xe8, 0x58, 0xe2,
282 0xe7, 0xda, 0x3c, 0x09, 0x7f,
283 ],
284 [
285 0x03, 0x30, 0x96, 0x23, 0x4e, 0x51, 0x78, 0xf3, 0x71, 0x03, 0xa6, 0x6d, 0x86, 0x81,
286 0x76, 0x02, 0x58, 0xdd, 0xc5, 0x2d, 0x1a, 0x06, 0xbd, 0xed, 0xa6, 0xaa, 0xa3, 0x2f,
287 0xbe, 0x32, 0xb8, 0x78, 0x60,
288 ],
289 [
290 0x03, 0xd4, 0xa4, 0x66, 0x9d, 0xbc, 0x8e, 0x33, 0x9a, 0x9c, 0x1d, 0xa3, 0x42, 0xf3,
291 0x14, 0x54, 0x04, 0x92, 0x4c, 0x65, 0x1d, 0x94, 0x16, 0xb0, 0xb5, 0x8c, 0xc3, 0x0b,
292 0x1f, 0xc8, 0x03, 0x7a, 0x92,
293 ],
294 [
295 0x03, 0xfb, 0x7a, 0xae, 0x5c, 0x57, 0x4c, 0xd5, 0x0e, 0x2a, 0xd6, 0xed, 0x8e, 0x15,
296 0x64, 0xa6, 0x70, 0x75, 0x56, 0xa1, 0x50, 0xa6, 0x4f, 0x24, 0x72, 0x67, 0xa2, 0x7d,
297 0xe5, 0x9b, 0x82, 0xe2, 0x63,
298 ],
299 [
300 0x02, 0x58, 0xbf, 0x41, 0xcf, 0xea, 0x2b, 0x1d, 0x34, 0x4c, 0xc3, 0x0b, 0xb7, 0x35,
301 0xa1, 0x32, 0xc1, 0x75, 0x5b, 0x11, 0x2d, 0xb5, 0x8f, 0xaa, 0x7e, 0x4c, 0x44, 0x65,
302 0x95, 0x2e, 0x00, 0x04, 0xbf,
303 ],
304 [
305 0x02, 0x5d, 0xc1, 0x4d, 0x6b, 0xc2, 0x04, 0x42, 0xbe, 0x79, 0xf5, 0x1c, 0xf5, 0x20,
306 0x33, 0xc3, 0x96, 0x7b, 0xcc, 0xdd, 0xc5, 0xd3, 0x66, 0x95, 0x95, 0x13, 0x73, 0x20,
307 0xdf, 0xe5, 0xc6, 0xab, 0xfc,
308 ],
309 [
310 0x03, 0xdc, 0xa6, 0x3a, 0x35, 0xd0, 0x48, 0xf7, 0x94, 0x5c, 0x95, 0x9d, 0x61, 0x8c,
311 0x2f, 0xe8, 0xee, 0x5d, 0x40, 0x00, 0x29, 0x19, 0xa4, 0x6d, 0xff, 0x81, 0x27, 0x9c,
312 0x04, 0xb9, 0x71, 0xe6, 0x06,
313 ],
314 ];
315
316 fn fixture_payload() -> AttestationPayload {
317 AttestationPayload {
318 value: FIXTURE_VALUE,
319 source_id: FIXTURE_SOURCE_ID,
320 registry_version: FIXTURE_REGISTRY_VERSION,
321 canonical_timestamp: FIXTURE_CANONICAL_TIMESTAMP,
322 signatures_required: FIXTURE_SIGNATURES_REQUIRED,
323 }
324 }
325
326 fn fixture_signature() -> SchnorrSignature {
327 SchnorrSignature {
328 agg_sig_s: FIXTURE_S,
329 commitment_addr: FIXTURE_COMMITMENT,
330 signers_bitmap: FIXTURE_SIGNERS_BITMAP,
331 }
332 }
333
334 fn fixture_signers_xy() -> Vec<SignerXy> {
335 use crate::bitmap::for_each_set_bit;
336 let mut signers = Vec::new();
337 for_each_set_bit(&FIXTURE_SIGNERS_BITMAP, |i| {
338 let c = &FIXTURE_PUBKEYS[i];
339 let pk = PublicKey::parse_slice(c, Some(PublicKeyFormat::Compressed))
340 .expect("fixture pubkey must be a valid curve point");
341 let full = pk.serialize();
342 let x: [u8; 32] = full[1..33].try_into().unwrap();
343 let y: [u8; 32] = full[33..65].try_into().unwrap();
344 signers.push((x, y));
345 });
346 signers
347 }
348
349 fn fixture_signer_pubkeys_compressed() -> Vec<[u8; 33]> {
350 use crate::bitmap::for_each_set_bit;
351 let mut signers = Vec::new();
352 for_each_set_bit(&FIXTURE_SIGNERS_BITMAP, |i| {
353 signers.push(FIXTURE_PUBKEYS[i]);
354 });
355 signers
356 }
357
358 #[test]
359 fn fixture_pubkeys_are_valid_curve_points() {
360 for (i, pk) in FIXTURE_PUBKEYS.iter().enumerate() {
361 PublicKey::parse_slice(pk, Some(PublicKeyFormat::Compressed))
362 .unwrap_or_else(|_| panic!("fixture pubkey {i} is not a valid curve point"));
363 }
364 }
365
366 #[test]
367 fn fixture_signers_bitmap_popcount_meets_threshold() {
368 use crate::bitmap::bitmap_popcount_evm;
369 let popcount = bitmap_popcount_evm(&FIXTURE_SIGNERS_BITMAP);
370 assert_eq!(popcount, FIXTURE_SIGNER_COUNT);
371 assert!(popcount >= FIXTURE_SIGNATURES_REQUIRED);
372 }
373
374 #[test]
376 fn reconstruct_coalition_key_matches_combine() {
377 let signer_pubkeys = fixture_signer_pubkeys_compressed();
378 let pks: Vec<PublicKey> = signer_pubkeys
379 .iter()
380 .map(|c| PublicKey::parse_slice(c, Some(PublicKeyFormat::Compressed)).unwrap())
381 .collect();
382 let combined = PublicKey::combine(&pks).unwrap().serialize_compressed();
383 let got = reconstruct_coalition_key(&fixture_signers_xy()).unwrap();
384 assert_eq!(got, combined);
385 let got_c = reconstruct_coalition_key_compressed(&signer_pubkeys).unwrap();
386 assert_eq!(got_c, combined);
387 }
388
389 fn fixture_attestation() -> Attestation {
390 Attestation {
391 payload: fixture_payload(),
392 signature: fixture_signature(),
393 }
394 }
395
396 #[test]
398 fn verify_attestation_accepts_evm_fixture() {
399 let attestation = fixture_attestation();
400 let signer_pubkeys = fixture_signer_pubkeys_compressed();
401 verify_attestation(
402 &attestation,
403 FIXTURE_REGISTERED_NODE_COUNT,
404 FIXTURE_REDUNDANCY_BUFFER,
405 &fixture_signers_xy(),
406 )
407 .expect("fixture attestation must verify");
408 verify_attestation_compressed(
409 &attestation,
410 FIXTURE_REGISTERED_NODE_COUNT,
411 FIXTURE_REDUNDANCY_BUFFER,
412 &signer_pubkeys,
413 )
414 .expect("compressed variant must verify");
415 }
416
417 #[test]
418 fn tampered_s_fails_verification() {
419 let mut attestation = fixture_attestation();
420 attestation.signature.agg_sig_s[31] ^= 0x01;
421 let res = verify_attestation(
422 &attestation,
423 FIXTURE_REGISTERED_NODE_COUNT,
424 FIXTURE_REDUNDANCY_BUFFER,
425 &fixture_signers_xy(),
426 );
427 assert_eq!(res, Err(AttestationError::InvalidAggregateSignature));
428 }
429
430 #[test]
431 fn wrong_signer_count_is_rejected() {
432 let attestation = fixture_attestation();
433 let mut signers = fixture_signers_xy();
434 signers.pop();
435 assert_eq!(
436 verify_attestation(
437 &attestation,
438 FIXTURE_REGISTERED_NODE_COUNT,
439 FIXTURE_REDUNDANCY_BUFFER,
440 &signers,
441 ),
442 Err(AttestationError::SignerCountMismatch)
443 );
444 }
445
446 #[test]
447 fn verify_aggregate_over_hash_roundtrip() {
448 let payload = fixture_payload();
449 let signature = fixture_signature();
450 let signers = fixture_signers_xy();
451 let message_hash = compute_message_hash(
452 &payload,
453 signature.signers_bitmap,
454 payload.signatures_required,
455 );
456 assert!(verify_aggregate_over_hash(
457 &signers,
458 &signature.agg_sig_s,
459 &signature.commitment_addr,
460 &message_hash,
461 )
462 .unwrap());
463
464 let mut bad_hash = message_hash;
466 bad_hash[0] ^= 0xff;
467 assert!(!verify_aggregate_over_hash(
468 &signers,
469 &signature.agg_sig_s,
470 &signature.commitment_addr,
471 &bad_hash,
472 )
473 .unwrap());
474 }
475
476 #[test]
477 fn message_prefix_matches_known_constant() {
478 assert_eq!(MESSAGE_PREFIX[0], 0xa7);
480 }
481}