use sha2::{Digest, Sha256};
use subtle::ConstantTimeEq;
use super::address::TransparentAddress;
use super::script::{
PREIMAGE_SIZE, ScriptItem, assert_p2sh_sizes, decompile, hash160, op, p2sh_script, push_number, read_script_num,
write_push,
};
use super::tx::LOCKTIME_THRESHOLD;
use super::{Error, Network};
use crate::keys::{PublicKey, Secret};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct HtlcParams {
pub hashlock: [u8; 32],
pub claim_pubkey: PublicKey,
pub refund_pubkey: PublicKey,
pub refund_height: u32,
}
impl HtlcParams {
pub fn from_slices(
hashlock: &[u8],
claim_pubkey: &[u8],
refund_pubkey: &[u8],
refund_height: u64,
) -> Result<Self, Error> {
let claim_pubkey = PublicKey::from_compressed(claim_pubkey)
.map_err(|_| Error::InvalidPubkey { what: "claim pubkey", shown: shown(claim_pubkey) })?;
let refund_pubkey = PublicKey::from_compressed(refund_pubkey)
.map_err(|_| Error::InvalidPubkey { what: "refund pubkey", shown: shown(refund_pubkey) })?;
let hashlock = <[u8; 32]>::try_from(hashlock).map_err(|_| Error::InvalidHashlockLength)?;
let refund_height = u32::try_from(refund_height).map_err(|_| Error::InvalidRefundHeight)?;
let params = Self { hashlock, claim_pubkey, refund_pubkey, refund_height };
htlc_redeem_script(¶ms)?;
Ok(params)
}
}
pub(crate) fn shown(bytes: &[u8]) -> String {
crate::encoding::to_hex(bytes).chars().take(80).collect()
}
fn template(hashlock: &[u8; 32], claim: &[u8], refund: &[u8], refund_height: u32) -> Result<Vec<u8>, Error> {
let mut s = Vec::with_capacity(120);
s.extend_from_slice(&[op::OP_IF, op::OP_SIZE]);
s.extend_from_slice(&push_number(PREIMAGE_SIZE as u64));
s.extend_from_slice(&[op::OP_EQUALVERIFY, op::OP_SHA256]);
write_push(&mut s, hashlock)?;
s.push(op::OP_EQUALVERIFY);
write_push(&mut s, claim)?;
s.push(op::OP_ELSE);
s.extend_from_slice(&push_number(u64::from(refund_height)));
s.extend_from_slice(&[op::OP_CHECKLOCKTIMEVERIFY, op::OP_DROP]);
write_push(&mut s, refund)?;
s.extend_from_slice(&[op::OP_ENDIF, op::OP_CHECKSIG]);
Ok(s)
}
fn check_height(refund_height: u32) -> Result<(), Error> {
if !(1..LOCKTIME_THRESHOLD).contains(&refund_height) {
return Err(Error::InvalidRefundHeight);
}
Ok(())
}
pub fn htlc_redeem_script(p: &HtlcParams) -> Result<Vec<u8>, Error> {
if p.claim_pubkey == p.refund_pubkey {
return Err(Error::IdenticalKeys("claim and refund keys must differ"));
}
check_height(p.refund_height)?;
template(&p.hashlock, p.claim_pubkey.as_bytes(), p.refund_pubkey.as_bytes(), p.refund_height)
}
pub fn parse_htlc_redeem_script(script: &[u8]) -> Result<HtlcParams, Error> {
let ops = decompile(script).map_err(|_| Error::NotHtlcScript)?;
let bytes_at = |i: usize, len: usize| -> Result<&[u8], Error> {
match ops.get(i) {
Some(ScriptItem::Push(data)) if data.len() == len => Ok(data),
_ => Err(Error::NotHtlcScript),
}
};
let mut hashlock = [0u8; 32];
hashlock.copy_from_slice(bytes_at(5, 32)?);
let claim = bytes_at(7, 33)?;
let refund = bytes_at(12, 33)?;
let height = match ops.get(9) {
Some(ScriptItem::Push(data)) if data.len() > 8 => return Err(Error::InvalidRefundHeight),
item => read_script_num(item)?,
};
if [claim, refund].iter().any(|k| !matches!(k.first(), Some(0x02 | 0x03))) {
return Err(Error::PubkeysNotCompressed);
}
if claim == refund {
return Err(Error::IdenticalKeys("claim and refund keys must differ"));
}
let refund_height = u32::try_from(height).map_err(|_| Error::InvalidRefundHeight)?;
check_height(refund_height)?;
if template(&hashlock, claim, refund, refund_height)? != script {
return Err(Error::NotHtlcScript);
}
let (Ok(claim_pubkey), Ok(refund_pubkey)) = (PublicKey::from_compressed(claim), PublicKey::from_compressed(refund))
else {
return Err(Error::HtlcKeyNotOnCurve);
};
Ok(HtlcParams { hashlock, claim_pubkey, refund_pubkey, refund_height })
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Htlc {
params: HtlcParams,
redeem_script: Vec<u8>,
script_pubkey: Vec<u8>,
address: String,
network: Network,
}
impl Htlc {
pub fn new(params: HtlcParams, network: Network) -> Result<Self, Error> {
let redeem_script = htlc_redeem_script(¶ms)?;
let hash = hash160(&redeem_script);
let address = TransparentAddress::p2sh(&redeem_script, network).encode();
Ok(Self { params, script_pubkey: p2sh_script(&hash), redeem_script, address, network })
}
pub fn from_redeem_script(redeem_script: &[u8], network: Network) -> Result<Self, Error> {
Self::new(parse_htlc_redeem_script(redeem_script)?, network)
}
pub const fn params(&self) -> &HtlcParams {
&self.params
}
pub fn redeem_script(&self) -> &[u8] {
&self.redeem_script
}
pub fn script_pubkey(&self) -> &[u8] {
&self.script_pubkey
}
pub fn address(&self) -> &str {
&self.address
}
pub const fn network(&self) -> Network {
self.network
}
pub fn transparent_address(&self) -> TransparentAddress {
TransparentAddress::p2sh(&self.redeem_script, self.network)
}
}
pub fn claim_script_sig(signature: &[u8], preimage: &[u8], redeem_script: &[u8]) -> Result<Vec<u8>, Error> {
if preimage.len() != PREIMAGE_SIZE {
return Err(Error::InvalidPreimageLength);
}
let mut s = Vec::with_capacity(signature.len() + preimage.len() + redeem_script.len() + 8);
write_push(&mut s, signature)?;
write_push(&mut s, preimage)?;
s.push(op::OP_1);
write_push(&mut s, redeem_script)?;
assert_p2sh_sizes(redeem_script, &s)?;
Ok(s)
}
pub fn refund_script_sig(signature: &[u8], redeem_script: &[u8]) -> Result<Vec<u8>, Error> {
let mut s = Vec::with_capacity(signature.len() + redeem_script.len() + 6);
write_push(&mut s, signature)?;
s.push(op::OP_0);
write_push(&mut s, redeem_script)?;
assert_p2sh_sizes(redeem_script, &s)?;
Ok(s)
}
pub fn extract_preimage(script_sig: &[u8], hashlock: &[u8; 32]) -> Result<Option<Secret>, Error> {
for item in decompile(script_sig)? {
if let ScriptItem::Push(data) = item {
if data.len() == PREIMAGE_SIZE {
let digest: [u8; 32] = Sha256::digest(data).into();
if bool::from(digest.ct_eq(hashlock)) {
return Ok(Some(Secret::from_slice(data)?));
}
}
}
}
Ok(None)
}
#[cfg(test)]
mod tests {
#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
use super::*;
use crate::keys::KeyPair;
fn params() -> HtlcParams {
HtlcParams {
hashlock: [0x11; 32],
claim_pubkey: KeyPair::random(&mut crate::testutil::TestRng).public_key(),
refund_pubkey: KeyPair::random(&mut crate::testutil::TestRng).public_key(),
refund_height: 1234,
}
}
#[test]
fn round_trips_the_template() {
let p = params();
let htlc = Htlc::new(p, Network::Regtest).unwrap();
assert_eq!(htlc.redeem_script().len(), 2 + 2 + 2 + 33 + 1 + 34 + 1 + 3 + 2 + 34 + 2);
assert_eq!(Htlc::from_redeem_script(htlc.redeem_script(), Network::Regtest).unwrap(), htlc);
assert!(htlc.address().starts_with("t2"));
assert_eq!(htlc.transparent_address().encode(), htlc.address());
assert_eq!(htlc.transparent_address().script_pubkey(), htlc.script_pubkey());
}
#[test]
fn rejects_invalid_or_identical_keys_and_heights() {
let p = params();
assert!(matches!(
htlc_redeem_script(&HtlcParams { refund_pubkey: p.claim_pubkey, ..p }),
Err(Error::IdenticalKeys(_))
));
assert_eq!(htlc_redeem_script(&HtlcParams { refund_height: 0, ..p }), Err(Error::InvalidRefundHeight));
assert_eq!(
htlc_redeem_script(&HtlcParams { refund_height: 500_000_000, ..p }),
Err(Error::InvalidRefundHeight)
);
assert!(htlc_redeem_script(&HtlcParams { refund_height: 499_999_999, ..p }).is_ok());
}
#[test]
fn rejects_lookalike_scripts() {
let script = htlc_redeem_script(¶ms()).unwrap();
for i in [0, 1, 2, 3, 4, 5, 6, 39, 40, 74, 75, 78, 79, 80, 114, 115] {
let mut s = script.clone();
s[i] ^= 0x01;
assert!(parse_htlc_redeem_script(&s).is_err(), "byte {i}");
}
assert!(parse_htlc_redeem_script(&script[..script.len() - 1]).is_err());
let mut longer = script.clone();
longer.push(op::OP_CHECKSIG);
assert!(parse_htlc_redeem_script(&longer).is_err());
let p = params();
let mut nonminimal = htlc_redeem_script(&p).unwrap();
assert_eq!(nonminimal[74], op::OP_ELSE);
let at = 75;
nonminimal.splice(at..at + 3, [0x05, 0xd2, 0x04, 0x00, 0x00, 0x00]);
assert_eq!(parse_htlc_redeem_script(&nonminimal), Err(Error::NotHtlcScript));
let mut size = script.clone();
size[2] = op::OP_1;
assert_eq!(parse_htlc_redeem_script(&size), Err(Error::NotHtlcScript));
}
#[test]
fn extracts_the_preimage_from_a_claim() {
let secret = Secret::from_bytes([42; 32]);
let p = HtlcParams { hashlock: secret.hashlock(), ..params() };
let script = htlc_redeem_script(&p).unwrap();
let sig = claim_script_sig(&[1; 72], secret.expose_secret(), &script).unwrap();
assert_eq!(extract_preimage(&sig, &p.hashlock).unwrap(), Some(secret));
let refund = refund_script_sig(&[1; 72], &script).unwrap();
assert_eq!(extract_preimage(&refund, &p.hashlock).unwrap(), None);
assert!(extract_preimage(&[0x05, 1], &p.hashlock).is_err());
assert_eq!(claim_script_sig(&[1; 72], &[0; 31], &script), Err(Error::InvalidPreimageLength));
}
#[test]
fn enforces_size_limits() {
assert_eq!(refund_script_sig(&[1; 72], &[0; 521]), Err(Error::RedeemScriptTooLarge(521)));
assert!(matches!(refund_script_sig(&[1; 1200], &[0; 500]), Err(Error::ScriptSigTooLarge(_))));
}
}