use std::collections::BTreeMap;
use bitcoin::hashes::{sha256, Hash};
use bitcoin::key::{TapTweak, TweakedPublicKey};
use bitcoin::secp256k1::{
schnorr::Signature, Keypair, Message, Parity, PublicKey, Scalar, SecretKey, XOnlyPublicKey,
};
use bitcoin::sighash::{Annex, Prevouts, SighashCache, TapSighashType};
use bitcoin::taproot::{ControlBlock, LeafVersion, TapLeafHash, TapNodeHash};
use bitcoin::{Script, ScriptBuf, Transaction, TxOut};
use crate::block::{
block_sighash_for, challenge_for_output_key, schnorr_verify, seal_block, secp, HeaderFamily,
SpendPath,
};
use crate::document::ChainDocument;
use crate::error::{Error, Result};
pub const NUMS_X: [u8; 32] = [
0x50, 0x92, 0x9b, 0x74, 0xc1, 0xa0, 0x49, 0x54, 0xb7, 0x8b, 0x4b, 0x60, 0x35, 0xe9, 0x7a, 0x5e,
0x07, 0x8a, 0x5a, 0x0f, 0x28, 0xec, 0x96, 0xd5, 0x47, 0xbf, 0xee, 0x9a, 0xce, 0x80, 0x3a, 0xc0,
];
pub const MAX_SIGNERS: usize = 16;
fn tagged(tag: &[u8], msg: &[u8]) -> [u8; 32] {
let t = sha256::Hash::hash(tag).to_byte_array();
let mut e = sha256::Hash::engine();
use bitcoin::hashes::HashEngine;
e.input(&t);
e.input(&t);
e.input(msg);
sha256::Hash::from_engine(e).to_byte_array()
}
pub fn nums_key(chain_id: &str) -> Result<XOnlyPublicKey> {
let t = tagged(b"sidestr/nums", chain_id.as_bytes());
let h = PublicKey::from_slice(&[&[0x02][..], &NUMS_X[..]].concat())?;
let scalar = Scalar::from_be_bytes(t)
.map_err(|_| Error::Federation("nums derivation failed: tweak out of range".into()))?;
Ok(h.add_exp_tweak(secp(), &scalar)?.x_only_public_key().0)
}
pub fn leaf_script(signers: &[XOnlyPublicKey], threshold: u8) -> Result<ScriptBuf> {
if signers.is_empty() || signers.len() > MAX_SIGNERS {
return Err(Error::Federation("1 to 16 signers".into()));
}
if threshold == 0 || usize::from(threshold) > signers.len() {
return Err(Error::Federation(
"threshold between 1 and the number of signers".into(),
));
}
let mut s = Vec::with_capacity(signers.len() * 34 + 2);
for (i, pk) in signers.iter().enumerate() {
s.push(0x20);
s.extend_from_slice(&pk.serialize());
s.push(if i == 0 { 0xac } else { 0xba });
}
s.push(0x50 + threshold);
s.push(0x9c);
Ok(ScriptBuf::from_bytes(s))
}
pub fn parse_multi_a(script: &Script) -> Option<(Vec<XOnlyPublicKey>, u8)> {
let b = script.as_bytes();
let mut keys = Vec::new();
let mut i = 0;
while i + 34 <= b.len() && b[i] == 0x20 {
let pk = XOnlyPublicKey::from_slice(&b[i + 1..i + 33]).ok()?;
let op = b[i + 33];
if (keys.is_empty() && op != 0xac) || (!keys.is_empty() && op != 0xba) {
return None;
}
keys.push(pk);
i += 34;
if keys.len() > MAX_SIGNERS {
return None;
}
}
if keys.is_empty() || b.len() != i + 2 || !(0x51..=0x60).contains(&b[i]) || b[i + 1] != 0x9c {
return None;
}
let k = b[i] - 0x50;
(usize::from(k) <= keys.len()).then_some((keys, k))
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Federation {
pub signers: Vec<XOnlyPublicKey>,
pub threshold: u8,
pub script: ScriptBuf,
pub leaf_hash: TapLeafHash,
pub internal_key: XOnlyPublicKey,
pub output_key: TweakedPublicKey,
pub parity: Parity,
pub control_block: Vec<u8>,
}
impl Federation {
pub fn new(chain_id: &str, signers: Vec<XOnlyPublicKey>, threshold: u8) -> Result<Self> {
let script = leaf_script(&signers, threshold)?;
for (i, a) in signers.iter().enumerate() {
if signers[..i].contains(a) {
return Err(Error::Federation(format!("signer {a} is listed twice")));
}
}
let leaf_hash = TapLeafHash::from_script(&script, LeafVersion::TapScript);
let internal_key = nums_key(chain_id)?;
let (output_key, parity) =
internal_key.tap_tweak(secp(), Some(TapNodeHash::from(leaf_hash)));
let mut control_block = vec![0xc0 | u8::from(parity)];
control_block.extend_from_slice(&internal_key.serialize());
Ok(Self {
signers,
threshold,
script,
leaf_hash,
internal_key,
output_key,
parity,
control_block,
})
}
pub fn for_document(doc: &ChainDocument) -> Result<Option<Self>> {
let (signers, threshold) = match (&doc.signers, doc.threshold) {
(None, None) => return Ok(None),
(Some(s), Some(t)) => (s, t),
_ => {
return Err(Error::Document(format!(
"chain {}: a level 2 document has both signers and threshold",
doc.id
)))
}
};
let keys = signers
.iter()
.map(|s| {
let bytes = hex::decode(s)
.map_err(|_| Error::Document(format!("signer {s} is not an x-only key")))?;
XOnlyPublicKey::from_slice(&bytes)
.map_err(|_| Error::Document(format!("signer {s} is not an x-only key")))
})
.collect::<Result<Vec<_>>>()?;
let k = u8::try_from(threshold).map_err(|_| {
Error::Federation("threshold between 1 and the number of signers".into())
})?;
let fed = Self::new(&doc.id, keys, k)?;
let derived = fed.challenge().to_hex_string();
if !doc.challenge.is_empty() && doc.challenge.to_ascii_lowercase() != derived {
return Err(Error::Document(format!(
"{}: challenge {}… is not the one {} signers with threshold {} derive ({}…)",
doc.id,
&doc.challenge[..doc.challenge.len().min(12)],
signers.len(),
threshold,
&derived[..12]
)));
}
Ok(Some(fed))
}
pub fn challenge(&self) -> ScriptBuf {
challenge_for_output_key(&self.output_key)
}
pub fn descriptor(&self) -> String {
format!(
"tr({},multi_a({},{}))",
self.internal_key,
self.threshold,
self.signers
.iter()
.map(|k| k.to_string())
.collect::<Vec<_>>()
.join(",")
)
}
}
pub fn partial_signature<F: HeaderFamily>(
family: &F,
block: &F::Block,
fed: &Federation,
key: &SecretKey,
aux: &[u8; 32],
) -> Result<Signature> {
let keypair = Keypair::from_secret_key(secp(), key);
let pk = keypair.x_only_public_key().0;
if !fed.signers.contains(&pk) {
return Err(Error::Federation(format!(
"this key ({pk}) is not one of the signers"
)));
}
let msg = block_sighash_for(
family,
block,
&fed.challenge(),
&SpendPath::ScriptPath {
leaf_hash: fed.leaf_hash,
annex: None,
codesep_pos: 0xffff_ffff,
},
)?;
Ok(secp().sign_schnorr_with_aux_rand(&Message::from_digest(msg), &keypair, aux))
}
pub fn verify_partial<F: HeaderFamily>(
family: &F,
block: &F::Block,
fed: &Federation,
pubkey: &XOnlyPublicKey,
sig: &Signature,
) -> bool {
let Ok(msg) = block_sighash_for(
family,
block,
&fed.challenge(),
&SpendPath::ScriptPath {
leaf_hash: fed.leaf_hash,
annex: None,
codesep_pos: 0xffff_ffff,
},
) else {
return false;
};
schnorr_verify(&msg, sig.as_ref(), &pubkey.serialize())
}
pub fn assemble_witness(
fed: &Federation,
sigs: &BTreeMap<XOnlyPublicKey, Signature>,
) -> Result<Vec<Vec<u8>>> {
let have: Vec<&XOnlyPublicKey> = fed
.signers
.iter()
.filter(|pk| sigs.contains_key(pk))
.collect();
if have.len() < usize::from(fed.threshold) {
return Err(Error::Federation(format!(
"{} of {} signatures",
have.len(),
fed.threshold
)));
}
let chosen: Vec<&XOnlyPublicKey> = have[..usize::from(fed.threshold)].to_vec();
let mut slots: Vec<Vec<u8>> = fed
.signers
.iter()
.map(|pk| {
if chosen.contains(&pk) {
sigs[pk].as_ref().to_vec()
} else {
Vec::new()
}
})
.collect();
slots.reverse();
slots.push(fed.script.to_bytes());
slots.push(fed.control_block.clone());
Ok(slots)
}
pub fn seal_federated<F: HeaderFamily>(
family: &F,
block: &F::Block,
fed: &Federation,
sigs: &BTreeMap<XOnlyPublicKey, Signature>,
) -> Result<F::Block> {
seal_block(family, block, &assemble_witness(fed, sigs)?)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
pub enum ScriptPathError {
#[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 the multi_a(k, pk_1 … pk_n) template; only that template is verified here"
)]
NotMultiA,
#[error("{have} signature slots for {need} signers")]
SlotCount {
have: usize,
need: usize,
},
#[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("{have} valid signatures, the leaf needs exactly {need}")]
SignatureCount {
have: usize,
need: u8,
},
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct MultiA {
pub signers: Vec<XOnlyPublicKey>,
pub threshold: u8,
pub signed: Vec<bool>,
}
pub fn verify_multi_a_input(
tx: &Transaction,
index: usize,
prevouts: &[TxOut],
) -> core::result::Result<MultiA, ScriptPathError> {
let prevout = prevouts.get(index).ok_or(ScriptPathError::ScriptType)?;
let input = tx.input.get(index).ok_or(ScriptPathError::ScriptType)?;
if !prevout.script_pubkey.is_p2tr() {
return Err(ScriptPathError::ScriptType);
}
if !input.script_sig.is_empty() {
return Err(ScriptPathError::ScriptSig);
}
let mut items: Vec<&[u8]> = input.witness.iter().collect();
let witness_size: i64 = items
.iter()
.map(|w| {
w.len() as i64
+ match w.len() {
0..=0xfc => 1,
0xfd..=0xffff => 3,
_ => 5,
}
})
.sum::<i64>()
+ 1;
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(|_| ScriptPathError::Annex)?;
Some(raw)
} else {
None
};
if items.len() < 2 {
return Err(ScriptPathError::TooFewItems);
}
let control = items.pop().expect("checked");
let script = Script::from_bytes(items.pop().expect("checked"));
let cb = ControlBlock::decode(control).map_err(|_| ScriptPathError::ControlBlock)?;
let output_key = XOnlyPublicKey::from_slice(&prevout.script_pubkey.as_bytes()[2..34])
.map_err(|_| ScriptPathError::ScriptType)?;
if !cb.verify_taproot_commitment(secp(), output_key, script) {
return Err(ScriptPathError::Commitment);
}
if cb.leaf_version != LeafVersion::TapScript {
return Err(ScriptPathError::LeafVersion(cb.leaf_version.to_consensus()));
}
let (signers, threshold) = parse_multi_a(script).ok_or(ScriptPathError::NotMultiA)?;
if items.len() != signers.len() {
return Err(ScriptPathError::SlotCount {
have: items.len(),
need: signers.len(),
});
}
let leaf_hash = TapLeafHash::from_script(script, LeafVersion::TapScript);
let mut budget = 50 + witness_size;
let mut signed = Vec::with_capacity(signers.len());
let mut count = 0usize;
for (slot, pk) in signers.iter().enumerate() {
let raw = items[items.len() - 1 - slot];
if raw.is_empty() {
signed.push(false);
continue;
}
let (sig, hash_type) = match raw.len() {
64 => (raw, TapSighashType::Default),
65 if raw[64] != 0 => (
&raw[..64],
TapSighashType::from_consensus_u8(raw[64])
.map_err(|_| ScriptPathError::BadSignatureEncoding { slot })?,
),
_ => return Err(ScriptPathError::BadSignatureEncoding { slot }),
};
budget -= 50;
if budget < 0 {
return Err(ScriptPathError::Budget);
}
let msg = SighashCache::new(tx)
.taproot_signature_hash(
index,
&Prevouts::All(prevouts),
annex.map(|a| Annex::new(a).expect("checked above")),
Some((leaf_hash, 0xffff_ffff)),
hash_type,
)
.map_err(|_| ScriptPathError::BadSignatureEncoding { slot })?;
if !schnorr_verify(&msg.to_byte_array(), sig, &pk.serialize()) {
return Err(ScriptPathError::InvalidSignature { slot });
}
signed.push(true);
count += 1;
}
if count != usize::from(threshold) {
return Err(ScriptPathError::SignatureCount {
have: count,
need: threshold,
});
}
Ok(MultiA {
signers,
threshold,
signed,
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::block::pubkey_of;
fn keys(n: u8) -> (Vec<SecretKey>, Vec<XOnlyPublicKey>) {
let ks: Vec<SecretKey> = (1..=n)
.map(|i| SecretKey::from_slice(&[i; 32]).unwrap())
.collect();
let ps = ks.iter().map(pubkey_of).collect();
(ks, ps)
}
#[test]
fn leaf_round_trips_and_bounds_hold() {
let (_, pubs) = keys(5);
for k in 1..=5u8 {
let s = leaf_script(&pubs, k).unwrap();
assert_eq!(parse_multi_a(&s), Some((pubs.clone(), k)));
}
assert!(leaf_script(&pubs, 0).is_err());
assert!(leaf_script(&pubs, 6).is_err());
assert!(leaf_script(&[], 1).is_err());
let (_, many) = keys(17);
assert!(leaf_script(&many, 1).is_err());
assert_eq!(parse_multi_a(Script::from_bytes(&[0x51])), None);
let mut s = leaf_script(&pubs, 2).unwrap().to_bytes();
s.push(0x00);
assert_eq!(parse_multi_a(Script::from_bytes(&s)), None);
let mut swapped = leaf_script(&pubs, 2).unwrap().to_bytes();
swapped[33] = 0xba;
assert_eq!(parse_multi_a(Script::from_bytes(&swapped)), None);
let mut over = leaf_script(&pubs[..2], 2).unwrap().to_bytes();
let last = over.len() - 2;
over[last] = 0x53;
assert_eq!(parse_multi_a(Script::from_bytes(&over)), None);
}
#[test]
fn federation_refuses_duplicates_and_derives_per_chain() {
let (_, pubs) = keys(3);
let a = Federation::new("sidestr:a", pubs.clone(), 2).unwrap();
let b = Federation::new("sidestr:b", pubs.clone(), 2).unwrap();
assert_ne!(a.internal_key, b.internal_key);
assert_ne!(a.challenge(), b.challenge());
assert_eq!(a.script, b.script);
assert_eq!(a.control_block.len(), 33);
assert_eq!(a.control_block[0] & 0xfe, 0xc0);
assert_eq!(a.control_block[0] & 1, u8::from(a.parity));
let e = Federation::new("sidestr:a", vec![pubs[0], pubs[1], pubs[0]], 2)
.unwrap_err()
.to_string();
assert!(e.contains("listed twice"), "{e}");
assert!(a
.descriptor()
.starts_with(&format!("tr({},multi_a(2,", a.internal_key)));
}
#[test]
fn nums_matches_the_reference_derivation() {
assert_eq!(
nums_key("sidestr:fedtest").unwrap().to_string(),
"d137cd79e4cf8765ff03016f140425d760d96b8bcd574cc7d3ebc3b99f1f666d"
);
}
}