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
57fn 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 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
89pub 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#[derive(Debug)]
107pub struct ArkoorCosignResponse {
108 pub pub_nonce: musig::PublicNonce,
109 pub partial_signature: musig::PartialSignature,
110}
111
112pub 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 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 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 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 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 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 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