1use std::marker::PhantomData;
15
16use bitcoin::sighash::{self, SighashCache};
17use bitcoin::taproot::TaprootSpendInfo;
18use bitcoin::{Amount, OutPoint, ScriptBuf, TapSighash, Transaction, TxOut, Txid};
19use bitcoin::hashes::Hash;
20use bitcoin::secp256k1::{rand, Keypair, PublicKey};
21use bitcoin_ext::{BlockDelta, BlockHeight, TaprootSpendInfoExt};
22
23use crate::error::IncorrectSigningKeyError;
24use crate::{musig, scripts, Vtxo, VtxoPolicy, SECP};
25use crate::tree::signed::cosign_taproot;
26use crate::vtxo::{self, exit_taproot, GenesisItem, GenesisTransition};
27
28use self::state::BuilderState;
29
30
31pub const BOARD_FUNDING_TX_VTXO_VOUT: u32 = 0;
33
34fn exit_tx_sighash(
35 prev_utxo: &TxOut,
36 utxo: OutPoint,
37 output: TxOut,
38) -> (TapSighash, Transaction) {
39 let exit_tx = vtxo::create_exit_tx(utxo, output, None);
40 let sighash = SighashCache::new(&exit_tx).taproot_key_spend_signature_hash(
41 0, &sighash::Prevouts::All(&[prev_utxo]), sighash::TapSighashType::Default,
42 ).expect("matching prevouts");
43 (sighash, exit_tx)
44}
45
46#[derive(Debug)]
48pub struct BoardCosignResponse {
49 pub pub_nonce: musig::PublicNonce,
50 pub partial_signature: musig::PartialSignature,
51}
52
53pub mod state {
54 mod sealed {
55 pub trait Sealed {}
57 impl Sealed for super::Preparing {}
58 impl Sealed for super::CanGenerateNonces {}
59 impl Sealed for super::ServerCanCosign {}
60 impl Sealed for super::CanFinish {}
61 }
62
63 pub trait BuilderState: sealed::Sealed {}
65
66 pub struct Preparing;
68 impl BuilderState for Preparing {}
69
70 pub struct CanGenerateNonces;
73 impl BuilderState for CanGenerateNonces {}
74
75 pub struct ServerCanCosign;
77 impl BuilderState for ServerCanCosign {}
78
79 pub struct CanFinish;
82 impl BuilderState for CanFinish {}
83
84 pub trait CanSign: BuilderState {}
87 impl CanSign for ServerCanCosign {}
88 impl CanSign for CanFinish {}
89}
90
91#[derive(Debug)]
99pub struct BoardBuilder<S: BuilderState> {
100 pub user_pubkey: PublicKey,
101 pub expiry_height: BlockHeight,
102 pub server_pubkey: PublicKey,
103 pub exit_delta: BlockDelta,
104
105 amount: Option<Amount>,
106 utxo: Option<OutPoint>,
107
108 user_pub_nonce: Option<musig::PublicNonce>,
109 user_sec_nonce: Option<musig::SecretNonce>,
110 _state: PhantomData<S>,
111}
112
113impl<S: BuilderState> BoardBuilder<S> {
114 pub fn funding_script_pubkey(&self) -> ScriptBuf {
116 let combined_pubkey = musig::combine_keys([self.user_pubkey, self.server_pubkey]);
117 cosign_taproot(combined_pubkey, self.server_pubkey, self.expiry_height).script_pubkey()
118 }
119}
120
121impl BoardBuilder<state::Preparing> {
122 pub fn new(
126 user_pubkey: PublicKey,
127 expiry_height: BlockHeight,
128 server_pubkey: PublicKey,
129 exit_delta: BlockDelta,
130 ) -> BoardBuilder<state::Preparing> {
131 BoardBuilder {
132 user_pubkey, expiry_height, server_pubkey, exit_delta,
133 amount: None,
134 utxo: None,
135 user_pub_nonce: None,
136 user_sec_nonce: None,
137 _state: PhantomData,
138 }
139 }
140
141 pub fn set_funding_details(
143 self,
144 amount: Amount,
145 utxo: OutPoint,
146 ) -> BoardBuilder<state::CanGenerateNonces> {
147 BoardBuilder {
148 amount: Some(amount),
149 utxo: Some(utxo),
150 user_pubkey: self.user_pubkey,
152 expiry_height: self.expiry_height,
153 server_pubkey: self.server_pubkey,
154 exit_delta: self.exit_delta,
155 user_pub_nonce: self.user_pub_nonce,
156 user_sec_nonce: self.user_sec_nonce,
157 _state: PhantomData,
158 }
159 }
160}
161
162impl BoardBuilder<state::CanGenerateNonces> {
163 pub fn generate_user_nonces(self) -> BoardBuilder<state::CanFinish> {
165 let combined_pubkey = musig::combine_keys([self.user_pubkey, self.server_pubkey]);
166 let funding_taproot = cosign_taproot(combined_pubkey, self.server_pubkey, self.expiry_height);
167 let funding_txout = TxOut {
168 script_pubkey: funding_taproot.script_pubkey(),
169 value: self.amount.expect("state invariant"),
170 };
171
172 let exit_taproot = exit_taproot(self.user_pubkey, self.server_pubkey, self.exit_delta);
173 let exit_txout = TxOut {
174 value: self.amount.expect("state invariant"),
175 script_pubkey: exit_taproot.script_pubkey(),
176 };
177
178 let utxo = self.utxo.expect("state invariant");
179 let (reveal_sighash, _tx) = exit_tx_sighash(&funding_txout, utxo, exit_txout);
180 let (agg, _) = musig::tweaked_key_agg(
181 [self.user_pubkey, self.server_pubkey],
182 funding_taproot.tap_tweak().to_byte_array(),
183 );
184 let (sec_nonce, pub_nonce) = agg.nonce_gen(
186 musig::SessionSecretRand::assume_unique_per_nonce_gen(rand::random()),
187 musig::pubkey_to(self.user_pubkey),
188 &reveal_sighash.to_byte_array(),
189 None,
190 );
191
192 BoardBuilder {
193 user_pub_nonce: Some(pub_nonce),
194 user_sec_nonce: Some(sec_nonce),
195 amount: self.amount,
197 user_pubkey: self.user_pubkey,
198 expiry_height: self.expiry_height,
199 server_pubkey: self.server_pubkey,
200 exit_delta: self.exit_delta,
201 utxo: self.utxo,
202 _state: PhantomData,
203 }
204 }
205}
206
207impl<S: state::CanSign> BoardBuilder<S> {
208 pub fn user_pub_nonce(&self) -> &musig::PublicNonce {
209 self.user_pub_nonce.as_ref().expect("state invariant")
210 }
211
212 fn exit_tx_sighash_data(&self) -> (TapSighash, TaprootSpendInfo, Txid) {
215 let combined_pubkey = musig::combine_keys([self.user_pubkey, self.server_pubkey]);
216 let funding_taproot = cosign_taproot(combined_pubkey, self.server_pubkey, self.expiry_height);
217 let funding_txout = TxOut {
218 value: self.amount.expect("state invariant"),
219 script_pubkey: funding_taproot.script_pubkey(),
220 };
221
222 let exit_taproot = exit_taproot(self.user_pubkey, self.server_pubkey, self.exit_delta);
223 let exit_txout = TxOut {
224 value: self.amount.expect("state invariant"),
225 script_pubkey: exit_taproot.script_pubkey(),
226 };
227
228 let utxo = self.utxo.expect("state invariant");
229 let (sighash, tx) = exit_tx_sighash(&funding_txout, utxo, exit_txout);
230 (sighash, funding_taproot, tx.compute_txid())
231 }
232}
233
234impl BoardBuilder<state::ServerCanCosign> {
235 pub fn new_for_cosign(
238 user_pubkey: PublicKey,
239 expiry_height: BlockHeight,
240 server_pubkey: PublicKey,
241 exit_delta: BlockDelta,
242 amount: Amount,
243 utxo: OutPoint,
244 user_pub_nonce: musig::PublicNonce,
245 ) -> BoardBuilder<state::ServerCanCosign> {
246 BoardBuilder {
247 user_pubkey, expiry_height, server_pubkey, exit_delta,
248 amount: Some(amount),
249 utxo: Some(utxo),
250 user_pub_nonce: Some(user_pub_nonce),
251 user_sec_nonce: None,
252 _state: PhantomData,
253 }
254 }
255
256 pub fn server_cosign(&self, key: &Keypair) -> BoardCosignResponse {
260 let (sighash, taproot, _txid) = self.exit_tx_sighash_data();
261 let (pub_nonce, partial_signature) = musig::deterministic_partial_sign(
262 key,
263 [self.user_pubkey],
264 &[&self.user_pub_nonce()],
265 sighash.to_byte_array(),
266 Some(taproot.tap_tweak().to_byte_array()),
267 );
268 BoardCosignResponse { pub_nonce, partial_signature }
269 }
270}
271
272impl BoardBuilder<state::CanFinish> {
273 pub fn verify_cosign_response(&self, server_cosign: &BoardCosignResponse) -> bool {
275 let (sighash, taproot, _txid) = self.exit_tx_sighash_data();
276 scripts::verify_partial_sig(
277 sighash,
278 taproot.tap_tweak(),
279 (self.server_pubkey, &server_cosign.pub_nonce),
280 (self.user_pubkey, self.user_pub_nonce()),
281 &server_cosign.partial_signature
282 )
283 }
284
285 pub fn build_vtxo(
287 mut self,
288 server_cosign: &BoardCosignResponse,
289 user_key: &Keypair,
290 ) -> Result<Vtxo, IncorrectSigningKeyError> {
291 if user_key.public_key() != self.user_pubkey {
292 return Err(IncorrectSigningKeyError {
293 required: Some(self.user_pubkey),
294 provided: user_key.public_key(),
295 });
296 }
297
298 let (sighash, taproot, exit_txid) = self.exit_tx_sighash_data();
299
300 let agg_nonce = musig::nonce_agg(&[&self.user_pub_nonce(), &server_cosign.pub_nonce]);
301 let (user_sig, final_sig) = musig::partial_sign(
302 [self.user_pubkey, self.server_pubkey],
303 agg_nonce,
304 user_key,
305 self.user_sec_nonce.take().expect("state invariant"),
306 sighash.to_byte_array(),
307 Some(taproot.tap_tweak().to_byte_array()),
308 Some(&[&server_cosign.partial_signature]),
309 );
310 debug_assert!(
311 scripts::verify_partial_sig(
312 sighash,
313 taproot.tap_tweak(),
314 (self.user_pubkey, self.user_pub_nonce()),
315 (self.server_pubkey, &server_cosign.pub_nonce),
316 &user_sig,
317 ),
318 "invalid board partial exit tx signature produced",
319 );
320
321 let final_sig = final_sig.expect("we provided the other sig");
322 debug_assert!(
323 SECP.verify_schnorr(
324 &final_sig, &sighash.into(), &taproot.output_key().to_x_only_public_key(),
325 ).is_ok(),
326 "invalid board exit tx signature produced",
327 );
328
329 Ok(Vtxo {
330 amount: self.amount.expect("state invariant"),
331 expiry_height: self.expiry_height,
332 server_pubkey: self.server_pubkey,
333 exit_delta: self.exit_delta,
334 anchor_point: self.utxo.expect("state invariant"),
335 genesis: vec![GenesisItem {
336 transition: GenesisTransition::Cosigned {
337 pubkeys: vec![self.user_pubkey, self.server_pubkey],
338 signature: final_sig,
339 },
340 output_idx: 0,
341 other_outputs: vec![],
342 }],
343 policy: VtxoPolicy::new_pubkey(self.user_pubkey),
344 point: OutPoint::new(exit_txid, BOARD_FUNDING_TX_VTXO_VOUT),
345 })
346 }
347}
348
349#[derive(Debug, Clone, thiserror::Error)]
350#[error("board funding tx validation error: {0}")]
351pub struct BoardFundingTxValidationError(String);
352
353
354#[cfg(test)]
355mod test {
356 use std::str::FromStr;
357
358 use bitcoin::{absolute, transaction, Amount};
359
360 use crate::encode::test::encoding_roundtrip;
361 use crate::vtxo::ValidationResult;
362
363 use super::*;
364
365 #[test]
366 fn test_board_builder() {
367 let user_key = Keypair::from_str("5255d132d6ec7d4fc2a41c8f0018bb14343489ddd0344025cc60c7aa2b3fda6a").unwrap();
371 let server_key = Keypair::from_str("1fb316e653eec61de11c6b794636d230379509389215df1ceb520b65313e5426").unwrap();
372
373 let amount = Amount::from_btc(1.5).unwrap();
375 let expiry = 100_000;
376 let server_pubkey = server_key.public_key();
377 let exit_delta = 24;
378 let builder = BoardBuilder::new(
379 user_key.public_key(), expiry, server_pubkey, exit_delta,
380 );
381 let funding_tx = Transaction {
382 version: transaction::Version::TWO,
383 lock_time: absolute::LockTime::ZERO,
384 input: vec![],
385 output: vec![TxOut {
386 value: amount,
387 script_pubkey: builder.funding_script_pubkey(),
388 }],
389 };
390 let utxo = OutPoint::new(funding_tx.compute_txid(), 0);
391 assert_eq!(utxo.to_string(), "8c4b87af4ce8456bbd682859959ba64b95d5425d761a367f4f20b8ffccb1bde0:0");
392 let builder = builder.set_funding_details(amount, utxo).generate_user_nonces();
393
394 let cosign = {
396 let server_builder = BoardBuilder::new_for_cosign(
397 builder.user_pubkey, expiry, server_pubkey, exit_delta, amount, utxo, *builder.user_pub_nonce(),
398 );
399 server_builder.server_cosign(&server_key)
400 };
401
402 assert!(builder.verify_cosign_response(&cosign));
404 let vtxo = builder.build_vtxo(&cosign, &user_key).unwrap();
405
406 encoding_roundtrip(&vtxo);
407
408 assert_eq!(vtxo.validate(&funding_tx).unwrap(), ValidationResult::Cosigned);
409 }
410}