use bitcoin::hashes::{sha256, Hash};
use bitcoin::taproot::{LeafVersion, TapLeafHash, TapNodeHash};
use bitcoin::{Script, ScriptBuf};
use serde::{Deserialize, Serialize};
use crate::address::script_to_address;
use crate::federation::NUMS_X;
use crate::keys::{self, KeyError};
pub const PEG_TAG: &str = "sidestr/peg-in";
pub const LEAF_VERSION: u8 = 0xc0;
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum PegError {
#[error("a block count between 1 and 2^31-1")]
BlockCount,
#[error("chainHash is the chain event's id (64 hex, SPEC 3)")]
ChainHash,
#[error("script is hex")]
ScriptHex,
#[error("a key is an x-only (64 hex) or compressed (66 hex) point")]
KeyForm,
#[error("a tree needs a leaf")]
EmptyTree,
#[error("{0} is not an address prefix")]
Prefix(String),
#[error(transparent)]
Key(#[from] KeyError),
}
pub type Result<T> = core::result::Result<T, PegError>;
fn is_hex_bytes(s: &str) -> bool {
!s.is_empty() && s.len() % 2 == 0 && s.bytes().all(|c| c.is_ascii_hexdigit())
}
fn unhex(s: &str) -> Result<Vec<u8>> {
if s.is_empty() {
return Ok(Vec::new());
}
if !is_hex_bytes(s) {
return Err(PegError::ScriptHex);
}
hex::decode(s).map_err(|_| PegError::ScriptHex)
}
pub fn x_only_of(key: &str) -> Result<String> {
let s = key.to_lowercase();
let hex64 = |t: &str| t.len() == 64 && t.bytes().all(|c| c.is_ascii_hexdigit());
if hex64(&s) {
return Ok(s);
}
if s.len() == 66 && (s.starts_with("02") || s.starts_with("03")) && hex64(&s[2..]) {
return Ok(s[2..].to_string());
}
Err(PegError::KeyForm)
}
fn push_num(n: u32) -> Result<String> {
if n == 0 || n > 0x7fff_ffff {
return Err(PegError::BlockCount);
}
if n <= 16 {
return Ok(format!("{:02x}", 0x50 + n));
}
let mut b = Vec::new();
let mut v = n;
while v > 0 {
b.push((v & 0xff) as u8);
v >>= 8;
}
if b[b.len() - 1] & 0x80 != 0 {
b.push(0);
}
Ok(format!("{:02x}{}", b.len(), hex::encode(b)))
}
pub fn refund_leaf(refund_key: &str, refund_blocks: u32) -> Result<String> {
Ok(format!(
"20{}ad{}b2",
x_only_of(refund_key)?,
push_num(refund_blocks)?
))
}
pub fn peg_commitment(chain_hash: &str, script: &str) -> Result<String> {
if chain_hash.len() != 64 || !chain_hash.bytes().all(|c| c.is_ascii_hexdigit()) {
return Err(PegError::ChainHash);
}
if !is_hex_bytes(script) {
return Err(PegError::ScriptHex);
}
let chain = hex::decode(chain_hash).map_err(|_| PegError::ChainHash)?;
let script_hash = sha256::Hash::hash(&unhex(script)?).to_byte_array();
Ok(keys::tagged_scalar(PEG_TAG, &[&chain, &script_hash])?)
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct CommitLeaf {
pub key: String,
pub script: String,
pub tweak: String,
}
pub fn commit_leaf(chain_hash: &str, script: &str) -> Result<CommitLeaf> {
let t = peg_commitment(chain_hash, script)?;
let c = keys::tweak_point(&format!("02{}", hex::encode(NUMS_X)), &t)?;
let key = keys::x_only(&c)?;
Ok(CommitLeaf {
script: format!("20{key}ac"),
key,
tweak: t,
})
}
fn leaf_hash_of(script: &[u8]) -> TapLeafHash {
TapLeafHash::from_script(Script::from_bytes(script), LeafVersion::TapScript)
}
pub fn leaf_hash(script: &str) -> Result<String> {
Ok(hex::encode(leaf_hash_of(&unhex(script)?).to_byte_array()))
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TapTree {
pub root: String,
pub paths: Vec<Vec<String>>,
}
pub fn tap_tree<T: AsRef<str>>(leaf_scripts: &[T]) -> Result<TapTree> {
if leaf_scripts.is_empty() {
return Err(PegError::EmptyTree);
}
struct Node {
hash: TapNodeHash,
leaves: Vec<usize>,
}
let mut paths: Vec<Vec<TapNodeHash>> = vec![Vec::new(); leaf_scripts.len()];
let mut level = leaf_scripts
.iter()
.enumerate()
.map(|(i, s)| {
Ok(Node {
hash: TapNodeHash::from(leaf_hash_of(&unhex(s.as_ref())?)),
leaves: vec![i],
})
})
.collect::<Result<Vec<_>>>()?;
while level.len() > 1 {
let mut next = Vec::with_capacity(level.len().div_ceil(2));
let mut it = level.into_iter();
while let Some(a) = it.next() {
match it.next() {
None => next.push(a),
Some(b) => {
for &i in &a.leaves {
paths[i].push(b.hash);
}
for &i in &b.leaves {
paths[i].push(a.hash);
}
let mut leaves = a.leaves;
leaves.extend(b.leaves);
next.push(Node {
hash: TapNodeHash::from_node_hashes(a.hash, b.hash),
leaves,
});
}
}
}
level = next;
}
Ok(TapTree {
root: hex::encode(level[0].hash.to_byte_array()),
paths: paths
.into_iter()
.map(|p| p.iter().map(|h| hex::encode(h.to_byte_array())).collect())
.collect(),
})
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PegReveal {
pub internal: String,
pub refund_key: String,
pub refund_blocks: u32,
pub chain_hash: String,
pub script: String,
#[serde(default)]
pub extra_leaves: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PegLeaf {
pub script: String,
pub hash: String,
pub control_block: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PegOutput {
pub internal_key: String,
pub output_key: String,
pub output_parity: u8,
#[serde(rename = "scriptPubKey")]
pub script_pubkey: String,
pub address: String,
pub root: String,
pub tweak: String,
pub commit_key: String,
pub commitment: String,
pub leaves: Vec<PegLeaf>,
pub refund: PegLeaf,
pub commit: PegLeaf,
pub descriptor: String,
pub reveal: PegReveal,
}
impl PegOutput {
pub fn script(&self) -> ScriptBuf {
ScriptBuf::from_hex(&self.script_pubkey).expect("written as hex here")
}
}
pub fn peg_output(reveal: &PegReveal, hrp: &str) -> Result<PegOutput> {
let internal = x_only_of(&reveal.internal)?;
let p = format!("02{internal}");
let refund_x = x_only_of(&reveal.refund_key)?;
let refund = refund_leaf(&refund_x, reveal.refund_blocks)?;
let commit = commit_leaf(&reveal.chain_hash, &reveal.script)?;
let mut leaf_scripts = vec![refund, commit.script.clone()];
leaf_scripts.extend(reveal.extra_leaves.iter().cloned());
let tree = tap_tree(&leaf_scripts)?;
let t = keys::tap_tweak(&p, Some(&tree.root))?;
let q = keys::tweak_point(&p, &t)?;
let output_key = keys::x_only(&q)?;
let parity = u8::from(q.starts_with("03"));
let leaves = leaf_scripts
.iter()
.enumerate()
.map(|(i, s)| {
Ok(PegLeaf {
script: s.clone(),
hash: leaf_hash(s)?,
control_block: format!(
"{:02x}{internal}{}",
LEAF_VERSION | parity,
tree.paths[i].concat()
),
})
})
.collect::<Result<Vec<_>>>()?;
let script_pubkey = format!("5120{output_key}");
let address = script_to_address(
&ScriptBuf::from_hex(&script_pubkey).expect("hex written here"),
hrp,
)
.ok_or_else(|| PegError::Prefix(hrp.to_string()))?;
let descriptor = format!(
"tr({internal},{{and_v(v:pk({refund_x}),older({})),pk({}){}}})",
reveal.refund_blocks,
commit.key,
if reveal.extra_leaves.is_empty() {
""
} else {
",…"
}
);
Ok(PegOutput {
internal_key: internal.clone(),
output_key,
output_parity: parity,
script_pubkey,
address,
root: tree.root,
tweak: t,
commit_key: commit.key,
commitment: commit.tweak,
refund: leaves[0].clone(),
commit: leaves[1].clone(),
leaves,
descriptor,
reveal: PegReveal {
internal,
refund_key: refund_x,
refund_blocks: reveal.refund_blocks,
chain_hash: reveal.chain_hash.to_lowercase(),
script: reveal.script.to_lowercase(),
extra_leaves: reveal.extra_leaves.clone(),
},
})
}
pub fn peg_matches(reveal: &PegReveal, script_pubkey: &str) -> Result<bool> {
Ok(peg_output(reveal, "tb")?.script_pubkey == script_pubkey.to_lowercase())
}
pub fn peg_spend_secret(secret: &str, out: &PegOutput) -> Result<String> {
Ok(keys::tweak_secret(&keys::normalize(secret)?, &out.tweak)?)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn minimal_pushes() {
assert_eq!(push_num(1).unwrap(), "51");
assert_eq!(push_num(16).unwrap(), "60");
assert_eq!(push_num(17).unwrap(), "0111");
assert_eq!(push_num(127).unwrap(), "017f");
assert_eq!(push_num(128).unwrap(), "028000");
assert_eq!(push_num(10_000).unwrap(), "021027");
assert_eq!(push_num(0x7fff_ffff).unwrap(), "04ffffff7f");
assert_eq!(push_num(0), Err(PegError::BlockCount));
assert_eq!(push_num(0x8000_0000), Err(PegError::BlockCount));
}
#[test]
fn key_forms() {
let x = "ab".repeat(32);
assert_eq!(x_only_of(&x.to_uppercase()).unwrap(), x);
assert_eq!(x_only_of(&format!("03{x}")).unwrap(), x);
assert_eq!(x_only_of(&format!("04{x}")), Err(PegError::KeyForm));
assert_eq!(x_only_of("abcd"), Err(PegError::KeyForm));
}
#[test]
fn peg_matches_takes_the_script_hex_in_either_case() {
let internal = keys::public_key(&"11".repeat(32)).unwrap();
let refund = keys::x_only(&keys::public_key(&"22".repeat(32)).unwrap()).unwrap();
let reveal = PegReveal {
internal,
refund_key: refund,
refund_blocks: 144,
chain_hash: "AA".repeat(32),
script: "51".into(),
extra_leaves: vec![],
};
let out = peg_output(&reveal, "tb").unwrap();
assert_eq!(out.reveal.chain_hash, "aa".repeat(32));
assert!(peg_matches(&out.reveal, &out.script_pubkey).unwrap());
assert!(peg_matches(&out.reveal, &out.script_pubkey.to_uppercase()).unwrap());
}
}