use crate::boarding_output::BoardingOutput;
use crate::vhtlc::VhtlcOptions;
use crate::vtxo::Vtxo;
use crate::Error;
use bitcoin::absolute;
use bitcoin::key::Secp256k1;
use bitcoin::secp256k1::All;
use bitcoin::taproot::ControlBlock;
use bitcoin::Network;
use bitcoin::ScriptBuf;
use bitcoin::Sequence;
use bitcoin::XOnlyPublicKey;
use serde::Deserialize;
use serde::Serialize;
use std::fmt;
#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct ContractType(String);
impl ContractType {
pub fn new(value: impl Into<String>) -> Result<Self, Error> {
let value = value.into();
if value.is_empty() {
return Err(Error::ad_hoc("contract type cannot be empty"));
}
Ok(Self(value))
}
pub fn as_str(&self) -> &str {
&self.0
}
pub fn default_vtxo() -> Self {
Self("default".to_string())
}
pub fn delegate_vtxo() -> Self {
Self("delegate".to_string())
}
pub fn boarding() -> Self {
Self("boarding".to_string())
}
pub fn vhtlc() -> Self {
Self("vhtlc".to_string())
}
}
impl fmt::Display for ContractType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.0.fmt(f)
}
}
impl From<&'static str> for ContractType {
fn from(value: &'static str) -> Self {
Self(value.to_string())
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ContractState {
Active,
Inactive,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct StoredContract {
pub contract_type: ContractType,
pub contract_version: u32,
pub script_pubkey: ScriptBuf,
pub state: ContractState,
pub created_at: u64,
pub key_index: Option<u32>,
pub data: serde_json::Value,
}
#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum SpendPathKind {
Forfeit,
Exit,
Delegate,
VhtlcClaim,
VhtlcRefund,
VhtlcRefundWithoutReceiver,
VhtlcUnilateralClaim,
VhtlcUnilateralRefund,
VhtlcUnilateralRefundWithoutReceiver,
Custom(String),
}
impl SpendPathKind {
pub fn from_vhtlc_name(name: String) -> Self {
match name.as_str() {
"claim" => Self::VhtlcClaim,
"refund" => Self::VhtlcRefund,
"refund_without_receiver" => Self::VhtlcRefundWithoutReceiver,
"unilateral_claim" => Self::VhtlcUnilateralClaim,
"unilateral_refund" => Self::VhtlcUnilateralRefund,
"unilateral_refund_without_receiver" => Self::VhtlcUnilateralRefundWithoutReceiver,
_ => Self::Custom(name),
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct SpendPath {
pub kind: SpendPathKind,
pub script: ScriptBuf,
pub control_block: ControlBlock,
}
impl SpendPath {
pub fn new(kind: SpendPathKind, script: ScriptBuf, control_block: ControlBlock) -> Self {
Self {
kind,
script,
control_block,
}
}
pub fn select(self) -> SpendSelection {
SpendSelection::new(self)
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct SpendSelection {
pub path: SpendPath,
pub sequence: Option<Sequence>,
pub locktime: Option<absolute::LockTime>,
pub extra_witness: Vec<Vec<u8>>,
}
impl SpendSelection {
pub fn new(path: SpendPath) -> Self {
Self {
path,
sequence: None,
locktime: None,
extra_witness: Vec::new(),
}
}
pub fn with_sequence(mut self, sequence: Sequence) -> Self {
self.sequence = Some(sequence);
self
}
pub fn with_locktime(mut self, locktime: absolute::LockTime) -> Self {
self.locktime = Some(locktime);
self
}
pub fn with_extra_witness(mut self, extra_witness: Vec<Vec<u8>>) -> Self {
self.extra_witness = extra_witness;
self
}
pub fn resolved_sequence(&self, default_sequence: Sequence) -> Sequence {
self.sequence.unwrap_or(default_sequence)
}
pub fn resolved_spend_info(
&self,
default_sequence: Sequence,
) -> (Sequence, (ScriptBuf, ControlBlock)) {
(self.resolved_sequence(default_sequence), self.spend_info())
}
pub fn spend_info(&self) -> (ScriptBuf, ControlBlock) {
(self.path.script.clone(), self.path.control_block.clone())
}
}
#[derive(Clone)]
pub struct ContractContext {
network: Network,
secp: Secp256k1<All>,
}
impl ContractContext {
pub fn new(network: Network) -> Self {
Self {
network,
secp: Secp256k1::new(),
}
}
pub fn network(&self) -> Network {
self.network
}
pub fn secp(&self) -> &Secp256k1<All> {
&self.secp
}
}
pub trait ContractSpec:
Clone + Serialize + for<'de> Deserialize<'de> + Send + Sync + 'static
{
const VERSION: u32;
fn contract_type() -> ContractType;
fn script_pubkey(&self, ctx: &ContractContext) -> Result<ScriptBuf, Error>;
fn spendable_paths(&self, ctx: &ContractContext) -> Result<Vec<SpendPath>, Error>;
fn spendable_selections(&self, ctx: &ContractContext) -> Result<Vec<SpendSelection>, Error> {
Ok(self
.spendable_paths(ctx)?
.into_iter()
.map(SpendPath::select)
.collect())
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct DefaultVtxoContract {
pub server: XOnlyPublicKey,
pub owner: XOnlyPublicKey,
pub exit_delay: Sequence,
}
impl ContractSpec for DefaultVtxoContract {
const VERSION: u32 = 1;
fn contract_type() -> ContractType {
ContractType::default_vtxo()
}
fn script_pubkey(&self, ctx: &ContractContext) -> Result<ScriptBuf, Error> {
Ok(self.vtxo(ctx)?.script_pubkey())
}
fn spendable_paths(&self, ctx: &ContractContext) -> Result<Vec<SpendPath>, Error> {
let vtxo = self.vtxo(ctx)?;
let (forfeit_script, forfeit_control_block) = vtxo.forfeit_spend_info()?;
let (exit_script, exit_control_block) = vtxo.exit_spend_info()?;
Ok(vec![
SpendPath::new(
SpendPathKind::Forfeit,
forfeit_script,
forfeit_control_block,
),
SpendPath::new(SpendPathKind::Exit, exit_script, exit_control_block),
])
}
fn spendable_selections(&self, ctx: &ContractContext) -> Result<Vec<SpendSelection>, Error> {
Ok(self
.spendable_paths(ctx)?
.into_iter()
.map(|path| {
if path.kind == SpendPathKind::Exit {
path.select().with_sequence(self.exit_delay)
} else {
path.select()
}
})
.collect())
}
}
impl DefaultVtxoContract {
pub fn vtxo(&self, ctx: &ContractContext) -> Result<Vtxo, Error> {
Vtxo::new_default(
ctx.secp(),
self.server,
self.owner,
self.exit_delay,
ctx.network(),
)
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct DelegateVtxoContract {
pub server: XOnlyPublicKey,
pub owner: XOnlyPublicKey,
pub delegator: XOnlyPublicKey,
pub exit_delay: Sequence,
}
impl ContractSpec for DelegateVtxoContract {
const VERSION: u32 = 1;
fn contract_type() -> ContractType {
ContractType::delegate_vtxo()
}
fn script_pubkey(&self, ctx: &ContractContext) -> Result<ScriptBuf, Error> {
Ok(self.vtxo(ctx)?.script_pubkey())
}
fn spendable_paths(&self, ctx: &ContractContext) -> Result<Vec<SpendPath>, Error> {
let vtxo = self.vtxo(ctx)?;
let (forfeit_script, forfeit_control_block) = vtxo.forfeit_spend_info()?;
let (exit_script, exit_control_block) = vtxo.exit_spend_info()?;
let (delegate_script, delegate_control_block) = vtxo.delegate_spend_info()?;
Ok(vec![
SpendPath::new(
SpendPathKind::Forfeit,
forfeit_script,
forfeit_control_block,
),
SpendPath::new(SpendPathKind::Exit, exit_script, exit_control_block),
SpendPath::new(
SpendPathKind::Delegate,
delegate_script,
delegate_control_block,
),
])
}
fn spendable_selections(&self, ctx: &ContractContext) -> Result<Vec<SpendSelection>, Error> {
Ok(self
.spendable_paths(ctx)?
.into_iter()
.map(|path| {
if path.kind == SpendPathKind::Exit {
path.select().with_sequence(self.exit_delay)
} else {
path.select()
}
})
.collect())
}
}
impl DelegateVtxoContract {
pub fn vtxo(&self, ctx: &ContractContext) -> Result<Vtxo, Error> {
Vtxo::new_with_delegator(
ctx.secp(),
self.server,
self.owner,
self.delegator,
self.exit_delay,
ctx.network(),
)
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct BoardingContract {
pub server: XOnlyPublicKey,
pub owner: XOnlyPublicKey,
pub exit_delay: Sequence,
}
impl ContractSpec for BoardingContract {
const VERSION: u32 = 1;
fn contract_type() -> ContractType {
ContractType::boarding()
}
fn script_pubkey(&self, ctx: &ContractContext) -> Result<ScriptBuf, Error> {
Ok(self.boarding_output(ctx)?.script_pubkey())
}
fn spendable_paths(&self, ctx: &ContractContext) -> Result<Vec<SpendPath>, Error> {
let boarding_output = self.boarding_output(ctx)?;
let (forfeit_script, forfeit_control_block) = boarding_output.forfeit_spend_info();
let (exit_script, exit_control_block) = boarding_output.exit_spend_info();
Ok(vec![
SpendPath::new(
SpendPathKind::Forfeit,
forfeit_script,
forfeit_control_block,
),
SpendPath::new(SpendPathKind::Exit, exit_script, exit_control_block),
])
}
fn spendable_selections(&self, ctx: &ContractContext) -> Result<Vec<SpendSelection>, Error> {
Ok(self
.spendable_paths(ctx)?
.into_iter()
.map(|path| {
if path.kind == SpendPathKind::Exit {
path.select().with_sequence(self.exit_delay)
} else {
path.select()
}
})
.collect())
}
}
impl BoardingContract {
pub fn boarding_output(&self, ctx: &ContractContext) -> Result<BoardingOutput, Error> {
BoardingOutput::new(
ctx.secp(),
self.server,
self.owner,
self.exit_delay,
ctx.network(),
)
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct VhtlcContract {
pub options: VhtlcOptions,
}
impl ContractSpec for VhtlcContract {
const VERSION: u32 = 1;
fn contract_type() -> ContractType {
ContractType::vhtlc()
}
fn script_pubkey(&self, ctx: &ContractContext) -> Result<ScriptBuf, Error> {
let script = crate::vhtlc::VhtlcScript::new(self.options.clone(), ctx.network())
.map_err(|e| Error::ad_hoc(format!("failed to build vhtlc: {e}")))?;
Ok(script.script_pubkey())
}
fn spendable_paths(&self, ctx: &ContractContext) -> Result<Vec<SpendPath>, Error> {
let script = crate::vhtlc::VhtlcScript::new(self.options.clone(), ctx.network())
.map_err(|e| Error::ad_hoc(format!("failed to build vhtlc: {e}")))?;
script
.get_script_map()
.into_iter()
.map(|(name, tapscript)| {
let control_block = script
.taproot_spend_info()
.control_block(&(tapscript.clone(), bitcoin::taproot::LeafVersion::TapScript))
.ok_or_else(|| Error::ad_hoc("missing vhtlc control block"))?;
Ok(SpendPath {
kind: SpendPathKind::from_vhtlc_name(name),
script: tapscript,
control_block,
})
})
.collect()
}
fn spendable_selections(&self, ctx: &ContractContext) -> Result<Vec<SpendSelection>, Error> {
Ok(self
.spendable_paths(ctx)?
.into_iter()
.filter_map(|path| match path.kind {
SpendPathKind::VhtlcClaim | SpendPathKind::VhtlcUnilateralClaim => None,
SpendPathKind::VhtlcRefundWithoutReceiver => Some(path.select().with_locktime(
absolute::LockTime::from_consensus(self.options.refund_locktime),
)),
SpendPathKind::VhtlcUnilateralRefund => Some(
path.select()
.with_sequence(self.options.unilateral_refund_delay),
),
SpendPathKind::VhtlcUnilateralRefundWithoutReceiver => Some(
path.select()
.with_sequence(self.options.unilateral_refund_without_receiver_delay),
),
SpendPathKind::Forfeit
| SpendPathKind::Exit
| SpendPathKind::Delegate
| SpendPathKind::VhtlcRefund
| SpendPathKind::Custom(_) => Some(path.select()),
})
.collect())
}
}
#[cfg(test)]
mod tests {
use super::*;
use bitcoin::hashes::ripemd160;
use bitcoin::hashes::Hash;
use std::str::FromStr;
fn test_key(hex: &str) -> XOnlyPublicKey {
XOnlyPublicKey::from_str(hex).unwrap()
}
fn vhtlc_contract() -> VhtlcContract {
VhtlcContract {
options: VhtlcOptions {
sender: test_key(
"f874c4fd782a63a2b078b424f9f4e5edae622880a29fe572bd71bee45ab55ea0",
),
receiver: test_key(
"b02b1a956cd0ed91d1a06b65ae976470ffd4b8c9215f446f266bb74fedd153b6",
),
server: test_key(
"e35799157be4b37565bb5afe4d04e6a0fa0a4b6a4f4e48b0d904685d253cdbdb",
),
preimage_hash: ripemd160::Hash::hash(b"preimage"),
refund_locktime: 500,
unilateral_claim_delay: Sequence::from_height(10),
unilateral_refund_delay: Sequence::from_height(20),
unilateral_refund_without_receiver_delay: Sequence::from_height(30),
},
}
}
#[test]
fn vhtlc_spendable_selections_include_required_constraints() {
let ctx = ContractContext::new(Network::Regtest);
let selections = vhtlc_contract().spendable_selections(&ctx).unwrap();
assert!(!selections
.iter()
.any(|selection| selection.path.kind == SpendPathKind::VhtlcClaim));
assert!(!selections
.iter()
.any(|selection| selection.path.kind == SpendPathKind::VhtlcUnilateralClaim));
let refund_without_receiver = selections
.iter()
.find(|selection| selection.path.kind == SpendPathKind::VhtlcRefundWithoutReceiver)
.unwrap();
assert_eq!(
refund_without_receiver.locktime,
Some(absolute::LockTime::from_consensus(500))
);
let unilateral_refund = selections
.iter()
.find(|selection| selection.path.kind == SpendPathKind::VhtlcUnilateralRefund)
.unwrap();
assert_eq!(unilateral_refund.sequence, Some(Sequence::from_height(20)));
let unilateral_refund_without_receiver = selections
.iter()
.find(|selection| {
selection.path.kind == SpendPathKind::VhtlcUnilateralRefundWithoutReceiver
})
.unwrap();
assert_eq!(
unilateral_refund_without_receiver.sequence,
Some(Sequence::from_height(30))
);
}
}