1use crate::boarding_output::BoardingOutput;
2use crate::vhtlc::VhtlcOptions;
3use crate::vtxo::Vtxo;
4use crate::Error;
5use bitcoin::absolute;
6use bitcoin::key::Secp256k1;
7use bitcoin::secp256k1::All;
8use bitcoin::taproot::ControlBlock;
9use bitcoin::Network;
10use bitcoin::ScriptBuf;
11use bitcoin::Sequence;
12use bitcoin::XOnlyPublicKey;
13use serde::Deserialize;
14use serde::Serialize;
15use std::fmt;
16
17#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
18pub struct ContractType(String);
19
20impl ContractType {
21 pub fn new(value: impl Into<String>) -> Result<Self, Error> {
22 let value = value.into();
23 if value.is_empty() {
24 return Err(Error::ad_hoc("contract type cannot be empty"));
25 }
26 Ok(Self(value))
27 }
28
29 pub fn as_str(&self) -> &str {
30 &self.0
31 }
32
33 pub fn default_vtxo() -> Self {
34 Self("default".to_string())
35 }
36
37 pub fn delegate_vtxo() -> Self {
38 Self("delegate".to_string())
39 }
40
41 pub fn boarding() -> Self {
42 Self("boarding".to_string())
43 }
44
45 pub fn vhtlc() -> Self {
46 Self("vhtlc".to_string())
47 }
48}
49
50impl fmt::Display for ContractType {
51 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
52 self.0.fmt(f)
53 }
54}
55
56impl From<&'static str> for ContractType {
57 fn from(value: &'static str) -> Self {
58 Self(value.to_string())
59 }
60}
61
62#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
63#[serde(rename_all = "snake_case")]
64pub enum ContractState {
65 Active,
66 Inactive,
67}
68
69#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
70pub struct StoredContract {
71 pub contract_type: ContractType,
72 pub contract_version: u32,
73 pub script_pubkey: ScriptBuf,
74 pub state: ContractState,
75 pub created_at: u64,
76 pub key_index: Option<u32>,
80 pub data: serde_json::Value,
81}
82
83#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
84#[serde(rename_all = "snake_case")]
85pub enum SpendPathKind {
86 Forfeit,
87 Exit,
88 Delegate,
89 VhtlcClaim,
90 VhtlcRefund,
91 VhtlcRefundWithoutReceiver,
92 VhtlcUnilateralClaim,
93 VhtlcUnilateralRefund,
94 VhtlcUnilateralRefundWithoutReceiver,
95 Custom(String),
96}
97
98impl SpendPathKind {
99 pub fn from_vhtlc_name(name: String) -> Self {
100 match name.as_str() {
101 "claim" => Self::VhtlcClaim,
102 "refund" => Self::VhtlcRefund,
103 "refund_without_receiver" => Self::VhtlcRefundWithoutReceiver,
104 "unilateral_claim" => Self::VhtlcUnilateralClaim,
105 "unilateral_refund" => Self::VhtlcUnilateralRefund,
106 "unilateral_refund_without_receiver" => Self::VhtlcUnilateralRefundWithoutReceiver,
107 _ => Self::Custom(name),
108 }
109 }
110}
111
112#[derive(Clone, Debug, PartialEq, Eq)]
113pub struct SpendPath {
114 pub kind: SpendPathKind,
115 pub script: ScriptBuf,
116 pub control_block: ControlBlock,
117}
118
119impl SpendPath {
120 pub fn new(kind: SpendPathKind, script: ScriptBuf, control_block: ControlBlock) -> Self {
121 Self {
122 kind,
123 script,
124 control_block,
125 }
126 }
127
128 pub fn select(self) -> SpendSelection {
129 SpendSelection::new(self)
130 }
131}
132
133#[derive(Clone, Debug, PartialEq, Eq)]
134pub struct SpendSelection {
135 pub path: SpendPath,
136 pub sequence: Option<Sequence>,
137 pub locktime: Option<absolute::LockTime>,
138 pub extra_witness: Vec<Vec<u8>>,
139}
140
141impl SpendSelection {
142 pub fn new(path: SpendPath) -> Self {
143 Self {
144 path,
145 sequence: None,
146 locktime: None,
147 extra_witness: Vec::new(),
148 }
149 }
150
151 pub fn with_sequence(mut self, sequence: Sequence) -> Self {
152 self.sequence = Some(sequence);
153 self
154 }
155
156 pub fn with_locktime(mut self, locktime: absolute::LockTime) -> Self {
157 self.locktime = Some(locktime);
158 self
159 }
160
161 pub fn with_extra_witness(mut self, extra_witness: Vec<Vec<u8>>) -> Self {
162 self.extra_witness = extra_witness;
163 self
164 }
165
166 pub fn resolved_sequence(&self, default_sequence: Sequence) -> Sequence {
167 self.sequence.unwrap_or(default_sequence)
168 }
169
170 pub fn resolved_spend_info(
171 &self,
172 default_sequence: Sequence,
173 ) -> (Sequence, (ScriptBuf, ControlBlock)) {
174 (self.resolved_sequence(default_sequence), self.spend_info())
175 }
176
177 pub fn spend_info(&self) -> (ScriptBuf, ControlBlock) {
178 (self.path.script.clone(), self.path.control_block.clone())
179 }
180}
181
182#[derive(Clone)]
183pub struct ContractContext {
184 network: Network,
185 secp: Secp256k1<All>,
186}
187
188impl ContractContext {
189 pub fn new(network: Network) -> Self {
190 Self {
191 network,
192 secp: Secp256k1::new(),
193 }
194 }
195
196 pub fn network(&self) -> Network {
197 self.network
198 }
199
200 pub fn secp(&self) -> &Secp256k1<All> {
201 &self.secp
202 }
203}
204
205pub trait ContractSpec:
206 Clone + Serialize + for<'de> Deserialize<'de> + Send + Sync + 'static
207{
208 const VERSION: u32;
209
210 fn contract_type() -> ContractType;
211 fn script_pubkey(&self, ctx: &ContractContext) -> Result<ScriptBuf, Error>;
212 fn spendable_paths(&self, ctx: &ContractContext) -> Result<Vec<SpendPath>, Error>;
213
214 fn spendable_selections(&self, ctx: &ContractContext) -> Result<Vec<SpendSelection>, Error> {
215 Ok(self
216 .spendable_paths(ctx)?
217 .into_iter()
218 .map(SpendPath::select)
219 .collect())
220 }
221}
222
223#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
224pub struct DefaultVtxoContract {
225 pub server: XOnlyPublicKey,
226 pub owner: XOnlyPublicKey,
227 pub exit_delay: Sequence,
228}
229
230impl ContractSpec for DefaultVtxoContract {
231 const VERSION: u32 = 1;
232
233 fn contract_type() -> ContractType {
234 ContractType::default_vtxo()
235 }
236
237 fn script_pubkey(&self, ctx: &ContractContext) -> Result<ScriptBuf, Error> {
238 Ok(self.vtxo(ctx)?.script_pubkey())
239 }
240
241 fn spendable_paths(&self, ctx: &ContractContext) -> Result<Vec<SpendPath>, Error> {
242 let vtxo = self.vtxo(ctx)?;
243 let (forfeit_script, forfeit_control_block) = vtxo.forfeit_spend_info()?;
244 let (exit_script, exit_control_block) = vtxo.exit_spend_info()?;
245 Ok(vec![
246 SpendPath::new(
247 SpendPathKind::Forfeit,
248 forfeit_script,
249 forfeit_control_block,
250 ),
251 SpendPath::new(SpendPathKind::Exit, exit_script, exit_control_block),
252 ])
253 }
254
255 fn spendable_selections(&self, ctx: &ContractContext) -> Result<Vec<SpendSelection>, Error> {
256 Ok(self
257 .spendable_paths(ctx)?
258 .into_iter()
259 .map(|path| {
260 if path.kind == SpendPathKind::Exit {
261 path.select().with_sequence(self.exit_delay)
262 } else {
263 path.select()
264 }
265 })
266 .collect())
267 }
268}
269
270impl DefaultVtxoContract {
271 pub fn vtxo(&self, ctx: &ContractContext) -> Result<Vtxo, Error> {
272 Vtxo::new_default(
273 ctx.secp(),
274 self.server,
275 self.owner,
276 self.exit_delay,
277 ctx.network(),
278 )
279 }
280}
281
282#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
283pub struct DelegateVtxoContract {
284 pub server: XOnlyPublicKey,
285 pub owner: XOnlyPublicKey,
286 pub delegator: XOnlyPublicKey,
287 pub exit_delay: Sequence,
288}
289
290impl ContractSpec for DelegateVtxoContract {
291 const VERSION: u32 = 1;
292
293 fn contract_type() -> ContractType {
294 ContractType::delegate_vtxo()
295 }
296
297 fn script_pubkey(&self, ctx: &ContractContext) -> Result<ScriptBuf, Error> {
298 Ok(self.vtxo(ctx)?.script_pubkey())
299 }
300
301 fn spendable_paths(&self, ctx: &ContractContext) -> Result<Vec<SpendPath>, Error> {
302 let vtxo = self.vtxo(ctx)?;
303 let (forfeit_script, forfeit_control_block) = vtxo.forfeit_spend_info()?;
304 let (exit_script, exit_control_block) = vtxo.exit_spend_info()?;
305 let (delegate_script, delegate_control_block) = vtxo.delegate_spend_info()?;
306 Ok(vec![
307 SpendPath::new(
308 SpendPathKind::Forfeit,
309 forfeit_script,
310 forfeit_control_block,
311 ),
312 SpendPath::new(SpendPathKind::Exit, exit_script, exit_control_block),
313 SpendPath::new(
314 SpendPathKind::Delegate,
315 delegate_script,
316 delegate_control_block,
317 ),
318 ])
319 }
320
321 fn spendable_selections(&self, ctx: &ContractContext) -> Result<Vec<SpendSelection>, Error> {
322 Ok(self
323 .spendable_paths(ctx)?
324 .into_iter()
325 .map(|path| {
326 if path.kind == SpendPathKind::Exit {
327 path.select().with_sequence(self.exit_delay)
328 } else {
329 path.select()
330 }
331 })
332 .collect())
333 }
334}
335
336impl DelegateVtxoContract {
337 pub fn vtxo(&self, ctx: &ContractContext) -> Result<Vtxo, Error> {
338 Vtxo::new_with_delegator(
339 ctx.secp(),
340 self.server,
341 self.owner,
342 self.delegator,
343 self.exit_delay,
344 ctx.network(),
345 )
346 }
347}
348
349#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
350pub struct BoardingContract {
351 pub server: XOnlyPublicKey,
352 pub owner: XOnlyPublicKey,
353 pub exit_delay: Sequence,
354}
355
356impl ContractSpec for BoardingContract {
357 const VERSION: u32 = 1;
358
359 fn contract_type() -> ContractType {
360 ContractType::boarding()
361 }
362
363 fn script_pubkey(&self, ctx: &ContractContext) -> Result<ScriptBuf, Error> {
364 Ok(self.boarding_output(ctx)?.script_pubkey())
365 }
366
367 fn spendable_paths(&self, ctx: &ContractContext) -> Result<Vec<SpendPath>, Error> {
368 let boarding_output = self.boarding_output(ctx)?;
369 let (forfeit_script, forfeit_control_block) = boarding_output.forfeit_spend_info();
370 let (exit_script, exit_control_block) = boarding_output.exit_spend_info();
371 Ok(vec![
372 SpendPath::new(
373 SpendPathKind::Forfeit,
374 forfeit_script,
375 forfeit_control_block,
376 ),
377 SpendPath::new(SpendPathKind::Exit, exit_script, exit_control_block),
378 ])
379 }
380
381 fn spendable_selections(&self, ctx: &ContractContext) -> Result<Vec<SpendSelection>, Error> {
382 Ok(self
383 .spendable_paths(ctx)?
384 .into_iter()
385 .map(|path| {
386 if path.kind == SpendPathKind::Exit {
387 path.select().with_sequence(self.exit_delay)
388 } else {
389 path.select()
390 }
391 })
392 .collect())
393 }
394}
395
396impl BoardingContract {
397 pub fn boarding_output(&self, ctx: &ContractContext) -> Result<BoardingOutput, Error> {
398 BoardingOutput::new(
399 ctx.secp(),
400 self.server,
401 self.owner,
402 self.exit_delay,
403 ctx.network(),
404 )
405 }
406}
407
408#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
409pub struct VhtlcContract {
410 pub options: VhtlcOptions,
411}
412
413impl ContractSpec for VhtlcContract {
414 const VERSION: u32 = 1;
415
416 fn contract_type() -> ContractType {
417 ContractType::vhtlc()
418 }
419
420 fn script_pubkey(&self, ctx: &ContractContext) -> Result<ScriptBuf, Error> {
421 let script = crate::vhtlc::VhtlcScript::new(self.options.clone(), ctx.network())
422 .map_err(|e| Error::ad_hoc(format!("failed to build vhtlc: {e}")))?;
423 Ok(script.script_pubkey())
424 }
425
426 fn spendable_paths(&self, ctx: &ContractContext) -> Result<Vec<SpendPath>, Error> {
427 let script = crate::vhtlc::VhtlcScript::new(self.options.clone(), ctx.network())
428 .map_err(|e| Error::ad_hoc(format!("failed to build vhtlc: {e}")))?;
429 script
430 .get_script_map()
431 .into_iter()
432 .map(|(name, tapscript)| {
433 let control_block = script
434 .taproot_spend_info()
435 .control_block(&(tapscript.clone(), bitcoin::taproot::LeafVersion::TapScript))
436 .ok_or_else(|| Error::ad_hoc("missing vhtlc control block"))?;
437 Ok(SpendPath {
438 kind: SpendPathKind::from_vhtlc_name(name),
439 script: tapscript,
440 control_block,
441 })
442 })
443 .collect()
444 }
445
446 fn spendable_selections(&self, ctx: &ContractContext) -> Result<Vec<SpendSelection>, Error> {
447 Ok(self
448 .spendable_paths(ctx)?
449 .into_iter()
450 .filter_map(|path| match path.kind {
451 SpendPathKind::VhtlcClaim | SpendPathKind::VhtlcUnilateralClaim => None,
452 SpendPathKind::VhtlcRefundWithoutReceiver => Some(path.select().with_locktime(
453 absolute::LockTime::from_consensus(self.options.refund_locktime),
454 )),
455 SpendPathKind::VhtlcUnilateralRefund => Some(
456 path.select()
457 .with_sequence(self.options.unilateral_refund_delay),
458 ),
459 SpendPathKind::VhtlcUnilateralRefundWithoutReceiver => Some(
460 path.select()
461 .with_sequence(self.options.unilateral_refund_without_receiver_delay),
462 ),
463 SpendPathKind::Forfeit
464 | SpendPathKind::Exit
465 | SpendPathKind::Delegate
466 | SpendPathKind::VhtlcRefund
467 | SpendPathKind::Custom(_) => Some(path.select()),
468 })
469 .collect())
470 }
471}
472
473#[cfg(test)]
474mod tests {
475 use super::*;
476 use bitcoin::hashes::ripemd160;
477 use bitcoin::hashes::Hash;
478 use std::str::FromStr;
479
480 fn test_key(hex: &str) -> XOnlyPublicKey {
481 XOnlyPublicKey::from_str(hex).unwrap()
482 }
483
484 fn vhtlc_contract() -> VhtlcContract {
485 VhtlcContract {
486 options: VhtlcOptions {
487 sender: test_key(
488 "f874c4fd782a63a2b078b424f9f4e5edae622880a29fe572bd71bee45ab55ea0",
489 ),
490 receiver: test_key(
491 "b02b1a956cd0ed91d1a06b65ae976470ffd4b8c9215f446f266bb74fedd153b6",
492 ),
493 server: test_key(
494 "e35799157be4b37565bb5afe4d04e6a0fa0a4b6a4f4e48b0d904685d253cdbdb",
495 ),
496 preimage_hash: ripemd160::Hash::hash(b"preimage"),
497 refund_locktime: 500,
498 unilateral_claim_delay: Sequence::from_height(10),
499 unilateral_refund_delay: Sequence::from_height(20),
500 unilateral_refund_without_receiver_delay: Sequence::from_height(30),
501 },
502 }
503 }
504
505 #[test]
506 fn vhtlc_spendable_selections_include_required_constraints() {
507 let ctx = ContractContext::new(Network::Regtest);
508 let selections = vhtlc_contract().spendable_selections(&ctx).unwrap();
509
510 assert!(!selections
511 .iter()
512 .any(|selection| selection.path.kind == SpendPathKind::VhtlcClaim));
513 assert!(!selections
514 .iter()
515 .any(|selection| selection.path.kind == SpendPathKind::VhtlcUnilateralClaim));
516
517 let refund_without_receiver = selections
518 .iter()
519 .find(|selection| selection.path.kind == SpendPathKind::VhtlcRefundWithoutReceiver)
520 .unwrap();
521 assert_eq!(
522 refund_without_receiver.locktime,
523 Some(absolute::LockTime::from_consensus(500))
524 );
525
526 let unilateral_refund = selections
527 .iter()
528 .find(|selection| selection.path.kind == SpendPathKind::VhtlcUnilateralRefund)
529 .unwrap();
530 assert_eq!(unilateral_refund.sequence, Some(Sequence::from_height(20)));
531
532 let unilateral_refund_without_receiver = selections
533 .iter()
534 .find(|selection| {
535 selection.path.kind == SpendPathKind::VhtlcUnilateralRefundWithoutReceiver
536 })
537 .unwrap();
538 assert_eq!(
539 unilateral_refund_without_receiver.sequence,
540 Some(Sequence::from_height(30))
541 );
542 }
543}