ark/arkoor/
package.rs

1use std::collections::HashMap;
2use std::borrow::Borrow;
3use std::iter;
4
5use bitcoin::{Amount, Transaction};
6use bitcoin::secp256k1::{Keypair, PublicKey};
7use bitcoin_ext::P2TR_DUST;
8
9use crate::{Vtxo, VtxoRequest, VtxoId, VtxoPolicy, musig, error::IncorrectSigningKeyError};
10use super::{ArkoorError, ArkoorBuilder, ArkoorCosignResponse};
11use super::{build_arkoor_vtxos, unsigned_arkoor_tx};
12
13
14/// This type helps both the client and server with building multiple arkoor transactions
15/// in a synchronized way. It's purely a functional type, initialized with
16/// the parameters that will make up the arkoor package: the input vtxos to be spent
17/// and the desired payment request with optional change.
18///
19/// The flow works as follows:
20/// - sender uses the constructor to check the payment request for validity
21/// - server uses the constructor to check the payment request for validity
22/// - server uses [ArkoorPackageBuilder::server_cosign] to construct a vector of
23///   [ArkoorCosignResponse] to send back to the sender
24/// - sender passes the responses into [ArkoorPackageBuilder::build_vtxos] to construct
25///   the signed resulting VTXOs and optional change VTXO
26///
27/// The package can handle multiple input VTXOs to fulfill a single payment request,
28/// automatically creating change outputs when necessary.
29pub struct ArkoorPackageBuilder<'a, T: Clone> {
30	/// Each transition from one input VTXO to one or two output VTXOs
31	pub arkoors: Vec<ArkoorBuilder<'a, T>>,
32	spending_tx_by_input: HashMap<VtxoId, Transaction>,
33}
34
35#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
36pub enum ArkoorPackageError {
37	#[error("Payment has non-null change amount but no change pubkey provided")]
38	MissingChangePk,
39	#[error("Invalid length of cosignature response")]
40	InvalidLength,
41	#[error("No vtxo created")]
42	MissingVtxo,
43	#[error("Invalid spk for revocation")]
44	InvalidRevocationSpk,
45	#[error("Invalid length of user nonces")]
46	InvalidUserNoncesLength,
47	#[error("Htlc amount does not match invoice amount")]
48	InvalidHtlcAmount,
49	#[error("An error occurred while building arkoor: {0}")]
50	ArkoorError(ArkoorError),
51	#[error("Too many outputs")]
52	TooManyOutputs,
53	#[error("incorrect signing key provided")]
54	Signing(#[from] IncorrectSigningKeyError),
55}
56
57impl<'a> ArkoorPackageBuilder<'a, VtxoRequest> {
58	pub fn new(
59		inputs: impl IntoIterator<Item = &'a Vtxo>,
60		user_nonces: &'a [musig::PublicNonce],
61		vtxo_request: VtxoRequest,
62		change_pubkey: Option<PublicKey>,
63	) -> Result<Self, ArkoorPackageError> {
64		let mut remaining_amount = vtxo_request.amount;
65		let mut arkoors = vec![];
66		let mut spending_tx_by_input = HashMap::new();
67
68		for (idx, input) in inputs.into_iter().enumerate() {
69			let user_nonce = user_nonces.get(idx).ok_or(ArkoorPackageError::InvalidUserNoncesLength)?;
70
71			let change_amount = input.amount().checked_sub(remaining_amount);
72			let (output_amount, change) = if let Some(change_amount) = change_amount {
73				// NB: If change amount is less than the dust amount, we don't add any change output
74				let change = if change_amount < P2TR_DUST {
75					None
76				} else {
77					Some(VtxoRequest {
78						amount: change_amount,
79						policy: VtxoPolicy::new_pubkey(change_pubkey.ok_or(ArkoorPackageError::MissingChangePk)?),
80					})
81				};
82
83				(remaining_amount, change)
84			} else {
85				(input.amount(), None)
86			};
87
88			let output = VtxoRequest {
89				amount: output_amount,
90				policy: vtxo_request.policy.clone(),
91			};
92
93			let pay_reqs = iter::once(output.clone()).chain(change).collect::<Vec<_>>();
94
95			let arkoor = ArkoorBuilder::new(&input, user_nonce, pay_reqs)
96				.map_err(ArkoorPackageError::ArkoorError)?;
97
98			spending_tx_by_input.insert(input.id(), arkoor.unsigned_transaction());
99			arkoors.push(arkoor);
100
101			remaining_amount = remaining_amount - output_amount;
102			if remaining_amount == Amount::ZERO {
103				break;
104			}
105		}
106
107		Ok(Self {
108			arkoors,
109			spending_tx_by_input,
110		})
111	}
112
113	pub fn new_htlc_revocation(
114		htlc_vtxos: &'a [Vtxo],
115		user_nonces: &'a [musig::PublicNonce],
116	) -> Result<Self, ArkoorPackageError> {
117		let arkoors = htlc_vtxos.iter().zip(user_nonces).map(|(v, u)| {
118			if !matches!(v.policy(), VtxoPolicy::ServerHtlcSend { .. }) {
119				return Err(ArkoorPackageError::InvalidRevocationSpk);
120			}
121
122			let refund = VtxoRequest {
123				amount: v.amount(),
124				policy: VtxoPolicy::new_pubkey(v.user_pubkey()),
125			};
126			ArkoorBuilder::new(v, u, vec![refund])
127				.map_err(ArkoorPackageError::ArkoorError)
128		}).collect::<Result<Vec<_>, ArkoorPackageError>>()?;
129
130		Self::from_arkoors(arkoors)
131	}
132
133	pub fn from_arkoors(
134		arkoors: Vec<ArkoorBuilder<'a, VtxoRequest>>,
135	) -> Result<Self, ArkoorPackageError> {
136		let mut spending_tx_by_input = HashMap::new();
137
138		for arkoor in arkoors.iter() {
139			spending_tx_by_input.insert(arkoor.input.id(), arkoor.unsigned_transaction());
140		}
141
142		Ok(Self {
143			arkoors,
144			spending_tx_by_input,
145		})
146	}
147
148	pub fn inputs(&self) -> Vec<&'a Vtxo> {
149		self.arkoors.iter().map(|a| a.input).collect::<Vec<_>>()
150	}
151
152	pub fn spending_tx(&self, input_id: VtxoId) -> Option<&Transaction> {
153		self.spending_tx_by_input.get(&input_id)
154	}
155
156	pub fn build_vtxos<'b>(
157		self,
158		sigs: impl IntoIterator<Item = &'a ArkoorCosignResponse>,
159		keypairs: impl IntoIterator<Item = &'a Keypair>,
160		sec_nonces: impl IntoIterator<Item = musig::SecretNonce>,
161	) -> Result<(Vec<Vtxo>, Option<Vtxo>), ArkoorPackageError> {
162		let mut sent_vtxos = vec![];
163		let mut change_vtxo = None;
164
165		let expected_len = self.arkoors.len();
166
167		let iter = self.arkoors.into_iter().zip(sigs).zip(keypairs).zip(sec_nonces);
168		for (((arkoor, cosign), keypair), sec_nonce) in iter {
169			let vtxos = arkoor.build_vtxos(sec_nonce, keypair, cosign)?;
170
171			// The first one is of the recipient, we will post it to their mailbox.
172			let mut vtxo_iter = vtxos.into_iter();
173			let user_vtxo = vtxo_iter.next().ok_or(ArkoorPackageError::MissingVtxo)?;
174			sent_vtxos.push(user_vtxo);
175
176			if let Some(vtxo) = vtxo_iter.next() {
177				assert!(change_vtxo.replace(vtxo).is_none(), "change vtxo already set");
178			}
179		}
180
181		if sent_vtxos.len() != expected_len {
182			return Err(ArkoorPackageError::InvalidLength);
183		}
184
185		Ok((sent_vtxos, change_vtxo))
186	}
187
188	pub fn new_vtxos(&self) -> Vec<Vec<Vtxo>> {
189		self.arkoors.iter().map(|arkoor| {
190			let txouts = arkoor.txouts();
191			let tx = unsigned_arkoor_tx(&arkoor.input, &txouts);
192			build_arkoor_vtxos(&arkoor.input, &arkoor.outputs, &txouts, tx.compute_txid(), None) //TODO(stevenroose) signaature
193		}).collect::<Vec<Vec<_>>>()
194	}
195
196	/// Used by the Ark server to cosign the arkoor request.
197	pub fn server_cosign(&self, keypair: &Keypair) -> Vec<ArkoorCosignResponse> {
198		let mut cosign = vec![];
199
200		for arkoor in self.arkoors.iter() {
201			cosign.push(arkoor.server_cosign(keypair));
202		}
203
204		cosign
205	}
206
207	pub fn verify_cosign_response<T: Borrow<ArkoorCosignResponse>>(
208		&self,
209		server_cosign: &[T],
210	) -> bool {
211		for (idx, builder) in self.arkoors.iter().enumerate() {
212			if let Some(cosign) = server_cosign.get(idx) {
213				if !builder.verify_cosign_response(cosign.borrow()) {
214					return false;
215				}
216			} else {
217				return false;
218			}
219		}
220		true
221	}
222}
223