silent-tweak-sdk 0.1.0

Zero-trust BIP352 Silent Payment client SDK — local scan key, verifiable tweaks, delta sync.
Documentation
//! BIP352 cryptographic primitives — constant-time, audited crates only.
//!
//! Key equation (BIP352 §scanning):
//!
//! ```text
//! P = B_spend + hash(tweak · b_scan · G) · G
//!   = B_spend + hash(tweak · B_scan) · G
//! ```
//!
//! where `tweak` is the server-provided 32-byte scalar (already the
//! result of the sender's ECDH: `hash(a·B_scan || outpoint_L)`).

use secp256k1::{PublicKey, Scalar, Secp256k1, SecretKey, XOnlyPublicKey};
use sha2::{Digest, Sha256};

use crate::{Error, Result};

/// Holds the wallet's scan and spend public keys.
///
/// `b_scan` (the private scan key) **never** leaves this struct — it is
/// used only inside local cryptographic operations and is never
/// serialised or sent over the network.
#[derive(Clone)]
pub struct ScanKeys {
    /// Private scan key — kept in memory, never exposed.
    b_scan: SecretKey,
    /// Public spend key — may be shared.
    b_spend: PublicKey,
    /// Cached secp context.
    secp: Secp256k1<secp256k1::All>,
}

impl std::fmt::Debug for ScanKeys {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("ScanKeys")
            .field("b_spend", &self.b_spend)
            .finish_non_exhaustive()
    }
}

impl ScanKeys {
    /// Construct from raw 32-byte secret scalars (big-endian).
    ///
    /// # Errors
    /// Returns `Error::Crypto` if either slice is not a valid secp256k1 scalar.
    pub fn from_secret_bytes(b_scan_bytes: &[u8], b_spend_bytes: &[u8]) -> Result<Self> {
        let secp = Secp256k1::new();
        let b_scan = SecretKey::from_slice(b_scan_bytes)
            .map_err(|e| Error::Crypto(format!("invalid b_scan: {e}")))?;
        let b_spend_sk = SecretKey::from_slice(b_spend_bytes)
            .map_err(|e| Error::Crypto(format!("invalid b_spend: {e}")))?;
        let b_spend = PublicKey::from_secret_key(&secp, &b_spend_sk);
        Ok(Self {
            b_scan,
            b_spend,
            secp,
        })
    }

    /// Return the public spend key.
    #[must_use]
    pub fn spend_pubkey(&self) -> &PublicKey {
        &self.b_spend
    }

    /// Derive the expected Silent Payment output pubkey for one tweak.
    ///
    /// `tweak_bytes` is the 32-byte big-endian server-provided tweak
    /// scalar `t = hash(a·B_scan || outpoint_L)`.
    ///
    /// Returns the x-only Taproot output pubkey.
    ///
    /// # Errors
    /// Returns `Error::Crypto` if the tweak scalar is out of range or key
    /// addition overflows.
    pub fn derive_output(&self, tweak_bytes: &[u8; 32]) -> Result<SilentPaymentOutput> {
        // Step 1: t · b_scan  — scalar multiplication of the tweak by b_scan
        //   = b_scan * G * t  which gives the shared secret point
        // BIP352 defines: ecdh_shared_secret = t * B_scan
        //   where t is provided by the server.
        // We replicate the hash step to get the addend:
        //   k = hash_BIP0352/SharedSecret( ser_P(t * B_scan) || 0 )
        // Then: P = B_spend + k·G

        // Compute t * B_scan using secp
        let tweak_scalar = Scalar::from_be_bytes(*tweak_bytes)
            .map_err(|_| Error::Crypto("tweak scalar overflow".into()))?;

        let b_scan_point = PublicKey::from_secret_key(&self.secp, &self.b_scan);
        let shared_point = b_scan_point
            .mul_tweak(&self.secp, &tweak_scalar)
            .map_err(|e| Error::Crypto(format!("b_scan * tweak: {e}")))?;

        // Compute k = hash_BIP0352/SharedSecret( ser_P(shared_point) || counter=0 )
        let k = hash_shared_secret(&shared_point.serialize(), 0);

        let k_sk = SecretKey::from_slice(&k)
            .map_err(|e| Error::Crypto(format!("hash scalar invalid: {e}")))?;
        let k_scalar = Scalar::from(k_sk);

        // P = B_spend + k·G
        let output_key = self
            .b_spend
            .add_exp_tweak(&self.secp, &k_scalar)
            .map_err(|e| Error::Crypto(format!("B_spend + k·G: {e}")))?;

        let (xonly, _parity) = output_key.x_only_public_key();
        Ok(SilentPaymentOutput {
            pubkey: xonly,
            tweak: *tweak_bytes,
        })
    }

