ark/
challenges.rs

1use std::io::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::encode::ProtocolEncoding;
10use crate::lightning::PaymentHash;
11use crate::offboard::OffboardRequest;
12
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15pub struct RoundAttemptChallenge([u8; 32]);
16
17impl RoundAttemptChallenge {
18	const CHALLENGE_MESSAGE_PREFIX: &'static [u8; 32] = b"Ark round input ownership proof ";
19
20	pub fn new(value: [u8; 32]) -> Self {
21		Self(value)
22	}
23
24	pub fn generate() -> Self {
25		Self(rand::random())
26	}
27
28	pub fn inner(&self) -> [u8; 32] {
29		self.0
30	}
31
32	/// Combines [RoundAttemptChallenge] and round submit data in a signable message
33	fn as_signable_message(
34		&self,
35		vtxo_id: VtxoId,
36		vtxo_reqs: &[SignedVtxoRequest],
37	) -> Message {
38		let mut engine = sha256::Hash::engine();
39		engine.write_all(Self::CHALLENGE_MESSAGE_PREFIX).unwrap();
40		engine.write_all(&self.0).unwrap();
41		engine.write_all(&vtxo_id.to_bytes()).unwrap();
42
43		engine.write_all(&vtxo_reqs.len().to_be_bytes()).unwrap();
44		for req in vtxo_reqs {
45			engine.write_all(&req.vtxo.amount.to_sat().to_be_bytes()).unwrap();
46			req.vtxo.policy.encode(&mut engine).unwrap();
47			req.cosign_pubkey.encode(&mut engine).unwrap();
48		}
49
50		let hash = sha256::Hash::from_engine(engine).to_byte_array();
51		Message::from_digest(hash)
52	}
53
54	pub fn sign_with(
55		&self,
56		vtxo_id: VtxoId,
57		vtxo_reqs: &[SignedVtxoRequest],
58		vtxo_keypair: &Keypair,
59	) -> schnorr::Signature {
60		let msg = self.as_signable_message(vtxo_id, vtxo_reqs);
61		SECP.sign_schnorr_with_aux_rand(&msg, &vtxo_keypair, &rand::random())
62	}
63
64	pub fn verify_input_vtxo_sig(
65		&self,
66		vtxo: &Vtxo,
67		vtxo_reqs: &[SignedVtxoRequest],
68		sig: &schnorr::Signature,
69	) -> Result<(), secp256k1::Error> {
70		let msg = self.as_signable_message(vtxo.id(), vtxo_reqs);
71		SECP.verify_schnorr( sig, &msg, &vtxo.user_pubkey().x_only_public_key().0)
72	}
73}
74
75#[derive(Debug, Clone, Copy, PartialEq, Eq)]
76pub struct NonInteractiveRoundParticipationChallenge;
77
78impl NonInteractiveRoundParticipationChallenge {
79	const CHALLENGE_MESSAGE_PREFIX: &'static [u8; 32] = b"hArk round join ownership proof ";
80
81	/// Combines [NonInteractiveRoundParticipationChallenge] and
82	/// round submit data in a signable message
83	fn signable_message(
84		vtxo_id: VtxoId,
85		vtxo_reqs: &[VtxoRequest],
86	) -> Message {
87		let mut engine = sha256::Hash::engine();
88		engine.write_all(Self::CHALLENGE_MESSAGE_PREFIX).unwrap();
89		engine.write_all(&vtxo_id.to_bytes()).unwrap();
90
91		engine.write_all(&vtxo_reqs.len().to_be_bytes()).unwrap();
92		for req in vtxo_reqs {
93			engine.write_all(&req.amount.to_sat().to_be_bytes()).unwrap();
94			req.policy.encode(&mut engine).unwrap();
95		}
96
97		let hash = sha256::Hash::from_engine(engine).to_byte_array();
98		Message::from_digest(hash)
99	}
100
101	pub fn sign_with(
102		vtxo_id: VtxoId,
103		vtxo_reqs: &[VtxoRequest],
104		vtxo_keypair: &Keypair,
105	) -> schnorr::Signature {
106		let msg = Self::signable_message(vtxo_id, vtxo_reqs);
107		SECP.sign_schnorr_with_aux_rand(&msg, &vtxo_keypair, &rand::random())
108	}
109
110	pub fn verify_input_vtxo_sig(
111		vtxo: &Vtxo,
112		vtxo_reqs: &[VtxoRequest],
113		sig: &schnorr::Signature,
114	) -> Result<(), secp256k1::Error> {
115		let msg = Self::signable_message(vtxo.id(), vtxo_reqs);
116		SECP.verify_schnorr( sig, &msg, &vtxo.user_pubkey().x_only_public_key().0)
117	}
118}
119
120/// Challenge for proving ownership of a VTXO when claiming a Lightning receive.
121///
122/// This challenge combines a payment hash with the input VTXO ID to create
123/// a unique signature proving the user controls the input VTXO and is authorised
124/// as a mitigation against liquidity denial-of-service attacks.
125#[derive(Clone, Copy, PartialEq, Eq, Hash, Ord, PartialOrd)]
126pub struct LightningReceiveChallenge(PaymentHash);
127
128impl LightningReceiveChallenge {
129	const CHALLENGE_MESSAGE_PREFIX: &'static [u8; 32] = b"Lightning receive VTXO challenge";
130
131	pub fn new(value: PaymentHash) -> Self {
132		Self(value)
133	}
134
135	/// Combines [VtxoId] and the inner [PaymentHash] to prove ownership of
136	/// a VTXO while commiting to the Lightning receive associated with the unique
137	/// payment hash.
138	fn as_signable_message(&self, vtxo_id: VtxoId) -> Message {
139		let mut engine = sha256::Hash::engine();
140		engine.write_all(Self::CHALLENGE_MESSAGE_PREFIX).unwrap();
141		engine.write_all(&self.0.to_byte_array()).unwrap();
142		engine.write_all(&vtxo_id.to_bytes()).unwrap();
143
144		let hash = sha256::Hash::from_engine(engine).to_byte_array();
145		Message::from_digest(hash)
146	}
147
148	pub fn sign_with(
149		&self,
150		vtxo_id: VtxoId,
151		vtxo_keypair: &Keypair,
152	) -> schnorr::Signature {
153		SECP.sign_schnorr_with_aux_rand(
154			&Self::as_signable_message(self, vtxo_id),
155			&vtxo_keypair,
156			&rand::random()
157		)
158	}
159
160	pub fn verify_input_vtxo_sig(
161		&self,
162		vtxo: &Vtxo,
163		sig: &schnorr::Signature,
164	) -> Result<(), secp256k1::Error> {
165		SECP.verify_schnorr(
166			sig,
167			&Self::as_signable_message(self, vtxo.id()),
168			&vtxo.user_pubkey().x_only_public_key().0,
169		)
170	}
171}
172
173/// Challenge for proving ownership of a VTXO when querying its status.
174///
175/// This is the simplest challenge - it only commits to the VTXO ID itself,
176/// with no additional challenge data or context. It proves the user controls
177/// the VTXO and is authorised to query its status.
178///
179/// No additional unique or random challenge data is necessary here.
180/// We're not concerned with guarding against "replay" attacks as this challenge
181/// is for informational purposes and knowledge of this proof by a third party
182/// would indicate some kind of prior privacy leak for the user.
183///
184/// A malicious third party that can access this signed message would only be able
185/// to query the status of this specific VTXO.
186#[derive(Clone, Copy, PartialEq, Eq, Hash, Ord, PartialOrd)]
187pub struct VtxoStatusChallenge;
188
189impl VtxoStatusChallenge {
190	const CHALLENGE_MESSAGE_PREFIX: &'static [u8; 32] = b"Ark VTXO status query challenge ";
191
192	pub fn new() -> Self {
193		Self
194	}
195
196	fn as_signable_message(&self, vtxo_id: VtxoId) -> Message {
197		let mut engine = sha256::Hash::engine();
198		engine.write_all(Self::CHALLENGE_MESSAGE_PREFIX).unwrap();
199		engine.write_all(&vtxo_id.to_bytes()).unwrap();
200
201		let hash = sha256::Hash::from_engine(engine).to_byte_array();
202		Message::from_digest(hash)
203	}
204
205	pub fn sign_with(
206		&self,
207		vtxo_id: VtxoId,
208		vtxo_keypair: &Keypair,
209	) -> schnorr::Signature {
210		SECP.sign_schnorr_with_aux_rand(
211			&Self::as_signable_message(self, vtxo_id),
212			&vtxo_keypair,
213			&rand::random(),
214		)
215	}
216
217	pub fn verify_input_vtxo_sig(
218		&self,
219		vtxo: &Vtxo,
220		sig: &schnorr::Signature,
221	) -> Result<(), secp256k1::Error> {
222		SECP.verify_schnorr(
223			sig,
224			&Self::as_signable_message(self, vtxo.id()),
225			&vtxo.user_pubkey().x_only_public_key().0,
226		)
227	}
228}
229
230/// Challenge for proving ownership of a VTXO when requesting an offboard
231///
232/// It commits to the offboard request and all input vtxos.
233#[derive(Clone, Copy, PartialEq, Eq, Hash, Ord, PartialOrd)]
234pub struct OffboardRequestChallenge {
235	message: Message,
236}
237
238impl OffboardRequestChallenge {
239	const CHALLENGE_MESSAGE_PREFIX: &'static [u8; 32] = b"Ark offboard request challenge  ";
240
241	pub fn new(
242		request: &OffboardRequest,
243		inputs: impl Iterator<Item = VtxoId> + ExactSizeIterator,
244	) -> Self {
245		let mut eng = sha256::Hash::engine();
246		eng.input(Self::CHALLENGE_MESSAGE_PREFIX);
247		request.to_txout().encode(&mut eng).unwrap();
248		eng.emit_u32(inputs.len() as u32).unwrap();
249		for vtxo in inputs {
250			eng.input(&vtxo.to_bytes());
251		}
252		Self {
253			message: Message::from_digest(sha256::Hash::from_engine(eng).to_byte_array()),
254		}
255	}
256
257	pub fn sign_with(
258		&self,
259		vtxo_keypair: &Keypair,
260	) -> schnorr::Signature {
261		SECP.sign_schnorr_with_aux_rand(
262			&self.message,
263			vtxo_keypair,
264			&rand::random(),
265		)
266	}
267
268	pub fn verify_input_vtxo_sig(
269		&self,
270		vtxo: &Vtxo,
271		sig: &schnorr::Signature,
272	) -> Result<(), secp256k1::Error> {
273		SECP.verify_schnorr(
274			sig,
275			&self.message,
276			&vtxo.user_pubkey().x_only_public_key().0,
277		)
278	}
279}