Skip to main content

bark/
arkoor.rs

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