    /// Expose the raw bytes of `b_scan` — only for threshold mode.
    /// Deliberately named to make callers think twice.
    #[must_use]
    #[allow(dead_code)]
    pub(crate) fn b_scan_secret_bytes(&self) -> [u8; 32] {
        self.b_scan.secret_bytes()
    }
}

/// A derived Silent Payment output.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SilentPaymentOutput {
    /// Taproot x-only output pubkey.
    pub pubkey: XOnlyPublicKey,
    /// The tweak that produced this output (for record-keeping).
    pub tweak: [u8; 32],
}

impl SilentPaymentOutput {
    /// 32-byte x-only pubkey as hex.
    #[must_use]
    pub fn pubkey_hex(&self) -> String {
        hex::encode(self.pubkey.serialize())
    }
}

// ── Tagged hash helpers ─────────────────────────────────────────────────────

/// BIP340 tagged hash: SHA256(SHA256(tag) || SHA256(tag) || data).
#[must_use]
pub fn tagged_hash(tag: &[u8], data: &[u8]) -> [u8; 32] {
    let tag_hash: [u8; 32] = Sha256::digest(tag).into();
    let mut h = Sha256::new();
    h.update(tag_hash);
    h.update(tag_hash);
    h.update(data);
    h.finalize().into()
}

/// BIP352 §scanning: `hash_BIP0352/SharedSecret(ser_P(point) || counter)`.
///
/// `counter` is a `u32` encoded as big-endian — used for label support
/// and multiple output scanning.
#[must_use]
pub fn hash_shared_secret(serialised_point: &[u8], counter: u32) -> [u8; 32] {
    let mut data = serialised_point.to_vec();
    data.extend_from_slice(&counter.to_be_bytes());
    tagged_hash(b"BIP0352/SharedSecret", &data)
}

/// Compute the BIP352 input hash (used during send, included here for
/// completeness and test vector verification).
///
/// `hash_BIP0352/Inputs(outpoints_hash || A_sum)`.
#[must_use]
pub fn hash_inputs(outpoints_hash: &[u8; 32], a_sum_33: &[u8; 33]) -> [u8; 32] {
    let mut data = Vec::with_capacity(65);
    data.extend_from_slice(outpoints_hash.as_ref());
    data.extend_from_slice(a_sum_33.as_ref());
    tagged_hash(b"BIP0352/Inputs", &data)
}

// ── Merkle commitment verification ─────────────────────────────────────────

/// Verify that a leaf hash is included in a Merkle tree given sibling
/// hashes from the leaf up to the root.
///
/// The tree is a simple binary Merkle tree using double-SHA256, the
/// same construction as Bitcoin's transaction Merkle tree.
#[must_use]
pub fn verify_merkle_proof(
    leaf_data: &[u8],
    leaf_index: u64,
    siblings: &[[u8; 32]],
    expected_root: &[u8; 32],
) -> bool {
    let mut current = double_sha256(leaf_data);
    let mut index = leaf_index;

    for sibling in siblings {
        let (left, right) = if index % 2 == 0 {
            (&current, sibling)
        } else {
            (sibling, &current)
        };
        let mut combined = [0u8; 64];
        combined[..32].copy_from_slice(left);
        combined[32..].copy_from_slice(right);
        current = double_sha256(combined.as_ref());
        index /= 2;
    }

    &current == expected_root
}

/// Standard Bitcoin double-SHA256.
#[must_use]
pub fn double_sha256(data: &[u8]) -> [u8; 32] {
    let first: [u8; 32] = Sha256::digest(data).into();
    Sha256::digest(first).into()
}

