Skip to main content

bark/
arkoor.rs

1use anyhow::Context;
2use bitcoin::{Amount, NetworkKind};
3use bitcoin::hex::DisplayHex;
4use bitcoin::secp256k1::Keypair;
5use log::{info, warn};
6
7use ark::VtxoPolicy;
8use ark::arkoor::ArkoorDestination;
9use ark::arkoor::package::{ArkoorPackageBuilder, ArkoorPackageCosignResponse};
10use ark::vtxo::{Full, Vtxo, VtxoId};
11use server_rpc::protos;
12
13use crate::{VtxoDelivery, Wallet, WalletVtxo};
14use crate::actions::DriveMode;
15use crate::actions::arkoor_send::{ArkoorSend, start_arkoor_send};
16
17/// The result of creating an arkoor transaction
18pub struct ArkoorCreateResult {
19	pub inputs: Vec<VtxoId>,
20	pub created: Vec<Vtxo<Full>>,
21	pub change: Vec<Vtxo<Full>>,
22}
23
24/// Error returned by [`Wallet::create_checkpointed_arkoor_with_vtxos`].
25///
26/// The cosign RPC failure is kept as a typed [`tonic::Status`] rather
27/// than flattened into `anyhow`, so a caller driving this as a wallet
28/// action can route a genuine server rejection to its `on_rejection`
29/// path (via `AdvanceError::is_server_rejection`) instead of retrying a
30/// doomed request forever. Every other failure is opaque `Other`.
31#[derive(Debug, thiserror::Error)]
32pub enum ArkoorCreateError {
33	/// The `request_arkoor_cosign` RPC failed. May be a rejection
34	/// (`InvalidArgument`/`NotFound`) or a transient error; the caller
35	/// classifies it via the status code.
36	#[error("server failed to cosign arkoor: {0}")]
37	Cosign(#[source] tonic::Status),
38	#[error(transparent)]
39	Other(#[from] anyhow::Error),
40}
41
42#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
43pub enum ArkoorAddressError {
44	#[error("Ark address is for different network")]
45	NetworkMismatch,
46	#[error("Ark address is for different server")]
47	ServerMismatch,
48	#[error("VTXO policy in address cannot be used for arkoor payment: {0:?}")]
49	PolicyNotSupported(VtxoPolicy),
50	#[error("No VTXO delivery mechanism provided in address")]
51	NoDeliveryMechanism,
52	#[error("Unknown delivery mechanism: {0}")]
53	UnknownDeliveryMechanism(String),
54	#[error("Other error: {0}")]
55	Other(String),
56}
57
58impl Wallet {
59	/// Validate if we can send arkoor payments to the given [ark::Address], for example an error
60	/// will be returned if the given [ark::Address] belongs to a different server (see
61	/// [ark::address::ArkId]).
62	pub async fn validate_arkoor_address(&self, address: &ark::Address) -> Result<(), ArkoorAddressError> {
63		let network = self.network().await
64			.map_err(|e| ArkoorAddressError::Other(e.to_string()))?;
65		let (_, ark_info) = self.require_server().await
66			.map_err(|e| ArkoorAddressError::Other(e.to_string()))?;
67
68		let network_kind = NetworkKind::from(network);
69		if address.is_testnet() == network_kind.is_mainnet() {
70			return Err(ArkoorAddressError::NetworkMismatch);
71		}
72
73		if !address.ark_id().is_for_server(ark_info.server_pubkey) {
74			return Err(ArkoorAddressError::ServerMismatch);
75		}
76
77		// Not all policies are supported for sending arkoor
78		match address.policy() {
79			VtxoPolicy::Pubkey(_) => {},
80			VtxoPolicy::ServerHtlcRecv(_) | VtxoPolicy::ServerHtlcSend(_) => {
81				return Err(ArkoorAddressError::PolicyNotSupported(address.policy().clone()));
82			}
83		}
84
85		if address.delivery().is_empty() {
86			return Err(ArkoorAddressError::NoDeliveryMechanism);
87		}
88		// We first see if we know any of the deliveries, if not, we will log
89		// the unknown onces.
90		// We do this in two parts because we shouldn't log unknown ones if there is one known.
91		if !address.delivery().iter().any(|d| !d.is_unknown()) {
92			for d in address.delivery() {
93				if let VtxoDelivery::Unknown { delivery_type, data } = d {
94					info!("Unknown delivery in address: type={:#x}, data={}",
95						delivery_type, data.as_hex(),
96					);
97				}
98			}
99		}
100
101		Ok(())
102	}
103
104	/// Build, cosign and split an arkoor package using a caller-provided
105	/// change keypair.
106	///
107	/// Reusing the same change keypair on a retry keeps the implied
108	/// `spending_txid` stable, so the server's `check_spendable_for_oor`
109	/// idempotency check accepts the retry rather than rejecting it as a
110	/// conflicting double-spend.
111	pub(crate) async fn create_checkpointed_arkoor_with_vtxos(
112		&self,
113		arkoor_dest: ArkoorDestination,
114		inputs: impl IntoIterator<Item = WalletVtxo>,
115		change_keypair: Keypair,
116	) -> Result<ArkoorCreateResult, ArkoorCreateError> {
117		let (mut srv, _) = self.require_server().await?;
118		let input_ids = inputs.into_iter().map(|v| v.id()).collect::<Vec<_>>();
119
120		// Hydrate the inputs to their full form: the arkoor builder needs
121		// the genesis chain and the server registration call sends the
122		// full bytes over the wire.
123		let inputs = self.inner.db.get_full_vtxos(&input_ids).await
124			.context("failed to hydrate arkoor input vtxos")?;
125
126		// Pre-register the input chains so the post-cosign register call
127		// for the outputs finds a signed chain anchor:
128		// register_vtxo_transactions validates a vtxo against its anchor's
129		// signed_tx in the DB, and boarded inputs sit unsigned in
130		// virtual_transaction (see register_board) until a
131		// register_vtxo_transactions call backfills them.
132		self.register_vtxo_transactions_with_server(&inputs).await
133			.context("failed to register arkoor input vtxo transactions with server")?;
134
135		let change_pubkey = change_keypair.public_key();
136		if arkoor_dest.policy.user_pubkey() == change_pubkey {
137			return Err(anyhow!("Cannot create arkoor to same address as change").into());
138		}
139
140		let mut user_keypairs = vec![];
141		for vtxo in &inputs {
142			user_keypairs.push(self.get_vtxo_key(vtxo).await?);
143		}
144
145		let builder = ArkoorPackageBuilder::new_single_output_with_checkpoints(
146			inputs.into_iter(),
147			arkoor_dest.clone(),
148			VtxoPolicy::new_pubkey(change_pubkey),
149		)
150			.context("Failed to construct arkoor package")?
151			.generate_user_nonces(&user_keypairs)
152			.context("invalid nb of keypairs")?;
153
154		let cosign_request = protos::ArkoorPackageCosignRequest::from(
155			builder.cosign_request(),
156		);
157
158		let response = srv.client.request_arkoor_cosign(cosign_request).await
159			.map_err(ArkoorCreateError::Cosign)?
160			.into_inner();
161
162		let cosign_responses = ArkoorPackageCosignResponse::try_from(response)
163			.context("Failed to parse cosign response from server")?;
164
165		let vtxos = builder
166			.user_cosign(&user_keypairs, cosign_responses)
167			.context("Failed to cosign vtxos")?
168			.build_signed_vtxos();
169
170		// divide between change and destination
171		let (dest, change) = vtxos.into_iter()
172			.partition::<Vec<_>, _>(|v| *v.policy() == arkoor_dest.policy);
173
174		Ok(ArkoorCreateResult {
175			inputs: input_ids,
176			created: dest,
177			change,
178		})
179	}
180
181	/// Makes an out-of-round payment to the given [ark::Address]. This does not require waiting for
182	/// a round, so it should be relatively instantaneous.
183	///
184	/// If the [Wallet] doesn't contain a VTXO larger than the given [Amount], multiple payments
185	/// will be chained together, resulting in the recipient receiving multiple VTXOs.
186	///
187	/// Note that a change [Vtxo] may be created as a result of this call. With each payment these
188	/// will become more uneconomical to unilaterally exit, so you should eventually refresh them
189	/// with [Wallet::refresh_vtxos] or periodically call [Wallet::maintenance_refresh].
190	pub async fn send_arkoor_payment(
191		&self,
192		destination: &ark::Address,
193		amount: Amount,
194	) -> anyhow::Result<()> {
195		let action = start_arkoor_send(self, destination.clone(), amount).await?;
196
197		// Persist the action together with the input locks so the executor has
198		// something to drive on restart; otherwise a crash between this point and
199		// `drive_action` leaves vtxos locked under an action id that has no
200		// checkpoint row.
201		self.inner.db.upsert_wallet_action_checkpoint(&action.id, &action.clone().into()).await?;
202
203		self.drive_action(action, DriveMode::UntilDone).await
204	}
205
206	/// Returns every in-progress arkoor send checkpoint.
207	pub async fn pending_arkoor_sends(&self) -> anyhow::Result<Vec<ArkoorSend>> {
208		Ok(self.inner.db.get_all_wallet_action_checkpoints().await?
209			.into_iter()
210			.filter_map(|cp| cp.into_arkoor_send())
211			.collect())
212	}
213
214	/// Drives every pending arkoor send forward by one step or to
215	/// completion if it's ready.
216	pub async fn sync_pending_arkoor_sends(&self) -> anyhow::Result<()> {
217		let pending = self.pending_arkoor_sends().await?;
218		if pending.is_empty() {
219			return Ok(());
220		}
221		info!("Syncing {} pending arkoor sends", pending.len());
222		for send in pending {
223			let id = send.id.clone();
224			if let Err(e) = self.drive_action(send, DriveMode::UntilParkOrDone).await {
225				warn!("Failed to sync arkoor send {}: {:#}", id, e);
226			}
227		}
228		Ok(())
229	}
230}