From ab25c36be9392f1b7cff919a9651231dae6a978d Mon Sep 17 00:00:00 2001 From: cygnet Date: Wed, 4 Feb 2026 13:41:33 +0100 Subject: [PATCH] feat: add message signing module --- Cargo.toml | 10 ++++- src/client/mod.rs | 1 + src/client/signing.rs | 92 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 102 insertions(+), 1 deletion(-) create mode 100644 src/client/signing.rs diff --git a/Cargo.toml b/Cargo.toml index 757a43a..2b0dc98 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,9 +17,17 @@ rayon = "1.10.0" futures = "0.3" log = "0.4" async-trait = "0.1" -reqwest = { version = "0.12.4", features = ["rustls-tls", "gzip", "json"], default-features = false, optional = true } +reqwest = { version = "0.12.4", features = [ + "rustls-tls", + "gzip", + "json", +], default-features = false, optional = true } hex = { version = "0.4.3", features = ["serde"], optional = true } bdk_coin_select = "0.4.0" +bip322 = "0.0.7" + +[dev-dependencies] +rand = "0.9.0" [features] blindbit-backend = ["reqwest", "hex"] diff --git a/src/client/mod.rs b/src/client/mod.rs index 665c581..5215b64 100644 --- a/src/client/mod.rs +++ b/src/client/mod.rs @@ -1,4 +1,5 @@ mod client; +mod signing; mod spend; mod structs; diff --git a/src/client/signing.rs b/src/client/signing.rs new file mode 100644 index 0000000..ac91ec9 --- /dev/null +++ b/src/client/signing.rs @@ -0,0 +1,92 @@ +use bitcoin::key::Secp256k1; +use bitcoin::{Address, Network, PrivateKey, Witness}; +use silentpayments::SilentPaymentAddress; + +use crate::SpClient; + +impl SpClient { + /// Signs a message using the client's spend key. + pub fn sign_message(&self, msg: &[u8]) -> anyhow::Result { + let network = Network::Bitcoin; + let secp = Secp256k1::new(); + + let spend_sk = self.try_get_secret_spend_key()?; + let spend_pk = spend_sk.public_key(&secp); + let spend_privkey = PrivateKey::new(spend_sk, network); + + let xonly = spend_pk.x_only_public_key().0; + + let address = Address::p2tr(&secp, xonly, None, network); + + Ok(bip322::sign_simple(&address, msg, spend_privkey)?) + } +} + +pub fn verify_message( + address: SilentPaymentAddress, + msg: &[u8], + signature: Witness, +) -> anyhow::Result<()> { + let spend_pk = address.get_spend_key(); + + let xonly = spend_pk.x_only_public_key().0; + + let network = Network::Bitcoin; + let secp = Secp256k1::new(); + let taproot_address = Address::p2tr(&secp, xonly, None, network); + + Ok(bip322::verify_simple(&taproot_address, msg, signature)?) +} + +#[cfg(test)] +mod test { + use bitcoin::{Network, Witness}; + use silentpayments::secp256k1::SecretKey; + + use crate::{client::signing::verify_message, SpClient, SpendKey}; + + fn create_random_client() -> SpClient { + use rand::prelude::*; + let mut rng = rand::rng(); + + let scan_sk_bytes: [u8; 32] = rng.random(); + let spend_sk_bytes: [u8; 32] = rng.random(); + + let network = Network::Bitcoin; + let scan_sk = SecretKey::from_slice(&scan_sk_bytes).unwrap(); + let spend_sk = SecretKey::from_slice(&spend_sk_bytes).unwrap(); + + SpClient::new(scan_sk, SpendKey::Secret(spend_sk), network).unwrap() + } + + #[test] + fn sign_and_verify() { + // message to sign + let message = b"random message to sign"; + + // different, unrelated message + let unrelated_message = b"wrong message"; + + // create a random sp-client with an sp-address + let client = create_random_client(); + let sp_address = client.get_receiving_address(); + + // create a different, unrelated client + let unrelated_client = create_random_client(); + let unrelated_sp_address = unrelated_client.get_receiving_address(); + + // sign message and verify it is correct with the client's sp-address + let witness = client.sign_message(message).unwrap(); + assert!(verify_message(sp_address, message, witness.clone()).is_ok()); + + // different message should be an error + assert!(verify_message(sp_address, unrelated_message, witness.clone()).is_err()); + + // different sp_address should be an error + assert!(verify_message(unrelated_sp_address, message, witness).is_err()); + + // different witness should be an error + let random_witness = Witness::from_slice(&[[]]); + assert!(verify_message(sp_address, message, random_witness).is_err()); + } +}