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