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