bark/
arkoor.rs

1use anyhow::Context;
2use bitcoin::Amount;
3use bitcoin::hex::DisplayHex;
4use bitcoin::secp256k1::PublicKey;
5use log::{info, error};
6
7use ark::{VtxoPolicy, ProtocolEncoding};
8use ark::arkoor::ArkoorDestination;
9use ark::arkoor::package::{ArkoorPackageBuilder, ArkoorPackageCosignResponse};
10use ark::vtxo::{Full, Vtxo, VtxoId};
11use server_rpc::protos;
12
13use crate::subsystem::Subsystem;
14use crate::{ArkoorMovement, VtxoDelivery, MovementUpdate, Wallet, WalletVtxo};
15use crate::movement::MovementDestination;
16use crate::movement::manager::OnDropStatus;
17
18/// The result of creating an arkoor transaction
19pub struct ArkoorCreateResult {
20	pub inputs: Vec<VtxoId>,
21	pub created: Vec<Vtxo<Full>>,
22	pub change: Vec<Vtxo<Full>>,
23}
24
25impl Wallet {
26	/// Validate if we can send arkoor payments to the given [ark::Address], for example an error
27	/// will be returned if the given [ark::Address] belongs to a different server (see
28	/// [ark::address::ArkId]).
29	pub async fn validate_arkoor_address(&self, address: &ark::Address) -> anyhow::Result<()> {
30		let (srv, _) = self.require_server().await?;
31
32		if !address.ark_id().is_for_server(srv.ark_info().await?.server_pubkey) {
33			bail!("Ark address is for different server");
34		}
35
36		// Not all policies are supported for sending arkoor
37		match address.policy() {
38			VtxoPolicy::Pubkey(_) => {},
39			VtxoPolicy::ServerHtlcRecv(_) | VtxoPolicy::ServerHtlcSend(_) => {
40				bail!("VTXO policy in address cannot be used for arkoor payment: {}",
41					address.policy().policy_type(),
42				);
43			}
44		}
45
46		if address.delivery().is_empty() {
47			bail!("No VTXO delivery mechanism provided in address");
48		}
49		// We first see if we know any of the deliveries, if not, we will log
50		// the unknown onces.
51		// We do this in two parts because we shouldn't log unknown ones if there is one known.
52		if !address.delivery().iter().any(|d| !d.is_unknown()) {
53			for d in address.delivery() {
54				if let VtxoDelivery::Unknown { delivery_type, data } = d {
55					info!("Unknown delivery in address: type={:#x}, data={}",
56						delivery_type, data.as_hex(),
57					);
58				}
59			}
60		}
61
62		Ok(())
63	}
64
65	pub(crate) async fn create_checkpointed_arkoor(
66		&self,
67		arkoor_dest: ArkoorDestination,
68		change_pubkey: PublicKey,
69	) -> anyhow::Result<ArkoorCreateResult> {
70		let inputs = self.select_vtxos_to_cover(arkoor_dest.total_amount).await?;
71		self.create_checkpointed_arkoor_with_vtxos(
72			arkoor_dest,
73			change_pubkey,
74			inputs.into_iter(),
75		).await
76	}
77
78	pub(crate) async fn create_checkpointed_arkoor_with_vtxos(
79		&self,
80		arkoor_dest: ArkoorDestination,
81		change_pubkey: PublicKey,
82		inputs: impl IntoIterator<Item = WalletVtxo>,
83	) -> anyhow::Result<ArkoorCreateResult> {
84		if arkoor_dest.policy.user_pubkey() == change_pubkey {
85			bail!("Cannot create arkoor to same address as change");
86		}
87
88		// Find vtxos to cover
89		let (mut srv, _) = self.require_server().await?;
90		let (input_ids, inputs) = inputs.into_iter()
91			.map(|v| (v.id(), v))
92			.collect::<(Vec<_>, Vec<_>)>();
93
94		let mut user_keypairs = Vec::with_capacity(inputs.len());
95		for vtxo in &inputs {
96			user_keypairs.push(self.get_vtxo_key(vtxo).await?);
97		}
98
99		let builder = ArkoorPackageBuilder::new_single_output_with_checkpoints(
100			inputs.into_iter().map(|v| v.vtxo),
101			arkoor_dest.clone(),
102			VtxoPolicy::new_pubkey(change_pubkey),
103		)
104			.context("Failed to construct arkoor package")?
105			.generate_user_nonces(&user_keypairs)
106			.context("invalid nb of keypairs")?;
107
108		let cosign_request = protos::ArkoorPackageCosignRequest::from(
109			builder.cosign_request().convert_vtxo(|vtxo| vtxo.id()),
110		);
111
112		let response = srv.client.request_arkoor_cosign(cosign_request).await
113			.context("server failed to cosign arkoor")?
114			.into_inner();
115
116		let cosign_responses = ArkoorPackageCosignResponse::try_from(response)
117			.context("Failed to parse cosign response from server")?;
118
119		let vtxos = builder
120			.user_cosign(&user_keypairs, cosign_responses)
121			.context("Failed to cosign vtxos")?
122			.build_signed_vtxos();
123
124		// divide between change and destination
125		let (dest, change) = vtxos.into_iter()
126			.partition::<Vec<_>, _>(|v| *v.policy() == arkoor_dest.policy);
127
128		Ok(ArkoorCreateResult {
129			inputs: input_ids,
130			created: dest,
131			change: change,
132		})
133	}
134
135	/// Makes an out-of-round payment to the given [ark::Address]. This does not require waiting for
136	/// a round, so it should be relatively instantaneous.
137	///
138	/// If the [Wallet] doesn't contain a VTXO larger than the given [Amount], multiple payments
139	/// will be chained together, resulting in the recipient receiving multiple VTXOs.
140	///
141	/// Note that a change [Vtxo] may be created as a result of this call. With each payment these
142	/// will become more uneconomical to unilaterally exit, so you should eventually refresh them
143	/// with [Wallet::refresh_vtxos] or periodically call [Wallet::maintenance_refresh].
144	pub async fn send_arkoor_payment(
145		&self,
146		destination: &ark::Address,
147		amount: Amount,
148	) -> anyhow::Result<Vec<Vtxo<Full>>> {
149		let (mut srv, _) = self.require_server().await?;
150
151		self.validate_arkoor_address(&destination).await
152			.context("address validation failed")?;
153
154		let negative_amount = -amount.to_signed().context("Amount out-of-range")?;
155
156		let change_pubkey = self.derive_store_next_keypair().await
157			.context("Failed to create change keypair")?.0;
158
159		let dest = ArkoorDestination { total_amount: amount, policy: destination.policy().clone() };
160		let arkoor = self.create_checkpointed_arkoor(
161			dest.clone(),
162			change_pubkey.public_key(),
163		).await.context("failed to create arkoor transaction")?;
164
165		let mut movement = self.movements.new_guarded_movement_with_update(
166			Subsystem::ARKOOR,
167			ArkoorMovement::Send.to_string(),
168			OnDropStatus::Failed,
169			MovementUpdate::new()
170				.intended_and_effective_balance(negative_amount)
171				.consumed_vtxos(&arkoor.inputs)
172				.sent_to([MovementDestination::ark(destination.clone(), amount)])
173		).await?;
174
175		let mut delivered = false;
176		for delivery in destination.delivery() {
177			match delivery {
178				VtxoDelivery::ServerMailbox { blinded_id } => {
179					let req = protos::mailbox_server::PostVtxosMailboxRequest {
180						blinded_id: blinded_id.to_vec(),
181						vtxos: arkoor.created.iter().map(|v| v.serialize().to_vec()).collect(),
182					};
183
184					if let Err(e) = srv.mailbox_client.post_vtxos_mailbox(req).await {
185						error!("Failed to post the vtxos to the destination's mailbox: '{:#}'", e);
186						//NB we will continue to at least not lose our own change
187					} else {
188						delivered = true;
189					}
190				},
191				VtxoDelivery::Unknown { delivery_type, data } => {
192					error!("Unknown delivery type {} for arkoor payment: {}", delivery_type, data.as_hex());
193				},
194				_ => {
195					error!("Unsupported delivery type for arkoor payment: {:?}", delivery);
196				}
197			}
198		}
199		self.mark_vtxos_as_spent(&arkoor.inputs).await?;
200		if !arkoor.change.is_empty() {
201			self.store_spendable_vtxos(&arkoor.change).await?;
202			movement.apply_update(MovementUpdate::new().produced_vtxos(arkoor.change)).await?;
203		}
204
205		if delivered {
206			movement.success().await?;
207		} else {
208			bail!("Failed to deliver arkoor vtxos to any destination");
209		}
210
211		Ok(arkoor.created)
212	}
213}