1use std::io::{self, Write as _};
2
3use bitcoin::consensus::WriteExt;
4use bitcoin::hashes::{sha256, Hash, HashEngine};
5use bitcoin::key::Keypair;
6use bitcoin::secp256k1::{self, schnorr, Message};
7
8use crate::{SignedVtxoRequest, Vtxo, VtxoId, VtxoRequest, SECP};
9use crate::arkoor::ArkoorDestination;
10use crate::encode::{ProtocolEncoding, ProtocolDecodingError};
11use crate::lightning::PaymentHash;
12use crate::offboard::OffboardRequest;
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq)]
16pub struct Challenge([u8; 32]);
17
18impl Challenge {
19 pub fn new(value: [u8; 32]) -> Self {
20 Self(value)
21 }
22
23 pub fn generate() -> Self {
24 Self(rand::random())
25 }
26
27 pub fn inner(&self) -> [u8; 32] {
28 self.0
29 }
30}
31
32impl AsRef<[u8]> for Challenge {
33 fn as_ref(&self) -> &[u8] {
34 &self.0[..]
35 }
36}
37
38#[derive(Debug, Clone, Copy, PartialEq, Eq, Ord, PartialOrd, Hash, Serialize, Deserialize)]
43pub struct RoundAttemptAttestation {
44 signature: schnorr::Signature,
45}
46
47impl RoundAttemptAttestation {
48 const CHALLENGE_MESSAGE_PREFIX: &'static [u8; 32] = b"Ark round input ownership proof ";
50
51 pub fn new(
53 challenge: Challenge,
54 vtxo_id: VtxoId,
55 vtxo_reqs: &[SignedVtxoRequest],
56 vtxo_keypair: &Keypair,
57 ) -> Self {
58 let msg = Self::compute_message(challenge, vtxo_id, vtxo_reqs);
59 let signature = SECP.sign_schnorr_with_aux_rand(&msg, vtxo_keypair, &rand::random());
60 Self { signature }
61 }
62
63 pub fn signature(&self) -> &schnorr::Signature {
65 &self.signature
66 }
67
68 pub fn verify(
70 &self,
71 challenge: Challenge,
72 vtxo: &Vtxo,
73 vtxo_reqs: &[SignedVtxoRequest],
74 ) -> Result<(), secp256k1::Error> {
75 let msg = Self::compute_message(challenge, vtxo.id(), vtxo_reqs);
76 SECP.verify_schnorr(&self.signature, &msg, &vtxo.user_pubkey().x_only_public_key().0)
77 }
78
79 fn compute_message(
80 challenge: Challenge,
81 vtxo_id: VtxoId,
82 vtxo_reqs: &[SignedVtxoRequest],
83 ) -> Message {
84 let mut engine = sha256::Hash::engine();
85 engine.write_all(Self::CHALLENGE_MESSAGE_PREFIX).unwrap();
86 engine.write_all(&challenge.inner()).unwrap();
87 engine.write_all(&vtxo_id.to_bytes()).unwrap();
88
89 engine.write_all(&(vtxo_reqs.len() as u64).to_be_bytes()).unwrap();
90 for req in vtxo_reqs {
91 engine.write_all(&req.vtxo.amount.to_sat().to_be_bytes()).unwrap();
92 req.vtxo.policy.encode(&mut engine).unwrap();
93 req.cosign_pubkey.encode(&mut engine).unwrap();
94 }
95
96 let hash = sha256::Hash::from_engine(engine).to_byte_array();
97 Message::from_digest(hash)
98 }
99}
100
101impl ProtocolEncoding for RoundAttemptAttestation {
102 fn encode<W: io::Write + ?Sized>(&self, writer: &mut W) -> Result<(), io::Error> {
103 self.signature.encode(writer)
104 }
105
106 fn decode<R: io::Read + ?Sized>(reader: &mut R) -> Result<Self, ProtocolDecodingError> {
107 let signature = schnorr::Signature::decode(reader)?;
108 Ok(Self { signature })
109 }
110}
111
112#[derive(Debug, Clone, Copy, PartialEq, Eq, Ord, PartialOrd, Hash, Serialize, Deserialize)]
114pub struct DelegatedRoundParticipationAttestation {
115 signature: schnorr::Signature,
116}
117
118impl DelegatedRoundParticipationAttestation {
119 const CHALLENGE_MESSAGE_PREFIX: &'static [u8; 32] = b"hArk round join ownership proof ";
121
122 pub fn new(
124 vtxo_id: VtxoId,
125 vtxo_reqs: &[VtxoRequest],
126 vtxo_keypair: &Keypair,
127 ) -> Self {
128 let msg = Self::compute_message(vtxo_id, vtxo_reqs);
129 let signature = SECP.sign_schnorr_with_aux_rand(&msg, vtxo_keypair, &rand::random());
130 Self { signature }
131 }
132
133 pub fn signature(&self) -> &schnorr::Signature {
135 &self.signature
136 }
137
138 pub fn verify(
140 &self,
141 vtxo: &Vtxo,
142 vtxo_reqs: &[VtxoRequest],
143 ) -> Result<(), secp256k1::Error> {
144 let msg = Self::compute_message(vtxo.id(), vtxo_reqs);
145 SECP.verify_schnorr(&self.signature, &msg, &vtxo.user_pubkey().x_only_public_key().0)
146 }
147
148 fn compute_message(
149 vtxo_id: VtxoId,
150 vtxo_reqs: &[VtxoRequest],
151 ) -> Message {
152 let mut engine = sha256::Hash::engine();
153 engine.write_all(Self::CHALLENGE_MESSAGE_PREFIX).unwrap();
154 engine.write_all(&vtxo_id.to_bytes()).unwrap();
155
156 engine.write_all(&(vtxo_reqs.len() as u64).to_be_bytes()).unwrap();
157 for req in vtxo_reqs {
158 engine.write_all(&req.amount.to_sat().to_be_bytes()).unwrap();
159 req.policy.encode(&mut engine).unwrap();
160 }
161
162 let hash = sha256::Hash::from_engine(engine).to_byte_array();
163 Message::from_digest(hash)
164 }
165}
166
167impl ProtocolEncoding for DelegatedRoundParticipationAttestation {
168 fn encode<W: io::Write + ?Sized>(&self, writer: &mut W) -> Result<(), io::Error> {
169 self.signature.encode(writer)
170 }
171
172 fn decode<R: io::Read + ?Sized>(reader: &mut R) -> Result<Self, ProtocolDecodingError> {
173 let signature = schnorr::Signature::decode(reader)?;
174 Ok(Self { signature })
175 }
176}
177
178#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Ord, PartialOrd, Serialize, Deserialize)]
184pub struct LightningReceiveAttestation {
185 signature: schnorr::Signature,
186}
187
188impl LightningReceiveAttestation {
189 const CHALLENGE_MESSAGE_PREFIX: &'static [u8; 32] = b"Lightning receive VTXO challenge";
191
192 pub fn new(
194 payment_hash: PaymentHash,
195 vtxo_id: VtxoId,
196 vtxo_keypair: &Keypair,
197 ) -> Self {
198 let msg = Self::compute_message(payment_hash, vtxo_id);
199 let signature = SECP.sign_schnorr_with_aux_rand(&msg, vtxo_keypair, &rand::random());
200 Self { signature }
201 }
202
203 pub fn signature(&self) -> &schnorr::Signature {
205 &self.signature
206 }
207
208 pub fn verify(&self, payment_hash: PaymentHash, vtxo: &Vtxo) -> Result<(), secp256k1::Error> {
210 let msg = Self::compute_message(payment_hash, vtxo.id());
211 SECP.verify_schnorr(&self.signature, &msg, &vtxo.user_pubkey().x_only_public_key().0)
212 }
213
214 fn compute_message(payment_hash: PaymentHash, vtxo_id: VtxoId) -> Message {
215 let mut engine = sha256::Hash::engine();
216 engine.write_all(Self::CHALLENGE_MESSAGE_PREFIX).unwrap();
217 engine.write_all(&payment_hash.to_byte_array()).unwrap();
218 engine.write_all(&vtxo_id.to_bytes()).unwrap();
219
220 let hash = sha256::Hash::from_engine(engine).to_byte_array();
221 Message::from_digest(hash)
222 }
223}
224
225impl ProtocolEncoding for LightningReceiveAttestation {
226 fn encode<W: io::Write + ?Sized>(&self, writer: &mut W) -> Result<(), io::Error> {
227 self.signature.encode(writer)
228 }
229
230 fn decode<R: io::Read + ?Sized>(reader: &mut R) -> Result<Self, ProtocolDecodingError> {
231 let signature = schnorr::Signature::decode(reader)?;
232 Ok(Self { signature })
233 }
234}
235
236#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Ord, PartialOrd, Serialize, Deserialize)]
250pub struct VtxoStatusAttestation {
251 signature: schnorr::Signature,
252}
253
254impl VtxoStatusAttestation {
255 const CHALLENGE_MESSAGE_PREFIX: &'static [u8; 32] = b"Ark VTXO status query challenge ";
257
258 pub fn new(vtxo_id: VtxoId, vtxo_keypair: &Keypair) -> Self {
260 let msg = Self::compute_message(vtxo_id);
261 let signature = SECP.sign_schnorr_with_aux_rand(&msg, vtxo_keypair, &rand::random());
262 Self { signature }
263 }
264
265 pub fn signature(&self) -> &schnorr::Signature {
267 &self.signature
268 }
269
270 pub fn verify(&self, vtxo: &Vtxo) -> Result<(), secp256k1::Error> {
272 let msg = Self::compute_message(vtxo.id());
273 SECP.verify_schnorr(&self.signature, &msg, &vtxo.user_pubkey().x_only_public_key().0)
274 }
275
276 fn compute_message(vtxo_id: VtxoId) -> Message {
277 let mut engine = sha256::Hash::engine();
278 engine.write_all(Self::CHALLENGE_MESSAGE_PREFIX).unwrap();
279 engine.write_all(&vtxo_id.to_bytes()).unwrap();
280
281 let hash = sha256::Hash::from_engine(engine).to_byte_array();
282 Message::from_digest(hash)
283 }
284}
285
286impl ProtocolEncoding for VtxoStatusAttestation {
287 fn encode<W: io::Write + ?Sized>(&self, writer: &mut W) -> Result<(), io::Error> {
288 self.signature.encode(writer)
289 }
290
291 fn decode<R: io::Read + ?Sized>(reader: &mut R) -> Result<Self, ProtocolDecodingError> {
292 let signature = schnorr::Signature::decode(reader)?;
293 Ok(Self { signature })
294 }
295}
296
297#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Ord, PartialOrd, Serialize, Deserialize)]
301pub struct OffboardRequestAttestation {
302 signature: schnorr::Signature,
303}
304
305impl OffboardRequestAttestation {
306 const CHALLENGE_MESSAGE_PREFIX: &'static [u8; 32] = b"Ark offboard request challenge ";
308
309 pub fn new(
311 req: &OffboardRequest,
312 inputs: &[VtxoId],
313 vtxo_keypair: &Keypair,
314 ) -> Self {
315 let msg = Self::compute_message(req, inputs);
316 let signature = SECP.sign_schnorr_with_aux_rand(&msg, vtxo_keypair, &rand::random());
317 Self { signature }
318 }
319
320 pub fn signature(&self) -> &schnorr::Signature {
322 &self.signature
323 }
324
325 pub fn verify(
327 &self,
328 req: &OffboardRequest,
329 inputs: &[VtxoId],
330 vtxo: &Vtxo,
331 ) -> Result<(), secp256k1::Error> {
332 let msg = Self::compute_message(req, inputs);
333 SECP.verify_schnorr(&self.signature, &msg, &vtxo.user_pubkey().x_only_public_key().0)
334 }
335
336 fn compute_message(req: &OffboardRequest, inputs: &[VtxoId]) -> Message {
337 let mut eng = sha256::Hash::engine();
338 eng.input(Self::CHALLENGE_MESSAGE_PREFIX);
339 req.to_txout().encode(&mut eng).unwrap();
340 eng.emit_u32(inputs.len() as u32).unwrap();
341 for vtxo in inputs {
342 eng.input(&vtxo.to_bytes());
343 }
344 Message::from_digest(sha256::Hash::from_engine(eng).to_byte_array())
345 }
346}
347
348impl ProtocolEncoding for OffboardRequestAttestation {
349 fn encode<W: io::Write + ?Sized>(&self, writer: &mut W) -> Result<(), io::Error> {
350 self.signature.encode(writer)
351 }
352
353 fn decode<R: io::Read + ?Sized>(reader: &mut R) -> Result<Self, ProtocolDecodingError> {
354 let signature = schnorr::Signature::decode(reader)?;
355 Ok(Self { signature })
356 }
357}
358
359#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Ord, PartialOrd)]
365pub struct ArkoorCosignAttestation {
366 signature: schnorr::Signature,
367}
368
369impl ArkoorCosignAttestation {
370 const CHALLENGE_MESSAGE_PREFIX: &'static [u8; 32] = b"arkoor cosign attestation ";
371
372 pub fn new(
373 vtxo_id: VtxoId,
374 outputs: &[&ArkoorDestination],
375 vtxo_keypair: &Keypair,
376 ) -> Self {
377 let msg = Self::compute_message(vtxo_id, outputs);
378 let signature = SECP.sign_schnorr_with_aux_rand(&msg, vtxo_keypair, &rand::random());
379 Self { signature }
380 }
381
382 pub fn verify(&self, vtxo: &Vtxo, outputs: &[&ArkoorDestination]) -> Result<(), secp256k1::Error> {
384 let msg = Self::compute_message(vtxo.id(), outputs);
385 SECP.verify_schnorr(&self.signature, &msg, &vtxo.user_pubkey().x_only_public_key().0)
386 }
387
388 fn compute_message(vtxo_id: VtxoId, outputs: &[&ArkoorDestination]) -> Message {
389 let mut eng = sha256::Hash::engine();
390 eng.input(Self::CHALLENGE_MESSAGE_PREFIX);
391 eng.input(&vtxo_id.to_bytes());
392
393 eng.emit_u32(outputs.len() as u32).unwrap();
394 for output in outputs {
395 eng.emit_u64(output.total_amount.to_sat()).unwrap();
396 output.policy.encode(&mut eng).unwrap();
397 }
398 Message::from_digest(sha256::Hash::from_engine(eng).to_byte_array())
399 }
400}
401
402impl ProtocolEncoding for ArkoorCosignAttestation {
403 fn encode<W: io::Write + ?Sized>(&self, writer: &mut W) -> Result<(), io::Error> {
404 self.signature.encode(writer)
405 }
406
407 fn decode<R: io::Read + ?Sized>(reader: &mut R) -> Result<Self, ProtocolDecodingError> {
408 let signature = schnorr::Signature::decode(reader)?;
409 Ok(Self { signature })
410 }
411}
412
413#[cfg(test)]
414mod tests {
415 use super::*;
416 use bitcoin::{Amount, PublicKey, ScriptBuf};
417 use bitcoin::hashes::Hash;
418 use bitcoin::hex::DisplayHex;
419 use std::str::FromStr;
420 use crate::FeeRate;
421 use crate::test_util::dummy::{DummyTestVtxoSpec, DUMMY_USER_KEY};
422 use crate::test_util::encoding_roundtrip;
423 use crate::vtxo::policy::{PubkeyVtxoPolicy, VtxoPolicy};
424 use crate::musig;
425
426 lazy_static! {
427 static ref TEST_CHALLENGE: Challenge = Challenge::new([0x42; 32]);
428 }
429
430 #[test]
431 fn test_round_attempt_attestation() {
432 let spec = DummyTestVtxoSpec::default();
433 let (_tx, vtxo) = spec.build();
434
435 let challenge = *TEST_CHALLENGE;
436 let vtxo_id = vtxo.id();
437
438 let vtxo_req = VtxoRequest {
439 amount: Amount::from_sat(100_000),
440 policy: VtxoPolicy::Pubkey(PubkeyVtxoPolicy {
441 user_pubkey: DUMMY_USER_KEY.public_key(),
442 }),
443 };
444 let (_, pub_nonce) = musig::nonce_pair(&DUMMY_USER_KEY);
445 let signed_vtxo_req = SignedVtxoRequest {
446 vtxo: vtxo_req,
447 cosign_pubkey: DUMMY_USER_KEY.public_key(),
448 nonces: vec![pub_nonce],
449 };
450 let vtxo_reqs = vec![signed_vtxo_req];
451
452 let attestation = RoundAttemptAttestation::new(
453 challenge,
454 vtxo_id,
455 &vtxo_reqs,
456 &DUMMY_USER_KEY,
457 );
458
459 println!("RoundAttemptAttestation hex: {}", attestation.serialize().as_hex());
460 encoding_roundtrip(&attestation);
461 attestation.verify(challenge, &vtxo, &vtxo_reqs).expect("verification failed");
462
463 let vector = "ddbf11d8022ec8bcc85704e0ee0c27f1c26d024c43692653d060284ad5cf32ebf89d849b6943d7b1cb9e9c108251c23f067e95bf585fabcf4be5baab8a53897a";
465 let hardcoded = RoundAttemptAttestation::deserialize_hex(&vector)
466 .expect("valid attestation");
467 hardcoded.verify(challenge, &vtxo, &vtxo_reqs).expect("hardcoded verification failed");
468 }
469
470 #[test]
471 fn test_delegated_round_participation_attestation() {
472 let spec = DummyTestVtxoSpec::default();
473 let (_tx, vtxo) = spec.build();
474
475 let vtxo_id = vtxo.id();
476
477 let vtxo_req = VtxoRequest {
478 amount: Amount::from_sat(100_000),
479 policy: VtxoPolicy::Pubkey(PubkeyVtxoPolicy {
480 user_pubkey: DUMMY_USER_KEY.public_key(),
481 }),
482 };
483 let vtxo_reqs = vec![vtxo_req];
484
485 let attestation = DelegatedRoundParticipationAttestation::new(
486 vtxo_id,
487 &vtxo_reqs,
488 &DUMMY_USER_KEY,
489 );
490 println!("DelegatedRoundParticipationAttestation hex: {}", attestation.serialize().as_hex());
491 encoding_roundtrip(&attestation);
492 attestation.verify(&vtxo, &vtxo_reqs).expect("verification failed");
493
494 let vector = "fc482a0ef7b86427416865657986032bb49d458c3649097a389670aabee26bd7789218f18b2b58cc68524cb7ae4f224b19fcb6d905e50e63d32dacc04d78cf32";
496 let hardcoded = DelegatedRoundParticipationAttestation::deserialize_hex(&vector)
497 .expect("valid attestation");
498 hardcoded.verify(&vtxo, &vtxo_reqs).expect("hardcoded verification failed");
499 }
500
501 #[test]
502 fn test_lightning_receive_attestation() {
503 let spec = DummyTestVtxoSpec::default();
504 let (_tx, vtxo) = spec.build();
505
506 let payment_hash = PaymentHash::from(sha256::Hash::hash(&[0x42; 32]));
507 let vtxo_id = vtxo.id();
508
509 let attestation = LightningReceiveAttestation::new(
510 payment_hash,
511 vtxo_id,
512 &DUMMY_USER_KEY,
513 );
514 println!("LightningReceiveAttestation hex: {}", attestation.serialize().as_hex());
515 encoding_roundtrip(&attestation);
516 attestation.verify(payment_hash, &vtxo).expect("verification failed");
517
518 let vector = "a1240a572d9298c08a75102fb872b8d30bae521228083ec717f220d32de3b4446e7214e6e9e1c586c797fdff8b67e26d6f81a497dee5d584bdc80851e06c7fd5";
520 let hardcoded = LightningReceiveAttestation::deserialize_hex(&vector)
521 .expect("valid attestation");
522 hardcoded.verify(payment_hash, &vtxo).expect("hardcoded verification failed");
523 }
524
525 #[test]
526 fn test_vtxo_status_attestation() {
527 let spec = DummyTestVtxoSpec::default();
528 let (_tx, vtxo) = spec.build();
529
530 let vtxo_id = vtxo.id();
531
532 let attestation = VtxoStatusAttestation::new(vtxo_id, &DUMMY_USER_KEY);
533 println!("VtxoStatusAttestation hex: {}", attestation.serialize().as_hex());
534 encoding_roundtrip(&attestation);
535 attestation.verify(&vtxo).expect("verification failed");
536
537 let vector = "ae3bb779ec3f700ccef8031f6589797ec7ac56443b01ed495bef210c8ca12a603c26ffaa2d30502a911c635b5926f1c898cad343b7cd31105aaa22c4c3688f54";
539 let hardcoded = VtxoStatusAttestation::deserialize_hex(&vector)
540 .expect("valid attestation");
541 hardcoded.verify(&vtxo).expect("hardcoded verification failed");
542 }
543
544 #[test]
545 fn test_arkoor_cosign_attestation() {
546 let spec = DummyTestVtxoSpec::default();
547 let (_tx, vtxo) = spec.build();
548
549 let vtxo_id = vtxo.id();
550
551 let dest = ArkoorDestination {
552 total_amount: Amount::from_sat(50_000),
553 policy: VtxoPolicy::Pubkey(PubkeyVtxoPolicy {
554 user_pubkey: DUMMY_USER_KEY.public_key(),
555 }),
556 };
557 let outputs = vec![&dest];
558
559 let attestation = ArkoorCosignAttestation::new(
560 vtxo_id,
561 &outputs,
562 &DUMMY_USER_KEY,
563 );
564 println!("ArkoorCosignAttestation hex: {}", attestation.serialize().as_hex());
565 encoding_roundtrip(&attestation);
566 attestation.verify(&vtxo, &outputs).expect("verification failed");
567 }
568
569 #[test]
570 fn test_offboard_request_attestation() {
571 let spec = DummyTestVtxoSpec::default();
572 let (_tx, vtxo) = spec.build();
573
574 let req_pk = PublicKey::from_str(
575 "02271fba79f590251099b07fa0393b4c55d5e50cd8fca2e2822b619f8aabf93b74",
576 ).unwrap();
577 let xonly_pk = req_pk.inner.x_only_public_key().0;
578 let offboard_req = OffboardRequest {
579 script_pubkey: ScriptBuf::new_p2tr(&*SECP, xonly_pk, None),
580 net_amount: Amount::from_sat(50_000),
581 deduct_fees_from_gross_amount: false,
582 fee_rate: FeeRate::from_sat_per_vb(10).unwrap(),
583 };
584 let inputs = vec![vtxo.id()];
585
586 let attestation = OffboardRequestAttestation::new(
587 &offboard_req,
588 &inputs,
589 &DUMMY_USER_KEY,
590 );
591 println!("OffboardRequestAttestation hex: {}", attestation.serialize().as_hex());
592 encoding_roundtrip(&attestation);
593 attestation.verify(&offboard_req, &inputs, &vtxo).expect("verification failed");
594
595 let vector = "d6c85934b715c086164294645fadbe6a08fe6263609f38b44c23239913935c2e30fe5e9ad099941e751facb039797b6c920a90095b5e1cf26cc8aa274082aead";
597 let hardcoded = OffboardRequestAttestation::deserialize_hex(&vector)
598 .expect("valid attestation");
599 hardcoded.verify(&offboard_req, &inputs, &vtxo).expect("hardcoded verification failed");
600 }
601}