ark/arkoor/
mod.rs

1pub mod package;
2pub mod checkpoint;
3pub mod checkpointed_package;
4
5use std::borrow::{Borrow, Cow};
6
7use bitcoin::hex::DisplayHex;
8use bitcoin::{Amount, OutPoint, ScriptBuf, Sequence, Transaction, TxIn, TxOut, Txid, Weight, Witness};
9use bitcoin::hashes::Hash;
10use bitcoin::secp256k1::{schnorr, Keypair};
11use bitcoin::sighash::{self, SighashCache, TapSighash, TapSighashType};
12
13use bitcoin_ext::{fee, P2TR_DUST, TAPROOT_KEYSPEND_WEIGHT};
14
15use crate::error::IncorrectSigningKeyError;
16use crate::{musig, scripts, ProtocolEncoding, Vtxo, VtxoRequest, SECP};
17use crate::vtxo::{GenesisItem, GenesisTransition};
18
19pub use package::ArkoorPackageBuilder;
20
21
22#[derive(Debug, Clone, PartialEq, Eq, Hash, thiserror::Error)]
23pub enum ArkoorError {
24	#[error("output amount of {input} exceeds input amount of {output}")]
25	Unbalanced {
26		input: Amount,
27		output: Amount,
28	},
29	#[error("arkoor output amounts cannot be below the p2tr dust threshold")]
30	Dust,
31	#[error("arkoor cannot have more than 2 outputs")]
32	TooManyOutputs,
33}
34
35pub fn arkoor_sighash(prevout: &TxOut, arkoor_tx: &Transaction) -> TapSighash {
36	let mut shc = SighashCache::new(arkoor_tx);
37
38	shc.taproot_key_spend_signature_hash(
39		0, &sighash::Prevouts::All(&[prevout]), TapSighashType::Default,
40	).expect("sighash error")
41}
42
43pub fn unsigned_arkoor_tx(input: &Vtxo, outputs: &[TxOut]) -> Transaction {
44	Transaction {
45		version: bitcoin::transaction::Version(3),
46		lock_time: bitcoin::absolute::LockTime::ZERO,
47		input: vec![TxIn {
48			previous_output: input.point(),
49			script_sig: ScriptBuf::new(),
50			sequence: Sequence::ZERO,
51			witness: Witness::new(),
52		}],
53		output: outputs.into_iter().cloned().chain([fee::fee_anchor()]).collect(),
54	}
55}
56
57/// Inner utility method to construct the arkoor vtxos.
58fn build_arkoor_vtxos<T: Borrow<VtxoRequest>>(
59	input: &Vtxo,
60	outputs: &[T],
61	txouts: &[TxOut],
62	arkoor_txid: Txid,
63	arkoor_signature: Option<schnorr::Signature>,
64) -> Vec<Vtxo> {
65	outputs.iter().enumerate().map(|(idx, output)| {
66		Vtxo {
67			amount: output.borrow().amount,
68			expiry_height: input.expiry_height,
69			server_pubkey: input.server_pubkey,
70			exit_delta: input.exit_delta,
71			anchor_point: input.anchor_point,
72			genesis: input.genesis.iter().cloned().chain([GenesisItem {
73				transition: GenesisTransition::Arkoor {
74					policy: input.policy.clone(),
75					signature: arkoor_signature,
76				},
77				output_idx: idx as u8,
78				// filter out our index from the txouts
79				other_outputs: txouts.iter().enumerate()
80					.filter(|(i, _)| *i != idx)
81					.map(|(_, o)| o).cloned().collect(),
82			}]).collect(),
83			policy: output.borrow().policy.clone(),
84			point: OutPoint::new(arkoor_txid, idx as u32),
85		}
86	}).collect()
87}
88
89/// Build oor tx and signs it
90///
91/// ## Panic
92///
93/// Will panic if inputs and signatures don't have the same length,
94/// or if some input witnesses are not empty
95pub fn signed_arkoor_tx(
96	input: &Vtxo,
97	signature: schnorr::Signature,
98	outputs: &[TxOut],
99) -> Transaction {
100	let mut tx = unsigned_arkoor_tx(input, outputs);
101	scripts::fill_taproot_sigs(&mut tx, &[signature]);
102	tx
103}
104
105/// The cosignature details received from the Ark server.
106#[derive(Debug)]
107pub struct ArkoorCosignResponse {
108	pub pub_nonce: musig::PublicNonce,
109	pub partial_signature: musig::PartialSignature,
110}
111
112/// This types helps both the client and server with building arkoor txs in
113/// a synchronized way. It's purely a functional type, initialized with
114/// the parameters that will make up the arkoor: the input vtxo to be spent
115/// and the desired outputs.
116///
117/// The flow works as follows:
118/// - sender uses the [ArkoorBuilder::new] to check the request for validity
119/// - server uses the [ArkoorBuilder::new] to check the request for validity
120/// - server uses [ArkoorBuilder::server_cosign] to construct a
121///   [ArkoorCosignResponse] to send back to the sender
122/// - sender passes the response into [ArkoorBuilder::build_vtxos] to construct
123///   the signed resulting VTXOs
124pub struct ArkoorBuilder<'a, T: Clone> {
125	pub input: &'a Vtxo,
126	pub user_nonce: &'a musig::PublicNonce,
127	pub outputs: Cow<'a, [T]>,
128}
129
130impl<'a, T: Borrow<VtxoRequest> + Clone> ArkoorBuilder<'a, T> {
131	/// Construct a generic arkoor builder for the given input and outputs.
132	pub fn new(
133		input: &'a Vtxo,
134		user_nonce: &'a musig::PublicNonce,
135		outputs: impl Into<Cow<'a, [T]>>,
136	) -> Result<Self, ArkoorError> {
137		let outputs = outputs.into();
138		if outputs.iter().any(|o| o.borrow().amount < P2TR_DUST) {
139			return Err(ArkoorError::Dust);
140		}
141		let output_amount = outputs.as_ref().iter().map(|o| o.borrow().amount).sum::<Amount>();
142		if output_amount > input.amount() {
143			return Err(ArkoorError::Unbalanced {
144				input: input.amount(),
145				output: output_amount,
146			});
147		}
148
149		if outputs.len() > 2 {
150			return Err(ArkoorError::TooManyOutputs);
151		}
152
153		Ok(Self {
154			input,
155			user_nonce,
156			outputs,
157		})
158	}
159
160	/// Construct the transaction outputs of the resulting arkoor tx.
161	pub fn txouts(&self) -> Vec<TxOut> {
162		self.outputs.iter().map(|out| {
163			out.borrow().policy.txout(
164				out.borrow().amount,
165				self.input.server_pubkey(),
166				self.input.exit_delta(),
167				self.input.expiry_height(),
168			)
169		}).collect()
170	}
171
172	pub fn unsigned_transaction(&self) -> Transaction {
173		unsigned_arkoor_tx(&self.input, &self.txouts())
174	}
175
176	pub fn sighash(&self) -> TapSighash {
177		arkoor_sighash(&self.input.txout(), &self.unsigned_transaction())
178	}
179
180	pub fn total_weight(&self) -> Weight {
181		let spend_weight = Weight::from_wu(TAPROOT_KEYSPEND_WEIGHT as u64);
182		self.unsigned_transaction().weight() + spend_weight
183	}
184
185	/// Used by the Ark server to cosign the arkoor request.
186	pub fn server_cosign(&self, keypair: &Keypair) -> ArkoorCosignResponse {
187		let (pub_nonce, partial_signature) = musig::deterministic_partial_sign(
188			keypair,
189			[self.input.user_pubkey()],
190			&[&self.user_nonce],
191			self.sighash().to_byte_array(),
192			Some(self.input.output_taproot().tap_tweak().to_byte_array()),
193		);
194		ArkoorCosignResponse { pub_nonce, partial_signature }
195	}
196
197	/// Validate the server's partial signature.
198	pub fn verify_cosign_response(
199		&self,
200		server_cosign: &ArkoorCosignResponse,
201	) -> bool {
202		scripts::verify_partial_sig(
203			self.sighash(),
204			self.input.output_taproot().tap_tweak(),
205			(self.input.server_pubkey(), &server_cosign.pub_nonce),
206			(self.input.user_pubkey(), &self.user_nonce),
207			&server_cosign.partial_signature,
208		)
209	}
210
211	/// Construct the vtxos of the outputs of this OOR tx.
212	///
213	/// These vtxos are not valid vtxos because they lack the signature.
214	pub fn unsigned_output_vtxos(&self) -> Vec<Vtxo> {
215		let txouts = self.txouts();
216		let tx = unsigned_arkoor_tx(&self.input, &txouts);
217		build_arkoor_vtxos(&self.input, self.outputs.as_ref(), &txouts, tx.compute_txid(), None)
218	}
219
220	/// Finish the arkoor process.
221	///
222	/// Returns the resulting vtxos and the signed arkoor tx.
223	pub fn build_vtxos(
224		&self,
225		user_sec_nonce: musig::SecretNonce,
226		user_key: &Keypair,
227		cosign_resp: &ArkoorCosignResponse,
228	) -> Result<Vec<Vtxo>, IncorrectSigningKeyError> {
229		if user_key.public_key() != self.input.user_pubkey() {
230			return Err(IncorrectSigningKeyError {
231				required: Some(self.input.user_pubkey()),
232				provided: user_key.public_key(),
233			});
234		}
235
236		let txouts = self.txouts();
237		let tx = unsigned_arkoor_tx(&self.input, &txouts);
238		let sighash = arkoor_sighash(&self.input.txout(), &tx);
239		let taptweak = self.input.output_taproot().tap_tweak();
240
241		let agg_nonce = musig::nonce_agg(&[&self.user_nonce, &cosign_resp.pub_nonce]);
242		let (_part_sig, final_sig) = musig::partial_sign(
243			[self.input.user_pubkey(), self.input.server_pubkey()],
244			agg_nonce,
245			user_key,
246			user_sec_nonce,
247			sighash.to_byte_array(),
248			Some(taptweak.to_byte_array()),
249			Some(&[&cosign_resp.partial_signature]),
250		);
251		let final_sig = final_sig.expect("we provided the other sig");
252		debug_assert!(
253			scripts::verify_partial_sig(
254				sighash,
255				taptweak,
256				(self.input.user_pubkey(), &self.user_nonce),
257				(self.input.server_pubkey(), &cosign_resp.pub_nonce),
258				&_part_sig,
259			),
260			"invalid partial signature produced",
261		);
262		debug_assert!(
263			SECP.verify_schnorr(
264				&final_sig,
265				&sighash.into(),
266				&self.input.output_taproot().output_key().to_x_only_public_key(),
267			).is_ok(),
268			"invalid arkoor tx signature produced: input={}, outputs={:?}",
269			self.input.serialize().as_hex(), &txouts,
270		);
271
272		Ok(build_arkoor_vtxos(
273			&self.input,
274			self.outputs.as_ref(),
275			&txouts,
276			tx.compute_txid(),
277			Some(final_sig),
278		))
279	}
280}
281