pub mod policy;
pub mod raw;
pub(crate) mod genesis;
mod validation;
pub use self::validation::VtxoValidationError;
pub use self::policy::{Policy, VtxoPolicy, VtxoPolicyKind, ServerVtxoPolicy};
pub(crate) use self::genesis::{GenesisItem, GenesisTransition};
pub use self::policy::{
PubkeyVtxoPolicy, CheckpointVtxoPolicy, ExpiryVtxoPolicy, HarkLeafVtxoPolicy,
ServerHtlcRecvVtxoPolicy, ServerHtlcSendVtxoPolicy
};
pub use self::policy::clause::{
VtxoClause, DelayedSignClause, DelayedTimelockSignClause, HashDelaySignClause,
TapScriptClause,
};
pub type ServerVtxo<G = Bare> = Vtxo<G, ServerVtxoPolicy>;
use std::borrow::Cow;
use std::iter::FusedIterator;
use std::{fmt, io};
use std::str::FromStr;
use bitcoin::{
taproot, Amount, OutPoint, ScriptBuf, Sequence, Transaction, TxIn, TxOut, Txid, Weight, Witness
};
use bitcoin::absolute::LockTime;
use bitcoin::hashes::{sha256, Hash};
use bitcoin::secp256k1::{schnorr, PublicKey, XOnlyPublicKey};
use bitcoin::taproot::TapTweakHash;
use bitcoin_ext::{fee, BlockDelta, BlockHeight, TxOutExt};
use crate::vtxo::policy::HarkForfeitVtxoPolicy;
use crate::scripts;
use crate::encode::{
LengthPrefixedVector, OversizedVectorError, ProtocolDecodingError, ProtocolEncoding, ReadExt,
WriteExt,
};
use crate::lightning::PaymentHash;
use crate::tree::signed::{UnlockHash, UnlockPreimage};
pub const EXIT_TX_WEIGHT: Weight = Weight::from_vb_unchecked(124);
const VTXO_ENCODING_VERSION: u16 = 2;
const VTXO_NO_FEE_AMOUNT_VERSION: u16 = 1;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, thiserror::Error)]
#[error("failed to parse vtxo id, must be 36 bytes")]
pub struct VtxoIdParseError;
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct VtxoId([u8; 36]);
impl VtxoId {
pub const ENCODE_SIZE: usize = 36;
pub fn from_slice(b: &[u8]) -> Result<VtxoId, VtxoIdParseError> {
if b.len() == 36 {
let mut ret = [0u8; 36];
ret[..].copy_from_slice(&b[0..36]);
Ok(Self(ret))
} else {
Err(VtxoIdParseError)
}
}
pub fn to_point(&self) -> OutPoint {
let txid = Txid::from_byte_array(self.0[0..32].try_into().expect("32 bytes"));
let vout_bytes = [self.0[32], self.0[33], self.0[34], self.0[35]];
let vout = u32::from_le_bytes(vout_bytes);
OutPoint::new(txid, vout)
}
#[deprecated(since = "0.1.3", note = "use to_point instead")]
pub fn utxo(self) -> OutPoint {
self.to_point()
}
pub fn to_bytes(self) -> [u8; 36] {
self.0
}
}
impl From<OutPoint> for VtxoId {
fn from(p: OutPoint) -> VtxoId {
let mut ret = [0u8; 36];
ret[0..32].copy_from_slice(&p.txid[..]);
ret[32..].copy_from_slice(&p.vout.to_le_bytes());
VtxoId(ret)
}
}
impl AsRef<[u8]> for VtxoId {
fn as_ref(&self) -> &[u8] {
&self.0
}
}
impl fmt::Display for VtxoId {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
fmt::Display::fmt(&self.to_point(), f)
}
}
impl fmt::Debug for VtxoId {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
fmt::Display::fmt(self, f)
}
}
impl FromStr for VtxoId {
type Err = VtxoIdParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(OutPoint::from_str(s).map_err(|_| VtxoIdParseError)?.into())
}
}
impl serde::Serialize for VtxoId {
fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
if s.is_human_readable() {
s.collect_str(self)
} else {
s.serialize_bytes(self.as_ref())
}
}
}
impl<'de> serde::Deserialize<'de> for VtxoId {
fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
struct Visitor;
impl<'de> serde::de::Visitor<'de> for Visitor {
type Value = VtxoId;
fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "a VtxoId")
}
fn visit_bytes<E: serde::de::Error>(self, v: &[u8]) -> Result<Self::Value, E> {
VtxoId::from_slice(v).map_err(serde::de::Error::custom)
}
fn visit_str<E: serde::de::Error>(self, v: &str) -> Result<Self::Value, E> {
VtxoId::from_str(v).map_err(serde::de::Error::custom)
}
}
if d.is_human_readable() {
d.deserialize_str(Visitor)
} else {
d.deserialize_bytes(Visitor)
}
}
}
impl ProtocolEncoding for VtxoId {
fn encode<W: io::Write + ?Sized>(&self, w: &mut W) -> Result<(), io::Error> {
w.emit_slice(&self.0)
}
fn decode<R: io::Read + ?Sized>(r: &mut R) -> Result<Self, ProtocolDecodingError> {
let array: [u8; 36] = r.read_byte_array()
.map_err(|_| ProtocolDecodingError::invalid("invalid vtxo id. Expected 36 bytes"))?;
Ok(VtxoId(array))
}
}
pub(crate) fn exit_clause(
user_pubkey: PublicKey,
exit_delta: BlockDelta,
) -> ScriptBuf {
scripts::delayed_sign(exit_delta, user_pubkey.x_only_public_key().0)
}
pub fn create_exit_tx(
prevout: OutPoint,
output: TxOut,
signature: Option<&schnorr::Signature>,
fee: Amount,
) -> Transaction {
Transaction {
version: bitcoin::transaction::Version(3),
lock_time: LockTime::ZERO,
input: vec![TxIn {
previous_output: prevout,
script_sig: ScriptBuf::new(),
sequence: Sequence::ZERO,
witness: {
let mut ret = Witness::new();
if let Some(sig) = signature {
ret.push(&sig[..]);
}
ret
},
}],
output: vec![output, fee::fee_anchor_with_amount(fee)],
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum MaybePreimage {
Preimage([u8; 32]),
Hash(sha256::Hash),
}
impl MaybePreimage {
pub fn hash(&self) -> sha256::Hash {
match self {
Self::Preimage(p) => sha256::Hash::hash(p),
Self::Hash(h) => *h,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub struct VtxoTxIterItem {
pub tx: Transaction,
pub output_idx: usize,
}
pub struct VtxoTxIter<'a, P: Policy = VtxoPolicy> {
vtxo: &'a Vtxo<Full, P>,
prev: OutPoint,
genesis_idx: usize,
current_amount: Amount,
}
impl<'a, P: Policy> VtxoTxIter<'a, P> {
fn new(vtxo: &'a Vtxo<Full, P>) -> VtxoTxIter<'a, P> {
let onchain_amount = vtxo.chain_anchor_amount()
.expect("This should only fail if the VTXO is invalid.");
VtxoTxIter {
prev: vtxo.anchor_point,
vtxo: vtxo,
genesis_idx: 0,
current_amount: onchain_amount,
}
}
}
impl<'a, P: Policy> Iterator for VtxoTxIter<'a, P> {
type Item = VtxoTxIterItem;
fn next(&mut self) -> Option<Self::Item> {
let item = self.vtxo.genesis.items.get(self.genesis_idx)?;
let next_amount = self.current_amount.checked_sub(
item.other_output_sum().expect("we calculated this amount beforehand")
).expect("we calculated this amount beforehand");
let next_output = if let Some(item) = self.vtxo.genesis.items.get(self.genesis_idx + 1) {
item.transition.input_txout(
next_amount,
self.vtxo.server_pubkey,
self.vtxo.expiry_height,
self.vtxo.exit_delta,
)
} else {
self.vtxo.policy.txout(
self.vtxo.amount,
self.vtxo.server_pubkey,
self.vtxo.exit_delta,
self.vtxo.expiry_height,
)
};
let tx = item.tx(self.prev, next_output, self.vtxo.server_pubkey, self.vtxo.expiry_height);
self.prev = OutPoint::new(tx.compute_txid(), item.output_idx as u32);
self.genesis_idx += 1;
self.current_amount = next_amount;
let output_idx = item.output_idx as usize;
Some(VtxoTxIterItem { tx, output_idx })
}
fn size_hint(&self) -> (usize, Option<usize>) {
let len = self.vtxo.genesis.items.len().saturating_sub(self.genesis_idx);
(len, Some(len))
}
}
impl<'a, P: Policy> ExactSizeIterator for VtxoTxIter<'a, P> {}
impl<'a, P: Policy> FusedIterator for VtxoTxIter<'a, P> {}
#[derive(Debug, Clone)]
pub struct Bare;
#[derive(Debug, Clone)]
pub struct Full {
pub(crate) items: Vec<genesis::GenesisItem>,
}
#[derive(Debug, Clone)]
pub struct Vtxo<G = Full, P = VtxoPolicy> {
pub(crate) policy: P,
pub(crate) amount: Amount,
pub(crate) expiry_height: BlockHeight,
pub(crate) server_pubkey: PublicKey,
pub(crate) exit_delta: BlockDelta,
pub(crate) anchor_point: OutPoint,
pub(crate) genesis: G,
pub(crate) point: OutPoint,
}
impl<G, P: Policy> Vtxo<G, P> {
pub fn id(&self) -> VtxoId {
self.point.into()
}
pub fn point(&self) -> OutPoint {
self.point
}
pub fn amount(&self) -> Amount {
self.amount
}
pub fn chain_anchor(&self) -> OutPoint {
self.anchor_point
}
pub fn policy(&self) -> &P {
&self.policy
}
pub fn policy_type(&self) -> VtxoPolicyKind {
self.policy.policy_type()
}
pub fn expiry_height(&self) -> BlockHeight {
self.expiry_height
}
pub fn server_pubkey(&self) -> PublicKey {
self.server_pubkey
}
pub fn exit_delta(&self) -> BlockDelta {
self.exit_delta
}
pub fn output_taproot(&self) -> taproot::TaprootSpendInfo {
self.policy.taproot(self.server_pubkey, self.exit_delta, self.expiry_height)
}
pub fn output_script_pubkey(&self) -> ScriptBuf {
self.policy.script_pubkey(self.server_pubkey, self.exit_delta, self.expiry_height)
}
pub fn txout(&self) -> TxOut {
self.policy.txout(self.amount, self.server_pubkey, self.exit_delta, self.expiry_height)
}
pub fn to_bare(&self) -> Vtxo<Bare, P> {
Vtxo {
point: self.point,
policy: self.policy.clone(),
amount: self.amount,
expiry_height: self.expiry_height,
server_pubkey: self.server_pubkey,
exit_delta: self.exit_delta,
anchor_point: self.anchor_point,
genesis: Bare,
}
}
pub fn into_bare(self) -> Vtxo<Bare, P> {
Vtxo {
point: self.point,
policy: self.policy,
amount: self.amount,
expiry_height: self.expiry_height,
server_pubkey: self.server_pubkey,
exit_delta: self.exit_delta,
anchor_point: self.anchor_point,
genesis: Bare,
}
}
}
impl<P: Policy> Vtxo<Bare, P> {
pub fn new(
point: OutPoint,
policy: P,
amount: Amount,
expiry_height: BlockHeight,
server_pubkey: PublicKey,
exit_delta: BlockDelta,
anchor_point: OutPoint,
) -> Self {
Vtxo { point, policy, amount, expiry_height, server_pubkey, exit_delta, anchor_point, genesis: Bare }
}
}
impl<P: Policy> Vtxo<Full, P> {
pub fn exit_depth(&self) -> u16 {
self.genesis.items.len() as u16
}
pub fn past_arkoor_pubkeys(&self) -> Vec<Vec<PublicKey>> {
self.genesis.items.iter().filter_map(|g| {
match &g.transition {
GenesisTransition::Arkoor(inner) => Some(inner.client_cosigners().collect()),
_ => None,
}
}).collect()
}
pub fn has_all_witnesses(&self) -> bool {
self.genesis.items.iter().all(|g| g.transition.has_all_witnesses())
}
pub fn is_standard(&self) -> bool {
self.txout().is_standard() && self.genesis.items.iter()
.all(|i| i.other_outputs.iter().all(|o| o.is_standard()))
}
pub fn unlock_hash(&self) -> Option<UnlockHash> {
match self.genesis.items.last()?.transition {
GenesisTransition::HashLockedCosigned(ref inner) => Some(inner.unlock.hash()),
_ => None,
}
}
pub fn provide_unlock_signature(&mut self, signature: schnorr::Signature) -> bool {
match self.genesis.items.last_mut().map(|g| &mut g.transition) {
Some(GenesisTransition::HashLockedCosigned(inner)) => {
inner.signature.replace(signature);
true
},
_ => false,
}
}
pub fn provide_unlock_preimage(&mut self, preimage: UnlockPreimage) -> bool {
match self.genesis.items.last_mut().map(|g| &mut g.transition) {
Some(GenesisTransition::HashLockedCosigned(ref mut inner)) => {
if inner.unlock.hash() == UnlockHash::hash(&preimage) {
inner.unlock = MaybePreimage::Preimage(preimage);
true
} else {
false
}
},
_ => false,
}
}
pub fn transactions(&self) -> VtxoTxIter<'_, P> {
VtxoTxIter::new(self)
}
pub fn validate(
&self,
chain_anchor_tx: &Transaction,
) -> Result<(), VtxoValidationError> {
self::validation::validate(self, chain_anchor_tx)
}
pub fn validate_unsigned(
&self,
chain_anchor_tx: &Transaction,
) -> Result<(), VtxoValidationError> {
self::validation::validate_unsigned(self, chain_anchor_tx)
}
pub(crate) fn chain_anchor_amount(&self) -> Option<Amount> {
self.amount.checked_add(self.genesis.items.iter().try_fold(Amount::ZERO, |sum, i| {
i.other_output_sum().and_then(|amt| sum.checked_add(amt))
})?)
}
}
impl<G> Vtxo<G, VtxoPolicy> {
pub fn user_pubkey(&self) -> PublicKey {
self.policy.user_pubkey()
}
pub fn arkoor_pubkey(&self) -> Option<PublicKey> {
self.policy.arkoor_pubkey()
}
}
impl Vtxo<Full, VtxoPolicy> {
#[cfg(any(test, feature = "test-util"))]
pub fn finalize_hark_leaf(
&mut self,
user_key: &bitcoin::secp256k1::Keypair,
server_key: &bitcoin::secp256k1::Keypair,
chain_anchor: &Transaction,
unlock_preimage: UnlockPreimage,
) {
use crate::tree::signed::{LeafVtxoCosignContext, LeafVtxoCosignResponse};
let (ctx, req) = LeafVtxoCosignContext::new(self, chain_anchor, user_key);
let cosign = LeafVtxoCosignResponse::new_cosign(&req, self, chain_anchor, server_key);
assert!(ctx.finalize(self, cosign));
assert!(self.provide_unlock_preimage(unlock_preimage));
}
}
impl<G> Vtxo<G, ServerVtxoPolicy> {
pub fn try_into_user_vtxo(self) -> Result<Vtxo<G, VtxoPolicy>, ServerVtxo<G>> {
if let Some(p) = self.policy.clone().into_user_policy() {
Ok(Vtxo {
policy: p,
amount: self.amount,
expiry_height: self.expiry_height,
server_pubkey: self.server_pubkey,
exit_delta: self.exit_delta,
anchor_point: self.anchor_point,
genesis: self.genesis,
point: self.point,
})
} else {
Err(self)
}
}
}
impl<G, P: Policy> PartialEq for Vtxo<G, P> {
fn eq(&self, other: &Self) -> bool {
PartialEq::eq(&self.id(), &other.id())
}
}
impl<G, P: Policy> Eq for Vtxo<G, P> {}
impl<G, P: Policy> PartialOrd for Vtxo<G, P> {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
PartialOrd::partial_cmp(&self.id(), &other.id())
}
}
impl<G, P: Policy> Ord for Vtxo<G, P> {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
Ord::cmp(&self.id(), &other.id())
}
}
impl<G, P: Policy> std::hash::Hash for Vtxo<G, P> {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
std::hash::Hash::hash(&self.id(), state)
}
}
impl<G, P: Policy> AsRef<Vtxo<G, P>> for Vtxo<G, P> {
fn as_ref(&self) -> &Vtxo<G, P> {
self
}
}
impl<G> From<Vtxo<G>> for ServerVtxo<G> {
fn from(vtxo: Vtxo<G>) -> ServerVtxo<G> {
ServerVtxo {
policy: vtxo.policy.into(),
amount: vtxo.amount,
expiry_height: vtxo.expiry_height,
server_pubkey: vtxo.server_pubkey,
exit_delta: vtxo.exit_delta,
anchor_point: vtxo.anchor_point,
genesis: vtxo.genesis,
point: vtxo.point,
}
}
}
pub trait VtxoRef<P: Policy = VtxoPolicy> {
fn vtxo_id(&self) -> VtxoId;
fn as_bare_vtxo(&self) -> Option<Cow<'_, Vtxo<Bare, P>>> { None }
fn as_full_vtxo(&self) -> Option<&Vtxo<Full, P>> { None }
fn into_full_vtxo(self) -> Option<Vtxo<Full, P>> where Self: Sized;
}
impl<P: Policy> VtxoRef<P> for VtxoId {
fn vtxo_id(&self) -> VtxoId { *self }
fn into_full_vtxo(self) -> Option<Vtxo<Full, P>> { None }
}
impl<'a, P: Policy> VtxoRef<P> for &'a VtxoId {
fn vtxo_id(&self) -> VtxoId { **self }
fn into_full_vtxo(self) -> Option<Vtxo<Full, P>> { None }
}
impl<P: Policy> VtxoRef<P> for Vtxo<Bare, P> {
fn vtxo_id(&self) -> VtxoId { self.id() }
fn as_bare_vtxo(&self) -> Option<Cow<'_, Vtxo<Bare, P>>> { Some(Cow::Borrowed(self)) }
fn into_full_vtxo(self) -> Option<Vtxo<Full, P>> { None }
}
impl<'a, P: Policy> VtxoRef<P> for &'a Vtxo<Bare, P> {
fn vtxo_id(&self) -> VtxoId { self.id() }
fn as_bare_vtxo(&self) -> Option<Cow<'_, Vtxo<Bare, P>>> { Some(Cow::Borrowed(*self)) }
fn into_full_vtxo(self) -> Option<Vtxo<Full, P>> { None }
}
impl<P: Policy> VtxoRef<P> for Vtxo<Full, P> {
fn vtxo_id(&self) -> VtxoId { self.id() }
fn as_bare_vtxo(&self) -> Option<Cow<'_, Vtxo<Bare, P>>> { Some(Cow::Owned(self.to_bare())) }
fn as_full_vtxo(&self) -> Option<&Vtxo<Full, P>> { Some(self) }
fn into_full_vtxo(self) -> Option<Vtxo<Full, P>> { Some(self) }
}
impl<'a, P: Policy> VtxoRef<P> for &'a Vtxo<Full, P> {
fn vtxo_id(&self) -> VtxoId { self.id() }
fn as_bare_vtxo(&self) -> Option<Cow<'_, Vtxo<Bare, P>>> { Some(Cow::Owned(self.to_bare())) }
fn as_full_vtxo(&self) -> Option<&Vtxo<Full, P>> { Some(*self) }
fn into_full_vtxo(self) -> Option<Vtxo<Full, P>> { Some(self.clone()) }
}
const VTXO_POLICY_PUBKEY: u8 = 0x00;
const VTXO_POLICY_SERVER_HTLC_SEND: u8 = 0x01;
const VTXO_POLICY_SERVER_HTLC_RECV: u8 = 0x02;
const VTXO_POLICY_CHECKPOINT: u8 = 0x03;
const VTXO_POLICY_EXPIRY: u8 = 0x04;
const VTXO_POLICY_HARK_LEAF: u8 = 0x05;
const VTXO_POLICY_HARK_FORFEIT: u8 = 0x06;
const VTXO_POLICY_SERVER_OWNED: u8 = 0x07;
impl ProtocolEncoding for VtxoPolicy {
fn encode<W: io::Write + ?Sized>(&self, w: &mut W) -> Result<(), io::Error> {
match self {
Self::Pubkey(PubkeyVtxoPolicy { user_pubkey }) => {
w.emit_u8(VTXO_POLICY_PUBKEY)?;
user_pubkey.encode(w)?;
},
Self::ServerHtlcSend(ServerHtlcSendVtxoPolicy { user_pubkey, payment_hash, htlc_expiry }) => {
w.emit_u8(VTXO_POLICY_SERVER_HTLC_SEND)?;
user_pubkey.encode(w)?;
payment_hash.to_sha256_hash().encode(w)?;
w.emit_u32(*htlc_expiry)?;
},
Self::ServerHtlcRecv(ServerHtlcRecvVtxoPolicy {
user_pubkey, payment_hash, htlc_expiry, htlc_expiry_delta,
}) => {
w.emit_u8(VTXO_POLICY_SERVER_HTLC_RECV)?;
user_pubkey.encode(w)?;
payment_hash.to_sha256_hash().encode(w)?;
w.emit_u32(*htlc_expiry)?;
w.emit_u16(*htlc_expiry_delta)?;
},
}
Ok(())
}
fn decode<R: io::Read + ?Sized>(r: &mut R) -> Result<Self, ProtocolDecodingError> {
let type_byte = r.read_u8()?;
decode_vtxo_policy(type_byte, r)
}
}
fn decode_vtxo_policy<R: io::Read + ?Sized>(
type_byte: u8,
r: &mut R,
) -> Result<VtxoPolicy, ProtocolDecodingError> {
match type_byte {
VTXO_POLICY_PUBKEY => {
let user_pubkey = PublicKey::decode(r)?;
Ok(VtxoPolicy::Pubkey(PubkeyVtxoPolicy { user_pubkey }))
},
VTXO_POLICY_SERVER_HTLC_SEND => {
let user_pubkey = PublicKey::decode(r)?;
let payment_hash = PaymentHash::from(sha256::Hash::decode(r)?.to_byte_array());
let htlc_expiry = r.read_u32()?;
Ok(VtxoPolicy::ServerHtlcSend(ServerHtlcSendVtxoPolicy { user_pubkey, payment_hash, htlc_expiry }))
},
VTXO_POLICY_SERVER_HTLC_RECV => {
let user_pubkey = PublicKey::decode(r)?;
let payment_hash = PaymentHash::from(sha256::Hash::decode(r)?.to_byte_array());
let htlc_expiry = r.read_u32()?;
let htlc_expiry_delta = r.read_u16()?;
Ok(VtxoPolicy::ServerHtlcRecv(ServerHtlcRecvVtxoPolicy { user_pubkey, payment_hash, htlc_expiry, htlc_expiry_delta }))
},
v => Err(ProtocolDecodingError::invalid(format_args!(
"invalid VtxoPolicy type byte: {v:#x}",
))),
}
}
impl ProtocolEncoding for ServerVtxoPolicy {
fn encode<W: io::Write + ?Sized>(&self, w: &mut W) -> Result<(), io::Error> {
match self {
Self::User(p) => p.encode(w)?,
Self::ServerOwned => {
w.emit_u8(VTXO_POLICY_SERVER_OWNED)?;
},
Self::Checkpoint(CheckpointVtxoPolicy { user_pubkey }) => {
w.emit_u8(VTXO_POLICY_CHECKPOINT)?;
user_pubkey.encode(w)?;
},
Self::Expiry(ExpiryVtxoPolicy { internal_key }) => {
w.emit_u8(VTXO_POLICY_EXPIRY)?;
internal_key.encode(w)?;
},
Self::HarkLeaf(HarkLeafVtxoPolicy { user_pubkey, unlock_hash }) => {
w.emit_u8(VTXO_POLICY_HARK_LEAF)?;
user_pubkey.encode(w)?;
unlock_hash.encode(w)?;
},
Self::HarkForfeit(HarkForfeitVtxoPolicy { user_pubkey, unlock_hash }) => {
w.emit_u8(VTXO_POLICY_HARK_FORFEIT)?;
user_pubkey.encode(w)?;
unlock_hash.encode(w)?;
},
}
Ok(())
}
fn decode<R: io::Read + ?Sized>(r: &mut R) -> Result<Self, ProtocolDecodingError> {
let type_byte = r.read_u8()?;
match type_byte {
VTXO_POLICY_PUBKEY | VTXO_POLICY_SERVER_HTLC_SEND | VTXO_POLICY_SERVER_HTLC_RECV => {
Ok(Self::User(decode_vtxo_policy(type_byte, r)?))
},
VTXO_POLICY_SERVER_OWNED => Ok(Self::ServerOwned),
VTXO_POLICY_CHECKPOINT => {
let user_pubkey = PublicKey::decode(r)?;
Ok(Self::Checkpoint(CheckpointVtxoPolicy { user_pubkey }))
},
VTXO_POLICY_EXPIRY => {
let internal_key = XOnlyPublicKey::decode(r)?;
Ok(Self::Expiry(ExpiryVtxoPolicy { internal_key }))
},
VTXO_POLICY_HARK_LEAF => {
let user_pubkey = PublicKey::decode(r)?;
let unlock_hash = sha256::Hash::decode(r)?;
Ok(Self::HarkLeaf(HarkLeafVtxoPolicy { user_pubkey, unlock_hash }))
},
VTXO_POLICY_HARK_FORFEIT => {
let user_pubkey = PublicKey::decode(r)?;
let unlock_hash = sha256::Hash::decode(r)?;
Ok(Self::HarkForfeit(HarkForfeitVtxoPolicy { user_pubkey, unlock_hash }))
},
v => Err(ProtocolDecodingError::invalid(format_args!(
"invalid ServerVtxoPolicy type byte: {v:#x}",
))),
}
}
}
const GENESIS_TRANSITION_TYPE_COSIGNED: u8 = 1;
const GENESIS_TRANSITION_TYPE_ARKOOR: u8 = 2;
const GENESIS_TRANSITION_TYPE_HASH_LOCKED_COSIGNED: u8 = 3;
impl ProtocolEncoding for GenesisTransition {
fn encode<W: io::Write + ?Sized>(&self, w: &mut W) -> Result<(), io::Error> {
match self {
Self::Cosigned(t) => {
w.emit_u8(GENESIS_TRANSITION_TYPE_COSIGNED)?;
LengthPrefixedVector::new(&t.pubkeys).encode(w)?;
t.signature.encode(w)?;
},
Self::HashLockedCosigned(t) => {
w.emit_u8(GENESIS_TRANSITION_TYPE_HASH_LOCKED_COSIGNED)?;
t.user_pubkey.encode(w)?;
t.signature.encode(w)?;
match t.unlock {
MaybePreimage::Preimage(p) => {
w.emit_u8(0)?;
w.emit_slice(&p[..])?;
},
MaybePreimage::Hash(h) => {
w.emit_u8(1)?;
w.emit_slice(&h[..])?;
},
}
},
Self::Arkoor(t) => {
w.emit_u8(GENESIS_TRANSITION_TYPE_ARKOOR)?;
LengthPrefixedVector::new(&t.client_cosigners).encode(w)?;
t.tap_tweak.encode(w)?;
t.signature.encode(w)?;
},
}
Ok(())
}
fn decode<R: io::Read + ?Sized>(r: &mut R) -> Result<Self, ProtocolDecodingError> {
match r.read_u8()? {
GENESIS_TRANSITION_TYPE_COSIGNED => {
let pubkeys = LengthPrefixedVector::decode(r)?.into_inner();
let signature = Option::<schnorr::Signature>::decode(r)?;
Ok(Self::new_cosigned(pubkeys, signature))
},
GENESIS_TRANSITION_TYPE_HASH_LOCKED_COSIGNED => {
let user_pubkey = PublicKey::decode(r)?;
let signature = Option::<schnorr::Signature>::decode(r)?;
let unlock = match r.read_u8()? {
0 => MaybePreimage::Preimage(r.read_byte_array()?),
1 => MaybePreimage::Hash(ProtocolEncoding::decode(r)?),
v => return Err(ProtocolDecodingError::invalid(format_args!(
"invalid MaybePreimage type byte: {v:#x}",
))),
};
Ok(Self::new_hash_locked_cosigned(user_pubkey, signature, unlock))
},
GENESIS_TRANSITION_TYPE_ARKOOR => {
let cosigners = LengthPrefixedVector::decode(r)?.into_inner();
let taptweak = TapTweakHash::decode(r)?;
let signature = Option::<schnorr::Signature>::decode(r)?;
Ok(Self::new_arkoor(cosigners, taptweak, signature))
},
v => Err(ProtocolDecodingError::invalid(format_args!(
"invalid GenesisTransistion type byte: {v:#x}",
))),
}
}
}
trait VtxoVersionedEncoding: Sized {
fn encode<W: io::Write + ?Sized>(&self, w: &mut W, version: u16) -> Result<(), io::Error>;
fn decode<R: io::Read + ?Sized>(
r: &mut R,
version: u16,
) -> Result<Self, ProtocolDecodingError>;
}
impl VtxoVersionedEncoding for Bare {
fn encode<W: io::Write + ?Sized>(&self, w: &mut W, _version: u16) -> Result<(), io::Error> {
w.emit_compact_size(0u64)?;
Ok(())
}
fn decode<R: io::Read + ?Sized>(
r: &mut R,
version: u16,
) -> Result<Self, ProtocolDecodingError> {
let _full = Full::decode(r, version)?;
Ok(Bare)
}
}
impl VtxoVersionedEncoding for Full {
fn encode<W: io::Write + ?Sized>(&self, w: &mut W, _version: u16) -> Result<(), io::Error> {
w.emit_compact_size(self.items.len() as u64)?;
for item in &self.items {
item.transition.encode(w)?;
let nb_outputs = item.other_outputs.len() + 1;
w.emit_u8(nb_outputs.try_into()
.map_err(|_| io::Error::other("too many outputs on genesis transaction"))?)?;
w.emit_u8(item.output_idx)?;
for txout in &item.other_outputs {
txout.encode(w)?;
}
w.emit_u64(item.fee_amount.to_sat())?;
}
Ok(())
}
fn decode<R: io::Read + ?Sized>(
r: &mut R,
version: u16,
) -> Result<Self, ProtocolDecodingError> {
let nb_genesis_items = r.read_compact_size()? as usize;
OversizedVectorError::check::<GenesisItem>(nb_genesis_items)?;
let mut genesis = Vec::with_capacity(nb_genesis_items);
for _ in 0..nb_genesis_items {
let transition = GenesisTransition::decode(r)?;
let nb_outputs = r.read_u8()? as usize;
let output_idx = r.read_u8()?;
let nb_other = nb_outputs.checked_sub(1)
.ok_or_else(|| ProtocolDecodingError::invalid("genesis item with 0 outputs"))?;
let mut other_outputs = Vec::with_capacity(nb_other);
for _ in 0..nb_other {
other_outputs.push(TxOut::decode(r)?);
}
let fee_amount = if version == VTXO_NO_FEE_AMOUNT_VERSION {
Amount::ZERO
} else {
Amount::from_sat(r.read_u64()?)
};
genesis.push(GenesisItem { transition, output_idx, other_outputs, fee_amount });
}
Ok(Full { items: genesis })
}
}
impl<G: VtxoVersionedEncoding, P: Policy + ProtocolEncoding> ProtocolEncoding for Vtxo<G, P> {
fn encode<W: io::Write + ?Sized>(&self, w: &mut W) -> Result<(), io::Error> {
let version = VTXO_ENCODING_VERSION;
w.emit_u16(version)?;
w.emit_u64(self.amount.to_sat())?;
w.emit_u32(self.expiry_height)?;
self.server_pubkey.encode(w)?;
w.emit_u16(self.exit_delta)?;
self.anchor_point.encode(w)?;
self.genesis.encode(w, version)?;
self.policy.encode(w)?;
self.point.encode(w)?;
Ok(())
}
fn decode<R: io::Read + ?Sized>(r: &mut R) -> Result<Self, ProtocolDecodingError> {
let version = r.read_u16()?;
if version != VTXO_ENCODING_VERSION && version != VTXO_NO_FEE_AMOUNT_VERSION {
return Err(ProtocolDecodingError::invalid(format_args!(
"invalid Vtxo encoding version byte: {version:#x}",
)));
}
let amount = Amount::from_sat(r.read_u64()?);
let expiry_height = r.read_u32()?;
if LockTime::from_height(expiry_height).is_err() {
return Err(ProtocolDecodingError::invalid(format_args!(
"expiry_height {expiry_height} is not a valid block height \
(must be below consensus LOCK_TIME_THRESHOLD)"
)));
}
let server_pubkey = PublicKey::decode(r)?;
let exit_delta = r.read_u16()?;
let anchor_point = OutPoint::decode(r)?;
let genesis = VtxoVersionedEncoding::decode(r, version)?;
let policy = P::decode(r)?;
let point = OutPoint::decode(r)?;
Ok(Self {
amount, expiry_height, server_pubkey, exit_delta, anchor_point, genesis, policy, point,
})
}
}
#[cfg(test)]
mod test {
use bitcoin::consensus::encode::serialize_hex;
use bitcoin::hex::DisplayHex;
use crate::test_util::encoding_roundtrip;
use crate::test_util::dummy::{DUMMY_SERVER_KEY, DUMMY_USER_KEY};
use crate::test_util::vectors::{
generate_vtxo_vectors, VTXO_VECTORS, VTXO_NO_FEE_AMOUNT_VERSION_HEXES,
};
use super::*;
#[test]
fn test_generate_vtxo_vectors() {
let g = generate_vtxo_vectors();
println!("\n\ngenerated:");
println!(" anchor_tx: {}", serialize_hex(&g.anchor_tx));
println!(" board_vtxo: {}", g.board_vtxo.serialize().as_hex().to_string());
println!(" arkoor_htlc_out_vtxo: {}", g.arkoor_htlc_out_vtxo.serialize().as_hex().to_string());
println!(" arkoor2_vtxo: {}", g.arkoor2_vtxo.serialize().as_hex().to_string());
println!(" round_tx: {}", serialize_hex(&g.round_tx));
println!(" round1_vtxo: {}", g.round1_vtxo.serialize().as_hex().to_string());
println!(" round2_vtxo: {}", g.round2_vtxo.serialize().as_hex().to_string());
println!(" arkoor3_vtxo: {}", g.arkoor3_vtxo.serialize().as_hex().to_string());
let v = &*VTXO_VECTORS;
println!("\n\nstatic:");
println!(" anchor_tx: {}", serialize_hex(&v.anchor_tx));
println!(" board_vtxo: {}", v.board_vtxo.serialize().as_hex().to_string());
println!(" arkoor_htlc_out_vtxo: {}", v.arkoor_htlc_out_vtxo.serialize().as_hex().to_string());
println!(" arkoor2_vtxo: {}", v.arkoor2_vtxo.serialize().as_hex().to_string());
println!(" round_tx: {}", serialize_hex(&v.round_tx));
println!(" round1_vtxo: {}", v.round1_vtxo.serialize().as_hex().to_string());
println!(" round2_vtxo: {}", v.round2_vtxo.serialize().as_hex().to_string());
println!(" arkoor3_vtxo: {}", v.arkoor3_vtxo.serialize().as_hex().to_string());
assert_eq!(g.anchor_tx, v.anchor_tx, "anchor_tx does not match");
assert_eq!(g.board_vtxo, v.board_vtxo, "board_vtxo does not match");
assert_eq!(g.arkoor_htlc_out_vtxo, v.arkoor_htlc_out_vtxo, "arkoor_htlc_out_vtxo does not match");
assert_eq!(g.arkoor2_vtxo, v.arkoor2_vtxo, "arkoor2_vtxo does not match");
assert_eq!(g.round_tx, v.round_tx, "round_tx does not match");
assert_eq!(g.round1_vtxo, v.round1_vtxo, "round1_vtxo does not match");
assert_eq!(g.round2_vtxo, v.round2_vtxo, "round2_vtxo does not match");
assert_eq!(g.arkoor3_vtxo, v.arkoor3_vtxo, "arkoor3_vtxo does not match");
assert_eq!(g, *v);
}
#[test]
fn test_vtxo_no_fee_amount_version_upgrade() {
let hexes = &*VTXO_NO_FEE_AMOUNT_VERSION_HEXES;
let v = hexes.deserialize_test_vectors();
v.validate_vtxos();
let board_hex = v.board_vtxo.serialize().as_hex().to_string();
let arkoor_htlc_out_vtxo_hex = v.arkoor_htlc_out_vtxo.serialize().as_hex().to_string();
let arkoor2_vtxo_hex = v.arkoor2_vtxo.serialize().as_hex().to_string();
let round1_vtxo_hex = v.round1_vtxo.serialize().as_hex().to_string();
let round2_vtxo_hex = v.round2_vtxo.serialize().as_hex().to_string();
let arkoor3_vtxo_hex = v.arkoor3_vtxo.serialize().as_hex().to_string();
assert_ne!(board_hex, hexes.board_vtxo);
assert_ne!(arkoor_htlc_out_vtxo_hex, hexes.arkoor_htlc_out_vtxo);
assert_ne!(arkoor2_vtxo_hex, hexes.arkoor2_vtxo);
assert_ne!(round1_vtxo_hex, hexes.round1_vtxo);
assert_ne!(round2_vtxo_hex, hexes.round2_vtxo);
assert_ne!(arkoor3_vtxo_hex, hexes.arkoor3_vtxo);
let board_vtxo = Vtxo::<Full>::deserialize_hex(&board_hex).unwrap();
assert_eq!(board_vtxo.serialize().as_hex().to_string(), board_hex);
let arkoor_htlc_out_vtxo = Vtxo::<Full>::deserialize_hex(&arkoor_htlc_out_vtxo_hex).unwrap();
assert_eq!(arkoor_htlc_out_vtxo.serialize().as_hex().to_string(), arkoor_htlc_out_vtxo_hex);
let arkoor2_vtxo = Vtxo::<Full>::deserialize_hex(&arkoor2_vtxo_hex).unwrap();
assert_eq!(arkoor2_vtxo.serialize().as_hex().to_string(), arkoor2_vtxo_hex);
let round1_vtxo = Vtxo::<Full>::deserialize_hex(&round1_vtxo_hex).unwrap();
assert_eq!(round1_vtxo.serialize().as_hex().to_string(), round1_vtxo_hex);
let round2_vtxo = Vtxo::<Full>::deserialize_hex(&round2_vtxo_hex).unwrap();
assert_eq!(round2_vtxo.serialize().as_hex().to_string(), round2_vtxo_hex);
let arkoor3_vtxo = Vtxo::<Full>::deserialize_hex(&arkoor3_vtxo_hex).unwrap();
assert_eq!(arkoor3_vtxo.serialize().as_hex().to_string(), arkoor3_vtxo_hex);
}
#[test]
fn exit_depth() {
let vtxos = &*VTXO_VECTORS;
assert_eq!(vtxos.board_vtxo.exit_depth(), 1 );
assert_eq!(vtxos.round1_vtxo.exit_depth(), 3 );
assert_eq!(
vtxos.arkoor_htlc_out_vtxo.exit_depth(),
1 + 1 + 1 ,
);
assert_eq!(
vtxos.arkoor2_vtxo.exit_depth(),
1 + 2 + 2 ,
);
assert_eq!(
vtxos.arkoor3_vtxo.exit_depth(),
3 + 1 + 1 ,
);
}
#[test]
fn test_genesis_length_257() {
let vtxo: Vtxo<Full> = Vtxo {
policy: VtxoPolicy::new_pubkey(DUMMY_USER_KEY.public_key()),
amount: Amount::from_sat(10_000),
expiry_height: 101_010,
server_pubkey: DUMMY_SERVER_KEY.public_key(),
exit_delta: 2016,
anchor_point: OutPoint::new(Txid::from_slice(&[1u8; 32]).unwrap(), 1),
genesis: Full {
items: (0..257).map(|_| {
GenesisItem {
transition: GenesisTransition::new_cosigned(
vec![DUMMY_USER_KEY.public_key()],
Some(schnorr::Signature::from_slice(&[2u8; 64]).unwrap()),
),
output_idx: 0,
other_outputs: vec![],
fee_amount: Amount::ZERO,
}
}).collect(),
},
point: OutPoint::new(Txid::from_slice(&[3u8; 32]).unwrap(), 3),
};
assert_eq!(vtxo.genesis.items.len(), 257);
encoding_roundtrip(&vtxo);
}
mod genesis_transition_encoding {
use bitcoin::hashes::{sha256, Hash};
use bitcoin::secp256k1::{Keypair, PublicKey};
use bitcoin::taproot::TapTweakHash;
use std::str::FromStr;
use crate::test_util::encoding_roundtrip;
use super::genesis::{
GenesisTransition, CosignedGenesis, HashLockedCosignedGenesis, ArkoorGenesis,
};
use super::MaybePreimage;
fn test_pubkey() -> PublicKey {
Keypair::from_str(
"916da686cedaee9a9bfb731b77439f2a3f1df8664e16488fba46b8d2bfe15e92"
).unwrap().public_key()
}
fn test_signature() -> bitcoin::secp256k1::schnorr::Signature {
"cc8b93e9f6fbc2506bb85ae8bbb530b178daac49704f5ce2e3ab69c266fd5932\
0b28d028eef212e3b9fdc42cfd2e0760a0359d3ea7d2e9e8cfe2040e3f1b71ea"
.parse().unwrap()
}
#[test]
fn cosigned_with_signature() {
let transition = GenesisTransition::Cosigned(CosignedGenesis {
pubkeys: vec![test_pubkey()],
signature: Some(test_signature()),
});
encoding_roundtrip(&transition);
}
#[test]
fn cosigned_without_signature() {
let transition = GenesisTransition::Cosigned(CosignedGenesis {
pubkeys: vec![test_pubkey()],
signature: None,
});
encoding_roundtrip(&transition);
}
#[test]
fn cosigned_multiple_pubkeys() {
let pk1 = test_pubkey();
let pk2 = Keypair::from_str(
"fab9e598081a3e74b2233d470c4ad87bcc285b6912ed929568e62ac0e9409879"
).unwrap().public_key();
let transition = GenesisTransition::Cosigned(CosignedGenesis {
pubkeys: vec![pk1, pk2],
signature: Some(test_signature()),
});
encoding_roundtrip(&transition);
}
#[test]
fn hash_locked_cosigned_with_preimage() {
let preimage = [0x42u8; 32];
let transition = GenesisTransition::HashLockedCosigned(HashLockedCosignedGenesis {
user_pubkey: test_pubkey(),
signature: Some(test_signature()),
unlock: MaybePreimage::Preimage(preimage),
});
encoding_roundtrip(&transition);
}
#[test]
fn hash_locked_cosigned_with_hash() {
let hash = sha256::Hash::hash(b"test preimage");
let transition = GenesisTransition::HashLockedCosigned(HashLockedCosignedGenesis {
user_pubkey: test_pubkey(),
signature: Some(test_signature()),
unlock: MaybePreimage::Hash(hash),
});
encoding_roundtrip(&transition);
}
#[test]
fn hash_locked_cosigned_without_signature() {
let preimage = [0x42u8; 32];
let transition = GenesisTransition::HashLockedCosigned(HashLockedCosignedGenesis {
user_pubkey: test_pubkey(),
signature: None,
unlock: MaybePreimage::Preimage(preimage),
});
encoding_roundtrip(&transition);
}
#[test]
fn arkoor_with_signature() {
let tap_tweak = TapTweakHash::from_slice(&[0xabu8; 32]).unwrap();
let transition = GenesisTransition::Arkoor(ArkoorGenesis {
client_cosigners: vec![test_pubkey()],
tap_tweak,
signature: Some(test_signature()),
});
encoding_roundtrip(&transition);
}
#[test]
fn arkoor_without_signature() {
let tap_tweak = TapTweakHash::from_slice(&[0xabu8; 32]).unwrap();
let transition = GenesisTransition::Arkoor(ArkoorGenesis {
client_cosigners: vec![test_pubkey()],
tap_tweak,
signature: None,
});
encoding_roundtrip(&transition);
}
}
}