1
2
3use std::borrow::{Borrow, Cow};
4use std::collections::HashMap;
5use std::iter;
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, PublicKey};
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, VtxoId, VtxoPolicy, VtxoRequest, SECP};
17use crate::vtxo::{GenesisItem, GenesisTransition};
18
19
20#[derive(Debug, Clone, PartialEq, Eq, Hash, thiserror::Error)]
21pub enum ArkoorError {
22 #[error("output amount of {input} exceeds input amount of {output}")]
23 Unbalanced {
24 input: Amount,
25 output: Amount,
26 },
27 #[error("arkoor output amounts cannot be below the p2tr dust threshold")]
28 Dust,
29 #[error("arkoor cannot have more than 2 outputs")]
30 TooManyOutputs,
31}
32
33pub fn arkoor_sighash(input_vtxo: &Vtxo, arkoor_tx: &Transaction) -> TapSighash {
34 let prev = input_vtxo.txout();
35 let mut shc = SighashCache::new(arkoor_tx);
36
37 shc.taproot_key_spend_signature_hash(
38 0, &sighash::Prevouts::All(&[prev]), TapSighashType::Default,
39 ).expect("sighash error")
40}
41
42pub fn unsigned_arkoor_tx(input: &Vtxo, outputs: &[TxOut]) -> Transaction {
43 Transaction {
44 version: bitcoin::transaction::Version(3),
45 lock_time: bitcoin::absolute::LockTime::ZERO,
46 input: vec![TxIn {
47 previous_output: input.point(),
48 script_sig: ScriptBuf::new(),
49 sequence: Sequence::ZERO,
50 witness: Witness::new(),
51 }],
52 output: outputs.into_iter().cloned().chain([fee::fee_anchor()]).collect(),
53 }
54}
55
56fn build_arkoor_vtxos<T: Borrow<VtxoRequest>>(
58 input: &Vtxo,
59 outputs: &[T],
60 txouts: &[TxOut],
61 arkoor_txid: Txid,
62 arkoor_signature: Option<schnorr::Signature>,
63) -> Vec<Vtxo> {
64 outputs.iter().enumerate().map(|(idx, output)| {
65 Vtxo {
66 amount: output.borrow().amount,
67 expiry_height: input.expiry_height,
68 server_pubkey: input.server_pubkey,
69 exit_delta: input.exit_delta,
70 anchor_point: input.anchor_point,
71 genesis: input.genesis.iter().cloned().chain([GenesisItem {
72 transition: GenesisTransition::Arkoor {
73 policy: input.policy.clone(),
74 signature: arkoor_signature,
75 },
76 output_idx: idx as u8,
77 other_outputs: txouts.iter().enumerate()
79 .filter(|(i, _)| *i != idx)
80 .map(|(_, o)| o).cloned().collect(),
81 }]).collect(),
82 policy: output.borrow().policy.clone(),
83 point: OutPoint::new(arkoor_txid, idx as u32),
84 }
85 }).collect()
86}
87
88pub fn signed_arkoor_tx(
95 input: &Vtxo,
96 signature: schnorr::Signature,
97 outputs: &[TxOut],
98) -> Transaction {
99 let mut tx = unsigned_arkoor_tx(input, outputs);
100 scripts::fill_taproot_sigs(&mut tx, &[signature]);
101 tx
102}
103
104#[derive(Debug)]
106pub struct ArkoorCosignResponse {
107 pub pub_nonce: musig::PublicNonce,
108 pub partial_signature: musig::PartialSignature,
109}
110
111pub struct ArkoorBuilder<'a, T: Clone> {
124 pub input: &'a Vtxo,
125 pub user_nonce: &'a musig::PublicNonce,
126 pub outputs: Cow<'a, [T]>,
127}
128
129impl<'a, T: Borrow<VtxoRequest> + Clone> ArkoorBuilder<'a, T> {
130 pub fn new(
132 input: &'a Vtxo,
133 user_nonce: &'a musig::PublicNonce,
134 outputs: impl Into<Cow<'a, [T]>>,
135 ) -> Result<Self, ArkoorError> {
136 let outputs = outputs.into();
137 if outputs.iter().any(|o| o.borrow().amount < P2TR_DUST) {
138 return Err(ArkoorError::Dust);
139 }
140 let output_amount = outputs.as_ref().iter().map(|o| o.borrow().amount).sum::<Amount>();
141 if output_amount > input.amount() {
142 return Err(ArkoorError::Unbalanced {
143 input: input.amount(),
144 output: output_amount,
145 });
146 }
147
148 if outputs.len() > 2 {
149 return Err(ArkoorError::TooManyOutputs);
150 }
151
152 Ok(Self {
153 input,
154 user_nonce,
155 outputs,
156 })
157 }
158
159 pub fn txouts(&self) -> Vec<TxOut> {
161 self.outputs.iter().map(|out| {
162 out.borrow().policy.txout(
163 out.borrow().amount,
164 self.input.server_pubkey(),
165 self.input.exit_delta(),
166 )
167 }).collect()
168 }
169
170 pub fn unsigned_transaction(&self) -> Transaction {
171 unsigned_arkoor_tx(&self.input, &self.txouts())
172 }
173
174 pub fn sighash(&self) -> TapSighash {
175 arkoor_sighash(&self.input, &self.unsigned_transaction())
176 }
177
178 pub fn total_weight(&self) -> Weight {
179 let spend_weight = Weight::from_wu(TAPROOT_KEYSPEND_WEIGHT as u64);
180 self.unsigned_transaction().weight() + spend_weight
181 }
182
183 pub fn server_cosign(&self, keypair: &Keypair) -> ArkoorCosignResponse {
185 let (pub_nonce, partial_signature) = musig::deterministic_partial_sign(
186 keypair,
187 [self.input.user_pubkey()],
188 &[&self.user_nonce],
189 self.sighash().to_byte_array(),
190 Some(self.input.output_taproot().tap_tweak().to_byte_array()),
191 );
192 ArkoorCosignResponse { pub_nonce, partial_signature }
193 }
194
195 pub fn verify_cosign_response(
197 &self,
198 server_cosign: &ArkoorCosignResponse,
199 ) -> bool {
200 scripts::verify_partial_sig(
201 self.sighash(),
202 self.input.output_taproot().tap_tweak(),
203 (self.input.server_pubkey(), &server_cosign.pub_nonce),
204 (self.input.user_pubkey(), &self.user_nonce),
205 &server_cosign.partial_signature,
206 )
207 }
208
209 pub fn unsigned_output_vtxos(&self) -> Vec<Vtxo> {
213 let txouts = self.txouts();
214 let tx = unsigned_arkoor_tx(&self.input, &txouts);
215 build_arkoor_vtxos(&self.input, self.outputs.as_ref(), &txouts, tx.compute_txid(), None)
216 }
217
218 pub fn build_vtxos(
222 &self,
223 user_sec_nonce: musig::SecretNonce,
224 user_key: &Keypair,
225 cosign_resp: &ArkoorCosignResponse,
226 ) -> Result<Vec<Vtxo>, IncorrectSigningKeyError> {
227 if user_key.public_key() != self.input.user_pubkey() {
228 return Err(IncorrectSigningKeyError {
229 required: Some(self.input.user_pubkey()),
230 provided: user_key.public_key(),
231 });
232 }
233
234 let txouts = self.txouts();
235 let tx = unsigned_arkoor_tx(&self.input, &txouts);
236 let sighash = arkoor_sighash(&self.input, &tx);
237 let taptweak = self.input.output_taproot().tap_tweak();
238
239 let agg_nonce = musig::nonce_agg(&[&self.user_nonce, &cosign_resp.pub_nonce]);
240 let (_part_sig, final_sig) = musig::partial_sign(
241 [self.input.user_pubkey(), self.input.server_pubkey()],
242 agg_nonce,
243 user_key,
244 user_sec_nonce,
245 sighash.to_byte_array(),
246 Some(taptweak.to_byte_array()),
247 Some(&[&cosign_resp.partial_signature]),
248 );
249 let final_sig = final_sig.expect("we provided the other sig");
250 debug_assert!(
251 scripts::verify_partial_sig(
252 sighash,
253 taptweak,
254 (self.input.user_pubkey(), &self.user_nonce),
255 (self.input.server_pubkey(), &cosign_resp.pub_nonce),
256 &_part_sig,
257 ),
258 "invalid partial signature produced",
259 );
260 debug_assert!(
261 SECP.verify_schnorr(
262 &final_sig,
263 &sighash.into(),
264 &self.input.output_taproot().output_key().to_x_only_public_key(),
265 ).is_ok(),
266 "invalid arkoor tx signature produced: input={}, outputs={:?}",
267 self.input.serialize().as_hex(), &txouts,
268 );
269
270 Ok(build_arkoor_vtxos(
271 &self.input,
272 self.outputs.as_ref(),
273 &txouts,
274 tx.compute_txid(),
275 Some(final_sig),
276 ))
277 }
278}
279
280pub struct ArkoorPackageBuilder<'a, T: Clone> {
296 pub arkoors: Vec<ArkoorBuilder<'a, T>>,
298 spending_tx_by_input: HashMap<VtxoId, Transaction>,
299}
300
301#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
302pub enum ArkoorPackageError {
303 #[error("Payment has non-null change amount but no change pubkey provided")]
304 MissingChangePk,
305 #[error("Invalid length of cosignature response")]
306 InvalidLength,
307 #[error("No vtxo created")]
308 MissingVtxo,
309 #[error("Invalid spk for revocation")]
310 InvalidRevocationSpk,
311 #[error("Invalid length of user nonces")]
312 InvalidUserNoncesLength,
313 #[error("Htlc amount does not match invoice amount")]
314 InvalidHtlcAmount,
315 #[error("An error occurred while building arkoor: {0}")]
316 ArkoorError(ArkoorError),
317 #[error("Too many outputs")]
318 TooManyOutputs,
319 #[error("incorrect signing key provided")]
320 Signing(#[from] IncorrectSigningKeyError),
321}
322
323impl<'a> ArkoorPackageBuilder<'a, VtxoRequest> {
324 pub fn new(
325 inputs: impl IntoIterator<Item = &'a Vtxo>,
326 user_nonces: &'a [musig::PublicNonce],
327 vtxo_request: VtxoRequest,
328 change_pubkey: Option<PublicKey>,
329 ) -> Result<Self, ArkoorPackageError> {
330 let mut remaining_amount = vtxo_request.amount;
331 let mut arkoors = vec![];
332 let mut spending_tx_by_input = HashMap::new();
333
334 for (idx, input) in inputs.into_iter().enumerate() {
335 let user_nonce = user_nonces.get(idx).ok_or(ArkoorPackageError::InvalidUserNoncesLength)?;
336
337 let change_amount = input.amount().checked_sub(remaining_amount);
338 let (output_amount, change) = if let Some(change_amount) = change_amount {
339 let change = if change_amount < P2TR_DUST {
341 None
342 } else {
343 Some(VtxoRequest {
344 amount: change_amount,
345 policy: VtxoPolicy::new_pubkey(change_pubkey.ok_or(ArkoorPackageError::MissingChangePk)?),
346 })
347 };
348
349 (remaining_amount, change)
350 } else {
351 (input.amount(), None)
352 };
353
354 let output = VtxoRequest {
355 amount: output_amount,
356 policy: vtxo_request.policy.clone(),
357 };
358
359 let pay_reqs = iter::once(output.clone()).chain(change).collect::<Vec<_>>();
360
361 let arkoor = ArkoorBuilder::new(&input, user_nonce, pay_reqs)
362 .map_err(ArkoorPackageError::ArkoorError)?;
363
364 spending_tx_by_input.insert(input.id(), arkoor.unsigned_transaction());
365 arkoors.push(arkoor);
366
367 remaining_amount = remaining_amount - output_amount;
368 if remaining_amount == Amount::ZERO {
369 break;
370 }
371 }
372
373 Ok(Self {
374 arkoors,
375 spending_tx_by_input,
376 })
377 }
378
379 pub fn new_htlc_revocation(
380 htlc_vtxos: &'a [Vtxo],
381 user_nonces: &'a [musig::PublicNonce],
382 ) -> Result<Self, ArkoorPackageError> {
383 let arkoors = htlc_vtxos.iter().zip(user_nonces).map(|(v, u)| {
384 if !matches!(v.policy(), VtxoPolicy::ServerHtlcSend { .. }) {
385 return Err(ArkoorPackageError::InvalidRevocationSpk);
386 }
387
388 let refund = VtxoRequest {
389 amount: v.amount(),
390 policy: VtxoPolicy::new_pubkey(v.user_pubkey()),
391 };
392 ArkoorBuilder::new(v, u, vec![refund])
393 .map_err(ArkoorPackageError::ArkoorError)
394 }).collect::<Result<Vec<_>, ArkoorPackageError>>()?;
395
396 Self::from_arkoors(arkoors)
397 }
398
399 pub fn from_arkoors(
400 arkoors: Vec<ArkoorBuilder<'a, VtxoRequest>>,
401 ) -> Result<Self, ArkoorPackageError> {
402 let mut spending_tx_by_input = HashMap::new();
403
404 for arkoor in arkoors.iter() {
405 spending_tx_by_input.insert(arkoor.input.id(), arkoor.unsigned_transaction());
406 }
407
408 Ok(Self {
409 arkoors,
410 spending_tx_by_input,
411 })
412 }
413
414 pub fn inputs(&self) -> Vec<&'a Vtxo> {
415 self.arkoors.iter().map(|a| a.input).collect::<Vec<_>>()
416 }
417
418 pub fn spending_tx(&self, input_id: VtxoId) -> Option<&Transaction> {
419 self.spending_tx_by_input.get(&input_id)
420 }
421
422 pub fn build_vtxos<'b>(
423 self,
424 sigs: impl IntoIterator<Item = &'a ArkoorCosignResponse>,
425 keypairs: impl IntoIterator<Item = &'a Keypair>,
426 sec_nonces: impl IntoIterator<Item = musig::SecretNonce>,
427 ) -> Result<(Vec<Vtxo>, Option<Vtxo>), ArkoorPackageError> {
428 let mut sent_vtxos = vec![];
429 let mut change_vtxo = None;
430
431 let expected_len = self.arkoors.len();
432
433 let iter = self.arkoors.into_iter().zip(sigs).zip(keypairs).zip(sec_nonces);
434 for (((arkoor, cosign), keypair), sec_nonce) in iter {
435 let vtxos = arkoor.build_vtxos(sec_nonce, keypair, cosign)?;
436
437 let mut vtxo_iter = vtxos.into_iter();
439 let user_vtxo = vtxo_iter.next().ok_or(ArkoorPackageError::MissingVtxo)?;
440 sent_vtxos.push(user_vtxo);
441
442 if let Some(vtxo) = vtxo_iter.next() {
443 assert!(change_vtxo.replace(vtxo).is_none(), "change vtxo already set");
444 }
445 }
446
447 if sent_vtxos.len() != expected_len {
448 return Err(ArkoorPackageError::InvalidLength);
449 }
450
451 Ok((sent_vtxos, change_vtxo))
452 }
453
454 pub fn new_vtxos(&self) -> Vec<Vec<Vtxo>> {
455 self.arkoors.iter().map(|arkoor| {
456 let txouts = arkoor.txouts();
457 let tx = unsigned_arkoor_tx(&arkoor.input, &txouts);
458 build_arkoor_vtxos(&arkoor.input, &arkoor.outputs, &txouts, tx.compute_txid(), None) }).collect::<Vec<Vec<_>>>()
460 }
461
462 pub fn server_cosign(&self, keypair: &Keypair) -> Vec<ArkoorCosignResponse> {
464 let mut cosign = vec![];
465
466 for arkoor in self.arkoors.iter() {
467 cosign.push(arkoor.server_cosign(keypair));
468 }
469
470 cosign
471 }
472
473 pub fn verify_cosign_response<T: Borrow<ArkoorCosignResponse>>(
474 &self,
475 server_cosign: &[T],
476 ) -> bool {
477 for (idx, builder) in self.arkoors.iter().enumerate() {
478 if let Some(cosign) = server_cosign.get(idx) {
479 if !builder.verify_cosign_response(cosign.borrow()) {
480 return false;
481 }
482 } else {
483 return false;
484 }
485 }
486 true
487 }
488}
489