use crate::error::{Result, WalletError};
use crate::wallet::{KeyShare, MpcWallet};
use tenzro_crypto::frost::{
aggregate_signature, build_signing_package, round1_commit, round2_sign, PublicKeyPackage,
SignatureShare, SigningCommitments, SigningNonces,
};
use tenzro_crypto::Signature;
use tracing::{debug, info};
pub struct MpcSigner;
impl MpcSigner {
pub fn sign(wallet: &MpcWallet, data: &[u8]) -> Result<Signature> {
if !wallet.can_sign() {
return Err(WalletError::ThresholdNotMet {
got: wallet.share_count(),
need: wallet.threshold as usize,
});
}
let pubkey_pkg = wallet.frost_pubkey_package()?;
debug!(
"FROST-signing with wallet {} ({}-of-{}) using {} shares",
wallet.wallet_id,
wallet.threshold,
wallet.total_shares,
wallet.share_count(),
);
let signing_set = &wallet.key_shares[..wallet.threshold as usize];
let signature = run_frost_session(signing_set, pubkey_pkg, data)?;
info!(
"Successfully FROST-signed with wallet {}",
wallet.wallet_id
);
Ok(signature)
}
pub fn sign_with_shares(
wallet: &MpcWallet,
shares: &[KeyShare],
threshold: usize,
data: &[u8],
) -> Result<Signature> {
if shares.len() < threshold {
return Err(WalletError::ThresholdNotMet {
got: shares.len(),
need: threshold,
});
}
let pubkey_pkg = wallet.frost_pubkey_package()?;
let signing_set = &shares[..threshold];
debug!(
"FROST-signing with {} of {} provided shares",
signing_set.len(),
shares.len()
);
run_frost_session(signing_set, pubkey_pkg, data)
}
}
fn run_frost_session(
signing_set: &[KeyShare],
pubkey_pkg: &PublicKeyPackage,
data: &[u8],
) -> Result<Signature> {
let mut nonces: Vec<(u16, SigningNonces)> = Vec::with_capacity(signing_set.len());
let mut commitments: Vec<SigningCommitments> = Vec::with_capacity(signing_set.len());
for share in signing_set {
let (n, c) = round1_commit(&share.secret_share)
.map_err(|e| WalletError::SignatureFailed(format!("FROST round1: {}", e)))?;
nonces.push((share.signer_index.0, n));
commitments.push(c);
}
let signing_pkg = build_signing_package(data, &commitments)
.map_err(|e| WalletError::SignatureFailed(format!("FROST build_signing_package: {}", e)))?;
let mut shares_out: Vec<SignatureShare> = Vec::with_capacity(signing_set.len());
for share in signing_set {
let n = nonces
.iter()
.find(|(idx, _)| *idx == share.signer_index.0)
.map(|(_, n)| n)
.ok_or_else(|| {
WalletError::SignatureFailed(format!(
"missing round-1 nonces for signer {}",
share.signer_index.0
))
})?;
let sig_share = round2_sign(&signing_pkg, n, &share.secret_share)
.map_err(|e| WalletError::SignatureFailed(format!("FROST round2: {}", e)))?;
shares_out.push(sig_share);
}
let signature = aggregate_signature(&signing_pkg, &shares_out, pubkey_pkg)
.map_err(|e| WalletError::SignatureFailed(format!("FROST aggregate: {}", e)))?;
Ok(signature)
}
pub struct TransactionSigner;
impl TransactionSigner {
pub fn sign_transaction(wallet: &MpcWallet, tx_data: &[u8]) -> Result<Vec<u8>> {
let signature = MpcSigner::sign(wallet, tx_data)?;
Self::verify_signature(&wallet.public_key, tx_data, &signature)?;
Ok(signature.to_bytes())
}
pub fn sign_message(wallet: &MpcWallet, message: &[u8]) -> Result<Vec<u8>> {
let message_hash = tenzro_crypto::hash::sha256(message);
let signature = MpcSigner::sign(wallet, message_hash.as_bytes())?;
Self::verify_signature(&wallet.public_key, message_hash.as_bytes(), &signature)?;
Ok(signature.to_bytes())
}
pub fn sign_raw(wallet: &MpcWallet, data: &[u8]) -> Result<Vec<u8>> {
let signature = MpcSigner::sign(wallet, data)?;
Self::verify_signature(&wallet.public_key, data, &signature)?;
Ok(signature.to_bytes())
}
pub fn verify_signature(
public_key: &tenzro_crypto::PublicKey,
data: &[u8],
signature: &Signature,
) -> Result<()> {
tenzro_crypto::signatures::verify(public_key, data, signature)
.map_err(|e| WalletError::SignatureVerificationFailed(e.to_string()))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::provisioning::WalletProvisioner;
use tenzro_crypto::KeyType;
#[test]
fn test_frost_signing_produces_valid_ed25519() {
let provisioner = WalletProvisioner::new();
let wallet = provisioner.provision_wallet().unwrap();
let data = b"Tenzro Network transaction data";
let signature = MpcSigner::sign(&wallet, data).unwrap();
assert_eq!(signature.key_type(), KeyType::Ed25519);
assert_eq!(signature.as_bytes().len(), 64);
tenzro_crypto::signatures::verify(&wallet.public_key, data, &signature)
.expect("FROST signature must verify under group key");
}
#[test]
fn test_threshold_signing_succeeds_with_min_set() {
let provisioner = WalletProvisioner::new();
let wallet = provisioner.provision_wallet().unwrap();
let data = b"test data";
let shares_to_use = wallet.key_shares[..2].to_vec();
let sig = MpcSigner::sign_with_shares(&wallet, &shares_to_use, 2, data).unwrap();
tenzro_crypto::signatures::verify(&wallet.public_key, data, &sig).unwrap();
}
#[test]
fn test_insufficient_shares_rejected() {
let provisioner = WalletProvisioner::new();
let wallet = provisioner.provision_wallet().unwrap();
let data = b"test data";
let shares_to_use = wallet.key_shares[..1].to_vec();
let result = MpcSigner::sign_with_shares(&wallet, &shares_to_use, 2, data);
assert!(result.is_err());
}
#[test]
fn test_transaction_signer_round_trips() {
let provisioner = WalletProvisioner::new();
let wallet = provisioner.provision_wallet().unwrap();
let tx_data = b"transaction data";
let sig_bytes = TransactionSigner::sign_transaction(&wallet, tx_data).unwrap();
assert_eq!(sig_bytes.len(), 64);
}
#[test]
fn test_message_signer_round_trips() {
let provisioner = WalletProvisioner::new();
let wallet = provisioner.provision_wallet().unwrap();
let message = b"Hello, Tenzro Network!";
let sig_bytes = TransactionSigner::sign_message(&wallet, message).unwrap();
assert_eq!(sig_bytes.len(), 64);
}
#[test]
fn test_signature_bound_to_message() {
let provisioner = WalletProvisioner::new();
let wallet = provisioner.provision_wallet().unwrap();
let sig = MpcSigner::sign(&wallet, b"message A").unwrap();
let result = tenzro_crypto::signatures::verify(&wallet.public_key, b"message B", &sig);
assert!(result.is_err());
}
}