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