use crate::ark_address::ArkAddress;
use crate::script::csv_sig_script;
use crate::script::multisig_3_of_3_script;
use crate::script::multisig_script;
use crate::script::tr_script_pubkey;
use crate::Error;
use crate::ErrorContext;
use crate::ExitDelayKind;
use crate::UNSPENDABLE_KEY;
use bitcoin::key::PublicKey;
use bitcoin::key::Secp256k1;
use bitcoin::key::Verification;
use bitcoin::taproot;
use bitcoin::taproot::LeafVersion;
use bitcoin::taproot::TaprootBuilder;
use bitcoin::taproot::TaprootSpendInfo;
use bitcoin::Address;
use bitcoin::Network;
use bitcoin::ScriptBuf;
use bitcoin::XOnlyPublicKey;
use std::time::Duration;
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct Vtxo {
server_forfeit: XOnlyPublicKey,
owner: XOnlyPublicKey,
owner_unilateral_exit: XOnlyPublicKey,
delegator: Option<XOnlyPublicKey>,
spend_info: TaprootSpendInfo,
tapscripts: Vec<ScriptBuf>,
address: Address,
exit_delay: bitcoin::Sequence,
exit_delay_kind: ExitDelayKind,
network: Network,
}
impl Vtxo {
pub fn new_with_custom_scripts<C>(
secp: &Secp256k1<C>,
server_forfeit: XOnlyPublicKey,
owner: XOnlyPublicKey,
scripts: Vec<ScriptBuf>,
exit_delay: bitcoin::Sequence,
network: Network,
) -> Result<Self, Error>
where
C: Verification,
{
let vtxo = Self::new_with_custom_scripts_and_split_owner_keys(
secp,
server_forfeit,
owner,
owner,
scripts,
exit_delay,
network,
)?;
Ok(vtxo)
}
pub fn new_with_custom_scripts_and_split_owner_keys<C>(
secp: &Secp256k1<C>,
server_forfeit: XOnlyPublicKey,
owner: XOnlyPublicKey,
owner_unilateral_exit: XOnlyPublicKey,
scripts: Vec<ScriptBuf>,
exit_delay: bitcoin::Sequence,
network: Network,
) -> Result<Self, Error>
where
C: Verification,
{
let unspendable_key: PublicKey = UNSPENDABLE_KEY
.parse()
.map_err(|e| Error::ad_hoc(format!("invalid unspendable key: {e}")))?;
let (unspendable_key, _) = unspendable_key.inner.x_only_public_key();
let leaf_distribution = calculate_leaf_depths(scripts.len());
let mut builder = TaprootBuilder::new();
for (script, depth) in scripts.iter().zip(leaf_distribution.iter()) {
builder = builder
.add_leaf(*depth as u8, script.clone())
.map_err(Error::ad_hoc)?;
}
let spend_info = builder
.finalize(secp, unspendable_key)
.map_err(|_| Error::ad_hoc("failed to finalize Taproot tree"))?;
let exit_delay_kind = ExitDelayKind::from_sequence(exit_delay)?;
let script_pubkey = tr_script_pubkey(&spend_info);
let address = Address::from_script(&script_pubkey, network)
.map_err(|e| Error::ad_hoc(format!("invalid script: {e}")))?;
Ok(Self {
server_forfeit,
owner,
owner_unilateral_exit,
delegator: None,
spend_info,
tapscripts: scripts,
address,
exit_delay,
exit_delay_kind,
network,
})
}
pub fn new_default<C>(
secp: &Secp256k1<C>,
server_signer: XOnlyPublicKey,
owner: XOnlyPublicKey,
exit_delay: bitcoin::Sequence,
network: Network,
) -> Result<Self, Error>
where
C: Verification,
{
let forfeit_script = multisig_script(server_signer, owner);
let redeem_script = csv_sig_script(exit_delay, owner);
Self::new_with_custom_scripts(
secp,
server_signer,
owner,
vec![forfeit_script, redeem_script],
exit_delay,
network,
)
}
pub fn new_with_delegator<C>(
secp: &Secp256k1<C>,
server_signer: XOnlyPublicKey,
owner: XOnlyPublicKey,
delegator: XOnlyPublicKey,
exit_delay: bitcoin::Sequence,
network: Network,
) -> Result<Self, Error>
where
C: Verification,
{
let forfeit_script = multisig_script(server_signer, owner);
let redeem_script = csv_sig_script(exit_delay, owner);
let delegate_script = multisig_3_of_3_script(owner, delegator, server_signer);
let mut vtxo = Self::new_with_custom_scripts(
secp,
server_signer,
owner,
vec![forfeit_script, redeem_script, delegate_script],
exit_delay,
network,
)?;
vtxo.delegator = Some(delegator);
Ok(vtxo)
}
pub fn script_pubkey(&self) -> ScriptBuf {
self.address.script_pubkey()
}
pub fn address(&self) -> &Address {
&self.address
}
pub fn owner_pk(&self) -> XOnlyPublicKey {
self.owner
}
pub fn server_pk(&self) -> XOnlyPublicKey {
self.server_forfeit
}
pub fn delegator_pk(&self) -> Option<XOnlyPublicKey> {
self.delegator
}
pub fn exit_delay(&self) -> bitcoin::Sequence {
self.exit_delay
}
pub fn to_ark_address(&self) -> ArkAddress {
let vtxo_tap_key = self.spend_info.output_key();
ArkAddress::new(self.network, self.server_forfeit, vtxo_tap_key)
}
pub fn get_spend_info(&self, script: ScriptBuf) -> Result<taproot::ControlBlock, Error> {
let control_block = self
.spend_info
.control_block(&(script, LeafVersion::TapScript))
.ok_or(Error::ad_hoc("could not build control block for script"))?;
Ok(control_block)
}
pub fn forfeit_spend_info(&self) -> Result<(ScriptBuf, taproot::ControlBlock), Error> {
let forfeit_script = multisig_script(self.server_forfeit, self.owner);
let control_block = self
.get_spend_info(forfeit_script.clone())
.context("missing default forfeit script")?;
Ok((forfeit_script, control_block))
}
pub fn exit_spend_info(&self) -> Result<(ScriptBuf, taproot::ControlBlock), Error> {
let exit_script = csv_sig_script(self.exit_delay, self.owner_unilateral_exit);
let control_block = self
.get_spend_info(exit_script.clone())
.context("missing default exit script")?;
Ok((exit_script, control_block))
}
pub fn delegate_spend_info(&self) -> Result<(ScriptBuf, taproot::ControlBlock), Error> {
let delegator = self
.delegator
.ok_or(Error::ad_hoc("VTXO has no delegate path"))?;
let delegate_script = multisig_3_of_3_script(self.owner, delegator, self.server_forfeit);
let control_block = self
.get_spend_info(delegate_script.clone())
.context("missing delegate script")?;
Ok((delegate_script, control_block))
}
pub fn tapscripts(&self) -> Vec<ScriptBuf> {
self.tapscripts.clone()
}
pub fn can_be_claimed_unilaterally_by_owner(
&self,
now: Duration,
confirmation_blocktime: Duration,
confirmations: u64,
) -> bool {
match self.exit_delay_kind {
ExitDelayKind::Time(seconds) => {
let exit_path_time = confirmation_blocktime + seconds;
now > exit_path_time
}
ExitDelayKind::Blocks(confirmations_required) => {
confirmations >= confirmations_required
}
}
}
}
fn calculate_leaf_depths(n: usize) -> Vec<usize> {
if n == 0 {
return vec![];
}
if n == 1 {
return vec![0]; }
if n == 2 {
return vec![1, 1];
}
let min_depth = (n as f64).log2().ceil() as usize;
let nodes_at_max_depth = n - (1 << (min_depth - 1)) + 1;
let nodes_at_min_depth = (1 << min_depth) - nodes_at_max_depth;
let mut result = Vec::with_capacity(n);
for _ in 0..nodes_at_max_depth {
result.push(min_depth);
}
for _ in 0..nodes_at_min_depth {
result.push(min_depth - 1);
}
result
}
#[cfg(test)]
mod tests {
use super::*;
use bitcoin::secp256k1::Secp256k1;
use std::str::FromStr;
fn test_keys() -> (XOnlyPublicKey, XOnlyPublicKey, XOnlyPublicKey) {
let server = XOnlyPublicKey::from_str(
"18845781f631c48f1c9709e23092067d06837f30aa0cd0544ac887fe91ddd166",
)
.unwrap();
let owner = XOnlyPublicKey::from_str(
"28845781f631c48f1c9709e23092067d06837f30aa0cd0544ac887fe91ddd166",
)
.unwrap();
let delegator = XOnlyPublicKey::from_str(
"38845781f631c48f1c9709e23092067d06837f30aa0cd0544ac887fe91ddd166",
)
.unwrap();
(server, owner, delegator)
}
#[test]
fn new_with_delegator_has_three_tapscripts() {
let secp = Secp256k1::new();
let (server, owner, delegator) = test_keys();
let exit_delay = bitcoin::Sequence::from_seconds_ceil(86400).unwrap();
let vtxo = Vtxo::new_with_delegator(
&secp,
server,
owner,
delegator,
exit_delay,
Network::Regtest,
)
.unwrap();
assert_eq!(vtxo.tapscripts().len(), 3);
assert_eq!(vtxo.delegator_pk(), Some(delegator));
}
#[test]
fn delegator_vtxo_all_spend_paths_resolve() {
let secp = Secp256k1::new();
let (server, owner, delegator) = test_keys();
let exit_delay = bitcoin::Sequence::from_seconds_ceil(86400).unwrap();
let vtxo = Vtxo::new_with_delegator(
&secp,
server,
owner,
delegator,
exit_delay,
Network::Regtest,
)
.unwrap();
let (forfeit_script, _cb) = vtxo.forfeit_spend_info().unwrap();
let (exit_script, _cb) = vtxo.exit_spend_info().unwrap();
let (delegate_script, _cb) = vtxo.delegate_spend_info().unwrap();
assert_ne!(forfeit_script, exit_script);
assert_ne!(forfeit_script, delegate_script);
assert_ne!(exit_script, delegate_script);
}
#[test]
fn default_vtxo_has_no_delegate_path() {
let secp = Secp256k1::new();
let (server, owner, _) = test_keys();
let exit_delay = bitcoin::Sequence::from_seconds_ceil(86400).unwrap();
let vtxo = Vtxo::new_default(&secp, server, owner, exit_delay, Network::Regtest).unwrap();
assert!(vtxo.delegator_pk().is_none());
assert!(vtxo.delegate_spend_info().is_err());
}
#[test]
fn delegator_vtxo_address_differs_from_default() {
let secp = Secp256k1::new();
let (server, owner, delegator) = test_keys();
let exit_delay = bitcoin::Sequence::from_seconds_ceil(86400).unwrap();
let default =
Vtxo::new_default(&secp, server, owner, exit_delay, Network::Regtest).unwrap();
let with_delegator = Vtxo::new_with_delegator(
&secp,
server,
owner,
delegator,
exit_delay,
Network::Regtest,
)
.unwrap();
assert_ne!(default.address(), with_delegator.address());
}
}