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;
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(
38 payload: &DataUpdate,
39 raw_value: &[u8],
40 node_count: u32,
41 redundancy_buffer: u8,
42 ordered_signers: &[SignerXy],
43) -> Result<(), DataUpdateError> {
44 if payload.agg_sig_s == [0u8; 32] || !secp256k1_scalar_is_valid_nonzero(&payload.agg_sig_s) {
45 return Err(DataUpdateError::InvalidAggregateSignature);
46 }
47
48 let signers = bitmap_load(&payload.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.feed_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(payload, raw_value);
72
73 if recover_and_match(
74 &x_coalition,
75 &message_hash,
76 &payload.agg_sig_s,
77 &payload.commitment_addr,
78 ) {
79 Ok(())
80 } else {
81 Err(DataUpdateError::InvalidAggregateSignature)
82 }
83}
84
85pub fn verify_data_update_compressed(
87 payload: &DataUpdate,
88 raw_value: &[u8],
89 node_count: u32,
90 redundancy_buffer: u8,
91 ordered_signers_compressed: &[[u8; 33]],
92) -> Result<(), DataUpdateError> {
93 let xy = decompress_all(ordered_signers_compressed)?;
94 verify_data_update(payload, raw_value, node_count, redundancy_buffer, &xy)
95}
96
97pub fn reconstruct_coalition_key(
101 ordered_signers: &[SignerXy],
102) -> Result<[u8; 33], DataUpdateError> {
103 if ordered_signers.is_empty() {
104 return Err(DataUpdateError::InvalidSignersBitmap);
105 }
106 let mut coalition = CoalitionAccumulator::default();
107 for (x, y) in ordered_signers {
108 coalition.add_stored_xy(x, y)?;
109 }
110 coalition.compressed_pubkey()
111}
112
113pub fn reconstruct_coalition_key_compressed(
115 ordered_signers_compressed: &[[u8; 33]],
116) -> Result<[u8; 33], DataUpdateError> {
117 let xy = decompress_all(ordered_signers_compressed)?;
118 reconstruct_coalition_key(&xy)
119}
120
121pub fn verify_aggregate_over_hash(
128 ordered_signers: &[SignerXy],
129 agg_sig_s: &[u8; 32],
130 commitment_addr: &[u8; 20],
131 message_hash: &[u8; 32],
132) -> Result<bool, DataUpdateError> {
133 if !secp256k1_scalar_is_valid_nonzero(agg_sig_s) {
134 return Ok(false);
135 }
136 let x_coalition = reconstruct_coalition_key(ordered_signers)?;
137 Ok(recover_and_match(
138 &x_coalition,
139 message_hash,
140 agg_sig_s,
141 commitment_addr,
142 ))
143}
144
145fn recover_and_match(
147 x_coalition: &[u8; 33],
148 message_hash: &[u8; 32],
149 agg_sig_s: &[u8; 32],
150 commitment_addr: &[u8; 20],
151) -> bool {
152 let (recovery_id, ecdsa_signature, ecdsa_hash) =
153 match evm_schnorr_ecdsa_inputs(x_coalition, message_hash, agg_sig_s, commitment_addr) {
154 Ok(v) => v,
155 Err(_) => return false,
156 };
157 let recovered = match secp256k1_recover(&ecdsa_hash, recovery_id, &ecdsa_signature) {
158 Ok(r) => r,
159 Err(_) => return false,
160 };
161 eth_address_from_uncompressed_pubkey(recovered.to_bytes()) == *commitment_addr
162}
163
164fn decompress_all(compressed: &[[u8; 33]]) -> Result<Vec<SignerXy>, DataUpdateError> {
165 use libsecp256k1::{PublicKey, PublicKeyFormat};
166 compressed
167 .iter()
168 .map(|c| {
169 let pk = PublicKey::parse_slice(c, Some(PublicKeyFormat::Compressed))
170 .map_err(|_| DataUpdateError::InvalidAggregateSignature)?;
171 let full = pk.serialize(); let x: [u8; 32] = full[1..33].try_into().unwrap();
173 let y: [u8; 32] = full[33..65].try_into().unwrap();
174 Ok((x, y))
175 })
176 .collect()
177}
178
179#[cfg(test)]
180mod tests {
181 use super::*;
182 use crate::message::MESSAGE_PREFIX;
183 use crate::test_signer;
184 use libsecp256k1::{PublicKey, PublicKeyFormat};
185
186 const FIXTURE_RAW_VALUE: &[u8] =
193 b"molpha raw value that is decidedly longer than thirty-two bytes";
194
195 const FIXTURE_FEED_ID: [u8; 32] = [
197 0x73, 0x6f, 0x6c, 0x61, 0x6e, 0x61, 0x2d, 0x63, 0x6f, 0x6d, 0x70, 0x61, 0x74, 0x2d, 0x6a,
198 0x6f, 0x62, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
199 0x00, 0x00,
200 ];
201
202 const FIXTURE_SIGNERS_BITMAP: [u8; 32] = [
204 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
205 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
206 0x00, 0xff,
207 ];
208
209 const FIXTURE_REGISTRY_VERSION: u32 = 1;
210 const FIXTURE_SIGNATURES_REQUIRED: u8 = 8;
211 const FIXTURE_CANONICAL_TIMESTAMP: i64 = 1_700_000_123;
212
213 fn signed_fixture() -> (DataUpdate, Vec<[u8; 33]>) {
215 let keys = test_signer::secret_keys(8);
216 let pubkeys = test_signer::pubkeys_compressed(&keys);
217 let mut payload = DataUpdate {
218 feed_id: FIXTURE_FEED_ID,
219 registry_version: FIXTURE_REGISTRY_VERSION,
220 canonical_timestamp: FIXTURE_CANONICAL_TIMESTAMP,
221 signatures_required: FIXTURE_SIGNATURES_REQUIRED,
222 agg_sig_s: [0u8; 32],
223 commitment_addr: [0u8; 20],
224 signers_bitmap: FIXTURE_SIGNERS_BITMAP,
225 };
226 let message_hash = compute_message_hash(&payload, FIXTURE_RAW_VALUE);
227 let (agg_sig_s, commitment_addr) =
228 test_signer::sign(&keys, &message_hash, b"verify-fixture");
229 payload.agg_sig_s = agg_sig_s;
230 payload.commitment_addr = commitment_addr;
231 (payload, pubkeys)
232 }
233
234 fn fixture_signers_xy(pubkeys: &[[u8; 33]]) -> Vec<SignerXy> {
235 decompress_all(pubkeys).expect("fixture pubkeys must be valid curve points")
236 }
237
238 #[test]
239 fn fixture_signers_bitmap_popcount_is_8() {
240 use crate::bitmap::bitmap_popcount_evm;
241 assert_eq!(
242 bitmap_popcount_evm(&FIXTURE_SIGNERS_BITMAP),
243 FIXTURE_SIGNATURES_REQUIRED as u32
244 );
245 }
246
247 #[test]
249 fn reconstruct_coalition_key_matches_combine() {
250 let (_, pubkeys) = signed_fixture();
251 let pks: Vec<PublicKey> = pubkeys
252 .iter()
253 .map(|c| PublicKey::parse_slice(c, Some(PublicKeyFormat::Compressed)).unwrap())
254 .collect();
255 let combined = PublicKey::combine(&pks).unwrap().serialize_compressed();
256 let got = reconstruct_coalition_key(&fixture_signers_xy(&pubkeys)).unwrap();
257 assert_eq!(got, combined);
258 let got_c = reconstruct_coalition_key_compressed(&pubkeys).unwrap();
259 assert_eq!(got_c, combined);
260 }
261
262 #[test]
264 fn verify_data_update_accepts_signed_fixture() {
265 let (payload, pubkeys) = signed_fixture();
266 assert!(FIXTURE_RAW_VALUE.len() > 32);
267 verify_data_update(
269 &payload,
270 FIXTURE_RAW_VALUE,
271 8,
272 0,
273 &fixture_signers_xy(&pubkeys),
274 )
275 .expect("fixture DataUpdate must verify");
276 verify_data_update_compressed(&payload, FIXTURE_RAW_VALUE, 8, 0, &pubkeys)
277 .expect("compressed variant must verify");
278 }
279
280 #[test]
281 fn tampered_raw_value_is_rejected() {
282 let (payload, pubkeys) = signed_fixture();
283 let mut raw = FIXTURE_RAW_VALUE.to_vec();
284 raw[0] ^= 0xff;
285 assert_eq!(
286 verify_data_update(&payload, &raw, 8, 0, &fixture_signers_xy(&pubkeys)),
287 Err(DataUpdateError::InvalidAggregateSignature)
288 );
289 }
290
291 #[test]
292 fn tampered_s_fails_verification() {
293 let (mut payload, pubkeys) = signed_fixture();
294 payload.agg_sig_s[31] ^= 0x01;
295 let res = verify_data_update(
296 &payload,
297 FIXTURE_RAW_VALUE,
298 8,
299 0,
300 &fixture_signers_xy(&pubkeys),
301 );
302 assert_eq!(res, Err(DataUpdateError::InvalidAggregateSignature));
303 }
304
305 #[test]
306 fn wrong_signer_count_is_rejected() {
307 let (payload, pubkeys) = signed_fixture();
308 let mut signers = fixture_signers_xy(&pubkeys);
309 signers.pop();
310 assert_eq!(
311 verify_data_update(&payload, FIXTURE_RAW_VALUE, 8, 0, &signers),
312 Err(DataUpdateError::SignerCountMismatch)
313 );
314 }
315
316 #[test]
317 fn verify_aggregate_over_hash_roundtrip() {
318 let (payload, pubkeys) = signed_fixture();
319 let signers = fixture_signers_xy(&pubkeys);
320 let message_hash = compute_message_hash(&payload, FIXTURE_RAW_VALUE);
321 assert!(verify_aggregate_over_hash(
322 &signers,
323 &payload.agg_sig_s,
324 &payload.commitment_addr,
325 &message_hash,
326 )
327 .unwrap());
328
329 let mut bad_hash = message_hash;
331 bad_hash[0] ^= 0xff;
332 assert!(!verify_aggregate_over_hash(
333 &signers,
334 &payload.agg_sig_s,
335 &payload.commitment_addr,
336 &bad_hash,
337 )
338 .unwrap());
339 }
340
341 #[test]
342 fn message_prefix_matches_known_constant() {
343 assert_eq!(MESSAGE_PREFIX[0], 0xa7);
345 }
346
347 #[test]
351 #[ignore]
352 fn regenerate_example_fixture() {
353 fn print_bytes(name: &str, bytes: &[u8]) {
354 println!("const {name}: [u8; {}] = [", bytes.len());
355 for chunk in bytes.chunks(16) {
356 let row: Vec<String> = chunk.iter().map(|b| format!("0x{b:02x}")).collect();
357 println!(" {},", row.join(", "));
358 }
359 println!("];");
360 }
361
362 let (payload, pubkeys) = signed_fixture();
363 let mut bytes = Vec::new();
365 bytes.extend_from_slice(&payload.feed_id);
366 bytes.extend_from_slice(&payload.registry_version.to_le_bytes());
367 bytes.extend_from_slice(&payload.canonical_timestamp.to_le_bytes());
368 bytes.push(payload.signatures_required);
369 bytes.extend_from_slice(&payload.agg_sig_s);
370 bytes.extend_from_slice(&payload.commitment_addr);
371 bytes.extend_from_slice(&payload.signers_bitmap);
372 assert_eq!(bytes.len(), 129);
373
374 println!(
375 "const FIXTURE_RAW_VALUE: &[u8] = b\"{}\";",
376 core::str::from_utf8(FIXTURE_RAW_VALUE).unwrap()
377 );
378 print_bytes("FIXTURE_BORSH", &bytes);
379 println!(
380 "const FIXTURE_SIGNER_PUBKEYS: [[u8; 33]; {}] = [",
381 pubkeys.len()
382 );
383 for pk in &pubkeys {
384 let row: Vec<String> = pk.iter().map(|b| format!("0x{b:02x}")).collect();
385 println!(" [{}],", row.join(", "));
386 }
387 println!("];");
388 }
389}