use std::sync::OnceLock;
use bitcoin::block::{Header, Version as HeaderVersion};
use bitcoin::consensus::encode::{deserialize, serialize};
use bitcoin::hashes::{sha256, sha256d, Hash, HashEngine};
use bitcoin::key::TweakedPublicKey;
use bitcoin::secp256k1::{
schnorr::Signature, All, Keypair, Message, Secp256k1, SecretKey, XOnlyPublicKey,
};
use bitcoin::sighash::{Annex, Prevouts, SighashCache, TapSighashType};
use bitcoin::taproot::TapLeafHash;
use bitcoin::transaction::Version as TxVersion;
use bitcoin::{
absolute::LockTime, merkle_tree, Amount, BlockHash, CompactTarget, OutPoint, Script, ScriptBuf,
Sequence, Target, Transaction, TxIn, TxMerkleNode, TxOut, Txid, Witness, Wtxid,
};
use crate::error::{Error, Result};
use crate::federation::{verify_multi_a_input, MultiA, ScriptPathError};
use crate::parents::Family;
use crate::rules::RuleResult;
use crate::sighash::{verify_taproot_key_path, SighashRules};
pub const SIGNET_HEADER: [u8; 4] = [0xec, 0xc7, 0xda, 0xa2];
pub const MARKER: &str = "sidestr";
const COMMITMENT_PREFIX: [u8; 6] = [0x6a, 0x24, 0xaa, 0x21, 0xa9, 0xed];
const COMMITMENT_LEN: usize = 38;
const MAX_WITNESS_ITEMS: usize = 256;
pub fn secp() -> &'static Secp256k1<All> {
static SECP: OnceLock<Secp256k1<All>> = OnceLock::new();
SECP.get_or_init(Secp256k1::new)
}
pub trait HeaderFamily:
core::fmt::Debug + Copy + Default + PartialEq + Eq + Send + Sync + 'static
{
type Header: Clone + core::fmt::Debug + PartialEq + Eq + Send + Sync + 'static;
type Block: SidestrBlock<Header = Self::Header>;
const FAMILY: Family;
const HEADER_LEN: usize;
fn family(&self) -> Family {
Self::FAMILY
}
fn header_len(&self) -> usize {
Self::HEADER_LEN
}
fn encode_header(&self, header: &Self::Header) -> Vec<u8>;
fn decode_header(&self, bytes: &[u8]) -> Result<Self::Header>;
fn block_hash(&self, header: &Self::Header) -> BlockHash;
fn signed_prefix(&self, header: &Self::Header) -> Vec<u8>;
fn header_height(&self, header: &Self::Header) -> Option<u32>;
fn new_header(
&self,
prev: BlockHash,
merkle_root: TxMerkleNode,
time: u32,
bits: CompactTarget,
height: u32,
tx_count: usize,
) -> Self::Header;
fn version(&self, header: &Self::Header) -> u32;
fn version_number(&self, header: &Self::Header) -> i64;
fn prev(&self, header: &Self::Header) -> BlockHash;
fn merkle_root(&self, header: &Self::Header) -> TxMerkleNode;
fn time(&self, header: &Self::Header) -> u32;
fn bits(&self, header: &Self::Header) -> CompactTarget;
fn nonce(&self, header: &Self::Header) -> u32;
fn set_merkle_root(&self, header: &mut Self::Header, root: TxMerkleNode);
fn set_nonce(&self, header: &mut Self::Header, nonce: u32);
fn header_rules(&self, header: &Self::Header, height: u32) -> Vec<RuleResult> {
let _ = (header, height);
Vec::new()
}
fn block_rules(&self, header: &Self::Header, tx_count: usize) -> Vec<RuleResult> {
let _ = (header, tx_count);
Vec::new()
}
fn sighash_rules(&self, height: u32) -> SighashRules {
let _ = height;
SighashRules::Bip341
}
}
pub trait SidestrBlock:
Clone + core::fmt::Debug + PartialEq + Eq + Send + Sync + Sized + 'static
{
type Header;
fn from_parts(header: Self::Header, txdata: Vec<Transaction>) -> Self;
fn header(&self) -> &Self::Header;
fn header_mut(&mut self) -> &mut Self::Header;
fn txdata(&self) -> &[Transaction];
fn txdata_mut(&mut self) -> &mut Vec<Transaction>;
fn encode(&self) -> Vec<u8>;
fn decode(bytes: &[u8]) -> Result<Self>;
}
pub const VERSION_HEADER_V2_FLAG: u32 = 0x8000_0000;
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct Stock;
fn stock_bit31(header: &Header) -> Result<()> {
if header.version.to_consensus() as u32 & VERSION_HEADER_V2_FLAG != 0 {
return Err(Error::Encoding(format!(
"stock header: version {:#010x} has bit 31 set (VERSION_HEADER_V2_FLAG): not a stock header",
header.version.to_consensus() as u32
)));
}
Ok(())
}
impl HeaderFamily for Stock {
type Header = Header;
type Block = bitcoin::Block;
const FAMILY: Family = Family::Stock;
const HEADER_LEN: usize = 80;
fn encode_header(&self, header: &Header) -> Vec<u8> {
serialize(header)
}
fn decode_header(&self, bytes: &[u8]) -> Result<Header> {
let header: Header =
deserialize(bytes).map_err(|e| Error::Encoding(format!("stock header: {e}")))?;
stock_bit31(&header)?;
Ok(header)
}
fn block_hash(&self, header: &Header) -> BlockHash {
header.block_hash()
}
fn signed_prefix(&self, header: &Header) -> Vec<u8> {
serialize(header)[..72].to_vec()
}
fn header_height(&self, _header: &Header) -> Option<u32> {
None
}
fn new_header(
&self,
prev: BlockHash,
merkle_root: TxMerkleNode,
time: u32,
bits: CompactTarget,
_height: u32,
_tx_count: usize,
) -> Header {
Header {
version: HeaderVersion::from_consensus(0x2000_0000),
prev_blockhash: prev,
merkle_root,
time,
bits,
nonce: 0,
}
}
fn version(&self, header: &Header) -> u32 {
header.version.to_consensus() as u32
}
fn version_number(&self, header: &Header) -> i64 {
i64::from(header.version.to_consensus())
}
fn prev(&self, header: &Header) -> BlockHash {
header.prev_blockhash
}
fn merkle_root(&self, header: &Header) -> TxMerkleNode {
header.merkle_root
}
fn time(&self, header: &Header) -> u32 {
header.time
}
fn bits(&self, header: &Header) -> CompactTarget {
header.bits
}
fn nonce(&self, header: &Header) -> u32 {
header.nonce
}
fn set_merkle_root(&self, header: &mut Header, root: TxMerkleNode) {
header.merkle_root = root;
}
fn set_nonce(&self, header: &mut Header, nonce: u32) {
header.nonce = nonce;
}
}
impl SidestrBlock for bitcoin::Block {
type Header = Header;
fn from_parts(header: Header, txdata: Vec<Transaction>) -> Self {
bitcoin::Block { header, txdata }
}
fn header(&self) -> &Header {
&self.header
}
fn header_mut(&mut self) -> &mut Header {
&mut self.header
}
fn txdata(&self) -> &[Transaction] {
&self.txdata
}
fn txdata_mut(&mut self) -> &mut Vec<Transaction> {
&mut self.txdata
}
fn encode(&self) -> Vec<u8> {
serialize(self)
}
fn decode(bytes: &[u8]) -> Result<Self> {
let block: bitcoin::Block =
deserialize(bytes).map_err(|e| Error::Encoding(e.to_string()))?;
stock_bit31(&block.header)?;
Ok(block)
}
}
pub type Block = <Stock as HeaderFamily>::Block;
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct FamilyBlock<F: HeaderFamily> {
pub header: F::Header,
pub txdata: Vec<Transaction>,
family: F,
}
impl<F: HeaderFamily> SidestrBlock for FamilyBlock<F> {
type Header = F::Header;
fn from_parts(header: F::Header, txdata: Vec<Transaction>) -> Self {
FamilyBlock {
header,
txdata,
family: F::default(),
}
}
fn header(&self) -> &F::Header {
&self.header
}
fn header_mut(&mut self) -> &mut F::Header {
&mut self.header
}
fn txdata(&self) -> &[Transaction] {
&self.txdata
}
fn txdata_mut(&mut self) -> &mut Vec<Transaction> {
&mut self.txdata
}
fn encode(&self) -> Vec<u8> {
let mut out = F::default().encode_header(&self.header);
out.extend(serialize(&self.txdata));
out
}
fn decode(bytes: &[u8]) -> Result<Self> {
if bytes.len() < F::HEADER_LEN {
return Err(Error::Encoding(format!(
"block of {} bytes is shorter than a {} header",
bytes.len(),
F::HEADER_LEN
)));
}
let header = F::default().decode_header(&bytes[..F::HEADER_LEN])?;
let txdata: Vec<Transaction> =
deserialize(&bytes[F::HEADER_LEN..]).map_err(|e| Error::Encoding(e.to_string()))?;
Ok(FamilyBlock {
header,
txdata,
family: F::default(),
})
}
}
pub fn block_weight<F: HeaderFamily>(family: &F, block: &F::Block) -> u64 {
let n = block.txdata().len();
let varint = match n {
0..=0xfc => 1,
0xfd..=0xffff => 3,
_ => 5,
};
let fixed = (family.header_len() as u64).saturating_add(varint);
let legacy = block
.txdata()
.iter()
.fold(fixed, |n, tx| n.saturating_add(tx.base_size() as u64));
let total = block
.txdata()
.iter()
.fold(fixed, |n, tx| n.saturating_add(tx.total_size() as u64));
legacy.saturating_mul(3).saturating_add(total)
}
pub fn merkle_root_of_txs(txdata: &[Transaction]) -> TxMerkleNode {
merkle_root_of(txdata.iter().map(Transaction::compute_txid))
}
pub fn witness_root_of_txs(txdata: &[Transaction]) -> [u8; 32] {
merkle_tree::calculate_root(
std::iter::once(Wtxid::all_zeros())
.chain(txdata.iter().skip(1).map(Transaction::compute_wtxid)),
)
.map(|h| h.to_byte_array())
.unwrap_or([0u8; 32])
}
fn compact_size(n: usize) -> Vec<u8> {
match n {
0..=0xfc => vec![n as u8],
0xfd..=0xffff => vec![0xfd, (n & 0xff) as u8, (n >> 8) as u8],
_ => vec![
0xfe,
(n & 0xff) as u8,
((n >> 8) & 0xff) as u8,
((n >> 16) & 0xff) as u8,
((n >> 24) & 0xff) as u8,
],
}
}
fn read_compact(b: &[u8], i: usize) -> Option<(usize, usize)> {
let first = *b.get(i)?;
match first {
0..=0xfc => Some((usize::from(first), i + 1)),
0xfd => {
let n = usize::from(*b.get(i + 1)?) | usize::from(*b.get(i + 2)?) << 8;
(n >= 0xfd).then_some((n, i + 3))
}
0xfe => {
let n = usize::from(*b.get(i + 1)?)
| usize::from(*b.get(i + 2)?) << 8
| usize::from(*b.get(i + 3)?) << 16
| usize::from(*b.get(i + 4)?) << 24;
(n >= 0x1_0000).then_some((n, i + 5))
}
_ => None,
}
}
pub fn encode_witness(items: &[Vec<u8>]) -> Vec<u8> {
let mut out = compact_size(items.len());
for it in items {
out.extend(compact_size(it.len()));
out.extend_from_slice(it);
}
out
}
pub fn decode_witness(bytes: &[u8]) -> Option<Vec<Vec<u8>>> {
let (n, mut i) = read_compact(bytes, 0)?;
if n > MAX_WITNESS_ITEMS {
return None;
}
let mut items = Vec::with_capacity(n);
for _ in 0..n {
let (len, at) = read_compact(bytes, i)?;
let end = at.checked_add(len)?;
items.push(bytes.get(at..end)?.to_vec());
i = end;
}
(i == bytes.len()).then_some(items)
}
pub fn commitment_output<B: SidestrBlock>(block: &B) -> Option<(usize, &Script)> {
commitment_output_of(block.txdata().first()?)
}
fn commitment_output_of(cb: &Transaction) -> Option<(usize, &Script)> {
cb.output
.iter()
.enumerate()
.rev()
.find(|(_, o)| {
o.script_pubkey.len() >= COMMITMENT_LEN
&& o.script_pubkey.as_bytes().starts_with(&COMMITMENT_PREFIX)
})
.map(|(i, o)| (i, o.script_pubkey.as_script()))
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Solution {
pub witness: Vec<Vec<u8>>,
pub index: usize,
}
pub fn solution_of<B: SidestrBlock>(block: &B) -> Option<Solution> {
let (index, spk) = commitment_output(block)?;
let rest = &spk.as_bytes()[COMMITMENT_LEN..];
if rest.is_empty() {
return None;
}
let (n, at) = match rest[0] {
1..=75 => (usize::from(rest[0]), 1),
0x4c if rest.len() >= 2 => (usize::from(rest[1]), 2),
0x4d if rest.len() >= 3 => (usize::from(rest[1]) | usize::from(rest[2]) << 8, 3),
_ => return None,
};
if n == 0 || rest.len() != at + n {
return None;
}
let push = &rest[at..];
let body = push.strip_prefix(&SIGNET_HEADER)?;
Some(Solution {
witness: decode_witness(body)?,
index,
})
}
pub fn with_solution<B: SidestrBlock>(block: &B, witness_items: &[Vec<u8>]) -> Result<B> {
let (index, spk) = commitment_output(block)
.ok_or_else(|| Error::Block("no witness commitment output to carry the solution".into()))?;
let mut push = SIGNET_HEADER.to_vec();
push.extend(encode_witness(witness_items));
let len = push.len();
let op: Vec<u8> = match len {
0..=75 => vec![len as u8],
76..=255 => vec![0x4c, len as u8],
256..=65535 => vec![0x4d, (len & 0xff) as u8, (len >> 8) as u8],
_ => return Err(Error::Block("solution too long for one push".into())),
};
let mut script = spk.as_bytes()[..COMMITMENT_LEN].to_vec();
script.extend(op);
script.extend(push);
let mut out = block.clone();
out.txdata_mut()[0].output[index].script_pubkey = ScriptBuf::from_bytes(script);
Ok(out)
}
fn stripped_coinbase(txdata: &[Transaction]) -> Transaction {
let mut cb = txdata[0].clone();
if let Some((i, spk)) = commitment_output_of(&txdata[0]) {
cb.output[i].script_pubkey =
ScriptBuf::from_bytes(spk.as_bytes()[..COMMITMENT_LEN].to_vec());
}
cb
}
fn merkle_root_of(txids: impl Iterator<Item = Txid>) -> TxMerkleNode {
merkle_tree::calculate_root(txids)
.map(TxMerkleNode::from)
.unwrap_or_else(TxMerkleNode::all_zeros)
}
pub fn block_data<F: HeaderFamily>(family: &F, block: &F::Block) -> [u8; 32] {
let txdata = block.txdata();
let cb = stripped_coinbase(txdata);
let root = merkle_root_of(
std::iter::once(cb.compute_txid())
.chain(txdata.iter().skip(1).map(Transaction::compute_txid)),
);
let mut header = block.header().clone();
family.set_merkle_root(&mut header, root);
sha256::Hash::hash(&family.signed_prefix(&header)).to_byte_array()
}
#[derive(Debug, Clone)]
pub struct VirtualTxs {
pub to_spend: Transaction,
pub to_sign: Transaction,
pub prevout: TxOut,
}
pub fn virtual_txs(data: &[u8; 32], challenge: &Script, witness: &[Vec<u8>]) -> VirtualTxs {
let mut script_sig = vec![0x00, 0x20];
script_sig.extend_from_slice(data);
let to_spend = Transaction {
version: TxVersion(0),
lock_time: LockTime::ZERO,
input: vec![TxIn {
previous_output: OutPoint::null(),
script_sig: ScriptBuf::from_bytes(script_sig),
sequence: Sequence::ZERO,
witness: Witness::new(),
}],
output: vec![TxOut {
value: Amount::ZERO,
script_pubkey: challenge.to_owned(),
}],
};
let prevout = TxOut {
value: Amount::ZERO,
script_pubkey: challenge.to_owned(),
};
let to_sign = Transaction {
version: TxVersion(0),
lock_time: LockTime::ZERO,
input: vec![TxIn {
previous_output: OutPoint {
txid: to_spend.compute_txid(),
vout: 0,
},
script_sig: ScriptBuf::new(),
sequence: Sequence::ZERO,
witness: Witness::from_slice(witness),
}],
output: vec![TxOut {
value: Amount::ZERO,
script_pubkey: ScriptBuf::from_bytes(vec![0x6a]),
}],
};
VirtualTxs {
to_spend,
to_sign,
prevout,
}
}
pub fn height_push(height: u32) -> Vec<u8> {
let mut out = Vec::new();
let mut n = height;
while n > 0 {
out.push((n & 0xff) as u8);
n >>= 8;
}
if out.last().is_some_and(|b| b & 0x80 != 0) {
out.push(0);
}
if out.is_empty() {
return vec![0x00];
}
let mut push = vec![out.len() as u8];
push.extend(out);
push
}
pub fn coinbase_height(coinbase: &Transaction) -> Result<u32> {
let sig = coinbase
.input
.first()
.map(|i| i.script_sig.as_bytes())
.unwrap_or(&[]);
let bad = |m: &str| Err(Error::CoinbaseHeight(m.into()));
if sig.is_empty() {
return bad("coinbase scriptSig is not a hex script");
}
let n = usize::from(sig[0]);
if n == 0 {
return Ok(0);
}
if (0x51..=0x60).contains(&n) {
return Ok((n - 0x50) as u32);
}
if n > 75 || sig.len() < 1 + n {
return bad("coinbase scriptSig does not start with a height push");
}
if n > 1 && sig[n] == 0 && sig[n - 1] & 0x80 == 0 {
return bad("coinbase height push is not minimal");
}
if sig[n] & 0x80 != 0 {
return bad("coinbase height push is negative");
}
if n > 5 {
return bad("coinbase height push is too long for a height");
}
let h = (1..=n).rev().fold(0u64, |h, i| h * 256 + u64::from(sig[i]));
u32::try_from(h)
.map_err(|_| Error::CoinbaseHeight("coinbase height push is too long for a height".into()))
}
pub fn block_height<F: HeaderFamily>(family: &F, block: &F::Block) -> Result<u32> {
match family.header_height(block.header()) {
Some(h) => Ok(h),
None => coinbase_height(
block
.txdata()
.first()
.ok_or_else(|| Error::CoinbaseHeight("block has no coinbase".into()))?,
),
}
}
#[derive(Debug, Clone)]
pub struct BlockTemplate {
pub height: u32,
pub prev: BlockHash,
pub time: u32,
pub transactions: Vec<Transaction>,
pub outputs: Vec<TxOut>,
pub bits: CompactTarget,
pub marker: String,
}
pub fn witness_commitment(transactions: &[Transaction]) -> [u8; 32] {
let root = merkle_tree::calculate_root(
std::iter::once(Wtxid::all_zeros())
.chain(transactions.iter().map(Transaction::compute_wtxid)),
)
.map(|h| h.to_byte_array())
.unwrap_or([0u8; 32]);
let mut cat = [0u8; 64];
cat[..32].copy_from_slice(&root);
sha256d::Hash::hash(&cat).to_byte_array()
}
pub fn build_block<F: HeaderFamily>(family: &F, t: &BlockTemplate) -> F::Block {
let commitment = witness_commitment(&t.transactions);
let mut commitment_spk = COMMITMENT_PREFIX.to_vec();
commitment_spk.extend_from_slice(&commitment);
let tag = t.marker.as_bytes();
let mut script_sig = height_push(t.height);
script_sig.push(tag.len() as u8);
script_sig.extend_from_slice(tag);
let mut outputs = t.outputs.clone();
outputs.push(TxOut {
value: Amount::ZERO,
script_pubkey: ScriptBuf::from_bytes(commitment_spk),
});
let coinbase = Transaction {
version: TxVersion::TWO,
lock_time: LockTime::ZERO,
input: vec![TxIn {
previous_output: OutPoint::null(),
script_sig: ScriptBuf::from_bytes(script_sig),
sequence: Sequence::MAX,
witness: Witness::from_slice(&[[0u8; 32]]),
}],
output: outputs,
};
let mut txdata = Vec::with_capacity(t.transactions.len() + 1);
txdata.push(coinbase);
txdata.extend(t.transactions.iter().cloned());
let merkle_root = merkle_root_of_txs(&txdata);
let header = family.new_header(t.prev, merkle_root, t.time, t.bits, t.height, txdata.len());
F::Block::from_parts(header, txdata)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SpendPath<'a> {
KeyPath,
ScriptPath {
leaf_hash: TapLeafHash,
annex: Option<&'a [u8]>,
codesep_pos: u32,
},
}
pub fn block_sighash_for<F: HeaderFamily>(
family: &F,
block: &F::Block,
challenge: &Script,
path: &SpendPath,
) -> Result<[u8; 32]> {
let data = block_data(family, block);
let v = virtual_txs(&data, challenge, &[]);
let mut cache = SighashCache::new(&v.to_sign);
let prevouts = [v.prevout];
let msg = match *path {
SpendPath::KeyPath => cache
.taproot_key_spend_signature_hash(0, &Prevouts::All(&prevouts), TapSighashType::Default)
.map_err(|e| Error::Block(e.to_string()))?,
SpendPath::ScriptPath {
leaf_hash,
annex,
codesep_pos,
} => {
let annex = annex
.map(Annex::new)
.transpose()
.map_err(|_| Error::Block("bad annex".into()))?;
cache
.taproot_signature_hash(
0,
&Prevouts::All(&prevouts),
annex,
Some((leaf_hash, codesep_pos)),
TapSighashType::Default,
)
.map_err(|e| Error::Block(e.to_string()))?
}
};
Ok(msg.to_byte_array())
}
pub fn block_sighash<F: HeaderFamily>(
family: &F,
block: &F::Block,
challenge: &Script,
) -> Result<[u8; 32]> {
block_sighash_for(family, block, challenge, &SpendPath::KeyPath)
}
pub fn template_id<F: HeaderFamily>(
family: &F,
block: &F::Block,
chain_id: &str,
genesis_hash: Option<BlockHash>,
) -> Result<[u8; 32]> {
let mut t = block.clone();
if let Some((i, spk)) = commitment_output(&t).map(|(i, s)| (i, s.to_owned())) {
t.txdata_mut()[0].output[i].script_pubkey =
ScriptBuf::from_bytes(spk.as_bytes()[..COMMITMENT_LEN].to_vec());
}
let root = merkle_root_of_txs(t.txdata());
family.set_merkle_root(t.header_mut(), root);
family.set_nonce(t.header_mut(), 0);
let height = block_height(family, block)?;
let tag = sha256::Hash::hash(b"sidestr/template-id").to_byte_array();
let mut e = sha256::Hash::engine();
e.input(&tag);
e.input(&tag);
e.input(&compact_size(chain_id.len()));
e.input(chain_id.as_bytes());
e.input(
&genesis_hash
.unwrap_or_else(BlockHash::all_zeros)
.to_byte_array(),
);
e.input(&height.to_le_bytes());
e.input(&family.prev(block.header()).to_byte_array());
e.input(&sha256::Hash::hash(&t.encode()).to_byte_array());
Ok(sha256::Hash::from_engine(e).to_byte_array())
}
pub fn seal_block<F: HeaderFamily>(
family: &F,
block: &F::Block,
witness_items: &[Vec<u8>],
) -> Result<F::Block> {
let mut sealed = with_solution(block, witness_items)?;
let root = merkle_root_of_txs(sealed.txdata());
family.set_merkle_root(sealed.header_mut(), root);
let target = Target::from_compact(family.bits(sealed.header()));
for nonce in 0..=u32::MAX {
family.set_nonce(sealed.header_mut(), nonce);
if target.is_met_by(family.block_hash(sealed.header())) {
return Ok(sealed);
}
}
Err(Error::Block("no nonce meets the target".into()))
}
pub fn sign_block<F: HeaderFamily>(
family: &F,
block: &F::Block,
challenge: &Script,
key: &SecretKey,
aux: &[u8; 32],
) -> Result<F::Block> {
let keypair = Keypair::from_secret_key(secp(), key);
let (xonly, _) = keypair.x_only_public_key();
let expected = [&[0x51, 0x20][..], &xonly.serialize()].concat();
if challenge.as_bytes() != expected.as_slice() {
return Err(Error::Block(
"the key is not the chain's signer: the challenge names another key".into(),
));
}
let msg = block_sighash(family, block, challenge)?;
let sig = secp().sign_schnorr_with_aux_rand(&Message::from_digest(msg), &keypair, aux);
seal_block(family, block, &[sig.serialize().to_vec()])
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum BlockSolution {
KeyPath,
ScriptPath(MultiA),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
pub enum SolutionError {
#[error("no solution")]
NoSolution,
#[error("key path: {0}")]
KeyPath(&'static str),
#[error("script path: {0}")]
ScriptPath(#[from] ScriptPathError),
}
pub fn verify_block_solution<F: HeaderFamily>(
family: &F,
block: &F::Block,
challenge: &Script,
) -> core::result::Result<BlockSolution, SolutionError> {
let sol = solution_of(block).ok_or(SolutionError::NoSolution)?;
let data = block_data(family, block);
let v = virtual_txs(&data, challenge, &sol.witness);
let prevouts = [v.prevout];
let items = sol.witness.len();
let has_annex = items >= 2 && sol.witness[items - 1].first() == Some(&0x50);
if items - usize::from(has_annex) <= 1 {
verify_key_path_input(&v.to_sign, 0, &prevouts)
.map(|()| BlockSolution::KeyPath)
.map_err(SolutionError::KeyPath)
} else {
verify_multi_a_input(&v.to_sign, 0, &prevouts)
.map(BlockSolution::ScriptPath)
.map_err(SolutionError::ScriptPath)
}
}
pub fn verify_block_signature<F: HeaderFamily>(
family: &F,
block: &F::Block,
challenge: &Script,
) -> bool {
verify_block_solution(family, block, challenge).is_ok()
}
pub fn verify_key_path_input(
tx: &Transaction,
index: usize,
prevouts: &[TxOut],
) -> core::result::Result<(), &'static str> {
verify_taproot_key_path(tx, index, prevouts, SighashRules::Bip341)
}
pub fn pubkey_of(key: &SecretKey) -> XOnlyPublicKey {
Keypair::from_secret_key(secp(), key).x_only_public_key().0
}
pub fn challenge_for(pubkey: &XOnlyPublicKey) -> ScriptBuf {
ScriptBuf::from_bytes([&[0x51, 0x20][..], &pubkey.serialize()].concat())
}
pub fn challenge_for_output_key(output_key: &TweakedPublicKey) -> ScriptBuf {
ScriptBuf::from_bytes([&[0x51, 0x20][..], &output_key.serialize()].concat())
}
pub fn key_from_hex(text: &str) -> Result<SecretKey> {
let bytes = hex::decode(text.trim()).map_err(|e| Error::Encoding(e.to_string()))?;
Ok(SecretKey::from_slice(&bytes)?)
}
pub(crate) fn schnorr_verify(msg: &[u8; 32], sig: &[u8], pk: &[u8]) -> bool {
let (Ok(sig), Ok(pk)) = (Signature::from_slice(sig), XOnlyPublicKey::from_slice(pk)) else {
return false;
};
secp()
.verify_schnorr(&sig, &Message::from_digest(*msg), &pk)
.is_ok()
}
pub(crate) fn annex_of<'a>(
items: &mut Vec<&'a [u8]>,
) -> core::result::Result<Option<Annex<'a>>, &'static str> {
if items.len() >= 2 && items.last().is_some_and(|a| a.first() == Some(&0x50)) {
let raw = items.pop().expect("checked");
return Annex::new(raw).map(Some).map_err(|_| "bad annex");
}
Ok(None)
}
#[cfg(test)]
mod tests {
use super::*;
fn cb(bytes: Vec<u8>) -> Transaction {
Transaction {
version: TxVersion::TWO,
lock_time: LockTime::ZERO,
input: vec![TxIn {
previous_output: OutPoint::null(),
script_sig: ScriptBuf::from_bytes(bytes),
sequence: Sequence::MAX,
witness: Witness::new(),
}],
output: vec![],
}
}
#[test]
fn coinbase_height_inverts_the_push() {
for h in [
0u32,
1,
16,
17,
127,
128,
255,
256,
65535,
70000,
8_388_608,
u32::MAX,
] {
assert_eq!(
coinbase_height(&cb([height_push(h), vec![0xff]].concat())).unwrap(),
h,
"height {h}"
);
}
let err = |b: Vec<u8>| coinbase_height(&cb(b)).unwrap_err().to_string();
assert!(err(vec![]).contains("not a hex script"));
assert!(coinbase_height(&Transaction {
input: vec![],
..cb(vec![])
})
.unwrap_err()
.to_string()
.contains("not a hex script"));
assert!(err(vec![0x4c, 0x01, 0x05]).contains("height push"));
assert!(err(vec![0x03, 0x01]).contains("height push"));
assert!(err(vec![0x01, 0x80]).contains("negative"));
assert!(err(vec![0x04, 0x00, 0x00, 0x00, 0x80]).contains("negative"));
assert_eq!(
coinbase_height(&cb(vec![0x03, 0xff, 0xff, 0x7f])).unwrap(),
8_388_607
);
assert!(err(vec![0x02, 0x05, 0x00]).contains("not minimal"));
assert_eq!(coinbase_height(&cb(vec![0x02, 0x80, 0x00])).unwrap(), 128);
assert_eq!(coinbase_height(&cb(vec![0x51])).unwrap(), 1);
assert_eq!(coinbase_height(&cb(vec![0x60])).unwrap(), 16);
assert!(err(vec![0x06, 1, 1, 1, 1, 1, 1]).contains("too long"));
}
#[test]
fn witness_round_trip_and_solution_push_sizes() {
let items = vec![vec![1u8; 64], vec![], vec![7u8; 300]];
assert_eq!(decode_witness(&encode_witness(&items)).unwrap(), items);
assert_eq!(decode_witness(&[2, 1, 9]), None);
let empty = witness_commitment(&[]);
assert_eq!(
hex::encode(empty),
"e2f61c3f71d1defd3fa999dfa36953755c690689799962b48bebd836974e8cf9"
);
let block = build_block(
&Stock,
&BlockTemplate {
height: 3,
prev: BlockHash::all_zeros(),
time: 1,
transactions: vec![],
outputs: vec![],
bits: CompactTarget::from_consensus(0x207f_ffff),
marker: MARKER.into(),
},
);
assert!(solution_of(&block).is_none());
for n in [64usize, 80, 300] {
let sealed = seal_block(&Stock, &block, &[vec![0xabu8; n]]).unwrap();
let sol = solution_of(&sealed).unwrap();
assert_eq!(sol.witness, vec![vec![0xabu8; n]]);
assert!(Target::from_compact(sealed.header.bits).is_met_by(sealed.header.block_hash()));
assert_eq!(coinbase_height(&sealed.txdata[0]).unwrap(), 3);
assert_eq!(
sealed.compute_merkle_root(),
Some(sealed.header.merkle_root)
);
assert_eq!(block_weight(&Stock, &sealed), sealed.weight().to_wu());
}
assert!(with_solution(&block, &[vec![0u8; 70_000]]).is_err());
}
#[test]
fn witness_decoder_is_strict() {
let one = encode_witness(&[vec![9u8; 3]]);
assert!(decode_witness(&one).is_some());
assert_eq!(decode_witness(&[&one[..], &[0u8][..]].concat()), None); assert_eq!(decode_witness(&one[..one.len() - 1]), None); assert_eq!(
decode_witness(&[0xfd, 0x03, 0x00, 0x01, 0x09, 0x01, 0x09, 0x01, 0x09]),
None
); assert_eq!(decode_witness(&[0xfd, 0xff, 0xff]), None); assert_eq!(decode_witness(&[0xff, 0, 0, 0, 0, 0, 0, 0, 0]), None); assert_eq!(decode_witness(&[]), None);
assert_eq!(decode_witness(&[0]), Some(vec![]));
let big = encode_witness(&[vec![0u8; 300]]);
assert_eq!(decode_witness(&big).unwrap()[0].len(), 300);
}
#[test]
fn a_family_block_round_trips_and_weighs_like_bitcoins() {
let b = build_block(
&Stock,
&BlockTemplate {
height: 1,
prev: BlockHash::all_zeros(),
time: 7,
transactions: vec![],
outputs: vec![],
bits: CompactTarget::from_consensus(0x207f_ffff),
marker: MARKER.into(),
},
);
let fb = FamilyBlock::<Stock>::from_parts(b.header, b.txdata.clone());
assert_eq!(fb.encode(), serialize(&b));
assert_eq!(FamilyBlock::<Stock>::decode(&serialize(&b)).unwrap(), fb);
assert!(FamilyBlock::<Stock>::decode(&serialize(&b)[..90]).is_err());
assert!(FamilyBlock::<Stock>::decode(&[serialize(&b), vec![0]].concat()).is_err());
assert_eq!(hex::encode(witness_root_of_txs(&b.txdata)), "00".repeat(32));
}
}