/// Build a Merkle root from a list of leaf data blobs.
///
/// Returns the root hash and a vector of per-leaf sibling paths.
#[must_use]
pub fn build_merkle_tree(leaves: &[Vec<u8>]) -> (Vec<u8>, Vec<Vec<[u8; 32]>>) {
    if leaves.is_empty() {
        return (vec![0u8; 32], vec![]);
    }
    let n = leaves.len();
    let leaf_hashes: Vec<[u8; 32]> = leaves.iter().map(|l| double_sha256(l)).collect();
    let mut proofs: Vec<Vec<[u8; 32]>> = vec![vec![]; n];
    let mut current_layer = leaf_hashes;

    loop {
        if current_layer.len() == 1 {
            break;
        }
        let mut next_layer = Vec::with_capacity(current_layer.len().div_ceil(2));
        let mut i = 0;
        while i < current_layer.len() {
            let left = current_layer[i];
            let right = if i + 1 < current_layer.len() {
                current_layer[i + 1]
            } else {
                current_layer[i] // duplicate last if odd
            };
            let mut combined = [0u8; 64];
            combined[..32].copy_from_slice(&left);
            combined[32..].copy_from_slice(&right);
            next_layer.push(double_sha256(combined.as_ref()));
            i += 2;
        }
        // Attach siblings into proofs (at current tree level)
        for (leaf_i, proof) in proofs.iter_mut().enumerate() {
            let level_depth = proof.len();
            let idx = leaf_i >> level_depth;
            let sibling_idx = if idx % 2 == 0 { idx + 1 } else { idx - 1 };
            let sibling = if sibling_idx < current_layer.len() {
                current_layer[sibling_idx]
            } else {
                current_layer[idx]
            };
            proof.push(sibling);
        }
        current_layer = next_layer;
    }

    let root = current_layer[0].to_vec();
    (root, proofs)
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn tagged_hash_known_vector() {
        let point = [0x02u8; 33];
        let h = hash_shared_secret(point.as_ref(), 0);
        assert_ne!(h, [0u8; 32]);
        assert_eq!(h, hash_shared_secret(point.as_ref(), 0));
    }

    #[test]
    fn merkle_single_leaf() {
        let leaves = vec![b"leaf0".to_vec()];
        let (root, proofs) = build_merkle_tree(&leaves);
        assert_eq!(root, double_sha256(b"leaf0" as &[u8]).to_vec());
        let root_arr: &[u8; 32] = root.as_slice().try_into().unwrap();
        assert!(verify_merkle_proof(
            b"leaf0" as &[u8],
            0,
            &proofs[0],
            root_arr
        ));
    }

    #[test]
    fn merkle_two_leaves() {
        let leaves = vec![b"leaf0".to_vec(), b"leaf1".to_vec()];
        let (root, proofs) = build_merkle_tree(&leaves);
        let root_arr: &[u8; 32] = root.as_slice().try_into().unwrap();
        assert!(verify_merkle_proof(
            b"leaf0" as &[u8],
            0,
            &proofs[0],
            root_arr
        ));
        assert!(verify_merkle_proof(
            b"leaf1" as &[u8],
            1,
            &proofs[1],
            root_arr
        ));
    }

    #[test]
    fn derive_output_deterministic() {
        let b_scan = [1u8; 32];
        let b_spend = [2u8; 32];
        let keys = ScanKeys::from_secret_bytes(b_scan.as_ref(), b_spend.as_ref()).unwrap();
        let tweak = [3u8; 32];
        let out1 = keys.derive_output(&tweak).unwrap();
        let out2 = keys.derive_output(&tweak).unwrap();
        assert_eq!(out1.pubkey, out2.pubkey);
    }

    #[test]
    fn double_sha256_known() {
        let empty: &[u8] = &[];
        let h = double_sha256(empty);
        assert_eq!(
            hex::encode(h),
            "5df6e0e2761359d30a8275058e299fcc0381534545f55cf43e41983f5d4c9456"
        );
    }
}