ark/
rounds.rs

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