use bitcoin::hashes::{sha256, Hash};
use bitcoin::secp256k1::XOnlyPublicKey;
use bitcoin::sighash::{Annex, Prevouts, SighashCache, TapSighashType};
use bitcoin::taproot::{ControlBlock, LeafVersion, TapLeafHash};
use bitcoin::{Script, ScriptBuf, Transaction, TxOut};
use crate::block::{schnorr_verify, secp};
use crate::sighash::{unified_taproot_sighash, SighashRules, UnifiedTaproot, SIGHASH_UNIFIED};
pub const LOCKTIME_THRESHOLD: u32 = 500_000_000;
pub const MAX_STACK_ELEMENT: usize = 520;
pub const SEQUENCE_DISABLE: u32 = 0x8000_0000;
pub const SEQUENCE_TYPE: u32 = 0x0040_0000;
pub const SEQUENCE_MASK: u32 = 0x0000_ffff;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ChannelLeaf {
Funding {
keys: [XOnlyPublicKey; 2],
},
Key {
key: XOnlyPublicKey,
},
Delayed {
delay: u16,
key: XOnlyPublicKey,
},
HashLock {
hash: [u8; 32],
key: XOnlyPublicKey,
},
HashLockDelayed {
hash: [u8; 32],
delay: u16,
key: XOnlyPublicKey,
},
Timeout {
expiry: u32,
key: XOnlyPublicKey,
},
TimeoutDelayed {
expiry: u32,
delay: u16,
key: XOnlyPublicKey,
},
}
const OP_0: u8 = 0x00;
const OP_2: u8 = 0x52;
const OP_DROP: u8 = 0x75;
const OP_EQUALVERIFY: u8 = 0x88;
const OP_NUMEQUAL: u8 = 0x9c;
const OP_SHA256: u8 = 0xa8;
const OP_CHECKSIG: u8 = 0xac;
const OP_CLTV: u8 = 0xb1;
const OP_CSV: u8 = 0xb2;
const OP_CHECKSIGADD: u8 = 0xba;
fn push_num(out: &mut Vec<u8>, n: u32) {
if n == 0 {
out.push(OP_0);
return;
}
if n <= 16 {
out.push(0x50 + n as u8);
return;
}
let mut bytes = Vec::with_capacity(5);
let mut v = n;
while v > 0 {
bytes.push((v & 0xff) as u8);
v >>= 8;
}
if bytes.last().is_some_and(|b| b & 0x80 != 0) {
bytes.push(0);
}
out.push(bytes.len() as u8);
out.extend_from_slice(&bytes);
}
fn push_key(out: &mut Vec<u8>, key: &XOnlyPublicKey) {
out.push(0x20);
out.extend_from_slice(&key.serialize());
}
struct Reader<'a> {
bytes: &'a [u8],
at: usize,
}
impl<'a> Reader<'a> {
fn op(&mut self, op: u8) -> Option<()> {
(self.bytes.get(self.at) == Some(&op)).then(|| self.at += 1)
}
fn peek(&self) -> Option<u8> {
self.bytes.get(self.at).copied()
}
fn push32(&mut self) -> Option<&'a [u8]> {
self.op(0x20)?;
let b = self.bytes.get(self.at..self.at + 32)?;
self.at += 32;
Some(b)
}
fn key(&mut self) -> Option<XOnlyPublicKey> {
XOnlyPublicKey::from_slice(self.push32()?).ok()
}
fn num(&mut self) -> Option<u32> {
let first = self.peek()?;
let n = match first {
OP_0 => {
self.at += 1;
return Some(0);
}
0x51..=0x60 => {
self.at += 1;
return Some(u32::from(first - 0x50));
}
1..=5 => {
let len = usize::from(first);
let b = self.bytes.get(self.at + 1..self.at + 1 + len)?;
if b.last().is_some_and(|x| x & 0x80 != 0) {
return None; }
let v = b
.iter()
.rev()
.try_fold(0u64, |acc, x| Some((acc << 8) | u64::from(*x)))?;
u32::try_from(v).ok()?
}
_ => return None,
};
let mut canon = Vec::with_capacity(6);
push_num(&mut canon, n);
let len = canon.len();
if self.bytes.get(self.at..self.at + len) != Some(canon.as_slice()) {
return None;
}
self.at += len;
Some(n)
}
fn done(&self) -> bool {
self.at == self.bytes.len()
}
}
impl ChannelLeaf {
pub fn to_script(&self) -> ScriptBuf {
let mut s = Vec::with_capacity(80);
let (hash, expiry, delay, key) = match *self {
ChannelLeaf::Funding { keys } => {
push_key(&mut s, &keys[0]);
s.push(OP_CHECKSIG);
push_key(&mut s, &keys[1]);
s.extend_from_slice(&[OP_CHECKSIGADD, OP_2, OP_NUMEQUAL]);
return ScriptBuf::from_bytes(s);
}
ChannelLeaf::Key { key } => (None, None, None, key),
ChannelLeaf::Delayed { delay, key } => (None, None, Some(delay), key),
ChannelLeaf::HashLock { hash, key } => (Some(hash), None, None, key),
ChannelLeaf::HashLockDelayed { hash, delay, key } => {
(Some(hash), None, Some(delay), key)
}
ChannelLeaf::Timeout { expiry, key } => (None, Some(expiry), None, key),
ChannelLeaf::TimeoutDelayed { expiry, delay, key } => {
(None, Some(expiry), Some(delay), key)
}
};
if let Some(h) = hash {
s.extend_from_slice(&[OP_SHA256, 0x20]);
s.extend_from_slice(&h);
s.push(OP_EQUALVERIFY);
}
if let Some(e) = expiry {
push_num(&mut s, e);
s.extend_from_slice(&[OP_CLTV, OP_DROP]);
}
if let Some(d) = delay {
push_num(&mut s, u32::from(d));
s.extend_from_slice(&[OP_CSV, OP_DROP]);
}
push_key(&mut s, &key);
s.push(OP_CHECKSIG);
ScriptBuf::from_bytes(s)
}
pub fn parse(script: &Script) -> Option<Self> {
let mut r = Reader {
bytes: script.as_bytes(),
at: 0,
};
if script.len() == 70 && r.peek() == Some(0x20) && script.as_bytes()[33] == OP_CHECKSIG {
let k1 = r.key()?;
r.op(OP_CHECKSIG)?;
let k2 = r.key()?;
r.op(OP_CHECKSIGADD)?;
r.op(OP_2)?;
r.op(OP_NUMEQUAL)?;
return r.done().then_some(ChannelLeaf::Funding { keys: [k1, k2] });
}
let hash = if r.op(OP_SHA256).is_some() {
let h: [u8; 32] = r.push32()?.try_into().ok()?;
r.op(OP_EQUALVERIFY)?;
Some(h)
} else {
None
};
let mut expiry = None;
let mut delay = None;
if r.peek() != Some(0x20) {
let n = r.num()?;
if r.op(OP_CLTV).is_some() {
r.op(OP_DROP)?;
expiry = Some(n);
if r.peek() != Some(0x20) {
let d = r.num()?;
r.op(OP_CSV)?;
r.op(OP_DROP)?;
delay = Some(d);
}
} else {
r.op(OP_CSV)?;
r.op(OP_DROP)?;
delay = Some(n);
}
}
let key = r.key()?;
r.op(OP_CHECKSIG)?;
if !r.done() {
return None;
}
if expiry.is_some_and(|e| e >= LOCKTIME_THRESHOLD) {
return None;
}
let delay = match delay {
Some(d) => Some(u16::try_from(d).ok()?),
None => None,
};
let leaf = match (hash, expiry, delay) {
(None, None, None) => ChannelLeaf::Key { key },
(None, None, Some(delay)) => ChannelLeaf::Delayed { delay, key },
(Some(hash), None, None) => ChannelLeaf::HashLock { hash, key },
(Some(hash), None, Some(delay)) => ChannelLeaf::HashLockDelayed { hash, delay, key },
(None, Some(expiry), None) => ChannelLeaf::Timeout { expiry, key },
(None, Some(expiry), Some(delay)) => ChannelLeaf::TimeoutDelayed { expiry, delay, key },
(Some(_), Some(_), _) => return None,
};
Some(leaf)
}
pub fn delay(&self) -> Option<u16> {
match *self {
ChannelLeaf::Delayed { delay, .. }
| ChannelLeaf::HashLockDelayed { delay, .. }
| ChannelLeaf::TimeoutDelayed { delay, .. } => Some(delay),
_ => None,
}
}
pub fn expiry(&self) -> Option<u32> {
match *self {
ChannelLeaf::Timeout { expiry, .. } | ChannelLeaf::TimeoutDelayed { expiry, .. } => {
Some(expiry)
}
_ => None,
}
}
pub fn hash_lock(&self) -> Option<[u8; 32]> {
match *self {
ChannelLeaf::HashLock { hash, .. } | ChannelLeaf::HashLockDelayed { hash, .. } => {
Some(hash)
}
_ => None,
}
}
pub fn stack_items(&self) -> usize {
match self {
ChannelLeaf::Funding { .. }
| ChannelLeaf::HashLock { .. }
| ChannelLeaf::HashLockDelayed { .. } => 2,
_ => 1,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
pub enum ChannelError {
#[error("unsupported script type: not a taproot output")]
ScriptType,
#[error("WITNESS_MALLEATED: taproot scriptSig is not empty")]
ScriptSig,
#[error("a script-path witness has at least a script and a control block")]
TooFewItems,
#[error("bad annex")]
Annex,
#[error("bad control block")]
ControlBlock,
#[error("control block commitment mismatch")]
Commitment,
#[error("unknown tapleaf version {0:#04x}: not verified, refused")]
LeafVersion(u8),
#[error(
"the leaf is not a Hitch channel template; only those are verified on transaction inputs"
)]
NotATemplate,
#[error("{have} witness items for a leaf that consumes {need}")]
StackItems {
have: usize,
need: usize,
},
#[error("a witness item is larger than 520 bytes")]
StackItemSize,
#[error("the preimage does not hash to the leaf's payment hash")]
HashLock,
#[error("CHECKLOCKTIMEVERIFY: nLockTime below the leaf's expiry, a time, or a final input")]
LockTime,
#[error(
"CHECKSEQUENCEVERIFY: the input's nSequence does not carry the leaf's delay in blocks"
)]
Sequence,
#[error("slot {slot}: bad tapscript signature encoding")]
BadSignatureEncoding {
slot: usize,
},
#[error("tapscript sigops budget exceeded")]
Budget,
#[error("slot {slot}: invalid schnorr signature")]
InvalidSignature {
slot: usize,
},
#[error("slot {slot}: no signature where the leaf needs one")]
MissingSignature {
slot: usize,
},
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ChannelSpend {
pub leaf: ChannelLeaf,
pub leaf_hash: TapLeafHash,
pub preimage: Option<Vec<u8>>,
}
fn compact_len(n: usize) -> i64 {
match n {
0..=0xfc => 1,
0xfd..=0xffff => 3,
_ => 5,
}
}
#[allow(clippy::too_many_arguments)]
fn check_signature(
tx: &Transaction,
index: usize,
prevouts: &[TxOut],
annex: Option<&[u8]>,
leaf_hash: TapLeafHash,
raw: &[u8],
key: &XOnlyPublicKey,
rules: SighashRules,
slot: usize,
) -> Result<(), ChannelError> {
let bad = ChannelError::BadSignatureEncoding { slot };
let (sig, hash_type) = match raw.len() {
64 => (raw, 0u8),
65 if raw[64] != 0 => (&raw[..64], raw[64]),
_ => return Err(bad),
};
let msg = if rules == SighashRules::KnotsUnified && hash_type & SIGHASH_UNIFIED != 0 {
let leaf = leaf_hash.to_byte_array();
unified_taproot_sighash(
tx,
index,
prevouts,
hash_type,
annex,
UnifiedTaproot::ScriptPath {
leaf_hash: &leaf,
codesep_pos: 0xffff_ffff,
},
)
.map_err(|_| bad)?
} else {
let ty = TapSighashType::from_consensus_u8(hash_type).map_err(|_| bad)?;
if prevouts.len() != tx.input.len() {
return Err(ChannelError::ScriptType);
}
SighashCache::new(tx)
.taproot_signature_hash(
index,
&Prevouts::All(prevouts),
annex.map(|a| Annex::new(a).expect("checked by the caller")),
Some((leaf_hash, 0xffff_ffff)),
ty,
)
.map_err(|_| bad)?
.to_byte_array()
};
if schnorr_verify(&msg, sig, &key.serialize()) {
Ok(())
} else {
Err(ChannelError::InvalidSignature { slot })
}
}
fn check_sequence(tx: &Transaction, index: usize, delay: u16) -> Result<(), ChannelError> {
let seq = tx.input[index].sequence.0;
if tx.version.0 < 2
|| seq & SEQUENCE_DISABLE != 0
|| seq & SEQUENCE_TYPE != 0
|| seq & SEQUENCE_MASK < u32::from(delay)
{
return Err(ChannelError::Sequence);
}
Ok(())
}
fn check_lock_time(tx: &Transaction, index: usize, expiry: u32) -> Result<(), ChannelError> {
let lt = tx.lock_time.to_consensus_u32();
if lt >= LOCKTIME_THRESHOLD || expiry > lt || tx.input[index].sequence.0 == 0xffff_ffff {
return Err(ChannelError::LockTime);
}
Ok(())
}
pub fn verify_channel_input(
tx: &Transaction,
index: usize,
prevouts: &[TxOut],
rules: SighashRules,
) -> Result<ChannelSpend, ChannelError> {
let prevout = prevouts.get(index).ok_or(ChannelError::ScriptType)?;
let input = tx.input.get(index).ok_or(ChannelError::ScriptType)?;
if !prevout.script_pubkey.is_p2tr() {
return Err(ChannelError::ScriptType);
}
if !input.script_sig.is_empty() {
return Err(ChannelError::ScriptSig);
}
let mut items: Vec<&[u8]> = input.witness.iter().collect();
let witness_size: i64 = items
.iter()
.map(|w| w.len() as i64 + compact_len(w.len()))
.sum::<i64>()
+ compact_len(items.len());
let annex: Option<&[u8]> =
if items.len() >= 2 && items.last().is_some_and(|a| a.first() == Some(&0x50)) {
let raw = items.pop().expect("checked");
Annex::new(raw).map_err(|_| ChannelError::Annex)?;
Some(raw)
} else {
None
};
if items.len() < 2 {
return Err(ChannelError::TooFewItems);
}
let control = items.pop().expect("checked");
let script = Script::from_bytes(items.pop().expect("checked"));
let cb = ControlBlock::decode(control).map_err(|_| ChannelError::ControlBlock)?;
let output_key = XOnlyPublicKey::from_slice(&prevout.script_pubkey.as_bytes()[2..34])
.map_err(|_| ChannelError::ScriptType)?;
if !cb.verify_taproot_commitment(secp(), output_key, script) {
return Err(ChannelError::Commitment);
}
if cb.leaf_version != LeafVersion::TapScript {
return Err(ChannelError::LeafVersion(cb.leaf_version.to_consensus()));
}
let leaf = ChannelLeaf::parse(script).ok_or(ChannelError::NotATemplate)?;
if items.len() != leaf.stack_items() {
return Err(ChannelError::StackItems {
have: items.len(),
need: leaf.stack_items(),
});
}
if items.iter().any(|i| i.len() > MAX_STACK_ELEMENT) {
return Err(ChannelError::StackItemSize);
}
let leaf_hash = TapLeafHash::from_script(script, LeafVersion::TapScript);
let mut budget = 50 + witness_size;
let mut sig = |raw: &[u8], key: &XOnlyPublicKey, slot: usize| -> Result<(), ChannelError> {
if raw.is_empty() {
return Err(ChannelError::MissingSignature { slot });
}
budget -= 50;
if budget < 0 {
return Err(ChannelError::Budget);
}
check_signature(tx, index, prevouts, annex, leaf_hash, raw, key, rules, slot)
};
let mut preimage = None;
match leaf {
ChannelLeaf::Funding { keys } => {
let top = items[1];
let under = items[0];
for (slot, raw) in [top, under].into_iter().enumerate() {
if !raw.is_empty() && raw.len() != 64 && !(raw.len() == 65 && raw[64] != 0) {
return Err(ChannelError::BadSignatureEncoding { slot });
}
}
sig(top, &keys[0], 0)?;
sig(under, &keys[1], 1)?;
}
_ => {
if let Some(hash) = leaf.hash_lock() {
let p = items[1];
if sha256::Hash::hash(p).to_byte_array() != hash {
return Err(ChannelError::HashLock);
}
preimage = Some(p.to_vec());
}
if let Some(expiry) = leaf.expiry() {
check_lock_time(tx, index, expiry)?;
}
if let Some(delay) = leaf.delay() {
check_sequence(tx, index, delay)?;
}
let key = match leaf {
ChannelLeaf::Key { key }
| ChannelLeaf::Delayed { key, .. }
| ChannelLeaf::HashLock { key, .. }
| ChannelLeaf::HashLockDelayed { key, .. }
| ChannelLeaf::Timeout { key, .. }
| ChannelLeaf::TimeoutDelayed { key, .. } => key,
ChannelLeaf::Funding { .. } => unreachable!("handled above"),
};
sig(items[0], &key, 0)?;
}
}
Ok(ChannelSpend {
leaf,
leaf_hash,
preimage,
})
}
pub fn earliest_height(tx: &Transaction, index: usize, coin_height: u32) -> Option<u32> {
let mut from = 0u32;
let seq = tx.input.get(index)?.sequence.0;
if tx.version.0 >= 2 && seq & SEQUENCE_DISABLE == 0 {
let value = seq & SEQUENCE_MASK;
if value > 0 {
if seq & SEQUENCE_TYPE != 0 {
return None;
}
from = from.max(coin_height.saturating_add(value));
}
}
let lt = tx.lock_time.to_consensus_u32();
if lt != 0 && !tx.input.iter().all(|i| i.sequence.0 == 0xffff_ffff) {
if lt >= LOCKTIME_THRESHOLD {
return None;
}
from = from.max(lt.saturating_add(1));
}
Some(from)
}
#[cfg(test)]
mod tests {
use super::*;
use bitcoin::secp256k1::{Keypair, SecretKey};
fn key(b: u8) -> XOnlyPublicKey {
Keypair::from_secret_key(secp(), &SecretKey::from_slice(&[b; 32]).unwrap())
.x_only_public_key()
.0
}
fn all() -> Vec<ChannelLeaf> {
let (a, b) = (key(1), key(2));
let mut v = vec![
ChannelLeaf::Funding { keys: [a, b] },
ChannelLeaf::Key { key: a },
];
for delay in [
0u16, 1, 3, 6, 16, 17, 127, 128, 144, 255, 256, 0x7fff, 0x8000, 0xffff,
] {
v.push(ChannelLeaf::Delayed { delay, key: a });
v.push(ChannelLeaf::HashLockDelayed {
hash: [7; 32],
delay,
key: b,
});
for expiry in [
0u32,
1,
16,
17,
1_000,
0x7f_ffff,
0x80_0000,
LOCKTIME_THRESHOLD - 1,
] {
v.push(ChannelLeaf::TimeoutDelayed {
expiry,
delay,
key: a,
});
}
}
for expiry in [
0u32,
1,
16,
17,
1_000,
0x7f_ffff,
0x80_0000,
LOCKTIME_THRESHOLD - 1,
] {
v.push(ChannelLeaf::Timeout { expiry, key: b });
}
v.push(ChannelLeaf::HashLock {
hash: [9; 32],
key: b,
});
v
}
#[test]
fn every_template_round_trips_exactly() {
for leaf in all() {
let s = leaf.to_script();
assert_eq!(ChannelLeaf::parse(&s), Some(leaf), "{s:?}");
}
}
#[test]
fn near_misses_are_not_templates() {
let a = key(1);
let refuse = |bytes: Vec<u8>, why: &str| {
assert_eq!(
ChannelLeaf::parse(Script::from_bytes(&bytes)),
None,
"{why}"
);
};
let delayed = ChannelLeaf::Delayed { delay: 6, key: a }
.to_script()
.to_bytes();
let mut v = vec![0x01, 0x06];
v.extend_from_slice(&delayed[1..]);
refuse(v, "push of 6 as data");
let mut v = vec![0x02, 0x06, 0x00];
v.extend_from_slice(&delayed[1..]);
refuse(v, "padded push");
let mut v = vec![0x01, 0x80];
v.extend_from_slice(&delayed[1..]);
refuse(v, "negative zero");
let mut v = vec![0x4f];
v.extend_from_slice(&delayed[1..]);
refuse(v, "OP_1NEGATE");
let mut v = vec![0x03, 0x00, 0x00, 0x01];
v.extend_from_slice(&delayed[1..]);
refuse(v, "delay above 0xffff");
let mut v = vec![0x03, 0x06, 0x00, 0x40];
v.extend_from_slice(&delayed[1..]);
refuse(v, "time-based delay");
let mut v = delayed.clone();
v[2] = 0x69;
refuse(v, "CSV VERIFY");
let mut v = delayed.clone();
*v.last_mut().unwrap() = 0xad;
refuse(v, "CHECKSIGVERIFY");
let mut v = delayed.clone();
v.push(0x51);
refuse(v, "trailing OP_1");
let mut t = Vec::new();
push_num(&mut t, LOCKTIME_THRESHOLD);
t.extend_from_slice(&[OP_CLTV, OP_DROP]);
push_key(&mut t, &a);
t.push(OP_CHECKSIG);
refuse(t, "time-based expiry");
let mut h = vec![OP_SHA256, 0x20];
h.extend_from_slice(&[1; 32]);
h.push(OP_EQUALVERIFY);
push_num(&mut h, 100);
h.extend_from_slice(&[OP_CLTV, OP_DROP]);
push_key(&mut h, &a);
h.push(OP_CHECKSIG);
refuse(h, "hash lock and timeout");
let mut o = Vec::new();
push_num(&mut o, 6);
o.extend_from_slice(&[OP_CSV, OP_DROP]);
push_num(&mut o, 100);
o.extend_from_slice(&[OP_CLTV, OP_DROP]);
push_key(&mut o, &a);
o.push(OP_CHECKSIG);
refuse(o, "CSV then CLTV");
let f = ChannelLeaf::Funding { keys: [a, key(2)] }
.to_script()
.to_bytes();
let mut v = f.clone();
v[68] = 0x51;
refuse(v, "1-of-2");
let mut v = f[..68].to_vec();
push_key(&mut v, &key(3));
v.extend_from_slice(&[OP_CHECKSIGADD, 0x52, OP_NUMEQUAL]);
refuse(v, "2-of-3");
refuse(vec![0x51], "OP_TRUE");
refuse(vec![], "empty");
let mut v = vec![0x20];
v.extend_from_slice(&[0xff; 32]);
v.push(OP_CHECKSIG);
refuse(v, "invalid key");
}
#[test]
fn hitch_numbers_match_script_num_encoding() {
let cases: [(u32, &[u8]); 9] = [
(0, &[0x00]),
(16, &[0x60]),
(17, &[0x01, 0x11]),
(127, &[0x01, 0x7f]),
(128, &[0x02, 0x80, 0x00]),
(144, &[0x02, 0x90, 0x00]),
(0x7fff, &[0x02, 0xff, 0x7f]),
(0x8000, &[0x03, 0x00, 0x80, 0x00]),
(LOCKTIME_THRESHOLD - 1, &[0x04, 0xff, 0x64, 0xcd, 0x1d]),
];
for (n, want) in cases {
let mut got = Vec::new();
push_num(&mut got, n);
assert_eq!(got, want, "{n}");
let mut r = Reader { bytes: &got, at: 0 };
assert_eq!(r.num(), Some(n));
assert!(r.done());
}
}
}