bark/
arkoor.rs

1use anyhow::Context;
2use bitcoin::Amount;
3use bitcoin::hex::DisplayHex;
4use bitcoin::secp256k1::PublicKey;
5use log::{info, error};
6
7use ark::{VtxoRequest, ProtocolEncoding};
8use ark::arkoor::checkpointed_package::{CheckpointedPackageBuilder, PackageCosignResponse};
9use ark::vtxo::{Vtxo, VtxoId, VtxoPolicyKind};
10use bitcoin_ext::P2TR_DUST;
11use server_rpc::protos;
12
13use crate::subsystem::Subsystem;
14use crate::{ArkoorMovement, VtxoDelivery, MovementUpdate, Wallet};
15use crate::movement::MovementDestination;
16use crate::movement::manager::OnDropStatus;
17
18/// The result of creating an arkoor transaction
19pub struct ArkoorCreateResult {
20	input: Vec<VtxoId>,
21	created: Vec<Vtxo>,
22	change: Option<Vtxo>,
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()?;
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().policy_type() {
38			VtxoPolicyKind::Pubkey => {},
39			VtxoPolicyKind::Checkpoint | VtxoPolicyKind::ServerHtlcRecv | VtxoPolicyKind::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		vtxo_request: VtxoRequest,
68		change_pubkey: PublicKey,
69	) -> anyhow::Result<ArkoorCreateResult> {
70		if vtxo_request.policy.user_pubkey() == change_pubkey {
71			bail!("Cannot create arkoor to same address as change");
72		}
73
74		// Find vtxos to cover
75		let mut srv = self.require_server()?;
76		let inputs = self.select_vtxos_to_cover(vtxo_request.amount).await?;
77		let input_ids = inputs.iter().map(|v| v.id()).collect();
78
79		let mut user_keypairs = vec![];
80		for vtxo in &inputs {
81			user_keypairs.push(self.get_vtxo_key(vtxo).await?);
82		}
83
84		let builder = CheckpointedPackageBuilder::new(
85			inputs.iter().map(|v| &v.vtxo).cloned(),
86			vtxo_request,
87			change_pubkey,
88		)
89			.context("Failed to construct arkoor package")?
90			.generate_user_nonces(&user_keypairs)
91			.context("invalid nb of keypairs")?;
92
93		let cosign_request = protos::CheckpointedPackageCosignRequest::from(
94			builder.cosign_requests().convert_vtxo(|vtxo| vtxo.id()));
95
96		let response = srv.client.checkpointed_cosign_oor(cosign_request).await
97			.context("server failed to cosign arkoor")?
98			.into_inner();
99
100		let cosign_responses = PackageCosignResponse::try_from(response)
101			.context("Failed to parse cosign response from server")?;
102
103		let vtxos = builder
104			.user_cosign(&user_keypairs, cosign_responses)
105			.context("Failed to cosign vtxos")?
106			.build_signed_vtxos();
107
108		// See if their is a change vtxo
109		if vtxos.last().expect("At least one vtxo").user_pubkey() == change_pubkey {
110			let nb_vtxos = vtxos.len();
111			let change = vtxos.last().cloned();
112			Ok(ArkoorCreateResult {
113				input: input_ids,
114				// The last one is change
115				created: vtxos.into_iter().take(nb_vtxos.saturating_sub(1)).collect::<Vec<_>>(),
116				change: change,
117			})
118		} else {
119			Ok(ArkoorCreateResult {
120				input: input_ids,
121				created: vtxos,
122				change: None,
123			})
124		}
125	}
126
127	/// Makes an out-of-round payment to the given [ark::Address]. This does not require waiting for
128	/// a round, so it should be relatively instantaneous.
129	///
130	/// If the [Wallet] doesn't contain a VTXO larger than the given [Amount], multiple payments
131	/// will be chained together, resulting in the recipient receiving multiple VTXOs.
132	///
133	/// Note that a change [Vtxo] may be created as a result of this call. With each payment these
134	/// will become more uneconomical to unilaterally exit, so you should eventually refresh them
135	/// with [Wallet::refresh_vtxos] or periodically call [Wallet::maintenance_refresh].
136	pub async fn send_arkoor_payment(
137		&self,
138		destination: &ark::Address,
139		amount: Amount,
140	) -> anyhow::Result<Vec<Vtxo>> {
141		let mut srv = self.require_server()?;
142
143		self.validate_arkoor_address(&destination).await
144			.context("address validation failed")?;
145
146		let negative_amount = -amount.to_signed().context("Amount out-of-range")?;
147		if amount < P2TR_DUST {
148			bail!("Sent amount must be at least {}", P2TR_DUST);
149		}
150
151		let change_pubkey = self.derive_store_next_keypair().await
152			.context("Failed to create change keypair")?.0;
153
154		let request = VtxoRequest { amount, policy: destination.policy().clone() };
155		let arkoor = self.create_checkpointed_arkoor(request.clone(), change_pubkey.public_key())
156			.await
157			.context("Failed to create checkpointed transactions")?;
158
159		let mut movement = self.movements.new_guarded_movement_with_update(
160			Subsystem::ARKOOR,
161			ArkoorMovement::Send.to_string(),
162			OnDropStatus::Failed,
163			MovementUpdate::new()
164				.intended_and_effective_balance(negative_amount)
165				.consumed_vtxos(&arkoor.input)
166				.sent_to([MovementDestination::ark(destination.clone(), amount)])
167		).await?;
168
169		let req = protos::ArkoorPackage {
170			arkoors: arkoor.created.iter().map(|v| protos::ArkoorVtxo {
171				pubkey: request.policy.user_pubkey().serialize().to_vec(),
172				vtxo: v.serialize().to_vec(),
173			}).collect(),
174		};
175
176		#[allow(deprecated)]
177		if let Err(e) = srv.client.post_arkoor_package_mailbox(req).await {
178			error!("Failed to post the arkoor vtxo to the recipients mailbox: '{:#}'", e);
179			//NB we will continue to at least not lose our own change
180		}
181		self.mark_vtxos_as_spent(&arkoor.input).await?;
182		if let Some(change) = arkoor.change {
183			self.store_spendable_vtxos([&change]).await?;
184			movement.apply_update(MovementUpdate::new().produced_vtxo(change)).await?;
185		}
186		movement.success().await?;
187		Ok(arkoor.created)
188	}
189}