use bitcoin::consensus::encode::{deserialize_hex, serialize_hex};
use bitcoin::key::XOnlyPublicKey;
use bitcoin::secp256k1::schnorr::Signature;
use bitcoin::{Transaction, TxOut, Witness};
use sidestr_core::document::ChainDocument;
use sidestr_core::parents::resolve_parent;
use sidestr_core::sighash::{rules_for, verify_taproot_key_path};
use crate::error::{Error, Result};
use crate::key::SpendSigner;
use crate::spend::Spend;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ExternalSigner {
pubkey: XOnlyPublicKey,
}
impl ExternalSigner {
pub fn new(pubkey: XOnlyPublicKey) -> Self {
Self { pubkey }
}
}
impl SpendSigner for ExternalSigner {
fn pubkey(&self) -> XOnlyPublicKey {
self.pubkey
}
fn sign_key_path(&self, _sighash: &[u8; 32]) -> Result<Signature> {
Err(Error::Signer(
"this key signs elsewhere: send the unsigned transaction to its signer".into(),
))
}
fn signs_elsewhere(&self) -> bool {
true
}
}
pub fn unsigned_hex(tx: &Transaction) -> String {
let mut bare = tx.clone();
for i in &mut bare.input {
i.witness = Witness::new();
}
serialize_hex(&bare)
}
pub fn accept_signed(
built: &Spend,
signed_hex: &str,
prevouts: &[TxOut],
chain: &ChainDocument,
) -> Result<Spend> {
let tx: Transaction = deserialize_hex(signed_hex.trim()).map_err(|_| {
Error::Signer("the signer returned something that is not a transaction".into())
})?;
if tx.compute_txid() != built.txid {
return Err(Error::Signer(
"the signer returned a different transaction from the one asked for".into(),
));
}
if prevouts.len() != tx.input.len() {
return Err(Error::Signer(format!(
"{} prevouts for {} inputs",
prevouts.len(),
tx.input.len()
)));
}
let parent = resolve_parent(&chain.parent)
.map_err(|_| Error::Signer(format!("unknown parent {}", chain.parent)))?;
let rules = rules_for(parent.family);
for i in 0..tx.input.len() {
verify_taproot_key_path(&tx, i, prevouts, rules)
.map_err(|e| Error::Signer(format!("input {i} is not validly signed: {e}")))?;
}
Ok(Spend {
hex: serialize_hex(&tx),
vsize: tx.weight().to_wu().div_ceil(4),
tx,
note: built.note.clone(),
..*built
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::compose::{build_outputs, OutputsRequest};
use crate::{build_spend, Coin, Permissive, PlainKey, SpendRequest};
use bitcoin::hashes::Hash;
use bitcoin::secp256k1::{Keypair, Message, SecretKey};
use bitcoin::{Amount, OutPoint, ScriptBuf, Txid};
use sidestr_core::block::secp;
use sidestr_core::sighash::{key_path_sighash, SighashRules};
fn chain() -> ChainDocument {
ChainDocument::from_json(
r#"{"id":"sidestr:ex","name":"ex","parent":"tbtc4",
"challenge":"5120aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"powLimit":"7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
"addressPrefix":"ex","genesisTime":1790000000,"pegs":[],"minFeeRate":1}"#,
)
.unwrap()
}
fn kp(b: u8) -> Keypair {
Keypair::from_secret_key(secp(), &SecretKey::from_slice(&[b; 32]).unwrap())
}
fn coins() -> Vec<Coin> {
(0..2u8)
.map(|i| Coin {
outpoint: OutPoint {
txid: Txid::from_byte_array([i + 1; 32]),
vout: 0,
},
value: 3_000,
height: 1,
coinbase: false,
})
.collect()
}
fn bob() -> String {
format!("5120{}", "bb".repeat(32))
}
fn prevouts(s: &Spend, me: &ScriptBuf) -> Vec<TxOut> {
s.tx.input
.iter()
.map(|i| {
let c = coins()
.into_iter()
.find(|c| c.outpoint == i.previous_output)
.unwrap();
TxOut {
value: Amount::from_sat(c.value),
script_pubkey: me.clone(),
}
})
.collect()
}
fn sign_with(tx: &Transaction, prevouts: &[TxOut], k: &Keypair) -> Transaction {
let mut t = tx.clone();
for i in 0..t.input.len() {
let (m, ht) = key_path_sighash(&t, i, prevouts, SighashRules::Bip341).unwrap();
let sig = secp().sign_schnorr_with_aux_rand(&Message::from_digest(m), k, &[0; 32]);
t.input[i].witness =
Witness::from_slice(&[[sig.serialize().as_slice(), &[ht]].concat()]);
}
t
}
fn build(ext: &ExternalSigner) -> Spend {
build_spend(
&SpendRequest {
chain: &chain(),
coins: &coins(),
tip_height: 10,
to: &bob(),
amount: 4_000,
fee: None,
},
ext,
&Permissive,
)
.unwrap()
}
#[test]
fn unsigned_build_matches_a_signed_one_in_layout_and_size() {
let k = kp(7);
let ext = ExternalSigner::new(k.x_only_public_key().0);
let unsigned = build(&ext);
let signed = build_spend(
&SpendRequest {
chain: &chain(),
coins: &coins(),
tip_height: 10,
to: &bob(),
amount: 4_000,
fee: None,
},
&PlainKey::new(SecretKey::from_slice(&[7; 32]).unwrap()),
&Permissive,
)
.unwrap();
assert_eq!(unsigned.txid, signed.txid);
assert_eq!(unsigned.vsize, signed.vsize);
assert_eq!(unsigned.fee, signed.fee);
assert_eq!(unsigned.inputs, 2);
let bare: Transaction = deserialize_hex(&unsigned_hex(&unsigned.tx)).unwrap();
assert!(bare.input.iter().all(|i| i.witness.is_empty()));
assert_eq!(bare.compute_txid(), unsigned.txid);
}
#[test]
fn a_correct_signature_is_accepted() {
let k = kp(7);
let ext = ExternalSigner::new(k.x_only_public_key().0);
let built = build(&ext);
let pv = prevouts(&built, &ext.script());
let answer = serialize_hex(&sign_with(&built.tx, &pv, &k));
let s = accept_signed(&built, &answer, &pv, &chain()).unwrap();
assert_eq!(s.txid, built.txid);
assert_eq!(s.vsize, built.vsize);
}
#[test]
fn another_keys_signature_is_refused() {
let k = kp(7);
let ext = ExternalSigner::new(k.x_only_public_key().0);
let built = build(&ext);
let pv = prevouts(&built, &ext.script());
let answer = serialize_hex(&sign_with(&built.tx, &pv, &kp(8)));
let e = accept_signed(&built, &answer, &pv, &chain()).unwrap_err();
assert!(e.to_string().contains("not validly signed"), "{e}");
}
#[test]
fn swapped_signatures_are_refused() {
let k = kp(7);
let ext = ExternalSigner::new(k.x_only_public_key().0);
let built = build(&ext);
let pv = prevouts(&built, &ext.script());
let mut t = sign_with(&built.tx, &pv, &k);
let w0 = t.input[0].witness.clone();
t.input[0].witness = t.input[1].witness.clone();
t.input[1].witness = w0;
assert!(accept_signed(&built, &serialize_hex(&t), &pv, &chain()).is_err());
}
#[test]
fn a_different_transaction_is_refused_even_if_signed() {
let k = kp(7);
let ext = ExternalSigner::new(k.x_only_public_key().0);
let built = build(&ext);
let pv = prevouts(&built, &ext.script());
let mut other = built.tx.clone();
other.output[0].script_pubkey =
ScriptBuf::from_hex(&format!("5120{}", "cc".repeat(32))).unwrap();
let answer = serialize_hex(&sign_with(&other, &pv, &k));
let e = accept_signed(&built, &answer, &pv, &chain()).unwrap_err();
assert!(e.to_string().contains("different transaction"), "{e}");
assert!(accept_signed(&built, "zz", &pv, &chain()).is_err());
assert!(accept_signed(&built, &unsigned_hex(&built.tx), &pv, &chain()).is_err());
}
#[test]
fn the_signer_never_signs_in_place() {
let ext = ExternalSigner::new(kp(7).x_only_public_key().0);
assert!(ext.signs_elsewhere());
assert!(ext.sign_key_path(&[0; 32]).is_err());
}
#[test]
fn compose_defers_too() {
let k = kp(7);
let ext = ExternalSigner::new(k.x_only_public_key().0);
let to = ScriptBuf::from_hex(&bob()).unwrap();
let built = build_outputs(
&OutputsRequest {
chain: &chain(),
coins: &coins(),
required: &[],
tip_height: 10,
outputs: &[(to, 1_000)],
records: &["hello".to_string()],
fee: None,
},
&ext,
&Permissive,
)
.unwrap();
let pv = prevouts(&built, &ext.script());
let answer = serialize_hex(&sign_with(&built.tx, &pv, &k));
assert!(accept_signed(&built, &answer, &pv, &chain()).is_ok());
}
}