ark/
board.rs

1//!
2//! Board flow:
3//!
4//! * user creates a builder using [BoardBuilder::new]
5//! * user creates the funding tx which pays to [BoardBuilder::funding_script_pubkey]
6//! * user sets the funding output in [BoardBuilder::set_funding_details]
7//! * user generates signing nonces using [BoardBuilder::generate_user_nonces]
8//! * user sends all board info to the server
9//! * server creates a builder using [BoardBuilder::new_for_cosign]
10//! * server cosigns using [BoardBuilder::server_cosign] and sends cosign response to user
11//! * user validates cosign response using [BoardBuilder::verify_cosign_response]
12//! * user finishes the vtxos by cross-signing using [BoardBuilder::build_vtxo]
13
14use 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
31/// The output index of the board vtxo in the board tx.
32pub 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/// Partial signature the server responds to a board request.
47#[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		/// Just a trait to seal the BuilderState trait.
56		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	/// A marker trait used as a generic for [super::BoardBuilder].
64	pub trait BuilderState: sealed::Sealed {}
65
66	/// The user is preparing the board tx.
67	pub struct Preparing;
68	impl BuilderState for Preparing {}
69
70	/// The UTXO that will be used to fund the board is known, so the
71	/// user's signing nonces can be generated.
72	pub struct CanGenerateNonces;
73	impl BuilderState for CanGenerateNonces {}
74
75	/// All the information for the server to cosign the VTXO is known.
76	pub struct ServerCanCosign;
77	impl BuilderState for ServerCanCosign {}
78
79	/// The user is ready to build the VTXO as soon as it has
80	/// a cosign response from the server.
81	pub struct CanFinish;
82	impl BuilderState for CanFinish {}
83
84	/// Trait to capture all states that have sufficient information
85	/// for either party to create signatures.
86	pub trait CanSign: BuilderState {}
87	impl CanSign for ServerCanCosign {}
88	impl CanSign for CanFinish {}
89}
90
91/// A request for the server to cosign an board vtxo.
92///
93/// An object of this type is created by the user, sent to the server who will
94/// cosign the request and return his partial signature (along with public nonce)
95/// back to the user so that the user can finish the request and create a [Vtxo].
96///
97/// Currently you can only create VTXOs with [VtxoPolicy::Pubkey].
98#[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	/// The scriptPubkey to send the board funds to.
115	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	/// Create a new builder to construct a board vtxo.
123	///
124	/// See module-level documentation for an overview of the board flow.
125	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	/// Set the UTXO where the board will be funded and the board amount.
142	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			// copy the rest
151			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	/// Generate user nonces.
164	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		//TODO(stevenroose) consider trying to move this to musig module
185		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			// copy the rest
196			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	/// The signature hash to sign the exit tx and the taproot info
213	/// (of the funding tx) used to calcualte it and the exit tx's txid.
214	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	/// This constructor is to be used by the server with the information provided
236	/// by the user.
237	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	/// This method is used by the server to cosign the board request.
257	///
258	/// Returns `None` if utxo or user_pub_nonce field is not provided.
259	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	/// Validate the server's partial signature.
274	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	/// Finishes the board request and create a vtxo.
286	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		//! Passes through the entire flow so that all assertions
368		//! inside the code are ran at least once.
369
370		let user_key = Keypair::from_str("5255d132d6ec7d4fc2a41c8f0018bb14343489ddd0344025cc60c7aa2b3fda6a").unwrap();
371		let server_key = Keypair::from_str("1fb316e653eec61de11c6b794636d230379509389215df1ceb520b65313e5426").unwrap();
372
373		// user
374		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		// server
395		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		// user
403		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}