#![deny(missing_docs)]
use std::collections::BTreeMap;
use bitcoin::hashes::{sha256, Hash, HashEngine};
use bitcoin::key::{TapTweak, TweakedPublicKey};
use bitcoin::secp256k1::{
schnorr::Signature, Keypair, Message, Parity, PublicKey, Scalar, SecretKey, XOnlyPublicKey,
};
use bitcoin::sighash::{Prevouts, SighashCache, TapSighashType};
use bitcoin::taproot::{LeafVersion, TapLeafHash, TapNodeHash};
use bitcoin::{
absolute::LockTime, transaction::Version, Amount, OutPoint, ScriptBuf, Sequence, Transaction,
TxIn, TxOut, Witness,
};
use sidestr_core::block::secp;
use sidestr_core::federation::NUMS_X;
use sidestr_core::sighash::{
key_path_hash_type, key_path_sighash, unified_taproot_sighash, SighashRules, UnifiedTaproot,
};
pub mod protocol;
pub mod route;
pub const DUST: u64 = 330;
pub const DEFAULT_FEE: u64 = 300;
pub const DEFAULT_DELAY: u16 = 6;
pub const MIN_DELAY: u16 = 3;
pub const MAX_DELAY: u16 = 144;
pub const MIN_FEE: u64 = 100;
pub const MAX_FEE: u64 = 10_000;
pub const MAX_EXPIRY: u32 = 500_000_000;
const SIGHASH_ALL: u8 = 0x01;
const INPUT_SEQUENCE: u32 = 0xffff_fffd;
const LOCKTIME_SEQUENCE: u32 = 0xffff_fffe;
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("fee must be between {MIN_FEE} and {MAX_FEE} sats")]
Fee,
#[error("delay must be between {MIN_DELAY} and {MAX_DELAY} blocks")]
Delay,
#[error("HTLC expiry must be below {MAX_EXPIRY}")]
Expiry,
#[error("channel state accounts for {state} sats but funding holds {funding} sats")]
Unbalanced {
state: u64,
funding: u64,
},
#[error("channel amount overflow")]
AmountOverflow,
#[error("nothing to pay out")]
NothingToPay,
#[error("HTLCs in flight: settle or fail them first")]
HtlcsInFlight,
#[error("the output is not worth claiming after the fee")]
DustClaim,
#[error("the commitment has no {0} output")]
MissingOutput(&'static str),
#[error("an HTLC success claim needs its 32-byte preimage")]
MissingPreimage,
#[error("script number out of range: {0}")]
ScriptNumber(u32),
#[error("NUMS derivation failed")]
Nums,
#[error("signature hash failed: {0}")]
Sighash(&'static str),
#[error("both funding signatures are needed")]
MissingSignature,
#[error("invalid leaf signature")]
InvalidSignature,
#[error("bad revocation key")]
RevocationKey,
#[error(transparent)]
Secp(#[from] bitcoin::secp256k1::Error),
}
pub type Result<T> = std::result::Result<T, Error>;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum Side {
A,
B,
}
impl Side {
pub const fn other(self) -> Self {
match self {
Self::A => Self::B,
Self::B => Self::A,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Balances {
pub a: Amount,
pub b: Amount,
}
impl Balances {
fn get(self, side: Side) -> Amount {
match side {
Side::A => self.a,
Side::B => self.b,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct RevocationKeys {
pub a: XOnlyPublicKey,
pub b: XOnlyPublicKey,
}
impl RevocationKeys {
fn get(self, side: Side) -> XOnlyPublicKey {
match side {
Side::A => self.a,
Side::B => self.b,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Htlc {
pub id: u64,
pub from: Side,
pub amount: Amount,
pub payment_hash: [u8; 32],
pub expiry: u32,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ChannelState {
pub balances: Balances,
pub revocation: RevocationKeys,
pub htlcs: Vec<Htlc>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct LeafSpend {
pub script: ScriptBuf,
pub leaf_hash: TapLeafHash,
pub control_block: Vec<u8>,
pub csv: bool,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FundingScript {
pub internal_key: XOnlyPublicKey,
pub output_key: TweakedPublicKey,
pub script_pubkey: ScriptBuf,
pub signers: [XOnlyPublicKey; 2],
pub leaf: LeafSpend,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ToLocalScript {
pub output_key: TweakedPublicKey,
pub script_pubkey: ScriptBuf,
pub delayed: LeafSpend,
pub revocation: LeafSpend,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct HtlcScript {
pub output_key: TweakedPublicKey,
pub script_pubkey: ScriptBuf,
pub success: LeafSpend,
pub timeout: LeafSpend,
pub revocation: LeafSpend,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum OutputKind {
ToLocal,
ToRemote,
Htlc,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CommitmentHtlc {
pub htlc: Htlc,
pub vout: u32,
pub scripts: HtlcScript,
pub offered_by_owner: bool,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Commitment {
pub tx: Transaction,
pub to_local: ToLocalScript,
pub kinds: Vec<OutputKind>,
pub htlcs: Vec<CommitmentHtlc>,
pub local_value: Amount,
pub remote_value: Amount,
pub to_local_vout: Option<u32>,
pub number: u64,
pub owner: Side,
pub delay: u16,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct LeafSignature([u8; 65]);
impl LeafSignature {
pub fn from_slice(bytes: &[u8]) -> Result<Self> {
let bytes: [u8; 65] = bytes.try_into().map_err(|_| Error::InvalidSignature)?;
Signature::from_slice(&bytes[..64]).map_err(|_| Error::InvalidSignature)?;
Ok(Self(bytes))
}
pub fn as_bytes(&self) -> &[u8; 65] {
&self.0
}
pub fn to_hex(self) -> String {
hex_bytes(&self.0)
}
pub fn from_hex(text: &str) -> Result<Self> {
if text.len() != 130 {
return Err(Error::InvalidSignature);
}
let mut bytes = [0u8; 65];
decode_hex_into(text, &mut bytes).ok_or(Error::InvalidSignature)?;
Self::from_slice(&bytes)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct PopSignature([u8; 64]);
impl PopSignature {
pub fn from_bytes(bytes: [u8; 64]) -> Self {
Self(bytes)
}
pub fn as_bytes(&self) -> &[u8; 64] {
&self.0
}
pub fn to_hex(self) -> String {
hex_bytes(&self.0)
}
pub fn from_hex(text: &str) -> Result<Self> {
let mut bytes = [0u8; 64];
decode_hex_into(text, &mut bytes).ok_or(Error::InvalidSignature)?;
Ok(Self(bytes))
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Channel {
pub keys: [XOnlyPublicKey; 2],
pub funding_outpoint: OutPoint,
pub funding_value: Amount,
pub funding_script: FundingScript,
pub fee: Amount,
pub delay: u16,
}
impl Channel {
pub fn new(
a: XOnlyPublicKey,
b: XOnlyPublicKey,
funding_outpoint: OutPoint,
funding_value: Amount,
fee: Amount,
delay: u16,
) -> Result<Self> {
if !(MIN_FEE..=MAX_FEE).contains(&fee.to_sat()) {
return Err(Error::Fee);
}
if !(MIN_DELAY..=MAX_DELAY).contains(&delay) {
return Err(Error::Delay);
}
Ok(Self {
keys: [a, b],
funding_outpoint,
funding_value,
funding_script: funding_script(a, b)?,
fee,
delay,
})
}
pub fn key(&self, side: Side) -> XOnlyPublicKey {
match side {
Side::A => self.keys[0],
Side::B => self.keys[1],
}
}
pub fn funding_prevout(&self) -> TxOut {
TxOut {
value: self.funding_value,
script_pubkey: self.funding_script.script_pubkey.clone(),
}
}
pub fn commitment(&self, number: u64, owner: Side, state: &ChannelState) -> Result<Commitment> {
self.check_state(state)?;
let remote_side = owner.other();
let owner_pub = self.key(owner);
let remote_pub = self.key(remote_side);
let local = payout_after_fee(state.balances.get(owner), owner, self.fee);
let remote = payout_after_fee(state.balances.get(remote_side), remote_side, self.fee);
let to_local = to_local_script(owner_pub, state.revocation.get(owner), self.delay)?;
let mut output = Vec::new();
let mut kinds = Vec::new();
let mut to_local_vout = None;
if local.to_sat() >= DUST {
to_local_vout = Some(output.len() as u32);
output.push(TxOut {
value: local,
script_pubkey: to_local.script_pubkey.clone(),
});
kinds.push(OutputKind::ToLocal);
}
if remote.to_sat() >= DUST {
output.push(TxOut {
value: remote,
script_pubkey: to_remote_script(remote_pub),
});
kinds.push(OutputKind::ToRemote);
}
let mut sorted = state.htlcs.clone();
sorted.sort_by_key(|h| h.id);
let mut htlcs = Vec::new();
for htlc in sorted {
if htlc.expiry >= MAX_EXPIRY {
return Err(Error::Expiry);
}
if htlc.amount.to_sat() < DUST {
continue;
}
let offered_by_owner = htlc.from == owner;
let scripts = htlc_script(HtlcScriptArgs {
owner_pub,
remote_pub,
revocation_pub: state.revocation.get(owner),
delay: self.delay,
payment_hash: htlc.payment_hash,
expiry: htlc.expiry,
offered_by_owner,
})?;
let vout = output.len() as u32;
output.push(TxOut {
value: htlc.amount,
script_pubkey: scripts.script_pubkey.clone(),
});
kinds.push(OutputKind::Htlc);
htlcs.push(CommitmentHtlc {
htlc,
vout,
scripts,
offered_by_owner,
});
}
if output.is_empty() {
return Err(Error::NothingToPay);
}
Ok(Commitment {
tx: one_input_tx(
self.funding_outpoint,
INPUT_SEQUENCE,
LockTime::ZERO,
output,
),
to_local,
kinds,
htlcs,
local_value: local,
remote_value: remote,
to_local_vout,
number,
owner,
delay: self.delay,
})
}
pub fn cooperative_close(&self, state: &ChannelState) -> Result<Transaction> {
if !state.htlcs.is_empty() {
return Err(Error::HtlcsInFlight);
}
self.check_state(state)?;
let mut output = Vec::new();
let a = payout_after_fee(state.balances.a, Side::A, self.fee);
if a.to_sat() >= DUST {
output.push(TxOut {
value: a,
script_pubkey: to_remote_script(self.keys[0]),
});
}
if state.balances.b.to_sat() >= DUST {
output.push(TxOut {
value: state.balances.b,
script_pubkey: to_remote_script(self.keys[1]),
});
}
if output.is_empty() {
return Err(Error::NothingToPay);
}
Ok(one_input_tx(
self.funding_outpoint,
INPUT_SEQUENCE,
LockTime::ZERO,
output,
))
}
pub fn sign_funding(
&self,
tx: &Transaction,
key: &SecretKey,
rules: SighashRules,
aux: &[u8; 32],
) -> Result<LeafSignature> {
sign_leaf(
tx,
0,
&[self.funding_prevout()],
self.funding_script.leaf.leaf_hash,
key,
rules,
aux,
)
}
pub fn verify_funding(
&self,
tx: &Transaction,
pubkey: &XOnlyPublicKey,
signature: &LeafSignature,
rules: SighashRules,
) -> bool {
verify_leaf_signature(
tx,
0,
&[self.funding_prevout()],
self.funding_script.leaf.leaf_hash,
pubkey,
signature,
rules,
)
}
pub fn funding_witness(
&self,
signatures: &BTreeMap<XOnlyPublicKey, LeafSignature>,
) -> Result<Witness> {
let mut slots = self
.funding_script
.signers
.iter()
.map(|key| {
signatures
.get(key)
.map(|signature| signature.as_bytes().to_vec())
.ok_or(Error::MissingSignature)
})
.collect::<Result<Vec<_>>>()?;
slots.reverse();
slots.push(self.funding_script.leaf.script.to_bytes());
slots.push(self.funding_script.leaf.control_block.clone());
Ok(Witness::from_slice(&slots))
}
fn check_state(&self, state: &ChannelState) -> Result<()> {
let total = state.htlcs.iter().try_fold(
state
.balances
.a
.to_sat()
.checked_add(state.balances.b.to_sat())
.ok_or(Error::AmountOverflow)?,
|sum, htlc| {
sum.checked_add(htlc.amount.to_sat())
.ok_or(Error::AmountOverflow)
},
)?;
if total != self.funding_value.to_sat() {
return Err(Error::Unbalanced {
state: total,
funding: self.funding_value.to_sat(),
});
}
Ok(())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct HtlcScriptArgs {
pub owner_pub: XOnlyPublicKey,
pub remote_pub: XOnlyPublicKey,
pub revocation_pub: XOnlyPublicKey,
pub delay: u16,
pub payment_hash: [u8; 32],
pub expiry: u32,
pub offered_by_owner: bool,
}
pub fn internal_key() -> Result<XOnlyPublicKey> {
let tweak = tagged_sha256(b"hitch/nums", b"hitch");
let mut encoded = [0u8; 33];
encoded[0] = 0x02;
encoded[1..].copy_from_slice(&NUMS_X);
let point = PublicKey::from_slice(&encoded)?;
let scalar = Scalar::from_be_bytes(tweak).map_err(|_| Error::Nums)?;
Ok(point.add_exp_tweak(secp(), &scalar)?.x_only_public_key().0)
}
pub fn even_secret(secret: &SecretKey) -> SecretKey {
let (_, parity) = secret.x_only_public_key(secp());
if parity == Parity::Odd {
secret.negate()
} else {
*secret
}
}
pub fn revocation_pub(point: XOnlyPublicKey, scalar: &SecretKey) -> Result<XOnlyPublicKey> {
let lifted = point.public_key(Parity::Even);
let tweak = Scalar::from(even_secret(scalar));
let combined = lifted
.add_exp_tweak(secp(), &tweak)
.map_err(|_| Error::RevocationKey)?;
Ok(combined.x_only_public_key().0)
}
pub fn revocation_key(basepoint_secret: &SecretKey, per_state: &SecretKey) -> Result<SecretKey> {
even_secret(basepoint_secret)
.add_tweak(&Scalar::from(even_secret(per_state)))
.map_err(|_| Error::RevocationKey)
}
fn pop_message(point: &XOnlyPublicKey, context: &str) -> [u8; 32] {
let mut message = Vec::with_capacity(context.len() + 32);
message.extend_from_slice(context.as_bytes());
message.extend_from_slice(&point.serialize());
tagged_sha256(b"hitch/pop", &message)
}
pub fn pop_sign(secret: &SecretKey, context: &str, aux: &[u8; 32]) -> PopSignature {
let keypair = Keypair::from_secret_key(secp(), secret);
let point = keypair.x_only_public_key().0;
let message = Message::from_digest(pop_message(&point, context));
let signature = secp().sign_schnorr_with_aux_rand(&message, &keypair, aux);
let mut bytes = [0u8; 64];
bytes.copy_from_slice(signature.as_ref());
PopSignature(bytes)
}
pub fn pop_verify(point: &XOnlyPublicKey, signature: &PopSignature, context: &str) -> bool {
let Ok(signature) = Signature::from_slice(&signature.0) else {
return false;
};
let message = Message::from_digest(pop_message(point, context));
secp().verify_schnorr(&signature, &message, point).is_ok()
}
pub fn preimage_in(tx: &Transaction, payment_hash: &[u8; 32]) -> Option<[u8; 32]> {
tx.input
.iter()
.flat_map(|input| input.witness.iter())
.filter(|item| item.len() == 32)
.find(|item| sha256::Hash::hash(item).to_byte_array() == *payment_hash)
.map(|item| item.try_into().expect("a 32-byte witness item"))
}
pub fn funding_script(a: XOnlyPublicKey, b: XOnlyPublicKey) -> Result<FundingScript> {
let mut signers = [a, b];
signers.sort_by_key(XOnlyPublicKey::serialize);
let mut bytes = Vec::with_capacity(70);
push_key(&mut bytes, signers[0]);
bytes.push(0xac); push_key(&mut bytes, signers[1]);
bytes.extend_from_slice(&[0xba, 0x52, 0x9c]); let script = ScriptBuf::from_bytes(bytes);
let leaf_hash = TapLeafHash::from_script(&script, LeafVersion::TapScript);
let internal_key = internal_key()?;
let (output_key, parity, script_pubkey) = taproot_output(internal_key, leaf_hash.into());
Ok(FundingScript {
internal_key,
output_key,
script_pubkey,
signers,
leaf: LeafSpend {
script,
leaf_hash,
control_block: control_block(internal_key, parity, &[]),
csv: false,
},
})
}
pub fn to_local_script(
owner_pub: XOnlyPublicKey,
revocation_pub: XOnlyPublicKey,
delay: u16,
) -> Result<ToLocalScript> {
let mut delayed = Vec::new();
push_num(&mut delayed, u32::from(delay))?;
delayed.extend_from_slice(&[0xb2, 0x75]); push_key(&mut delayed, owner_pub);
delayed.push(0xac);
let delayed = ScriptBuf::from_bytes(delayed);
let mut revocation = Vec::new();
push_key(&mut revocation, revocation_pub);
revocation.push(0xac);
let revocation = ScriptBuf::from_bytes(revocation);
let delayed_hash = TapLeafHash::from_script(&delayed, LeafVersion::TapScript);
let revocation_hash = TapLeafHash::from_script(&revocation, LeafVersion::TapScript);
let root = TapNodeHash::from_node_hashes(delayed_hash.into(), revocation_hash.into());
let internal_key = internal_key()?;
let (output_key, parity, script_pubkey) = taproot_output(internal_key, root);
Ok(ToLocalScript {
output_key,
script_pubkey,
delayed: LeafSpend {
script: delayed,
leaf_hash: delayed_hash,
control_block: control_block(internal_key, parity, &[revocation_hash.into()]),
csv: true,
},
revocation: LeafSpend {
script: revocation,
leaf_hash: revocation_hash,
control_block: control_block(internal_key, parity, &[delayed_hash.into()]),
csv: false,
},
})
}
pub fn to_remote_script(pubkey: XOnlyPublicKey) -> ScriptBuf {
let mut script = Vec::with_capacity(34);
script.extend_from_slice(&[0x51, 0x20]);
script.extend_from_slice(&pubkey.serialize());
ScriptBuf::from_bytes(script)
}
pub fn htlc_script(args: HtlcScriptArgs) -> Result<HtlcScript> {
if args.expiry >= MAX_EXPIRY {
return Err(Error::Expiry);
}
let mut success = Vec::new();
success.extend_from_slice(&[0xa8, 0x20]); success.extend_from_slice(&args.payment_hash);
success.push(0x88); if !args.offered_by_owner {
push_num(&mut success, u32::from(args.delay))?;
success.extend_from_slice(&[0xb2, 0x75]); }
push_key(
&mut success,
if args.offered_by_owner {
args.remote_pub
} else {
args.owner_pub
},
);
success.push(0xac);
let success = ScriptBuf::from_bytes(success);
let mut timeout = Vec::new();
push_num(&mut timeout, args.expiry)?;
timeout.extend_from_slice(&[0xb1, 0x75]); if args.offered_by_owner {
push_num(&mut timeout, u32::from(args.delay))?;
timeout.extend_from_slice(&[0xb2, 0x75]); }
push_key(
&mut timeout,
if args.offered_by_owner {
args.owner_pub
} else {
args.remote_pub
},
);
timeout.push(0xac);
let timeout = ScriptBuf::from_bytes(timeout);
let mut revocation = Vec::new();
push_key(&mut revocation, args.revocation_pub);
revocation.push(0xac);
let revocation = ScriptBuf::from_bytes(revocation);
let success_hash = TapLeafHash::from_script(&success, LeafVersion::TapScript);
let timeout_hash = TapLeafHash::from_script(&timeout, LeafVersion::TapScript);
let revocation_hash = TapLeafHash::from_script(&revocation, LeafVersion::TapScript);
let inner = TapNodeHash::from_node_hashes(success_hash.into(), timeout_hash.into());
let root = TapNodeHash::from_node_hashes(inner, revocation_hash.into());
let internal_key = internal_key()?;
let (output_key, parity, script_pubkey) = taproot_output(internal_key, root);
Ok(HtlcScript {
output_key,
script_pubkey,
success: LeafSpend {
script: success,
leaf_hash: success_hash,
control_block: control_block(
internal_key,
parity,
&[timeout_hash.into(), revocation_hash.into()],
),
csv: !args.offered_by_owner,
},
timeout: LeafSpend {
script: timeout,
leaf_hash: timeout_hash,
control_block: control_block(
internal_key,
parity,
&[success_hash.into(), revocation_hash.into()],
),
csv: args.offered_by_owner,
},
revocation: LeafSpend {
script: revocation,
leaf_hash: revocation_hash,
control_block: control_block(internal_key, parity, &[inner]),
csv: false,
},
})
}
pub fn script_path_sighash(
tx: &Transaction,
input_index: usize,
prevouts: &[TxOut],
leaf_hash: TapLeafHash,
rules: SighashRules,
) -> Result<([u8; 32], u8)> {
let hash_type = SIGHASH_ALL
| if rules == SighashRules::KnotsUnified {
sidestr_core::sighash::SIGHASH_UNIFIED
} else {
0
};
let leaf_hash_bytes = leaf_hash.to_byte_array();
let message = match rules {
SighashRules::KnotsUnified => unified_taproot_sighash(
tx,
input_index,
prevouts,
hash_type,
None,
UnifiedTaproot::ScriptPath {
leaf_hash: &leaf_hash_bytes,
codesep_pos: u32::MAX,
},
)
.map_err(Error::Sighash)?,
SighashRules::Bip341 => SighashCache::new(tx)
.taproot_script_spend_signature_hash(
input_index,
&Prevouts::All(prevouts),
leaf_hash,
TapSighashType::All,
)
.map_err(|_| Error::Sighash("taproot script-path sighash"))?
.to_byte_array(),
};
Ok((message, hash_type))
}
pub fn sign_leaf(
tx: &Transaction,
input_index: usize,
prevouts: &[TxOut],
leaf_hash: TapLeafHash,
key: &SecretKey,
rules: SighashRules,
aux: &[u8; 32],
) -> Result<LeafSignature> {
let (message, hash_type) = script_path_sighash(tx, input_index, prevouts, leaf_hash, rules)?;
let keypair = Keypair::from_secret_key(secp(), key);
let signature =
secp().sign_schnorr_with_aux_rand(&Message::from_digest(message), &keypair, aux);
let mut wire = [0u8; 65];
wire[..64].copy_from_slice(signature.as_ref());
wire[64] = hash_type;
Ok(LeafSignature(wire))
}
pub fn verify_leaf_signature(
tx: &Transaction,
input_index: usize,
prevouts: &[TxOut],
leaf_hash: TapLeafHash,
pubkey: &XOnlyPublicKey,
signature: &LeafSignature,
rules: SighashRules,
) -> bool {
let Ok((message, hash_type)) = script_path_sighash(tx, input_index, prevouts, leaf_hash, rules)
else {
return false;
};
if signature.0[64] != hash_type {
return false;
}
let Ok(signature) = Signature::from_slice(&signature.0[..64]) else {
return false;
};
secp()
.verify_schnorr(&signature, &Message::from_digest(message), pubkey)
.is_ok()
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SweepPath {
Delayed,
Revocation,
}
pub fn sweep_to_local(
commitment: &Commitment,
path: SweepPath,
destination: ScriptBuf,
fee: Amount,
key: &SecretKey,
rules: SighashRules,
aux: &[u8; 32],
) -> Result<Transaction> {
let vout = commitment
.to_local_vout
.ok_or(Error::MissingOutput("to_local"))?;
let value = commitment.local_value;
let output_value = claim_value(value, fee)?;
let (leaf, sequence) = match path {
SweepPath::Delayed => (&commitment.to_local.delayed, u32::from(commitment.delay)),
SweepPath::Revocation => (&commitment.to_local.revocation, INPUT_SEQUENCE),
};
let mut tx = one_input_tx(
OutPoint {
txid: commitment.tx.compute_txid(),
vout,
},
sequence,
LockTime::ZERO,
vec![TxOut {
value: output_value,
script_pubkey: destination,
}],
);
let prevouts = [TxOut {
value,
script_pubkey: commitment.to_local.script_pubkey.clone(),
}];
let signature = sign_leaf(&tx, 0, &prevouts, leaf.leaf_hash, key, rules, aux)?;
tx.input[0].witness = Witness::from_slice(&[
signature.as_bytes().as_slice(),
leaf.script.as_bytes(),
&leaf.control_block,
]);
Ok(tx)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum HtlcClaimPath {
Success,
Timeout,
Revocation,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct HtlcClaim {
pub path: HtlcClaimPath,
pub destination: ScriptBuf,
pub fee: Amount,
pub preimage: Option<[u8; 32]>,
}
pub fn claim_htlc(
commitment: &Commitment,
htlc: &CommitmentHtlc,
claim: HtlcClaim,
key: &SecretKey,
rules: SighashRules,
aux: &[u8; 32],
) -> Result<Transaction> {
let (leaf, lock_time) = match claim.path {
HtlcClaimPath::Success => (&htlc.scripts.success, LockTime::ZERO),
HtlcClaimPath::Timeout => (
&htlc.scripts.timeout,
LockTime::from_consensus(htlc.htlc.expiry),
),
HtlcClaimPath::Revocation => (&htlc.scripts.revocation, LockTime::ZERO),
};
let sequence = if claim.path == HtlcClaimPath::Revocation {
INPUT_SEQUENCE
} else if leaf.csv {
u32::from(commitment.delay)
} else {
LOCKTIME_SEQUENCE
};
let output_value = claim_value(htlc.htlc.amount, claim.fee)?;
let mut tx = one_input_tx(
OutPoint {
txid: commitment.tx.compute_txid(),
vout: htlc.vout,
},
sequence,
lock_time,
vec![TxOut {
value: output_value,
script_pubkey: claim.destination,
}],
);
let prevouts = [TxOut {
value: htlc.htlc.amount,
script_pubkey: htlc.scripts.script_pubkey.clone(),
}];
let signature = sign_leaf(&tx, 0, &prevouts, leaf.leaf_hash, key, rules, aux)?;
let mut witness = vec![signature.as_bytes().to_vec()];
if claim.path == HtlcClaimPath::Success {
witness.push(claim.preimage.ok_or(Error::MissingPreimage)?.to_vec());
}
witness.push(leaf.script.to_bytes());
witness.push(leaf.control_block.clone());
tx.input[0].witness = Witness::from_slice(&witness);
Ok(tx)
}
pub fn key_path_spend(
outpoint: OutPoint,
prevout: TxOut,
destination: ScriptBuf,
fee: Amount,
key: &SecretKey,
rules: SighashRules,
aux: &[u8; 32],
) -> Result<Transaction> {
let output_value = claim_value(prevout.value, fee)?;
let mut tx = one_input_tx(
outpoint,
INPUT_SEQUENCE,
LockTime::ZERO,
vec![TxOut {
value: output_value,
script_pubkey: destination,
}],
);
let hash_type = key_path_hash_type(rules);
let (message, returned_type) =
key_path_sighash(&tx, 0, &[prevout], rules).map_err(Error::Sighash)?;
debug_assert_eq!(hash_type, returned_type);
let keypair = Keypair::from_secret_key(secp(), key);
let signature =
secp().sign_schnorr_with_aux_rand(&Message::from_digest(message), &keypair, aux);
let mut wire = signature.as_ref().to_vec();
wire.push(hash_type);
tx.input[0].witness = Witness::from_slice(&[wire]);
Ok(tx)
}
fn payout_after_fee(balance: Amount, side: Side, fee: Amount) -> Amount {
if side == Side::A {
Amount::from_sat(balance.to_sat().saturating_sub(fee.to_sat()))
} else {
balance
}
}
fn claim_value(value: Amount, fee: Amount) -> Result<Amount> {
let value = value
.to_sat()
.checked_sub(fee.to_sat())
.filter(|value| *value >= DUST)
.ok_or(Error::DustClaim)?;
Ok(Amount::from_sat(value))
}
fn tagged_sha256(tag: &[u8], message: &[u8]) -> [u8; 32] {
let tag_hash = sha256::Hash::hash(tag).to_byte_array();
let mut engine = sha256::Hash::engine();
engine.input(&tag_hash);
engine.input(&tag_hash);
engine.input(message);
sha256::Hash::from_engine(engine).to_byte_array()
}
fn hex_bytes(bytes: &[u8]) -> String {
const HEX: &[u8; 16] = b"0123456789abcdef";
let mut out = String::with_capacity(bytes.len() * 2);
for byte in bytes {
out.push(HEX[usize::from(byte >> 4)] as char);
out.push(HEX[usize::from(byte & 0x0f)] as char);
}
out
}
fn decode_hex_into(text: &str, out: &mut [u8]) -> Option<()> {
if text.len() != out.len() * 2 {
return None;
}
for (target, pair) in out.iter_mut().zip(text.as_bytes().chunks_exact(2)) {
*target = (hex_nibble(pair[0])? << 4) | hex_nibble(pair[1])?;
}
Some(())
}
fn hex_nibble(byte: u8) -> Option<u8> {
match byte {
b'0'..=b'9' => Some(byte - b'0'),
b'a'..=b'f' => Some(byte - b'a' + 10),
_ => None,
}
}
fn taproot_output(
internal_key: XOnlyPublicKey,
root: TapNodeHash,
) -> (TweakedPublicKey, Parity, ScriptBuf) {
let (output_key, parity) = internal_key.tap_tweak(secp(), Some(root));
let mut script = Vec::with_capacity(34);
script.extend_from_slice(&[0x51, 0x20]);
script.extend_from_slice(&output_key.serialize());
(output_key, parity, ScriptBuf::from_bytes(script))
}
fn control_block(
internal_key: XOnlyPublicKey,
parity: Parity,
merkle_path: &[TapNodeHash],
) -> Vec<u8> {
let mut control = Vec::with_capacity(33 + merkle_path.len() * 32);
control.push(LeafVersion::TapScript.to_consensus() | u8::from(parity));
control.extend_from_slice(&internal_key.serialize());
for node in merkle_path {
control.extend_from_slice(node.as_ref());
}
control
}
fn push_key(script: &mut Vec<u8>, key: XOnlyPublicKey) {
script.push(0x20);
script.extend_from_slice(&key.serialize());
}
fn push_num(script: &mut Vec<u8>, n: u32) -> Result<()> {
if n > 0x7fff_ffff {
return Err(Error::ScriptNumber(n));
}
if n == 0 {
script.push(0x00);
return Ok(());
}
if n <= 16 {
script.push(0x50 + n as u8);
return Ok(());
}
let mut value = n;
let mut bytes = Vec::new();
while value > 0 {
bytes.push((value & 0xff) as u8);
value >>= 8;
}
if bytes.last().is_some_and(|byte| byte & 0x80 != 0) {
bytes.push(0);
}
script.push(bytes.len() as u8);
script.extend_from_slice(&bytes);
Ok(())
}
fn one_input_tx(
outpoint: OutPoint,
sequence: u32,
lock_time: LockTime,
output: Vec<TxOut>,
) -> Transaction {
Transaction {
version: Version::TWO,
lock_time,
input: vec![TxIn {
previous_output: outpoint,
script_sig: ScriptBuf::new(),
sequence: Sequence::from_consensus(sequence),
witness: Witness::new(),
}],
output,
}
}
#[cfg(test)]
mod tests {
use super::*;
use bitcoin::consensus::encode::{deserialize, serialize};
use bitcoin::hashes::Hash;
use bitcoin::Txid;
fn key(byte: u8) -> SecretKey {
SecretKey::from_slice(&[byte; 32]).unwrap()
}
fn public(key: &SecretKey) -> XOnlyPublicKey {
Keypair::from_secret_key(secp(), key).x_only_public_key().0
}
fn channel() -> (Channel, SecretKey, SecretKey) {
let a = key(0x11);
let b = key(0x22);
let channel = Channel::new(
public(&a),
public(&b),
OutPoint {
txid: Txid::from_byte_array([0xab; 32]),
vout: 1,
},
Amount::from_sat(100_000),
Amount::from_sat(DEFAULT_FEE),
DEFAULT_DELAY,
)
.unwrap();
(channel, a, b)
}
#[test]
fn scripts_match_hitch_golden_vectors() {
let (channel, a, b) = channel();
assert_eq!(
internal_key().unwrap().to_string(),
"257ea3139d352eb3705808452e5268c6b932af03a03e09bfe9017bb8be3b3ece"
);
assert_eq!(
channel.funding_script.script_pubkey.to_hex_string(),
"51201c17bc9ec453913ba2e233ea77bab2fb84e8091fd232568b3eef046717dbed72"
);
assert_eq!(
channel.funding_script.leaf.script.to_hex_string(),
"20466d7fcae563e5cb09a0d1870bb580344804617879a14949cf22285f1bae3f27ac204f355bdcb7cc0af728ef3cceb9615d90684bb5b2ca5f859ab0f0b704075871aaba529c"
);
let local = to_local_script(public(&a), public(&b), 6).unwrap();
assert_eq!(
local.script_pubkey.to_hex_string(),
"5120cc7c54c2ace9c757d114cc18cb05b3865580ced8004519f72dbb104fdbc7bbc1"
);
assert_eq!(
local.delayed.script.to_hex_string(),
"56b275204f355bdcb7cc0af728ef3cceb9615d90684bb5b2ca5f859ab0f0b704075871aaac"
);
let htlc = htlc_script(HtlcScriptArgs {
owner_pub: public(&a),
remote_pub: public(&b),
revocation_pub: public(&b),
delay: 6,
payment_hash: [0xab; 32],
expiry: 152_200,
offered_by_owner: true,
})
.unwrap();
assert_eq!(
htlc.script_pubkey.to_hex_string(),
"5120cf2924765a12e11e9fed368d5251a591d40159e95c9d4da98066232542956a20"
);
assert_eq!(
htlc.success.script.to_hex_string(),
"a820abababababababababababababababababababababababababababababababab8820466d7fcae563e5cb09a0d1870bb580344804617879a14949cf22285f1bae3f27ac"
);
assert_eq!(
htlc.timeout.script.to_hex_string(),
"03885202b17556b275204f355bdcb7cc0af728ef3cceb9615d90684bb5b2ca5f859ab0f0b704075871aaac"
);
}
#[test]
fn commitments_and_close_match_hitch_shape() {
let (channel, _, _) = channel();
let state = ChannelState {
balances: Balances {
a: Amount::from_sat(60_000),
b: Amount::from_sat(40_000),
},
revocation: RevocationKeys {
a: public(&key(0x33)),
b: public(&key(0x44)),
},
htlcs: vec![],
};
let a = channel.commitment(1, Side::A, &state).unwrap();
let b = channel.commitment(1, Side::B, &state).unwrap();
assert_eq!(a.kinds, [OutputKind::ToLocal, OutputKind::ToRemote]);
assert_eq!(a.local_value, Amount::from_sat(59_700));
assert_eq!(a.remote_value, Amount::from_sat(40_000));
assert_eq!(b.local_value, Amount::from_sat(40_000));
assert_eq!(b.remote_value, Amount::from_sat(59_700));
assert_ne!(a.tx.compute_txid(), b.tx.compute_txid());
let close = channel.cooperative_close(&state).unwrap();
assert_eq!(close.output.len(), 2);
assert_eq!(
close.output.iter().map(|o| o.value.to_sat()).sum::<u64>(),
99_700
);
let encoded = serialize(&a.tx);
let decoded: Transaction = deserialize(&encoded).unwrap();
assert_eq!(decoded.compute_txid(), a.tx.compute_txid());
}
#[test]
fn both_sighash_families_sign_the_funding_leaf() {
let (channel, a, b) = channel();
let state = ChannelState {
balances: Balances {
a: Amount::from_sat(60_000),
b: Amount::from_sat(40_000),
},
revocation: RevocationKeys {
a: public(&key(0x33)),
b: public(&key(0x44)),
},
htlcs: vec![],
};
for rules in [SighashRules::Bip341, SighashRules::KnotsUnified] {
let mut commitment = channel.commitment(1, Side::A, &state).unwrap();
let sig_a = channel
.sign_funding(&commitment.tx, &a, rules, &[0; 32])
.unwrap();
let sig_b = channel
.sign_funding(&commitment.tx, &b, rules, &[0; 32])
.unwrap();
assert!(channel.verify_funding(&commitment.tx, &public(&a), &sig_a, rules));
assert!(channel.verify_funding(&commitment.tx, &public(&b), &sig_b, rules));
assert!(!channel.verify_funding(&commitment.tx, &public(&a), &sig_b, rules));
let signatures = BTreeMap::from([(public(&a), sig_a), (public(&b), sig_b)]);
commitment.tx.input[0].witness = channel.funding_witness(&signatures).unwrap();
assert_eq!(commitment.tx.input[0].witness.len(), 4);
let expected_type = if rules == SighashRules::KnotsUnified {
0x21
} else {
0x01
};
assert_eq!(commitment.tx.input[0].witness[0][64], expected_type);
assert_eq!(commitment.tx.input[0].witness[1][64], expected_type);
}
}
#[test]
fn every_htlc_claim_path_has_the_reference_sequence_and_locktime() {
let (channel, a, b) = channel();
let preimage = [0x55; 32];
let state = ChannelState {
balances: Balances {
a: Amount::from_sat(40_000),
b: Amount::from_sat(40_000),
},
revocation: RevocationKeys {
a: public(&key(0x33)),
b: public(&key(0x44)),
},
htlcs: vec![Htlc {
id: 1,
from: Side::A,
amount: Amount::from_sat(20_000),
payment_hash: sha256::Hash::hash(&preimage).to_byte_array(),
expiry: 152_200,
}],
};
let owner_a = channel.commitment(2, Side::A, &state).unwrap();
let owner_b = channel.commitment(2, Side::B, &state).unwrap();
let destination_b = to_remote_script(public(&b));
let success_a = claim_htlc(
&owner_a,
&owner_a.htlcs[0],
HtlcClaim {
path: HtlcClaimPath::Success,
destination: destination_b.clone(),
fee: Amount::from_sat(200),
preimage: Some(preimage),
},
&b,
SighashRules::KnotsUnified,
&[0; 32],
)
.unwrap();
assert_eq!(
success_a.input[0].sequence.to_consensus_u32(),
LOCKTIME_SEQUENCE
);
assert_eq!(success_a.lock_time, LockTime::ZERO);
let timeout_a = claim_htlc(
&owner_a,
&owner_a.htlcs[0],
HtlcClaim {
path: HtlcClaimPath::Timeout,
destination: to_remote_script(public(&a)),
fee: Amount::from_sat(200),
preimage: None,
},
&a,
SighashRules::KnotsUnified,
&[0; 32],
)
.unwrap();
assert_eq!(timeout_a.input[0].sequence.to_consensus_u32(), 6);
assert_eq!(timeout_a.lock_time.to_consensus_u32(), 152_200);
let success_b = claim_htlc(
&owner_b,
&owner_b.htlcs[0],
HtlcClaim {
path: HtlcClaimPath::Success,
destination: destination_b,
fee: Amount::from_sat(200),
preimage: Some(preimage),
},
&b,
SighashRules::KnotsUnified,
&[0; 32],
)
.unwrap();
assert_eq!(success_b.input[0].sequence.to_consensus_u32(), 6);
let revocation = claim_htlc(
&owner_a,
&owner_a.htlcs[0],
HtlcClaim {
path: HtlcClaimPath::Revocation,
destination: to_remote_script(public(&b)),
fee: Amount::from_sat(200),
preimage: None,
},
&key(0x33),
SighashRules::KnotsUnified,
&[0; 32],
)
.unwrap();
assert_eq!(
revocation.input[0].sequence.to_consensus_u32(),
INPUT_SEQUENCE
);
}
#[test]
fn two_party_revocation_key_matches_hitch_golden_vectors() {
let r = key(0x33);
let s = key(0x44);
let p1 = revocation_pub(public(&r), &s).unwrap();
let p2 = revocation_pub(public(&s), &r).unwrap();
assert_eq!(p1, p2, "R + s·G = S + r·G");
assert_eq!(
p1.to_string(),
"4f355bdcb7cc0af728ef3cceb9615d90684bb5b2ca5f859ab0f0b704075871aa"
);
let combined = revocation_key(&r, &s).unwrap();
assert_eq!(public(&combined), p1);
let a = key(0x12);
let b = key(0x23);
let channel = Channel::new(
public(&a),
public(&b),
OutPoint {
txid: Txid::from_byte_array([0xab; 32]),
vout: 1,
},
Amount::from_sat(100_000),
Amount::from_sat(DEFAULT_FEE),
DEFAULT_DELAY,
)
.unwrap();
let local = to_local_script(public(&a), p1, 6).unwrap();
let state = ChannelState {
balances: Balances {
a: Amount::from_sat(60_000),
b: Amount::from_sat(40_000),
},
revocation: RevocationKeys { a: p1, b: p1 },
htlcs: vec![],
};
let commitment = channel.commitment(1, Side::A, &state).unwrap();
assert_eq!(commitment.to_local, local);
let prevouts = [TxOut {
value: commitment.local_value,
script_pubkey: local.script_pubkey.clone(),
}];
for rules in [SighashRules::Bip341, SighashRules::KnotsUnified] {
let both = sweep_to_local(
&commitment,
SweepPath::Revocation,
to_remote_script(public(&b)),
Amount::from_sat(200),
&combined,
rules,
&[0; 32],
)
.unwrap();
let sig = LeafSignature::from_slice(&both.input[0].witness[0]).unwrap();
assert!(verify_leaf_signature(
&both,
0,
&prevouts,
local.revocation.leaf_hash,
&p1,
&sig,
rules
));
for alone in [s, r, a] {
let one = sweep_to_local(
&commitment,
SweepPath::Revocation,
to_remote_script(public(&b)),
Amount::from_sat(200),
&alone,
rules,
&[0; 32],
)
.unwrap();
let sig = LeafSignature::from_slice(&one.input[0].witness[0]).unwrap();
assert!(!verify_leaf_signature(
&one,
0,
&prevouts,
local.revocation.leaf_hash,
&p1,
&sig,
rules
));
}
}
}
#[test]
fn commitment_and_close_txids_match_hitch_golden_vectors() {
let (channel, _, _) = channel();
let p1 = revocation_pub(public(&key(0x33)), &key(0x44)).unwrap();
let revocation = RevocationKeys { a: p1, b: p1 };
let state = ChannelState {
balances: Balances {
a: Amount::from_sat(60_000),
b: Amount::from_sat(40_000),
},
revocation,
htlcs: vec![],
};
assert_eq!(
channel
.commitment(1, Side::A, &state)
.unwrap()
.tx
.compute_txid()
.to_string(),
"138fe22589d87f2f363907ad877a25f73d20d5415f233c7dcc0dbd9ac2f03c13"
);
assert_eq!(
channel
.commitment(1, Side::B, &state)
.unwrap()
.tx
.compute_txid()
.to_string(),
"56ae7f0fb0b450356387a552da8a952eae772286320e2a94f36ee7c07ce05152"
);
let with_htlc = ChannelState {
balances: Balances {
a: Amount::from_sat(40_000),
b: Amount::from_sat(40_000),
},
revocation,
htlcs: vec![Htlc {
id: 1,
from: Side::A,
amount: Amount::from_sat(20_000),
payment_hash: [0xab; 32],
expiry: 152_200,
}],
};
assert_eq!(
channel
.commitment(2, Side::A, &with_htlc)
.unwrap()
.tx
.compute_txid()
.to_string(),
"4cd9bf1f440e682fcfae5d1bb0e0dd11ff9ce71d738d9ad60e8b6f3e4fe30110"
);
assert_eq!(
channel
.cooperative_close(&state)
.unwrap()
.compute_txid()
.to_string(),
"5a31ac748e2e364b1c11f21b32b85d8e539dbf9baa9f4a8c41c596a41065d1b7"
);
}
#[test]
fn htlc_revocation_leaf_needs_both_secrets_and_preimage_is_read_back() {
let (channel, a, b) = channel();
let r = key(0x33);
let s = key(0x44);
let p1 = revocation_pub(public(&r), &s).unwrap();
let preimage = [0x55; 32];
let payment_hash = sha256::Hash::hash(&preimage).to_byte_array();
let state = ChannelState {
balances: Balances {
a: Amount::from_sat(40_000),
b: Amount::from_sat(40_000),
},
revocation: RevocationKeys { a: p1, b: p1 },
htlcs: vec![Htlc {
id: 1,
from: Side::B,
amount: Amount::from_sat(20_000),
payment_hash,
expiry: 152_200,
}],
};
let commitment = channel.commitment(2, Side::A, &state).unwrap();
let htlc = &commitment.htlcs[0];
let prevouts = [TxOut {
value: htlc.htlc.amount,
script_pubkey: htlc.scripts.script_pubkey.clone(),
}];
let claim = |key: &SecretKey, path, preimage| {
claim_htlc(
&commitment,
htlc,
HtlcClaim {
path,
destination: to_remote_script(public(&b)),
fee: Amount::from_sat(200),
preimage,
},
key,
SighashRules::KnotsUnified,
&[0; 32],
)
.unwrap()
};
let verifies = |tx: &Transaction, leaf: TapLeafHash, pubkey: XOnlyPublicKey| {
let sig = LeafSignature::from_slice(&tx.input[0].witness[0]).unwrap();
verify_leaf_signature(
tx,
0,
&prevouts,
leaf,
&pubkey,
&sig,
SighashRules::KnotsUnified,
)
};
let leaf = htlc.scripts.revocation.leaf_hash;
let combined = revocation_key(&r, &s).unwrap();
assert!(verifies(
&claim(&combined, HtlcClaimPath::Revocation, None),
leaf,
p1
));
assert!(!verifies(
&claim(&s, HtlcClaimPath::Revocation, None),
leaf,
p1
));
let success = claim(&a, HtlcClaimPath::Success, Some(preimage));
assert_eq!(preimage_in(&success, &payment_hash), Some(preimage));
let timeout = claim(&b, HtlcClaimPath::Timeout, None);
assert_eq!(preimage_in(&timeout, &payment_hash), None);
}
#[test]
fn proof_of_possession_binds_the_point_and_its_place() {
let secret = key(0x51);
let point = public(&secret);
let proof = pop_sign(&secret, "abababababababab/a/base", &[7; 32]);
assert!(pop_verify(&point, &proof, "abababababababab/a/base"));
assert!(!pop_verify(&point, &proof, "abababababababab/a/0"));
assert!(!pop_verify(
&public(&key(0x52)),
&proof,
"abababababababab/a/base"
));
let forged = pop_sign(&key(0x54), "abababababababab/a/base", &[7; 32]);
assert!(!pop_verify(&point, &forged, "abababababababab/a/base"));
assert_eq!(PopSignature::from_hex(&proof.to_hex()).unwrap(), proof);
assert!(PopSignature::from_hex("ab").is_err());
}
#[test]
fn even_secret_lifts_to_the_even_point() {
for byte in 1..=40u8 {
let k = key(byte);
let even = even_secret(&k);
assert_eq!(public(&even), public(&k));
assert_eq!(even.x_only_public_key(secp()).1, Parity::Even);
}
}
#[test]
fn state_value_and_policy_bounds_are_checked() {
let (channel, _, _) = channel();
let bad = ChannelState {
balances: Balances {
a: Amount::from_sat(100_001),
b: Amount::ZERO,
},
revocation: RevocationKeys {
a: channel.keys[0],
b: channel.keys[1],
},
htlcs: vec![],
};
assert!(matches!(
channel.commitment(0, Side::A, &bad),
Err(Error::Unbalanced { .. })
));
assert!(Channel::new(
channel.keys[0],
channel.keys[1],
channel.funding_outpoint,
channel.funding_value,
Amount::from_sat(99),
6,
)
.is_err());
}
}