ark/
rounds.rs

1
2use std::fmt;
3use std::collections::HashMap;
4use std::io::Write;
5use std::str::FromStr;
6
7use bitcoin::hashes::{sha256, Hash};
8use bitcoin::hex::DisplayHex;
9use bitcoin::secp256k1::PublicKey;
10use bitcoin::{key::Keypair, FeeRate, Transaction, Txid};
11use bitcoin::secp256k1::{self, schnorr, Message};
12
13use crate::{musig, OffboardRequest, SECP, SignedVtxoRequest, Vtxo, VtxoId};
14use crate::encode::ProtocolEncoding;
15use crate::tree::signed::VtxoTreeSpec;
16
17/// A round tx must have at least vtxo tree and connector chain outputs.
18/// Can also have several offboard outputs on next indices.
19pub const MIN_ROUND_TX_OUTPUTS: usize = 2;
20
21/// The output index of the vtxo tree root in the round tx.
22pub const ROUND_TX_VTXO_TREE_VOUT: u32 = 0;
23/// The output index of the connector chain  start in the round tx.
24pub const ROUND_TX_CONNECTOR_VOUT: u32 = 1;
25
26#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27pub struct VtxoOwnershipChallenge([u8; 32]);
28
29impl VtxoOwnershipChallenge {
30	const CHALENGE_MESSAGE_PREFIX: &'static [u8; 32] = b"Ark round input ownership proof ";
31
32	pub fn new(value: [u8; 32]) -> Self {
33		Self(value)
34	}
35
36	pub fn generate() -> Self {
37		Self(rand::random())
38	}
39
40	pub fn inner(&self) -> [u8; 32] {
41		self.0
42	}
43
44	/// Combines [VtxoOwnershipChallenge] and [VtxoId] in a signable message
45	///
46	/// Note: because we use [`VtxoId`] in the message, there is no
47	fn as_signable_message(&self, vtxo_id: VtxoId, vtxo_reqs: &[SignedVtxoRequest], offboard_reqs: &[OffboardRequest]) -> Message {
48		let mut engine = sha256::Hash::engine();
49		engine.write_all(Self::CHALENGE_MESSAGE_PREFIX).unwrap();
50		engine.write_all(&self.0).unwrap();
51		engine.write_all(&vtxo_id.to_bytes()).unwrap();
52
53		engine.write_all(&vtxo_reqs.len().to_be_bytes()).unwrap();
54		for req in vtxo_reqs {
55			engine.write_all(&req.vtxo.amount.to_sat().to_be_bytes()).unwrap();
56			req.vtxo.policy.encode(&mut engine).unwrap();
57			req.cosign_pubkey.encode(&mut engine).unwrap();
58		}
59
60		engine.write_all(&offboard_reqs.len().to_be_bytes()).unwrap();
61		for req in offboard_reqs {
62			req.to_txout().encode(&mut engine).unwrap();
63		}
64		let hash = sha256::Hash::from_engine(engine).to_byte_array();
65		Message::from_digest(hash)
66	}
67
68	pub fn sign_with(&self, vtxo_id: VtxoId, vtxo_reqs: &[SignedVtxoRequest], offboard_reqs: &[OffboardRequest], vtxo_keypair: Keypair) -> schnorr::Signature {
69		SECP.sign_schnorr(&self.as_signable_message(vtxo_id, vtxo_reqs, offboard_reqs), &vtxo_keypair)
70	}
71
72	pub fn verify_input_vtxo_sig(
73		&self,
74		vtxo: &Vtxo,
75		vtxo_reqs: &[SignedVtxoRequest],
76		offboard_reqs: &[OffboardRequest],
77		sig: &schnorr::Signature,
78	) -> Result<(), secp256k1::Error> {
79		SECP.verify_schnorr(
80			sig,
81			&self.as_signable_message(vtxo.id(), vtxo_reqs, offboard_reqs),
82			&vtxo.user_pubkey().x_only_public_key().0,
83		)
84	}
85}
86
87/// Unique identifier for a round.
88///
89/// It is a simple sequence number.
90///
91/// It is used to identify a round before we have a round tx.
92/// [RoundId] should be used as soon as we have a round tx.
93#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
94pub struct RoundSeq(u64);
95
96impl RoundSeq {
97	pub const fn new(seq: u64) -> Self {
98		Self(seq)
99	}
100
101	pub fn increment(&mut self) {
102		self.0 += 1;
103	}
104
105	pub fn inner(&self) -> u64 {
106		self.0
107	}
108}
109
110impl From<u64> for RoundSeq {
111	fn from(v: u64) -> Self {
112	    Self::new(v)
113	}
114}
115
116impl From<RoundSeq> for u64 {
117	fn from(v: RoundSeq) -> u64 {
118	    v.0
119	}
120}
121
122impl fmt::Display for RoundSeq {
123	fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "{}", self.0) }
124}
125
126impl fmt::Debug for RoundSeq {
127	fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fmt::Display::fmt(self, f) }
128}
129
130/// Identifier for a past round.
131///
132/// It is the txid of the round tx.
133#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
134pub struct RoundId(Txid);
135
136impl RoundId {
137	/// Create a new [RoundId] from the round tx's [Txid].
138	pub const fn new(txid: Txid) -> RoundId {
139		RoundId(txid)
140	}
141
142	pub fn from_slice(bytes: &[u8]) -> Result<RoundId, bitcoin::hashes::FromSliceError> {
143		Txid::from_slice(bytes).map(RoundId::new)
144	}
145
146	pub fn as_round_txid(&self) -> Txid {
147		self.0
148	}
149}
150
151impl From<Txid> for RoundId {
152	fn from(txid: Txid) -> RoundId {
153		RoundId::new(txid)
154	}
155}
156
157impl std::ops::Deref for RoundId {
158	type Target = Txid;
159	fn deref(&self) -> &Self::Target {
160		&self.0
161	}
162}
163
164impl fmt::Display for RoundId {
165	fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
166		write!(f, "{}", self.0)
167	}
168}
169
170impl FromStr for RoundId {
171	type Err = bitcoin::hashes::hex::HexToArrayError;
172	fn from_str(s: &str) -> Result<Self, Self::Err> {
173		Txid::from_str(s).map(RoundId::new)
174	}
175}
176
177impl serde::Serialize for RoundId {
178	fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
179		if s.is_human_readable() {
180			s.collect_str(self)
181		} else {
182			s.serialize_bytes(self.as_ref())
183		}
184	}
185}
186
187impl<'de> serde::Deserialize<'de> for RoundId {
188	fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
189		struct Visitor;
190		impl<'de> serde::de::Visitor<'de> for Visitor {
191			type Value = RoundId;
192			fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
193				write!(f, "a RoundId, which is a Txid")
194			}
195			fn visit_bytes<E: serde::de::Error>(self, v: &[u8]) -> Result<Self::Value, E> {
196				RoundId::from_slice(v).map_err(serde::de::Error::custom)
197			}
198			fn visit_str<E: serde::de::Error>(self, v: &str) -> Result<Self::Value, E> {
199				RoundId::from_str(v).map_err(serde::de::Error::custom)
200			}
201		}
202		if d.is_human_readable() {
203			d.deserialize_str(Visitor)
204		} else {
205			d.deserialize_bytes(Visitor)
206		}
207	}
208}
209
210
211#[derive(Debug, Clone)]
212pub struct RoundInfo {
213	pub round_seq: RoundSeq,
214	pub offboard_feerate: FeeRate,
215}
216
217#[derive(Debug, Clone)]
218pub struct RoundAttempt {
219	pub round_seq: RoundSeq,
220	pub attempt_seq: usize,
221	pub challenge: VtxoOwnershipChallenge,
222}
223
224#[derive(Debug, Clone)]
225pub struct VtxoProposal {
226	pub round_seq: RoundSeq,
227	pub attempt_seq: usize,
228	pub unsigned_round_tx: Transaction,
229	pub vtxos_spec: VtxoTreeSpec,
230	pub cosign_agg_nonces: Vec<musig::AggregatedNonce>,
231}
232
233#[derive(Debug, Clone)]
234pub struct RoundProposal {
235	pub round_seq: RoundSeq,
236	pub attempt_seq: usize,
237	pub cosign_sigs: Vec<schnorr::Signature>,
238	pub forfeit_nonces: HashMap<VtxoId, Vec<musig::PublicNonce>>,
239	pub connector_pubkey: PublicKey,
240}
241
242#[derive(Debug, Clone)]
243pub struct RoundFinished {
244	pub round_seq: RoundSeq,
245	pub attempt_seq: usize,
246	pub signed_round_tx: Transaction,
247}
248
249#[derive(Debug, Clone)]
250pub enum RoundEvent {
251	Start(RoundInfo),
252	Attempt(RoundAttempt),
253	VtxoProposal(VtxoProposal),
254	RoundProposal(RoundProposal),
255	Finished(RoundFinished),
256}
257
258impl RoundEvent {
259	pub fn round_seq(&self) -> RoundSeq {
260		match self {
261			Self::Start(e) => e.round_seq,
262			Self::Attempt(e) => e.round_seq,
263			Self::VtxoProposal(e) => e.round_seq,
264			Self::RoundProposal(e) => e.round_seq,
265			Self::Finished(e) => e.round_seq,
266		}
267	}
268}
269
270/// A more concise way to display [RoundEvent].
271impl fmt::Display for RoundEvent {
272	fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
273		match self {
274			Self::Start(RoundInfo { round_seq, offboard_feerate }) => {
275				f.debug_struct("Start")
276					.field("round_seq", round_seq)
277					.field("offboard_feerate", offboard_feerate)
278					.finish()
279			},
280			Self::Attempt(RoundAttempt { round_seq, attempt_seq, challenge }) => {
281				f.debug_struct("Attempt")
282					.field("round_seq", round_seq)
283					.field("attempt_seq", attempt_seq)
284					.field("challenge", &challenge.inner().as_hex())
285					.finish()
286			},
287			Self::VtxoProposal(VtxoProposal { round_seq, attempt_seq, unsigned_round_tx, .. }) => {
288				f.debug_struct("VtxoProposal")
289					.field("round_seq", round_seq)
290					.field("attempt_seq", attempt_seq)
291					.field("unsigned_round_txid", &unsigned_round_tx.compute_txid())
292					.finish()
293			},
294			Self::RoundProposal(RoundProposal { round_seq, attempt_seq, connector_pubkey, .. }) => {
295				f.debug_struct("RoundProposal")
296					.field("round_seq", round_seq)
297					.field("attempt_seq", attempt_seq)
298					.field("connector_pubkey", &connector_pubkey)
299					.finish()
300			},
301			Self::Finished(RoundFinished { round_seq, attempt_seq, signed_round_tx }) => {
302				f.debug_struct("Finished")
303					.field("round_seq", round_seq)
304					.field("attempt_seq", attempt_seq)
305					.field("signed_round_txid", &signed_round_tx.compute_txid())
306					.finish()
307			},
308		}
309	}
310}