1use solana_secp256k1_recover::secp256k1_recover;
8
9use crate::bitmap::{bitmap_is_subset_u256, bitmap_load};
10use crate::coalition::CoalitionAccumulator;
11use crate::error::DataUpdateError;
12use crate::message::compute_message_hash;
13use crate::payload::{DataUpdate, 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_data_update(
37 payload: &DataUpdate,
38 signature: &SchnorrSignature,
39 node_count: u32,
40 redundancy_buffer: u8,
41 ordered_signers: &[SignerXy],
42) -> Result<(), DataUpdateError> {
43 if signature.agg_sig_s == [0u8; 32] || !secp256k1_scalar_is_valid_nonzero(&signature.agg_sig_s)
44 {
45 return Err(DataUpdateError::InvalidAggregateSignature);
46 }
47
48 let signers = bitmap_load(&signature.signers_bitmap);
49 let signer_count = signers.count_ones();
50 if signer_count < u32::from(payload.signatures_required) {
51 return Err(DataUpdateError::InsufficientSigners);
52 }
53
54 let expected_selection = derive_selection_bitmap(
55 &payload.source_id,
56 payload.registry_version,
57 payload.canonical_timestamp,
58 node_count,
59 payload.signatures_required,
60 redundancy_buffer,
61 )?;
62 if !bitmap_is_subset_u256(signers, bitmap_load(&expected_selection)) {
63 return Err(DataUpdateError::SignersNotSubsetOfSelection);
64 }
65
66 if ordered_signers.len() != signer_count as usize {
67 return Err(DataUpdateError::SignerCountMismatch);
68 }
69
70 let x_coalition = reconstruct_coalition_key(ordered_signers)?;
71 let message_hash = compute_message_hash(
72 payload,
73 signature.signers_bitmap,
74 payload.signatures_required,
75 );
76
77 if recover_and_match(
78 &x_coalition,
79 &message_hash,
80 &signature.agg_sig_s,
81 &signature.commitment_addr,
82 ) {
83 Ok(())
84 } else {
85 Err(DataUpdateError::InvalidAggregateSignature)
86 }
87}
88
89pub fn verify_data_update_compressed(
91 payload: &DataUpdate,
92 signature: &SchnorrSignature,
93 node_count: u32,
94 redundancy_buffer: u8,
95 ordered_signers_compressed: &[[u8; 33]],
96) -> Result<(), DataUpdateError> {
97 let xy = decompress_all(ordered_signers_compressed)?;
98 verify_data_update(payload, signature, node_count, redundancy_buffer, &xy)
99}
100
101pub fn reconstruct_coalition_key(
105 ordered_signers: &[SignerXy],
106) -> Result<[u8; 33], DataUpdateError> {
107 if ordered_signers.is_empty() {
108 return Err(DataUpdateError::InvalidSignersBitmap);
109 }
110 let mut coalition = CoalitionAccumulator::default();
111 for (x, y) in ordered_signers {
112 coalition.add_stored_xy(x, y)?;
113 }
114 coalition.compressed_pubkey()
115}
116
117pub fn reconstruct_coalition_key_compressed(
119 ordered_signers_compressed: &[[u8; 33]],
120) -> Result<[u8; 33], DataUpdateError> {
121 let xy = decompress_all(ordered_signers_compressed)?;
122 reconstruct_coalition_key(&xy)
123}
124
125pub fn verify_aggregate_over_hash(
132 ordered_signers: &[SignerXy],
133 agg_sig_s: &[u8; 32],
134 commitment_addr: &[u8; 20],
135 message_hash: &[u8; 32],
136) -> Result<bool, DataUpdateError> {
137 if !secp256k1_scalar_is_valid_nonzero(agg_sig_s) {
138 return Ok(false);
139 }
140 let x_coalition = reconstruct_coalition_key(ordered_signers)?;
141 Ok(recover_and_match(
142 &x_coalition,
143 message_hash,
144 agg_sig_s,
145 commitment_addr,
146 ))
147}
148
149fn recover_and_match(
151 x_coalition: &[u8; 33],
152 message_hash: &[u8; 32],
153 agg_sig_s: &[u8; 32],
154 commitment_addr: &[u8; 20],
155) -> bool {
156 let (recovery_id, ecdsa_signature, ecdsa_hash) =
157 match evm_schnorr_ecdsa_inputs(x_coalition, message_hash, agg_sig_s, commitment_addr) {
158 Ok(v) => v,
159 Err(_) => return false,
160 };
161 let recovered = match secp256k1_recover(&ecdsa_hash, recovery_id, &ecdsa_signature) {
162 Ok(r) => r,
163 Err(_) => return false,
164 };
165 eth_address_from_uncompressed_pubkey(recovered.to_bytes()) == *commitment_addr
166}
167
168fn decompress_all(compressed: &[[u8; 33]]) -> Result<Vec<SignerXy>, DataUpdateError> {
169 use libsecp256k1::{PublicKey, PublicKeyFormat};
170 compressed
171 .iter()
172 .map(|c| {
173 let pk = PublicKey::parse_slice(c, Some(PublicKeyFormat::Compressed))
174 .map_err(|_| DataUpdateError::InvalidAggregateSignature)?;
175 let full = pk.serialize(); let x: [u8; 32] = full[1..33].try_into().unwrap();
177 let y: [u8; 32] = full[33..65].try_into().unwrap();
178 Ok((x, y))
179 })
180 .collect()
181}
182
183#[cfg(test)]
184mod tests {
185 use super::*;
186 use crate::message::MESSAGE_PREFIX;
187 use libsecp256k1::{PublicKey, PublicKeyFormat};
188
189 const FIXTURE_SOURCE_ID: [u8; 32] = [
196 0x73, 0x6f, 0x6c, 0x61, 0x6e, 0x61, 0x2d, 0x63, 0x6f, 0x6d, 0x70, 0x61, 0x74, 0x2d, 0x6a,
197 0x6f, 0x62, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
198 0x00, 0x00,
199 ];
200
201 const FIXTURE_VALUE: [u8; 32] = [
203 0x73, 0x6f, 0x6c, 0x61, 0x6e, 0x61, 0x2d, 0x63, 0x6f, 0x6d, 0x70, 0x61, 0x74, 0x2d, 0x76,
204 0x61, 0x6c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
205 0x00, 0x00,
206 ];
207
208 const FIXTURE_SIGNERS_BITMAP: [u8; 32] = [
210 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
211 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
212 0x00, 0xff,
213 ];
214
215 const FIXTURE_REGISTRY_VERSION: u32 = 1;
216 const FIXTURE_SIGNATURES_REQUIRED: u8 = 8;
217 const FIXTURE_CANONICAL_TIMESTAMP: i64 = 1_700_000_123;
218
219 const FIXTURE_S: [u8; 32] = [
221 0xc7, 0xe0, 0x99, 0x60, 0x3c, 0xee, 0xd2, 0xa1, 0x13, 0xd7, 0x5a, 0x9d, 0x95, 0xe2, 0x0f,
222 0x92, 0x00, 0x6b, 0x06, 0xc5, 0x49, 0x7a, 0xdd, 0x09, 0x81, 0x7d, 0xa8, 0x90, 0x8d, 0x39,
223 0x0d, 0xa5,
224 ];
225
226 const FIXTURE_COMMITMENT: [u8; 20] = [
228 0xc6, 0xb9, 0x4f, 0xea, 0x5d, 0xd5, 0xf9, 0x65, 0xd8, 0x67, 0x14, 0xb1, 0xd9, 0x9d, 0xcf,
229 0xaf, 0x1e, 0x72, 0xee, 0x35,
230 ];
231
232 const FIXTURE_PUBKEYS: [[u8; 33]; 8] = [
234 [
235 0x03, 0xc0, 0x95, 0x27, 0xe9, 0x78, 0xf6, 0xea, 0x69, 0xf0, 0xc6, 0xb7, 0xac, 0x0f,
236 0xb6, 0x3a, 0xd0, 0x81, 0xa8, 0xa2, 0x91, 0x15, 0x1c, 0x5a, 0x0b, 0x11, 0x5c, 0xce,
237 0x43, 0x57, 0x51, 0xbe, 0x7d,
238 ],
239 [
240 0x02, 0x64, 0xa7, 0x27, 0x04, 0xf3, 0x9f, 0x8d, 0xd1, 0x7f, 0x20, 0xd7, 0x1c, 0x5b,
241 0x21, 0xf3, 0x7b, 0x58, 0x52, 0x65, 0x6b, 0xc0, 0x55, 0x54, 0x42, 0xbf, 0x72, 0x72,
242 0x22, 0xf2, 0x9d, 0x7e, 0x58,
243 ],
244 [
245 0x02, 0x75, 0xae, 0x1e, 0x3d, 0xac, 0x00, 0xeb, 0x7d, 0xf0, 0x2e, 0x9f, 0xe8, 0xd9,
246 0x70, 0x9c, 0x8a, 0x2c, 0x09, 0xa1, 0x1e, 0xd4, 0xf7, 0xd9, 0xaa, 0x46, 0xa7, 0xde,
247 0xa6, 0xcf, 0x37, 0x6d, 0x7f,
248 ],
249 [
250 0x02, 0x6c, 0xe2, 0x5b, 0x3a, 0x16, 0x1a, 0xb8, 0xe0, 0xf0, 0x5e, 0x4c, 0xd1, 0xc7,
251 0x7b, 0x77, 0x69, 0x6d, 0x26, 0xc6, 0x41, 0xeb, 0xde, 0xa4, 0xe8, 0x1a, 0xa8, 0x9a,
252 0x90, 0xf3, 0x2c, 0xfc, 0x54,
253 ],
254 [
255 0x03, 0x5b, 0x95, 0xd7, 0x03, 0x22, 0x8b, 0xef, 0xcc, 0xc3, 0x78, 0x62, 0x9d, 0xc1,
256 0x98, 0x04, 0xce, 0xfe, 0x56, 0xc3, 0x3c, 0x64, 0x5f, 0xa4, 0xbc, 0x1a, 0xa0, 0xf3,
257 0x75, 0xe3, 0xb4, 0xfa, 0x5e,
258 ],
259 [
260 0x03, 0x99, 0x5e, 0x4b, 0xe0, 0xec, 0xd4, 0x22, 0xbf, 0x25, 0x0a, 0x3d, 0xa3, 0xa0,
261 0xb8, 0x34, 0x2e, 0x52, 0x89, 0x3a, 0x3e, 0x06, 0x4f, 0xa6, 0x35, 0x55, 0x73, 0x78,
262 0xb5, 0x9a, 0xfa, 0x8b, 0x50,
263 ],
264 [
265 0x03, 0xec, 0x90, 0x6d, 0x0a, 0x1c, 0xfc, 0x3c, 0x7d, 0xec, 0x18, 0x08, 0x8c, 0x3d,
266 0x14, 0x4f, 0x32, 0x15, 0x80, 0xec, 0xe0, 0xa6, 0xba, 0xe5, 0xce, 0xb2, 0x8d, 0xcf,
267 0x8d, 0xc6, 0xe3, 0xda, 0x03,
268 ],
269 [
270 0x03, 0x27, 0x5f, 0xcf, 0x98, 0x38, 0xb4, 0x7a, 0xac, 0xff, 0x25, 0x1f, 0x4f, 0x09,
271 0x9f, 0x80, 0xc6, 0x4a, 0x1a, 0x9a, 0xed, 0xbd, 0xb6, 0x28, 0xc2, 0xc8, 0x7f, 0x2c,
272 0x5e, 0x12, 0x3d, 0xd0, 0x40,
273 ],
274 ];
275
276 fn fixture_payload() -> DataUpdate {
277 DataUpdate {
278 source_id: FIXTURE_SOURCE_ID,
279 registry_version: FIXTURE_REGISTRY_VERSION,
280 value: FIXTURE_VALUE.to_vec(),
281 canonical_timestamp: FIXTURE_CANONICAL_TIMESTAMP,
282 signatures_required: FIXTURE_SIGNATURES_REQUIRED,
283 }
284 }
285
286 fn fixture_signature() -> SchnorrSignature {
287 SchnorrSignature {
288 agg_sig_s: FIXTURE_S,
289 commitment_addr: FIXTURE_COMMITMENT,
290 signers_bitmap: FIXTURE_SIGNERS_BITMAP,
291 }
292 }
293
294 fn fixture_signers_xy() -> Vec<SignerXy> {
295 FIXTURE_PUBKEYS
296 .iter()
297 .map(|c| {
298 let pk = PublicKey::parse_slice(c, Some(PublicKeyFormat::Compressed))
299 .expect("fixture pubkey must be a valid curve point");
300 let full = pk.serialize();
301 let x: [u8; 32] = full[1..33].try_into().unwrap();
302 let y: [u8; 32] = full[33..65].try_into().unwrap();
303 (x, y)
304 })
305 .collect()
306 }
307
308 #[test]
309 fn fixture_pubkeys_are_valid_curve_points() {
310 for (i, pk) in FIXTURE_PUBKEYS.iter().enumerate() {
311 PublicKey::parse_slice(pk, Some(PublicKeyFormat::Compressed))
312 .unwrap_or_else(|_| panic!("fixture pubkey {i} is not a valid curve point"));
313 }
314 }
315
316 #[test]
317 fn fixture_signers_bitmap_popcount_is_8() {
318 use crate::bitmap::bitmap_popcount_evm;
319 assert_eq!(
320 bitmap_popcount_evm(&FIXTURE_SIGNERS_BITMAP),
321 FIXTURE_SIGNATURES_REQUIRED as u32
322 );
323 }
324
325 #[test]
327 fn reconstruct_coalition_key_matches_combine() {
328 let pks: Vec<PublicKey> = FIXTURE_PUBKEYS
329 .iter()
330 .map(|c| PublicKey::parse_slice(c, Some(PublicKeyFormat::Compressed)).unwrap())
331 .collect();
332 let combined = PublicKey::combine(&pks).unwrap().serialize_compressed();
333 let got = reconstruct_coalition_key(&fixture_signers_xy()).unwrap();
334 assert_eq!(got, combined);
335 let got_c = reconstruct_coalition_key_compressed(&FIXTURE_PUBKEYS).unwrap();
336 assert_eq!(got_c, combined);
337 }
338
339 #[test]
341 fn verify_data_update_accepts_evm_fixture() {
342 let payload = fixture_payload();
343 let signature = fixture_signature();
344 verify_data_update(&payload, &signature, 8, 0, &fixture_signers_xy())
346 .expect("fixture DataUpdate must verify");
347 verify_data_update_compressed(&payload, &signature, 8, 0, &FIXTURE_PUBKEYS)
348 .expect("compressed variant must verify");
349 }
350
351 #[test]
352 fn tampered_s_fails_verification() {
353 let payload = fixture_payload();
354 let mut signature = fixture_signature();
355 signature.agg_sig_s[31] ^= 0x01;
356 let res = verify_data_update(&payload, &signature, 8, 0, &fixture_signers_xy());
357 assert_eq!(res, Err(DataUpdateError::InvalidAggregateSignature));
358 }
359
360 #[test]
361 fn wrong_signer_count_is_rejected() {
362 let payload = fixture_payload();
363 let signature = fixture_signature();
364 let mut signers = fixture_signers_xy();
365 signers.pop();
366 assert_eq!(
367 verify_data_update(&payload, &signature, 8, 0, &signers),
368 Err(DataUpdateError::SignerCountMismatch)
369 );
370 }
371
372 #[test]
373 fn verify_aggregate_over_hash_roundtrip() {
374 let payload = fixture_payload();
375 let signature = fixture_signature();
376 let signers = fixture_signers_xy();
377 let message_hash = compute_message_hash(
378 &payload,
379 signature.signers_bitmap,
380 payload.signatures_required,
381 );
382 assert!(verify_aggregate_over_hash(
383 &signers,
384 &signature.agg_sig_s,
385 &signature.commitment_addr,
386 &message_hash,
387 )
388 .unwrap());
389
390 let mut bad_hash = message_hash;
392 bad_hash[0] ^= 0xff;
393 assert!(!verify_aggregate_over_hash(
394 &signers,
395 &signature.agg_sig_s,
396 &signature.commitment_addr,
397 &bad_hash,
398 )
399 .unwrap());
400 }
401
402 #[test]
403 fn message_prefix_matches_known_constant() {
404 assert_eq!(MESSAGE_PREFIX[0], 0xa7);
406 }
407}