Skip to main content

ark/vtxo/policy/
mod.rs

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