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::vtxo::TapScriptClause;
16use crate::vtxo::policy::clause::{
17	DelayedSignClause, DelayedTimelockSignClause, HashDelaySignClause, TimelockSignClause,
18	VtxoClause,
19};
20
21/// Type enum of [VtxoPolicy].
22#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
23pub enum VtxoPolicyKind {
24	/// Standard VTXO output protected with a public key.
25	Pubkey,
26	/// A public policy that grants bitcoin back to the server after expiry
27	/// It is used to construct checkpoint transactions
28	Checkpoint,
29	/// A VTXO that represents an HTLC with the Ark server to send money.
30	ServerHtlcSend,
31	/// A VTXO that represents an HTLC with the Ark server to receive money.
32	ServerHtlcRecv,
33}
34
35impl fmt::Display for VtxoPolicyKind {
36	fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
37	    match self {
38			Self::Pubkey => f.write_str("pubkey"),
39			Self::Checkpoint => f.write_str("checkpoint"),
40			Self::ServerHtlcSend => f.write_str("server-htlc-send"),
41			Self::ServerHtlcRecv => f.write_str("server-htlc-receive"),
42		}
43	}
44}
45
46impl FromStr for VtxoPolicyKind {
47	type Err = String;
48	fn from_str(s: &str) -> Result<Self, Self::Err> {
49		Ok(match s {
50			"pubkey" => Self::Pubkey,
51			"checkpoint" => Self::Checkpoint,
52			"server-htlc-send" => Self::ServerHtlcSend,
53			"server-htlc-receive" => Self::ServerHtlcRecv,
54			_ => return Err(format!("unknown VtxoPolicyKind: {}", s)),
55		})
56	}
57}
58
59impl serde::Serialize for VtxoPolicyKind {
60	fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
61		s.collect_str(self)
62	}
63}
64
65impl<'de> serde::Deserialize<'de> for VtxoPolicyKind {
66	fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
67		struct Visitor;
68		impl<'de> serde::de::Visitor<'de> for Visitor {
69			type Value = VtxoPolicyKind;
70			fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
71				write!(f, "a VtxoPolicyKind")
72			}
73			fn visit_str<E: serde::de::Error>(self, v: &str) -> Result<Self::Value, E> {
74				VtxoPolicyKind::from_str(v).map_err(serde::de::Error::custom)
75			}
76		}
77		d.deserialize_str(Visitor)
78	}
79}
80
81/// Policy enabling VTXO protected with a public key.
82///
83/// This will build a taproot with 2 spending paths:
84/// 1. The keyspend path allows Alice and Server to collaborate to spend
85/// the VTXO.
86///
87/// 2. The script-spend path allows Alice to unilaterally spend the VTXO
88/// after a delay.
89#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
90pub struct PubkeyVtxoPolicy {
91	pub user_pubkey: PublicKey,
92}
93
94impl From<PubkeyVtxoPolicy> for VtxoPolicy {
95	fn from(policy: PubkeyVtxoPolicy) -> Self {
96		Self::Pubkey(policy)
97	}
98}
99
100impl PubkeyVtxoPolicy {
101	/// Allows Alice to spend the VTXO after a delay.
102	pub fn user_pubkey_claim_clause(&self, exit_delta: BlockDelta) -> DelayedSignClause {
103		DelayedSignClause { pubkey: self.user_pubkey, block_delta: exit_delta }
104	}
105
106	pub fn clauses(&self, exit_delta: BlockDelta) -> Vec<VtxoClause> {
107		vec![self.user_pubkey_claim_clause(exit_delta).into()]
108	}
109
110	pub fn taproot(
111		&self,
112		server_pubkey: PublicKey,
113		exit_delta: BlockDelta,
114	) -> taproot::TaprootSpendInfo {
115		let combined_pk = musig::combine_keys([self.user_pubkey, server_pubkey]);
116
117		let user_pubkey_claim_clause = self.user_pubkey_claim_clause(exit_delta);
118		taproot::TaprootBuilder::new()
119			.add_leaf(0, user_pubkey_claim_clause.tapscript()).unwrap()
120			.finalize(&SECP, combined_pk).unwrap()
121	}
122}
123
124/// Policy enabling server checkpoints
125///
126/// This will build a taproot with 2 clauses:
127/// 1. The keyspend path allows Alice and Server to collaborate to spend
128/// the checkpoint.
129///
130/// 2. The script-spend path allows Server to spend the checkpoint after
131/// the expiry height.
132#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
133pub struct CheckpointVtxoPolicy {
134	pub user_pubkey: PublicKey,
135}
136
137impl From<CheckpointVtxoPolicy> for VtxoPolicy {
138	fn from(policy: CheckpointVtxoPolicy) -> Self {
139		Self::Checkpoint(policy)
140	}
141}
142
143impl CheckpointVtxoPolicy {
144	/// Allows Server to spend the checkpoint after expiry height.
145	pub fn server_sweeping_clause(
146		&self,
147		expiry_height: BlockHeight,
148		server_pubkey: PublicKey,
149	) -> TimelockSignClause {
150		TimelockSignClause { pubkey: server_pubkey, timelock_height: expiry_height }
151	}
152
153	pub fn clauses(
154		&self,
155		expiry_height: BlockHeight,
156		server_pubkey: PublicKey,
157	) -> Vec<VtxoClause> {
158		vec![self.server_sweeping_clause(expiry_height, server_pubkey).into()]
159	}
160
161	pub fn taproot(
162		&self,
163		server_pubkey: PublicKey,
164		expiry_height: BlockHeight,
165	) -> taproot::TaprootSpendInfo {
166		let combined_pk = musig::combine_keys([self.user_pubkey, server_pubkey]);
167		let server_sweeping_clause = self.server_sweeping_clause(expiry_height, server_pubkey);
168
169		taproot::TaprootBuilder::new()
170			.add_leaf(0, server_sweeping_clause.tapscript()).unwrap()
171			.finalize(&SECP, combined_pk).unwrap()
172	}
173}
174
175/// Policy enabling outgoing Lightning payments.
176///
177/// This will build a taproot with 3 clauses:
178/// 1. The keyspend path allows Alice and Server to collaborate to spend
179/// the HTLC. The Server can use this path to revoke the HTLC if payment
180/// failed
181///
182/// 2. The script-spend path contains one leaf that allows Server to spend
183/// the HTLC after the expiry, if it knows the preimage. Server can use
184/// this path if Alice tries to spend using her clause.
185///
186/// 3. The second leaf allows Alice to spend the HTLC after its expiry
187/// and with a delay. Alice must use this path if the server fails to
188/// provide the preimage and refuse to revoke the HTLC. It will either
189/// force the Server to reveal the preimage (by spending using her clause)
190/// or give Alice her money back.
191#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
192pub struct ServerHtlcSendVtxoPolicy {
193	pub user_pubkey: PublicKey,
194	pub payment_hash: PaymentHash,
195	pub htlc_expiry: BlockHeight,
196}
197
198impl From<ServerHtlcSendVtxoPolicy> for VtxoPolicy {
199	fn from(policy: ServerHtlcSendVtxoPolicy) -> Self {
200		Self::ServerHtlcSend(policy)
201	}
202}
203
204impl ServerHtlcSendVtxoPolicy {
205	/// Allows Server to spend the HTLC after the delta, if it knows the
206	/// preimage. Server can use this path if Alice tries to spend using her
207	/// clause.
208	pub fn server_reveals_preimage_clause(
209		&self,
210		server_pubkey: PublicKey,
211		exit_delta: BlockDelta,
212	) -> HashDelaySignClause {
213		HashDelaySignClause {
214			pubkey: server_pubkey,
215			payment_hash: self.payment_hash,
216			block_delta: exit_delta
217		}
218	}
219
220	/// Allows Alice to spend the HTLC after its expiry and with a delay.
221	/// Alice must use this path if the server fails to provide the preimage
222	/// and refuse to revoke the HTLC. It will either force the server to
223	/// reveal the preimage (by spending using its clause) or give Alice her
224	/// money back.
225	pub fn user_claim_after_expiry_clause(
226		&self,
227		exit_delta: BlockDelta,
228	) -> DelayedTimelockSignClause {
229		DelayedTimelockSignClause {
230			pubkey: self.user_pubkey,
231			timelock_height: self.htlc_expiry,
232			block_delta: 2 * exit_delta
233		}
234	}
235
236
237	pub fn clauses(&self, exit_delta: BlockDelta, server_pubkey: PublicKey) -> Vec<VtxoClause> {
238		vec![
239			self.server_reveals_preimage_clause(server_pubkey, exit_delta).into(),
240			self.user_claim_after_expiry_clause(exit_delta).into(),
241		]
242	}
243
244	pub fn taproot(&self, server_pubkey: PublicKey, exit_delta: BlockDelta) -> taproot::TaprootSpendInfo {
245		let server_reveals_preimage_clause = self.server_reveals_preimage_clause(server_pubkey, exit_delta);
246		let user_claim_after_expiry_clause = self.user_claim_after_expiry_clause(exit_delta);
247
248		let combined_pk = musig::combine_keys([self.user_pubkey, server_pubkey]);
249		bitcoin::taproot::TaprootBuilder::new()
250			.add_leaf(1, server_reveals_preimage_clause.tapscript()).unwrap()
251			.add_leaf(1, user_claim_after_expiry_clause.tapscript()).unwrap()
252			.finalize(&SECP, combined_pk).unwrap()
253	}
254}
255
256
257/// Policy enabling incoming Lightning payments.
258///
259/// This will build a taproot with 3 clauses:
260/// 1. The keyspend path allows Alice and Server to collaborate to spend
261/// the HTLC. This is the expected path to be used. Server should only
262/// accept to collaborate if Alice reveals the preimage.
263///
264/// 2. The script-spend path contains one leaf that allows Server to spend
265/// the HTLC after the expiry, with an exit delta delay. Server can use
266/// this path if Alice tries to spend the HTLC using the 3rd path after
267/// the HTLC expiry
268///
269/// 3. The second leaf allows Alice to spend the HTLC if she knows the
270/// preimage, but with a greater exit delta delay than server's clause.
271/// Alice must use this path if she revealed the preimage but Server
272/// refused to collaborate.
273#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
274pub struct ServerHtlcRecvVtxoPolicy {
275	pub user_pubkey: PublicKey,
276	pub payment_hash: PaymentHash,
277	pub htlc_expiry_delta: BlockDelta,
278	pub htlc_expiry: BlockHeight,
279}
280
281impl ServerHtlcRecvVtxoPolicy {
282	/// Allows Alice to spend the HTLC if she knows the preimage, but with a
283	/// greater exit delta delay than server's clause. Alice must use this
284	/// path if she revealed the preimage but server refused to cosign
285	/// claim VTXO.
286	pub fn user_reveals_preimage_clause(&self, exit_delta: BlockDelta) -> HashDelaySignClause {
287		HashDelaySignClause {
288			pubkey: self.user_pubkey,
289			payment_hash: self.payment_hash,
290			block_delta: self.htlc_expiry_delta + exit_delta
291		}
292	}
293
294	/// Allows Server to spend the HTLC after the HTLC expiry, with an exit
295	/// delta delay. Server can use this path if Alice tries to spend the
296	/// HTLC using her clause after the HTLC expiry.
297	pub fn server_claim_after_expiry_clause(
298		&self,
299		server_pubkey: PublicKey,
300		exit_delta: BlockDelta,
301	) -> DelayedTimelockSignClause {
302		DelayedTimelockSignClause {
303			pubkey: server_pubkey,
304			timelock_height: self.htlc_expiry,
305			block_delta: exit_delta
306		}
307	}
308
309	pub fn clauses(&self, exit_delta: BlockDelta, server_pubkey: PublicKey) -> Vec<VtxoClause> {
310		vec![
311			self.user_reveals_preimage_clause(exit_delta).into(),
312			self.server_claim_after_expiry_clause(server_pubkey, exit_delta).into(),
313		]
314	}
315
316	pub fn taproot(&self, server_pubkey: PublicKey, exit_delta: BlockDelta) -> taproot::TaprootSpendInfo {
317		let server_claim_after_expiry_clause = self.server_claim_after_expiry_clause(server_pubkey, exit_delta);
318		let user_reveals_preimage_clause = self.user_reveals_preimage_clause(exit_delta);
319
320		let combined_pk = musig::combine_keys([self.user_pubkey, server_pubkey]);
321		bitcoin::taproot::TaprootBuilder::new()
322			.add_leaf(1, server_claim_after_expiry_clause.tapscript()).unwrap()
323			.add_leaf(1, user_reveals_preimage_clause.tapscript()).unwrap()
324			.finalize(&SECP, combined_pk).unwrap()
325	}
326}
327
328impl From<ServerHtlcRecvVtxoPolicy> for VtxoPolicy {
329	fn from(policy: ServerHtlcRecvVtxoPolicy) -> Self {
330		Self::ServerHtlcRecv(policy)
331	}
332}
333
334/// The output policy of the VTXO.
335#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
336pub enum VtxoPolicy {
337	/// Standard VTXO output protected with a public key.
338	///
339	/// This can be the result of either:
340	/// - a board
341	/// - a round
342	/// - an arkoor tx
343	/// - change from a LN payment
344	Pubkey(PubkeyVtxoPolicy),
345	/// A policy which returns all funds to the server after expiry
346	Checkpoint(CheckpointVtxoPolicy),
347	/// A VTXO that represents an HTLC with the Ark server to send money.
348	ServerHtlcSend(ServerHtlcSendVtxoPolicy),
349	/// A VTXO that represents an HTLC with the Ark server to receive money.
350	ServerHtlcRecv(ServerHtlcRecvVtxoPolicy),
351}
352
353impl VtxoPolicy {
354	pub fn new_pubkey(user_pubkey: PublicKey) -> Self {
355		Self::Pubkey(PubkeyVtxoPolicy { user_pubkey })
356	}
357
358	pub fn new_checkpoint(user_pubkey: PublicKey) -> Self {
359		Self::Checkpoint(CheckpointVtxoPolicy { user_pubkey })
360	}
361
362	pub fn new_server_htlc_send(
363		user_pubkey: PublicKey,
364		payment_hash: PaymentHash,
365		htlc_expiry: BlockHeight,
366	) -> Self {
367		Self::ServerHtlcSend(ServerHtlcSendVtxoPolicy { user_pubkey, payment_hash, htlc_expiry })
368	}
369
370	/// Creates a new htlc from server to client
371	/// - user_pubkey: A public key owned by the client
372	/// - payment_hash: The payment hash, the client can claim the HTLC
373	/// by revealing the corresponding pre-image
374	/// - htlc_expiry: An absolute blockheight at which the HTLC expires
375	/// - htlc_expiry_delta: A safety margin for the server. If the user
376	/// tries to exit after time-out the server will have at-least
377	/// `htlc_expiry_delta` blocks to claim the payment
378	pub fn new_server_htlc_recv(
379		user_pubkey: PublicKey,
380		payment_hash: PaymentHash,
381		htlc_expiry: BlockHeight,
382		htlc_expiry_delta: BlockDelta,
383	) -> Self {
384		Self::ServerHtlcRecv(ServerHtlcRecvVtxoPolicy {
385			user_pubkey, payment_hash, htlc_expiry, htlc_expiry_delta,
386		})
387	}
388
389	pub fn as_pubkey(&self) -> Option<&PubkeyVtxoPolicy> {
390		match self {
391			Self::Pubkey(v) => Some(v),
392			_ => None,
393		}
394	}
395
396	pub fn as_server_htlc_send(&self) -> Option<&ServerHtlcSendVtxoPolicy> {
397		match self {
398			Self::ServerHtlcSend(v) => Some(v),
399			_ => None,
400		}
401	}
402
403	pub fn as_server_htlc_recv(&self) -> Option<&ServerHtlcRecvVtxoPolicy> {
404		match self {
405			Self::ServerHtlcRecv(v) => Some(v),
406			_ => None,
407		}
408	}
409
410	/// The policy type id.
411	pub fn policy_type(&self) -> VtxoPolicyKind {
412		match self {
413			Self::Pubkey { .. } => VtxoPolicyKind::Pubkey,
414			Self::Checkpoint { .. } => VtxoPolicyKind::Checkpoint,
415			Self::ServerHtlcSend { .. } => VtxoPolicyKind::ServerHtlcSend,
416			Self::ServerHtlcRecv { .. } => VtxoPolicyKind::ServerHtlcRecv,
417		}
418	}
419
420	/// Whether a [Vtxo](crate::Vtxo) with this output can be spend in an arkoor tx.
421	pub fn is_arkoor_compatible(&self) -> bool {
422		match self {
423			Self::Pubkey { .. } => true,
424			Self::Checkpoint { .. } => true,
425			Self::ServerHtlcSend { .. } => false,
426			Self::ServerHtlcRecv { .. } => false,
427		}
428	}
429
430	/// The public key used to cosign arkoor txs spending a [Vtxo](crate::Vtxo)
431	/// with this output.
432	/// This will return [None] if [VtxoPolicy::is_arkoor_compatible] returns false.
433	pub fn arkoor_pubkey(&self) -> Option<PublicKey> {
434		match self {
435			Self::Pubkey(PubkeyVtxoPolicy { user_pubkey }) => Some(*user_pubkey),
436			Self::Checkpoint(CheckpointVtxoPolicy { user_pubkey }) => Some(*user_pubkey),
437			Self::ServerHtlcSend(ServerHtlcSendVtxoPolicy { user_pubkey, .. }) => Some(*user_pubkey),
438			Self::ServerHtlcRecv(ServerHtlcRecvVtxoPolicy { user_pubkey, .. }) => Some(*user_pubkey),
439		}
440	}
441
442	/// Returns the user pubkey associated with a [Vtxo](crate::Vtxo) with this output.
443	pub fn user_pubkey(&self) -> PublicKey {
444		match self {
445			Self::Pubkey(PubkeyVtxoPolicy { user_pubkey }) => *user_pubkey,
446			Self::Checkpoint(CheckpointVtxoPolicy { user_pubkey }) => *user_pubkey,
447			Self::ServerHtlcSend(ServerHtlcSendVtxoPolicy { user_pubkey, .. }) => *user_pubkey,
448			Self::ServerHtlcRecv(ServerHtlcRecvVtxoPolicy { user_pubkey, .. }) => *user_pubkey,
449		}
450	}
451
452	pub fn taproot(
453		&self,
454		server_pubkey: PublicKey,
455		exit_delta: BlockDelta,
456		expiry_height: BlockHeight,
457	) -> taproot::TaprootSpendInfo {
458		match self {
459			Self::Pubkey(policy) => policy.taproot(server_pubkey, exit_delta),
460			Self::Checkpoint(policy) => policy.taproot(server_pubkey, expiry_height),
461			Self::ServerHtlcSend(policy) => policy.taproot(server_pubkey, exit_delta),
462			Self::ServerHtlcRecv(policy) => policy.taproot(server_pubkey, exit_delta),
463		}
464	}
465
466	pub(crate) fn script_pubkey(
467		&self,
468		server_pubkey: PublicKey,
469		exit_delta: BlockDelta,
470		expiry_height: BlockHeight,
471	) -> ScriptBuf {
472		self.taproot(server_pubkey, exit_delta, expiry_height).script_pubkey()
473	}
474
475	pub(crate) fn txout(
476		&self,
477		amount: Amount,
478		server_pubkey: PublicKey,
479		exit_delta: BlockDelta,
480		expiry_height: BlockHeight,
481	) -> TxOut {
482		TxOut {
483			value: amount,
484			script_pubkey: self.script_pubkey(server_pubkey, exit_delta, expiry_height),
485		}
486	}
487
488	pub fn clauses(
489		&self,
490		exit_delta: u16,
491		expiry_height: BlockHeight,
492		server_pubkey: PublicKey,
493	) -> Vec<VtxoClause> {
494		match self {
495			Self::Pubkey(policy) => policy.clauses(exit_delta),
496			Self::Checkpoint(policy) => policy.clauses(expiry_height, server_pubkey),
497			Self::ServerHtlcSend(policy) => policy.clauses(exit_delta, server_pubkey),
498			Self::ServerHtlcRecv(policy) => policy.clauses(exit_delta, server_pubkey),
499		}
500	}
501}