Skip to main content

ark/vtxo/policy/
mod.rs

1//!
2//! VTXO policies
3//! =============
4//!
5//! # Block height and block delta invariants
6//!
7//! Policy heights and deltas are raw `BlockHeight` (u32) and `BlockDelta`
8//! (u16), but every value crossing a deserialization boundary (protocol
9//! decode, gRPC ingress, JSON, postgres) must be validated through
10//! [check_block_height] / [check_block_delta]. The `arithmetic_side_effects`
11//! clippy lint enforces that interior arithmetic on these values goes through
12//! `checked_*`/`saturating_*`/`wrapping_*`.
13//!
14//! The bounds (see [MAX_BLOCK_DELTA], [MAX_BLOCK_HEIGHT] and the
15//! `const _: () = { ... }` block below):
16//!
17//! * Up to four policy deltas sum into a value that fits in `BlockDelta` (u16).
18//!   This lets clause `block_delta` (relative locktime) fields hold any
19//!   in-codebase composition without overflowing u16.
20//! * Any chain tip plus up to four policy deltas stays below
21//!   `LOCK_TIME_THRESHOLD`, so the result is always a valid absolute locktime
22//!   height (and therefore `LockTime::from_height` succeeds).
23//!
24//! Today's maximum composition is two policy deltas (htlc-send clause's
25//! `2 * exit_delta`, watchman `confirmed_at + 2 * exit_delta`, htlc-recv clause
26//! and watchman `exit_delta + htlc_expiry_delta`); the extra 2x of headroom is
27//! defensive so future operations can be added without retuning the bounds.
28
29pub mod clause;
30pub mod signing;
31
32use std::fmt;
33use std::str::FromStr;
34
35use bitcoin::{Amount, ScriptBuf, TxOut, taproot};
36use bitcoin::secp256k1::PublicKey;
37
38use bitcoin_ext::{BlockDelta, BlockHeight, TaprootSpendInfoExt};
39
40use crate::{SECP, musig };
41use crate::lightning::PaymentHash;
42use crate::tree::signed::UnlockHash;
43use crate::vtxo::TapScriptClause;
44use crate::vtxo::policy::clause::{
45	DelayedSignClause, DelayedTimelockSignClause, HashDelaySignClause, HashSignClause,
46	TimelockSignClause, VtxoClause,
47};
48
49#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
50#[error("invalid policy data: {msg}")]
51pub struct PolicyError {
52	msg: &'static str,
53}
54
55impl PolicyError {
56	fn new(msg: &'static str) -> Self {
57		Self { msg }
58	}
59}
60
61/// The maximum value of a block delta accepted in policies.
62///
63/// Equals `u16::MAX / 4 = 16383` blocks, or roughly 114 days (~3.8 months).
64pub const MAX_BLOCK_DELTA: BlockDelta = u16::MAX / 4;
65
66/// The maximum value of a block height accepted in policies.
67///
68/// Reserves enough headroom below [bitcoin::absolute::LOCK_TIME_THRESHOLD]
69/// for any accepted height plus up to `4 * MAX_BLOCK_DELTA` of additional
70/// blocks to still produce a valid absolute locktime height.
71pub const MAX_BLOCK_HEIGHT: BlockHeight =
72	bitcoin::absolute::LOCK_TIME_THRESHOLD - 1 - 4 * MAX_BLOCK_DELTA as BlockHeight;
73
74const _: () = {
75	// Up to four policy deltas fit in BlockDelta (u16).
76	assert!(4 * (MAX_BLOCK_DELTA as u32) <= u16::MAX as u32);
77	// Any accepted height plus up to 4 deltas stays below LOCK_TIME_THRESHOLD.
78	assert!((MAX_BLOCK_HEIGHT as u64) + 4 * (MAX_BLOCK_DELTA as u64)
79		< (bitcoin::absolute::LOCK_TIME_THRESHOLD as u64));
80};
81
82/// Boundary check for a block delta arriving from an untrusted source (protocol
83/// decode, gRPC, JSON, DB).
84pub fn check_block_delta<T: TryInto<BlockDelta>>(v: T) -> Result<BlockDelta, PolicyError> {
85	let v: BlockDelta = v.try_into()
86		.map_err(|_| PolicyError::new("block delta out of u16 range"))?;
87	if v > MAX_BLOCK_DELTA {
88		Err(PolicyError::new("block delta exceeds maximum value"))
89	} else {
90		Ok(v)
91	}
92}
93
94/// Boundary check for a block height arriving from an untrusted source.
95pub fn check_block_height<T: TryInto<BlockHeight>>(v: T) -> Result<BlockHeight, PolicyError> {
96	let v: BlockHeight = v.try_into()
97		.map_err(|_| PolicyError::new("block height out of u32 range"))?;
98	if v > MAX_BLOCK_HEIGHT {
99		Err(PolicyError::new("block height exceeds maximum value"))
100	} else {
101		Ok(v)
102	}
103}
104
105/// Trait for policy types that can be used in a Vtxo.
106pub trait Policy: Clone + Send + Sync + 'static {
107	fn policy_type(&self) -> VtxoPolicyKind;
108
109	fn taproot(
110		&self,
111		server_pubkey: PublicKey,
112		exit_delta: BlockDelta,
113		expiry_height: BlockHeight,
114	) -> taproot::TaprootSpendInfo;
115
116	fn script_pubkey(
117		&self,
118		server_pubkey: PublicKey,
119		exit_delta: BlockDelta,
120		expiry_height: BlockHeight,
121	) -> ScriptBuf {
122		Policy::taproot(self, server_pubkey, exit_delta, expiry_height).script_pubkey()
123	}
124
125	fn txout(
126		&self,
127		amount: Amount,
128		server_pubkey: PublicKey,
129		exit_delta: BlockDelta,
130		expiry_height: BlockHeight,
131	) -> TxOut {
132		TxOut {
133			script_pubkey: Policy::script_pubkey(self, server_pubkey, exit_delta, expiry_height),
134			value: amount,
135		}
136	}
137
138	fn clauses(
139		&self,
140		exit_delta: u16,
141		expiry_height: BlockHeight,
142		server_pubkey: PublicKey,
143	) -> Vec<VtxoClause>;
144}
145
146/// Type enum of [VtxoPolicy].
147#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
148pub enum VtxoPolicyKind {
149	/// Standard VTXO output protected with a public key.
150	Pubkey,
151	/// A VTXO that represents an HTLC with the Ark server to send money.
152	ServerHtlcSend,
153	/// A VTXO that represents an HTLC with the Ark server to receive money.
154	ServerHtlcRecv,
155	/// Simple VTXO owned by the server key
156	ServerOwned,
157	/// A public policy that grants bitcoin back to the server after expiry
158	/// It is used to construct checkpoint transactions
159	Checkpoint,
160	/// Server-only policy where coins can only be swept by the server after expiry.
161	Expiry,
162	/// hArk leaf output policy (intermediate outputs spent by leaf txs).
163	HarkLeaf,
164	/// hArk forfeit tx output policy
165	HarkForfeit,
166}
167
168impl fmt::Display for VtxoPolicyKind {
169	fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
170		match self {
171			Self::Pubkey => f.write_str("pubkey"),
172			Self::ServerHtlcSend => f.write_str("server-htlc-send"),
173			Self::ServerHtlcRecv => f.write_str("server-htlc-receive"),
174			Self::ServerOwned => f.write_str("server-owned"),
175			Self::Checkpoint => f.write_str("checkpoint"),
176			Self::Expiry => f.write_str("expiry"),
177			Self::HarkLeaf => f.write_str("hark-leaf"),
178			Self::HarkForfeit => f.write_str("hark-forfeit"),
179		}
180	}
181}
182
183impl FromStr for VtxoPolicyKind {
184	type Err = String;
185	fn from_str(s: &str) -> Result<Self, Self::Err> {
186		Ok(match s {
187			"pubkey" => Self::Pubkey,
188			"server-htlc-send" => Self::ServerHtlcSend,
189			"server-htlc-receive" => Self::ServerHtlcRecv,
190			"server-owned" => Self::ServerOwned,
191			"checkpoint" => Self::Checkpoint,
192			"expiry" => Self::Expiry,
193			"hark-leaf" => Self::HarkLeaf,
194			"hark-forfeit" => Self::HarkForfeit,
195			_ => return Err(format!("unknown VtxoPolicyKind: {}", s)),
196		})
197	}
198}
199
200impl serde::Serialize for VtxoPolicyKind {
201	fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
202		s.collect_str(self)
203	}
204}
205
206impl<'de> serde::Deserialize<'de> for VtxoPolicyKind {
207	fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
208		struct Visitor;
209		impl<'de> serde::de::Visitor<'de> for Visitor {
210			type Value = VtxoPolicyKind;
211			fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
212				write!(f, "a VtxoPolicyKind")
213			}
214			fn visit_str<E: serde::de::Error>(self, v: &str) -> Result<Self::Value, E> {
215				VtxoPolicyKind::from_str(v).map_err(serde::de::Error::custom)
216			}
217		}
218		d.deserialize_str(Visitor)
219	}
220}
221
222/// Policy enabling VTXO protected with a public key.
223///
224/// This will build a taproot with 2 spending paths:
225/// 1. The keyspend path allows Alice and Server to collaborate to spend
226/// the VTXO.
227///
228/// 2. The script-spend path allows Alice to unilaterally spend the VTXO
229/// after a delay.
230#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
231pub struct PubkeyVtxoPolicy {
232	pub user_pubkey: PublicKey,
233}
234
235impl From<PubkeyVtxoPolicy> for VtxoPolicy {
236	fn from(policy: PubkeyVtxoPolicy) -> Self {
237		Self::Pubkey(policy)
238	}
239}
240
241impl PubkeyVtxoPolicy {
242	/// Allows Alice to spend the VTXO after a delay.
243	pub fn user_pubkey_claim_clause(&self, exit_delta: BlockDelta) -> DelayedSignClause {
244		DelayedSignClause { pubkey: self.user_pubkey, block_delta: exit_delta }
245	}
246
247	pub fn clauses(&self, exit_delta: BlockDelta) -> Vec<VtxoClause> {
248		vec![self.user_pubkey_claim_clause(exit_delta).into()]
249	}
250
251	pub fn taproot(
252		&self,
253		server_pubkey: PublicKey,
254		exit_delta: BlockDelta,
255	) -> taproot::TaprootSpendInfo {
256		let combined_pk = musig::combine_keys([self.user_pubkey, server_pubkey])
257			.x_only_public_key().0;
258
259		let user_pubkey_claim_clause = self.user_pubkey_claim_clause(exit_delta);
260		taproot::TaprootBuilder::new()
261			.add_leaf(0, user_pubkey_claim_clause.tapscript()).unwrap()
262			.finalize(&SECP, combined_pk).unwrap()
263	}
264}
265
266/// Policy enabling server checkpoints
267///
268/// This will build a taproot with 2 clauses:
269/// 1. The keyspend path allows Alice and Server to collaborate to spend
270/// the checkpoint.
271///
272/// 2. The script-spend path allows Server to spend the checkpoint after
273/// the expiry height.
274#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
275pub struct CheckpointVtxoPolicy {
276	pub user_pubkey: PublicKey,
277}
278
279impl From<CheckpointVtxoPolicy> for ServerVtxoPolicy {
280	fn from(policy: CheckpointVtxoPolicy) -> Self {
281		Self::Checkpoint(policy)
282	}
283}
284
285impl CheckpointVtxoPolicy {
286	/// Allows Server to spend the checkpoint after expiry height.
287	pub fn server_sweeping_clause(
288		&self,
289		expiry_height: BlockHeight,
290		server_pubkey: PublicKey,
291	) -> TimelockSignClause {
292		TimelockSignClause { pubkey: server_pubkey, timelock_height: expiry_height }
293	}
294
295	pub fn clauses(
296		&self,
297		expiry_height: BlockHeight,
298		server_pubkey: PublicKey,
299	) -> Vec<VtxoClause> {
300		vec![self.server_sweeping_clause(expiry_height, server_pubkey).into()]
301	}
302
303	pub fn taproot(
304		&self,
305		server_pubkey: PublicKey,
306		expiry_height: BlockHeight,
307	) -> taproot::TaprootSpendInfo {
308		let combined_pk = musig::combine_keys([self.user_pubkey, server_pubkey])
309			.x_only_public_key().0;
310		let server_sweeping_clause = self.server_sweeping_clause(expiry_height, server_pubkey);
311
312		taproot::TaprootBuilder::new()
313			.add_leaf(0, server_sweeping_clause.tapscript()).unwrap()
314			.finalize(&SECP, combined_pk).unwrap()
315	}
316}
317
318/// Server-only policy where coins can only be swept by the server after expiry.
319#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
320pub struct ExpiryVtxoPolicy {
321	pub internal_key: bitcoin::secp256k1::XOnlyPublicKey,
322}
323
324impl ExpiryVtxoPolicy {
325	/// Creates a new expiry policy with the given internal key.
326	pub fn new(internal_key: bitcoin::secp256k1::XOnlyPublicKey) -> Self {
327		Self { internal_key }
328	}
329
330	/// Allows Server to spend after expiry height.
331	pub fn server_sweeping_clause(
332		&self,
333		expiry_height: BlockHeight,
334		server_pubkey: PublicKey,
335	) -> TimelockSignClause {
336		TimelockSignClause { pubkey: server_pubkey, timelock_height: expiry_height }
337	}
338
339	pub fn clauses(
340		&self,
341		expiry_height: BlockHeight,
342		server_pubkey: PublicKey,
343	) -> Vec<VtxoClause> {
344		vec![self.server_sweeping_clause(expiry_height, server_pubkey).into()]
345	}
346
347	pub fn taproot(
348		&self,
349		server_pubkey: PublicKey,
350		expiry_height: BlockHeight,
351	) -> taproot::TaprootSpendInfo {
352		let server_sweeping_clause = self.server_sweeping_clause(expiry_height, server_pubkey);
353
354		taproot::TaprootBuilder::new()
355			.add_leaf(0, server_sweeping_clause.tapscript()).unwrap()
356			.finalize(&SECP, self.internal_key).unwrap()
357	}
358}
359
360/// Policy for hArk leaf outputs (intermediate outputs spent by leaf txs).
361///
362/// These are the outputs that feed into the final leaf transactions in a signed
363/// VTXO tree. They are locked by:
364/// 1. An expiry clause allowing the server to sweep after expiry
365/// 2. An unlock clause requiring a preimage and a signature from user+server
366///
367/// The internal key is set to the MuSig of user's VTXO key + server pubkey.
368#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
369pub struct HarkLeafVtxoPolicy {
370	pub user_pubkey: PublicKey,
371	pub unlock_hash: UnlockHash,
372}
373
374impl HarkLeafVtxoPolicy {
375	/// Creates the expiry clause allowing the server to sweep after expiry.
376	pub fn expiry_clause(
377		&self,
378		expiry_height: BlockHeight,
379		server_pubkey: PublicKey,
380	) -> TimelockSignClause {
381		TimelockSignClause { pubkey: server_pubkey, timelock_height: expiry_height }
382	}
383
384	/// Creates the unlock clause requiring a preimage and aggregate signature.
385	pub fn unlock_clause(&self, server_pubkey: PublicKey) -> HashSignClause {
386		let agg_pk = musig::combine_keys([self.user_pubkey, server_pubkey]);
387		HashSignClause { pubkey: agg_pk, hash: self.unlock_hash }
388	}
389
390	/// Returns the clauses for this policy.
391	pub fn clauses(
392		&self,
393		expiry_height: BlockHeight,
394		server_pubkey: PublicKey,
395	) -> Vec<VtxoClause> {
396		vec![
397			self.expiry_clause(expiry_height, server_pubkey).into(),
398			self.unlock_clause(server_pubkey).into(),
399		]
400	}
401
402	/// Build the taproot spend info for this policy.
403	pub fn taproot(
404		&self,
405		server_pubkey: PublicKey,
406		expiry_height: BlockHeight,
407	) -> taproot::TaprootSpendInfo {
408		let agg_pk = musig::combine_keys([self.user_pubkey, server_pubkey]);
409		let expiry_clause = self.expiry_clause(expiry_height, server_pubkey);
410		let unlock_clause = self.unlock_clause(server_pubkey);
411
412		taproot::TaprootBuilder::new()
413			.add_leaf(1, expiry_clause.tapscript()).unwrap()
414			.add_leaf(1, unlock_clause.tapscript()).unwrap()
415			.finalize(&SECP, agg_pk.x_only_public_key().0).unwrap()
416	}
417}
418
419/// Policy enabling outgoing Lightning payments.
420///
421/// This will build a taproot with 3 clauses:
422/// 1. The keyspend path allows Alice and Server to collaborate to spend
423/// the HTLC. The Server can use this path to revoke the HTLC if payment
424/// failed
425///
426/// 2. The script-spend path contains one leaf that allows Server to spend
427/// the HTLC after the expiry, if it knows the preimage. Server can use
428/// this path if Alice tries to spend using her clause.
429///
430/// 3. The second leaf allows Alice to spend the HTLC after its expiry
431/// and with a delay. Alice must use this path if the server fails to
432/// provide the preimage and refuse to revoke the HTLC. It will either
433/// force the Server to reveal the preimage (by spending using her clause)
434/// or give Alice her money back.
435#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
436pub struct ServerHtlcSendVtxoPolicy {
437	pub user_pubkey: PublicKey,
438	pub payment_hash: PaymentHash,
439	pub htlc_expiry: BlockHeight,
440}
441
442impl From<ServerHtlcSendVtxoPolicy> for VtxoPolicy {
443	fn from(policy: ServerHtlcSendVtxoPolicy) -> Self {
444		Self::ServerHtlcSend(policy)
445	}
446}
447
448impl ServerHtlcSendVtxoPolicy {
449	/// Allows Server to spend the HTLC after the delta, if it knows the
450	/// preimage. Server can use this path if Alice tries to spend using her
451	/// clause.
452	pub fn server_reveals_preimage_clause(
453		&self,
454		server_pubkey: PublicKey,
455		exit_delta: BlockDelta,
456	) -> HashDelaySignClause {
457		HashDelaySignClause {
458			pubkey: server_pubkey,
459			hash: self.payment_hash.to_sha256_hash(),
460			block_delta: exit_delta
461		}
462	}
463
464	/// Allows Alice to spend the HTLC after its expiry and with a delay.
465	/// Alice must use this path if the server fails to provide the preimage
466	/// and refuse to revoke the HTLC. It will either force the server to
467	/// reveal the preimage (by spending using its clause) or give Alice her
468	/// money back.
469	pub fn user_claim_after_expiry_clause(
470		&self,
471		exit_delta: BlockDelta,
472	) -> DelayedTimelockSignClause {
473		DelayedTimelockSignClause {
474			pubkey: self.user_pubkey,
475			timelock_height: self.htlc_expiry,
476			block_delta: exit_delta.checked_mul(2)
477				.expect("2*exit_delta fits in BlockDelta by MAX_BLOCK_DELTA invariant"),
478		}
479	}
480
481
482	pub fn clauses(&self, exit_delta: BlockDelta, server_pubkey: PublicKey) -> Vec<VtxoClause> {
483		vec![
484			self.server_reveals_preimage_clause(server_pubkey, exit_delta).into(),
485			self.user_claim_after_expiry_clause(exit_delta).into(),
486		]
487	}
488
489	pub fn taproot(&self, server_pubkey: PublicKey, exit_delta: BlockDelta) -> taproot::TaprootSpendInfo {
490		let server_reveals_preimage_clause = self.server_reveals_preimage_clause(server_pubkey, exit_delta);
491		let user_claim_after_expiry_clause = self.user_claim_after_expiry_clause(exit_delta);
492
493		let combined_pk = musig::combine_keys([self.user_pubkey, server_pubkey])
494			.x_only_public_key().0;
495		bitcoin::taproot::TaprootBuilder::new()
496			.add_leaf(1, server_reveals_preimage_clause.tapscript()).unwrap()
497			.add_leaf(1, user_claim_after_expiry_clause.tapscript()).unwrap()
498			.finalize(&SECP, combined_pk).unwrap()
499	}
500}
501
502
503/// Policy enabling incoming Lightning payments.
504///
505/// This will build a taproot with 3 clauses:
506/// 1. The keyspend path allows Alice and Server to collaborate to spend
507/// the HTLC. This is the expected path to be used. Server should only
508/// accept to collaborate if Alice reveals the preimage.
509///
510/// 2. The script-spend path contains one leaf that allows Server to spend
511/// the HTLC after the expiry, with an exit delta delay. Server can use
512/// this path if Alice tries to spend the HTLC using the 3rd path after
513/// the HTLC expiry
514///
515/// 3. The second leaf allows Alice to spend the HTLC if she knows the
516/// preimage, but with a greater exit delta delay than server's clause.
517/// Alice must use this path if she revealed the preimage but Server
518/// refused to collaborate.
519#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
520pub struct ServerHtlcRecvVtxoPolicy {
521	pub user_pubkey: PublicKey,
522	pub payment_hash: PaymentHash,
523	pub htlc_expiry_delta: BlockDelta,
524	pub htlc_expiry: BlockHeight,
525}
526
527impl ServerHtlcRecvVtxoPolicy {
528	/// Allows Alice to spend the HTLC if she knows the preimage, but with a
529	/// greater exit delta delay than server's clause. Alice must use this
530	/// path if she revealed the preimage but server refused to cosign
531	/// claim VTXO.
532	pub fn user_reveals_preimage_clause(&self, exit_delta: BlockDelta) -> HashDelaySignClause {
533		HashDelaySignClause {
534			pubkey: self.user_pubkey,
535			hash: self.payment_hash.to_sha256_hash(),
536			block_delta: self.htlc_expiry_delta.checked_add(exit_delta)
537				.expect("htlc_expiry_delta+exit_delta fits in BlockDelta by MAX_BLOCK_DELTA invariant"),
538		}
539	}
540
541	/// Allows Server to spend the HTLC after the HTLC expiry, with an exit
542	/// delta delay. Server can use this path if Alice tries to spend the
543	/// HTLC using her clause after the HTLC expiry.
544	pub fn server_claim_after_expiry_clause(
545		&self,
546		server_pubkey: PublicKey,
547		exit_delta: BlockDelta,
548	) -> DelayedTimelockSignClause {
549		DelayedTimelockSignClause {
550			pubkey: server_pubkey,
551			timelock_height: self.htlc_expiry,
552			block_delta: exit_delta
553		}
554	}
555
556	pub fn clauses(&self, exit_delta: BlockDelta, server_pubkey: PublicKey) -> Vec<VtxoClause> {
557		vec![
558			self.user_reveals_preimage_clause(exit_delta).into(),
559			self.server_claim_after_expiry_clause(server_pubkey, exit_delta).into(),
560		]
561	}
562
563	pub fn taproot(&self, server_pubkey: PublicKey, exit_delta: BlockDelta) -> taproot::TaprootSpendInfo {
564		let server_claim_after_expiry_clause = self.server_claim_after_expiry_clause(server_pubkey, exit_delta);
565		let user_reveals_preimage_clause = self.user_reveals_preimage_clause(exit_delta);
566
567		let combined_pk = musig::combine_keys([self.user_pubkey, server_pubkey])
568			.x_only_public_key().0;
569		bitcoin::taproot::TaprootBuilder::new()
570			.add_leaf(1, server_claim_after_expiry_clause.tapscript()).unwrap()
571			.add_leaf(1, user_reveals_preimage_clause.tapscript()).unwrap()
572			.finalize(&SECP, combined_pk).unwrap()
573	}
574}
575
576impl From<ServerHtlcRecvVtxoPolicy> for VtxoPolicy {
577	fn from(policy: ServerHtlcRecvVtxoPolicy) -> Self {
578		Self::ServerHtlcRecv(policy)
579	}
580}
581
582/// The server-only VTXO policy on hArk forfeit txs
583///
584/// This policy allows the server to claim the forfeited coins by revealing
585/// the hArk unlock preimage or allow the user to recover its money in case
586/// the server doesn't.
587#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
588pub struct HarkForfeitVtxoPolicy {
589	pub user_pubkey: PublicKey,
590	pub unlock_hash: UnlockHash,
591}
592
593impl HarkForfeitVtxoPolicy {
594	/// Server claims the forfeit revealing the unlock preimage
595	pub fn server_claim_clause(
596		&self,
597		server_pubkey: PublicKey,
598	) -> HashSignClause {
599		HashSignClause {
600			pubkey: server_pubkey,
601			hash: self.unlock_hash,
602		}
603	}
604
605	/// If the server doesn't reveal the preimage, the user can claim the funds
606	pub fn user_exit_clause(
607		&self,
608		exit_delta: BlockDelta,
609	) -> DelayedSignClause {
610		DelayedSignClause {
611			pubkey: self.user_pubkey,
612			block_delta: exit_delta
613		}
614	}
615
616	pub fn clauses(&self, exit_delta: BlockDelta, server_pubkey: PublicKey) -> Vec<VtxoClause> {
617		vec![
618			self.server_claim_clause(server_pubkey).into(),
619			self.user_exit_clause(exit_delta).into(),
620		]
621	}
622
623	pub fn taproot(
624		&self,
625		server_pubkey: PublicKey,
626		exit_delta: BlockDelta,
627	) -> taproot::TaprootSpendInfo {
628		let server_claim_clause = self.server_claim_clause(server_pubkey);
629		let user_exit_clause = self.user_exit_clause(exit_delta);
630
631		let combined_pk = musig::combine_keys([self.user_pubkey, server_pubkey])
632			.x_only_public_key().0;
633		bitcoin::taproot::TaprootBuilder::new()
634			.add_leaf(1, server_claim_clause.tapscript()).unwrap()
635			.add_leaf(1, user_exit_clause.tapscript()).unwrap()
636			.finalize(&SECP, combined_pk).unwrap()
637	}
638}
639
640impl From<HarkForfeitVtxoPolicy> for ServerVtxoPolicy {
641	fn from(v: HarkForfeitVtxoPolicy) -> Self {
642	    ServerVtxoPolicy::HarkForfeit(v)
643	}
644}
645
646/// User-facing VTXO output policy.
647///
648/// All variants have an associated user public key, accessible via the infallible
649/// `user_pubkey()` method. These policies are used in protocol messages and by clients.
650#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
651pub enum VtxoPolicy {
652	/// Standard VTXO output protected with a public key.
653	///
654	/// This can be the result of either:
655	/// - a board
656	/// - a round
657	/// - an arkoor tx
658	/// - change from a LN payment
659	Pubkey(PubkeyVtxoPolicy),
660	/// A VTXO that represents an HTLC with the Ark server to send money.
661	ServerHtlcSend(ServerHtlcSendVtxoPolicy),
662	/// A VTXO that represents an HTLC with the Ark server to receive money.
663	ServerHtlcRecv(ServerHtlcRecvVtxoPolicy),
664}
665
666impl VtxoPolicy {
667	pub fn new_pubkey(user_pubkey: PublicKey) -> Self {
668		Self::Pubkey(PubkeyVtxoPolicy { user_pubkey })
669	}
670
671	pub fn new_server_htlc_send(
672		user_pubkey: PublicKey,
673		payment_hash: PaymentHash,
674		htlc_expiry: BlockHeight,
675	) -> Self {
676		Self::ServerHtlcSend(ServerHtlcSendVtxoPolicy { user_pubkey, payment_hash, htlc_expiry })
677	}
678
679	/// Creates a new htlc from server to client
680	/// - user_pubkey: A public key owned by the client
681	/// - payment_hash: The payment hash, the client can claim the HTLC
682	/// by revealing the corresponding pre-image
683	/// - htlc_expiry: An absolute blockheight at which the HTLC expires
684	/// - htlc_expiry_delta: A safety margin for the server. If the user
685	/// tries to exit after time-out the server will have at-least
686	/// `htlc_expiry_delta` blocks to claim the payment
687	pub fn new_server_htlc_recv(
688		user_pubkey: PublicKey,
689		payment_hash: PaymentHash,
690		htlc_expiry: BlockHeight,
691		htlc_expiry_delta: BlockDelta,
692	) -> Self {
693		Self::ServerHtlcRecv(ServerHtlcRecvVtxoPolicy {
694			user_pubkey, payment_hash, htlc_expiry, htlc_expiry_delta,
695		})
696	}
697
698	pub fn as_pubkey(&self) -> Option<&PubkeyVtxoPolicy> {
699		match self {
700			Self::Pubkey(v) => Some(v),
701			_ => None,
702		}
703	}
704
705	pub fn as_server_htlc_send(&self) -> Option<&ServerHtlcSendVtxoPolicy> {
706		match self {
707			Self::ServerHtlcSend(v) => Some(v),
708			_ => None,
709		}
710	}
711
712	pub fn as_server_htlc_recv(&self) -> Option<&ServerHtlcRecvVtxoPolicy> {
713		match self {
714			Self::ServerHtlcRecv(v) => Some(v),
715			_ => None,
716		}
717	}
718
719	/// The policy type id.
720	pub fn policy_type(&self) -> VtxoPolicyKind {
721		match self {
722			Self::Pubkey { .. } => VtxoPolicyKind::Pubkey,
723			Self::ServerHtlcSend { .. } => VtxoPolicyKind::ServerHtlcSend,
724			Self::ServerHtlcRecv { .. } => VtxoPolicyKind::ServerHtlcRecv,
725		}
726	}
727
728	/// Whether a [Vtxo](crate::Vtxo) with this output can be spent in an arkoor tx.
729	pub fn is_arkoor_compatible(&self) -> bool {
730		match self {
731			Self::Pubkey { .. } => true,
732			Self::ServerHtlcSend { .. } => false,
733			Self::ServerHtlcRecv { .. } => false,
734		}
735	}
736
737	/// The public key used to cosign arkoor txs spending a [Vtxo](crate::Vtxo)
738	/// with this output.
739	/// Returns [None] for HTLC policies.
740	pub fn arkoor_pubkey(&self) -> Option<PublicKey> {
741		match self {
742			Self::Pubkey(PubkeyVtxoPolicy { user_pubkey }) => Some(*user_pubkey),
743			Self::ServerHtlcSend { .. } => None,
744			Self::ServerHtlcRecv { .. } => None,
745		}
746	}
747
748	/// Returns the user pubkey associated with this policy.
749	pub fn user_pubkey(&self) -> PublicKey {
750		match self {
751			Self::Pubkey(PubkeyVtxoPolicy { user_pubkey }) => *user_pubkey,
752			Self::ServerHtlcSend(ServerHtlcSendVtxoPolicy { user_pubkey, .. }) => *user_pubkey,
753			Self::ServerHtlcRecv(ServerHtlcRecvVtxoPolicy { user_pubkey, .. }) => *user_pubkey,
754		}
755	}
756
757	pub fn taproot(
758		&self,
759		server_pubkey: PublicKey,
760		exit_delta: BlockDelta,
761		expiry_height: BlockHeight,
762	) -> taproot::TaprootSpendInfo {
763		let _ = expiry_height; // not used by user-facing policies
764		match self {
765			Self::Pubkey(policy) => policy.taproot(server_pubkey, exit_delta),
766			Self::ServerHtlcSend(policy) => policy.taproot(server_pubkey, exit_delta),
767			Self::ServerHtlcRecv(policy) => policy.taproot(server_pubkey, exit_delta),
768		}
769	}
770
771	pub fn script_pubkey(
772		&self,
773		server_pubkey: PublicKey,
774		exit_delta: BlockDelta,
775		expiry_height: BlockHeight,
776	) -> ScriptBuf {
777		self.taproot(server_pubkey, exit_delta, expiry_height).script_pubkey()
778	}
779
780	pub(crate) fn txout(
781		&self,
782		amount: Amount,
783		server_pubkey: PublicKey,
784		exit_delta: BlockDelta,
785		expiry_height: BlockHeight,
786	) -> TxOut {
787		TxOut {
788			value: amount,
789			script_pubkey: self.script_pubkey(server_pubkey, exit_delta, expiry_height),
790		}
791	}
792
793	pub fn clauses(
794		&self,
795		exit_delta: u16,
796		_expiry_height: BlockHeight,
797		server_pubkey: PublicKey,
798	) -> Vec<VtxoClause> {
799		match self {
800			Self::Pubkey(policy) => policy.clauses(exit_delta),
801			Self::ServerHtlcSend(policy) => policy.clauses(exit_delta, server_pubkey),
802			Self::ServerHtlcRecv(policy) => policy.clauses(exit_delta, server_pubkey),
803		}
804	}
805}
806
807/// Server-internal VTXO policy.
808///
809/// This is a superset of [VtxoPolicy] used by the server for internal tracking.
810/// Includes policies without user public keys.
811#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
812pub enum ServerVtxoPolicy {
813	/// Wraps any user-facing policy.
814	User(VtxoPolicy),
815	/// Simple output owned only by the server key
816	ServerOwned,
817	/// A policy which returns all coins to the server after expiry.
818	Checkpoint(CheckpointVtxoPolicy),
819	/// Server-only policy where coins can only be swept by the server after expiry.
820	Expiry(ExpiryVtxoPolicy),
821	/// hArk leaf output policy (intermediate outputs spent by leaf txs).
822	HarkLeaf(HarkLeafVtxoPolicy),
823	/// hArk forfeit tx output policy
824	HarkForfeit(HarkForfeitVtxoPolicy),
825}
826
827impl From<VtxoPolicy> for ServerVtxoPolicy {
828	fn from(p: VtxoPolicy) -> Self {
829		Self::User(p)
830	}
831}
832
833impl From<HarkLeafVtxoPolicy> for ServerVtxoPolicy {
834	fn from(p: HarkLeafVtxoPolicy) -> Self {
835		Self::HarkLeaf(p)
836	}
837}
838
839impl ServerVtxoPolicy {
840	pub fn new_server_owned() -> Self {
841		Self::ServerOwned
842	}
843
844	pub fn new_checkpoint(user_pubkey: PublicKey) -> Self {
845		Self::Checkpoint(CheckpointVtxoPolicy { user_pubkey })
846	}
847
848	pub fn new_expiry(internal_key: bitcoin::secp256k1::XOnlyPublicKey) -> Self {
849		Self::Expiry(ExpiryVtxoPolicy { internal_key })
850	}
851
852	pub fn new_hark_leaf(user_pubkey: PublicKey, unlock_hash: UnlockHash) -> Self {
853		Self::HarkLeaf(HarkLeafVtxoPolicy { user_pubkey, unlock_hash })
854	}
855
856	pub fn new_hark_forfeit(user_pubkey: PublicKey, unlock_hash: UnlockHash) -> Self {
857		Self::HarkForfeit(HarkForfeitVtxoPolicy { user_pubkey, unlock_hash })
858	}
859
860	/// The policy type id.
861	pub fn policy_type(&self) -> VtxoPolicyKind {
862		match self {
863			Self::User(p) => p.policy_type(),
864			Self::ServerOwned => VtxoPolicyKind::ServerOwned,
865			Self::Checkpoint { .. } => VtxoPolicyKind::Checkpoint,
866			Self::Expiry { .. } => VtxoPolicyKind::Expiry,
867			Self::HarkLeaf { .. } => VtxoPolicyKind::HarkLeaf,
868			Self::HarkForfeit { .. } => VtxoPolicyKind::HarkForfeit,
869		}
870	}
871
872	/// Whether a [Vtxo](crate::Vtxo) with this output can be spent in an arkoor tx.
873	pub fn is_arkoor_compatible(&self) -> bool {
874		match self {
875			Self::User(p) => p.is_arkoor_compatible(),
876			Self::ServerOwned => false,
877			Self::Checkpoint { .. } => true,
878			Self::Expiry { .. } => false,
879			Self::HarkLeaf { .. } => false,
880			Self::HarkForfeit { .. } => false,
881		}
882	}
883
884	/// Returns the user pubkey if this policy has one.
885	pub fn user_pubkey(&self) -> Option<PublicKey> {
886		match self {
887			Self::User(p) => Some(p.user_pubkey()),
888			Self::ServerOwned => None,
889			Self::Checkpoint(CheckpointVtxoPolicy { user_pubkey }) => Some(*user_pubkey),
890			Self::Expiry { .. } => None,
891			Self::HarkLeaf(HarkLeafVtxoPolicy { user_pubkey, .. }) => Some(*user_pubkey),
892			Self::HarkForfeit(HarkForfeitVtxoPolicy { user_pubkey, .. }) => Some(*user_pubkey),
893		}
894	}
895
896	pub fn taproot(
897		&self,
898		server_pubkey: PublicKey,
899		exit_delta: BlockDelta,
900		expiry_height: BlockHeight,
901	) -> taproot::TaprootSpendInfo {
902		match self {
903			Self::User(p) => p.taproot(server_pubkey, exit_delta, expiry_height),
904			Self::ServerOwned => {
905				taproot::TaprootBuilder::new()
906					.finalize(&SECP, server_pubkey.x_only_public_key().0).unwrap()
907			},
908			Self::Checkpoint(policy) => policy.taproot(server_pubkey, expiry_height),
909			Self::Expiry(policy) => policy.taproot(server_pubkey, expiry_height),
910			Self::HarkLeaf(policy) => policy.taproot(server_pubkey, expiry_height),
911			Self::HarkForfeit(policy) => policy.taproot(server_pubkey, exit_delta),
912		}
913	}
914
915	pub fn script_pubkey(
916		&self,
917		server_pubkey: PublicKey,
918		exit_delta: BlockDelta,
919		expiry_height: BlockHeight,
920	) -> ScriptBuf {
921		self.taproot(server_pubkey, exit_delta, expiry_height).script_pubkey()
922	}
923
924	pub fn clauses(
925		&self,
926		exit_delta: u16,
927		expiry_height: BlockHeight,
928		server_pubkey: PublicKey,
929	) -> Vec<VtxoClause> {
930		match self {
931			Self::User(p) => p.clauses(exit_delta, expiry_height, server_pubkey),
932			Self::ServerOwned => vec![], // only keyspend
933			Self::Checkpoint(policy) => policy.clauses(expiry_height, server_pubkey),
934			Self::Expiry(policy) => policy.clauses(expiry_height, server_pubkey),
935			Self::HarkLeaf(policy) => policy.clauses(expiry_height, server_pubkey),
936			Self::HarkForfeit(policy) => policy.clauses(exit_delta, server_pubkey),
937		}
938	}
939
940	/// Check whether this is a user policy
941	pub fn is_user_policy(&self) -> bool {
942		matches!(self, ServerVtxoPolicy::User(_))
943	}
944
945	/// Try to convert to a user policy if it is one
946	pub fn into_user_policy(self) -> Option<VtxoPolicy> {
947		match self {
948			ServerVtxoPolicy::User(p) => Some(p),
949			_ => None,
950		}
951	}
952}
953
954impl Policy for VtxoPolicy {
955	fn policy_type(&self) -> VtxoPolicyKind {
956		VtxoPolicy::policy_type(self)
957	}
958
959	fn taproot(
960		&self,
961		server_pubkey: PublicKey,
962		exit_delta: BlockDelta,
963		expiry_height: BlockHeight,
964	) -> taproot::TaprootSpendInfo {
965		VtxoPolicy::taproot(self, server_pubkey, exit_delta, expiry_height)
966	}
967
968	fn clauses(
969		&self,
970		exit_delta: u16,
971		expiry_height: BlockHeight,
972		server_pubkey: PublicKey,
973	) -> Vec<VtxoClause> {
974		VtxoPolicy::clauses(self, exit_delta, expiry_height, server_pubkey)
975	}
976}
977
978impl Policy for ServerVtxoPolicy {
979	fn policy_type(&self) -> VtxoPolicyKind {
980		ServerVtxoPolicy::policy_type(self)
981	}
982
983	fn taproot(
984		&self,
985		server_pubkey: PublicKey,
986		exit_delta: BlockDelta,
987		expiry_height: BlockHeight,
988	) -> taproot::TaprootSpendInfo {
989		ServerVtxoPolicy::taproot(self, server_pubkey, exit_delta, expiry_height)
990	}
991
992	fn clauses(
993		&self,
994		exit_delta: u16,
995		expiry_height: BlockHeight,
996		server_pubkey: PublicKey,
997	) -> Vec<VtxoClause> {
998		ServerVtxoPolicy::clauses(self, exit_delta, expiry_height, server_pubkey)
999	}
1000}
1001
1002#[cfg(test)]
1003mod tests {
1004	use std::str::FromStr;
1005
1006	use bitcoin::hashes::{sha256, Hash};
1007	use bitcoin::key::Keypair;
1008	use bitcoin::sighash::{self, SighashCache};
1009	use bitcoin::{Amount, OutPoint, ScriptBuf, Sequence, TxIn, TxOut, Txid, Witness};
1010	use bitcoin::taproot::{self, TapLeafHash};
1011	use bitcoin_ext::{TaprootSpendInfoExt, fee};
1012
1013	use crate::{SECP, musig};
1014	use crate::test_util::verify_tx;
1015	use crate::vtxo::policy::clause::TapScriptClause;
1016
1017	use super::*;
1018
1019	lazy_static! {
1020		static ref USER_KEYPAIR: Keypair = Keypair::from_str("5255d132d6ec7d4fc2a41c8f0018bb14343489ddd0344025cc60c7aa2b3fda6a").unwrap();
1021		static ref SERVER_KEYPAIR: Keypair = Keypair::from_str("1fb316e653eec61de11c6b794636d230379509389215df1ceb520b65313e5426").unwrap();
1022	}
1023
1024	fn transaction() -> bitcoin::Transaction {
1025		let address = bitcoin::Address::from_str("tb1q00h5delzqxl7xae8ufmsegghcl4jwfvdnd8530")
1026			.unwrap().assume_checked();
1027
1028		bitcoin::Transaction {
1029			version: bitcoin::transaction::Version(3),
1030			lock_time: bitcoin::absolute::LockTime::ZERO,
1031			input: vec![],
1032			output: vec![TxOut {
1033				script_pubkey: address.script_pubkey(),
1034				value: Amount::from_sat(900_000),
1035			}, fee::fee_anchor()]
1036		}
1037	}
1038
1039	#[test]
1040	fn test_hark_leaf_vtxo_policy_unlock_clause() {
1041		let preimage = [0u8; 32];
1042		let unlock_hash = sha256::Hash::hash(&preimage);
1043
1044		let policy = HarkLeafVtxoPolicy {
1045			user_pubkey: USER_KEYPAIR.public_key(),
1046			unlock_hash,
1047		};
1048
1049		let expiry_height = 100_000;
1050
1051		// Build the taproot spend info using the policy
1052		let taproot = policy.taproot(SERVER_KEYPAIR.public_key(), expiry_height);
1053		let unlock_clause = policy.unlock_clause(SERVER_KEYPAIR.public_key());
1054
1055		let tx_in = TxOut {
1056			script_pubkey: taproot.script_pubkey(),
1057			value: Amount::from_sat(1_000_000),
1058		};
1059
1060		// Build the spending transaction
1061		let mut tx = transaction();
1062		tx.input.push(TxIn {
1063			previous_output: OutPoint::new(Txid::all_zeros(), 0),
1064			script_sig: ScriptBuf::default(),
1065			sequence: Sequence::ZERO,
1066			witness: Witness::new(),
1067		});
1068
1069		// Get the control block for the unlock clause
1070		let cb = taproot
1071			.control_block(&(unlock_clause.tapscript(), taproot::LeafVersion::TapScript))
1072			.expect("script is in taproot");
1073
1074		// Compute sighash
1075		let leaf_hash = TapLeafHash::from_script(
1076			&unlock_clause.tapscript(),
1077			taproot::LeafVersion::TapScript,
1078		);
1079		let mut shc = SighashCache::new(&tx);
1080		let sighash = shc.taproot_script_spend_signature_hash(
1081			0, &sighash::Prevouts::All(&[tx_in.clone()]), leaf_hash, sighash::TapSighashType::Default,
1082		).expect("all prevouts provided");
1083
1084		// Create MuSig signature from user + server
1085		let (user_sec_nonce, user_pub_nonce) = musig::nonce_pair(&*USER_KEYPAIR);
1086		let (server_pub_nonce, server_part_sig) = musig::deterministic_partial_sign(
1087			&*SERVER_KEYPAIR,
1088			[USER_KEYPAIR.public_key()],
1089			&[&user_pub_nonce],
1090			sighash.to_byte_array(),
1091			None,
1092		);
1093		let agg_nonce = musig::nonce_agg(&[&user_pub_nonce, &server_pub_nonce]);
1094
1095		let (_user_part_sig, final_sig) = musig::partial_sign(
1096			[USER_KEYPAIR.public_key(), SERVER_KEYPAIR.public_key()],
1097			agg_nonce,
1098			&*USER_KEYPAIR,
1099			user_sec_nonce,
1100			sighash.to_byte_array(),
1101			None,
1102			Some(&[&server_part_sig]),
1103		);
1104		let final_sig = final_sig.expect("should have final signature");
1105
1106		tx.input[0].witness = unlock_clause.witness(&(final_sig, preimage), &cb);
1107
1108		// Verify the transaction
1109		verify_tx(&[tx_in], 0, &tx).expect("unlock clause spending should be valid");
1110	}
1111
1112	#[test]
1113	fn test_hark_leaf_vtxo_policy_expiry_clause() {
1114		let preimage = [0u8; 32];
1115		let unlock_hash = sha256::Hash::hash(&preimage);
1116
1117		let policy = HarkLeafVtxoPolicy {
1118			user_pubkey: USER_KEYPAIR.public_key(),
1119			unlock_hash,
1120		};
1121
1122		let expiry_height = 100;
1123
1124		// Build the taproot spend info using the policy
1125		let taproot = policy.taproot(SERVER_KEYPAIR.public_key(), expiry_height);
1126		let expiry_clause = policy.expiry_clause(expiry_height, SERVER_KEYPAIR.public_key());
1127
1128		let tx_in = TxOut {
1129			script_pubkey: taproot.script_pubkey(),
1130			value: Amount::from_sat(1_000_000),
1131		};
1132
1133		// Build the spending transaction with locktime
1134		let mut tx = transaction();
1135		tx.lock_time = expiry_clause.locktime();
1136		tx.input.push(TxIn {
1137			previous_output: OutPoint::new(Txid::all_zeros(), 0),
1138			script_sig: ScriptBuf::default(),
1139			sequence: Sequence::ZERO,
1140			witness: Witness::new(),
1141		});
1142
1143		// Get the control block for the expiry clause
1144		let cb = taproot
1145			.control_block(&(expiry_clause.tapscript(), taproot::LeafVersion::TapScript))
1146			.expect("script is in taproot");
1147
1148		// Compute sighash
1149		let leaf_hash = TapLeafHash::from_script(
1150			&expiry_clause.tapscript(),
1151			taproot::LeafVersion::TapScript,
1152		);
1153		let mut shc = SighashCache::new(&tx);
1154		let sighash = shc.taproot_script_spend_signature_hash(
1155			0, &sighash::Prevouts::All(&[tx_in.clone()]), leaf_hash, sighash::TapSighashType::Default,
1156		).expect("all prevouts provided");
1157
1158		// Server signs
1159		let signature = SECP.sign_schnorr(&sighash.into(), &*SERVER_KEYPAIR);
1160
1161		tx.input[0].witness = expiry_clause.witness(&signature, &cb);
1162
1163		// Verify the transaction
1164		verify_tx(&[tx_in], 0, &tx).expect("expiry clause spending should be valid");
1165	}
1166}