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
16pub const MIN_ROUND_TX_OUTPUTS: usize = 2;
19
20pub const ROUND_TX_VTXO_TREE_VOUT: u32 = 0;
22pub 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 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#[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#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
142pub struct RoundId(Txid);
143
144impl RoundId {
145 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 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
288impl 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}