Skip to main content

bark/
arkoor.rs

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