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