ark/
challenges.rs

1use std::io::Write as _;
2
3use bitcoin::hashes::{sha256, Hash};
4use bitcoin::key::Keypair;
5use bitcoin::secp256k1::{self, schnorr, Message};
6
7use crate::{OffboardRequest, SignedVtxoRequest, Vtxo, VtxoId, SECP};
8use crate::encode::ProtocolEncoding;
9use crate::lightning::PaymentHash;
10
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13pub struct RoundAttemptChallenge([u8; 32]);
14
15impl RoundAttemptChallenge {
16	const CHALLENGE_MESSAGE_PREFIX: &'static [u8; 32] = b"Ark round input ownership proof ";
17
18	pub fn new(value: [u8; 32]) -> Self {
19		Self(value)
20	}
21
22	pub fn generate() -> Self {
23		Self(rand::random())
24	}
25
26	pub fn inner(&self) -> [u8; 32] {
27		self.0
28	}
29
30	/// Combines [RoundAttemptChallenge] and round submit data in a signable message
31	fn as_signable_message(
32		&self,
33		vtxo_id: VtxoId,
34		vtxo_reqs: &[SignedVtxoRequest],
35		offboard_reqs: &[OffboardRequest],
36	) -> Message {
37		let mut engine = sha256::Hash::engine();
38		engine.write_all(Self::CHALLENGE_MESSAGE_PREFIX).unwrap();
39		engine.write_all(&self.0).unwrap();
40		engine.write_all(&vtxo_id.to_bytes()).unwrap();
41
42		engine.write_all(&vtxo_reqs.len().to_be_bytes()).unwrap();
43		for req in vtxo_reqs {
44			engine.write_all(&req.vtxo.amount.to_sat().to_be_bytes()).unwrap();
45			req.vtxo.policy.encode(&mut engine).unwrap();
46			req.cosign_pubkey.encode(&mut engine).unwrap();
47		}
48
49		engine.write_all(&offboard_reqs.len().to_be_bytes()).unwrap();
50		for req in offboard_reqs {
51			req.to_txout().encode(&mut engine).unwrap();
52		}
53		let hash = sha256::Hash::from_engine(engine).to_byte_array();
54		Message::from_digest(hash)
55	}
56
57	pub fn sign_with(
58		&self,
59		vtxo_id: VtxoId,
60		vtxo_reqs: &[SignedVtxoRequest],
61		offboard_reqs: &[OffboardRequest],
62		vtxo_keypair: &Keypair,
63	) -> schnorr::Signature {
64		let msg = self.as_signable_message(vtxo_id, vtxo_reqs, offboard_reqs);
65		SECP.sign_schnorr_with_aux_rand(&msg, &vtxo_keypair, &rand::random())
66	}
67
68	pub fn verify_input_vtxo_sig(
69		&self,
70		vtxo: &Vtxo,
71		vtxo_reqs: &[SignedVtxoRequest],
72		offboard_reqs: &[OffboardRequest],
73		sig: &schnorr::Signature,
74	) -> Result<(), secp256k1::Error> {
75		let msg = self.as_signable_message(vtxo.id(), vtxo_reqs, offboard_reqs);
76		SECP.verify_schnorr( sig, &msg, &vtxo.user_pubkey().x_only_public_key().0)
77	}
78}
79
80/// Challenge for proving ownership of a VTXO when claiming a Lightning receive.
81///
82/// This challenge combines a payment hash with the input VTXO ID to create
83/// a unique signature proving the user controls the input VTXO and is authorised
84/// as a mitigation against liquidity denial-of-service attacks.
85#[derive(Clone, Copy, PartialEq, Eq, Hash, Ord, PartialOrd)]
86pub struct LightningReceiveChallenge(PaymentHash);
87
88impl LightningReceiveChallenge {
89	const CHALLENGE_MESSAGE_PREFIX: &'static [u8; 32] = b"Lightning receive VTXO challenge";
90
91	pub fn new(value: PaymentHash) -> Self {
92		Self(value)
93	}
94
95	/// Combines [VtxoId] and the inner [PaymentHash] to prove ownership of
96	/// a VTXO while commiting to the Lightning receive associated with the unique
97	/// payment hash.
98	fn as_signable_message(&self, vtxo_id: VtxoId) -> Message {
99		let mut engine = sha256::Hash::engine();
100		engine.write_all(Self::CHALLENGE_MESSAGE_PREFIX).unwrap();
101		engine.write_all(&self.0.to_byte_array()).unwrap();
102		engine.write_all(&vtxo_id.to_bytes()).unwrap();
103
104		let hash = sha256::Hash::from_engine(engine).to_byte_array();
105		Message::from_digest(hash)
106	}
107
108	pub fn sign_with(
109		&self,
110		vtxo_id: VtxoId,
111		vtxo_keypair: &Keypair,
112	) -> schnorr::Signature {
113		SECP.sign_schnorr_with_aux_rand(
114			&Self::as_signable_message(self, vtxo_id),
115			&vtxo_keypair,
116			&rand::random()
117		)
118	}
119
120	pub fn verify_input_vtxo_sig(
121		&self,
122		vtxo: &Vtxo,
123		sig: &schnorr::Signature,
124	) -> Result<(), secp256k1::Error> {
125		SECP.verify_schnorr(
126			sig,
127			&Self::as_signable_message(self, vtxo.id()),
128			&vtxo.user_pubkey().x_only_public_key().0,
129		)
130	}
131}
132
133/// Challenge for proving ownership of a VTXO when querying its status.
134///
135/// This is the simplest challenge - it only commits to the VTXO ID itself,
136/// with no additional challenge data or context. It proves the user controls
137/// the VTXO and is authorised to query its status.
138///
139/// No additional unique or random challenge data is necessary here.
140/// We're not concerned with guarding against "replay" attacks as this challenge
141/// is for informational purposes and knowledge of this proof by a third party
142/// would indicate some kind of prior privacy leak for the user.
143///
144/// A malicious third party that can access this signed message would only be able
145/// to query the status of this specific VTXO.
146#[derive(Clone, Copy, PartialEq, Eq, Hash, Ord, PartialOrd)]
147pub struct VtxoStatusChallenge;
148
149impl VtxoStatusChallenge {
150	const CHALLENGE_MESSAGE_PREFIX: &'static [u8; 32] = b"Ark VTXO status query challenge ";
151
152	pub fn new() -> Self {
153		Self
154	}
155
156	fn as_signable_message(&self, vtxo_id: VtxoId) -> Message {
157		let mut engine = sha256::Hash::engine();
158		engine.write_all(Self::CHALLENGE_MESSAGE_PREFIX).unwrap();
159		engine.write_all(&vtxo_id.to_bytes()).unwrap();
160
161		let hash = sha256::Hash::from_engine(engine).to_byte_array();
162		Message::from_digest(hash)
163	}
164
165	pub fn sign_with(
166		&self,
167		vtxo_id: VtxoId,
168		vtxo_keypair: &Keypair,
169	) -> schnorr::Signature {
170		SECP.sign_schnorr_with_aux_rand(
171			&Self::as_signable_message(self, vtxo_id),
172			&vtxo_keypair,
173			&rand::random(),
174		)
175	}
176
177	pub fn verify_input_vtxo_sig(
178		&self,
179		vtxo: &Vtxo,
180		sig: &schnorr::Signature,
181	) -> Result<(), secp256k1::Error> {
182		SECP.verify_schnorr(
183			sig,
184			&Self::as_signable_message(self, vtxo.id()),
185			&vtxo.user_pubkey().x_only_public_key().0,
186		)
187	}
188}