use std::collections::BTreeMap;
use std::sync::Mutex;
use frost_ed25519 as frost;
use rand::rngs::OsRng;
use rand::RngCore;
use crate::rpc::{NonceSlotId, QuorumRpcMessage, SignCertRequestPayload, SignCertResponsePayload};
#[derive(Clone, Debug)]
pub struct SignerPolicy {
pub max_ttl_secs: u64,
pub admin_provisioning_pubkey: Option<ed25519_dalek::VerifyingKey>,
pub ipv4_network_mask: Option<(u32, u32)>,
}
impl Default for SignerPolicy {
fn default() -> Self {
Self {
max_ttl_secs: 7 * 86_400, admin_provisioning_pubkey: None,
ipv4_network_mask: None,
}
}
}
pub struct EphemeralNonceVault {
boot_epoch_id: u128,
next_gen_idx: u64,
active_nonces: BTreeMap<NonceSlotId, frost::round1::SigningNonces>,
}
impl EphemeralNonceVault {
pub fn new() -> Self {
let mut bytes = [0u8; 16];
OsRng.fill_bytes(&mut bytes);
Self {
boot_epoch_id: u128::from_le_bytes(bytes),
next_gen_idx: 1,
active_nonces: BTreeMap::new(),
}
}
pub fn boot_epoch_id(&self) -> u128 {
self.boot_epoch_id
}
pub fn preprocess_batch(
&mut self,
signing_share: &frost::keys::SigningShare,
count: usize,
) -> Vec<(NonceSlotId, frost::round1::SigningCommitments)> {
let mut out = Vec::with_capacity(count);
let mut rng = OsRng;
for _ in 0..count {
let seq = self.next_gen_idx;
self.next_gen_idx = self
.next_gen_idx
.checked_add(1)
.expect("nonce counter overflow");
let (nonces, commitments) = frost::round1::commit(signing_share, &mut rng);
let slot = NonceSlotId {
boot_epoch_id: self.boot_epoch_id,
sequence_idx: seq,
};
self.active_nonces.insert(slot, nonces);
out.push((slot, commitments));
}
out
}
pub fn take_once(&mut self, slot: &NonceSlotId) -> Option<frost::round1::SigningNonces> {
if slot.boot_epoch_id != self.boot_epoch_id {
return None;
}
self.active_nonces.remove(slot)
}
pub fn len(&self) -> usize {
self.active_nonces.len()
}
pub fn is_empty(&self) -> bool {
self.active_nonces.is_empty()
}
}
impl Default for EphemeralNonceVault {
fn default() -> Self {
Self::new()
}
}
pub struct SignerState {
pub key_package: frost::keys::KeyPackage,
pub pubkey_package: frost::keys::PublicKeyPackage,
pub policy: SignerPolicy,
pub vault: Mutex<EphemeralNonceVault>,
}
impl SignerState {
pub fn new(
key_package: frost::keys::KeyPackage,
pubkey_package: frost::keys::PublicKeyPackage,
policy: SignerPolicy,
) -> Self {
Self {
key_package,
pubkey_package,
policy,
vault: Mutex::new(EphemeralNonceVault::new()),
}
}
pub fn handle_sign_request(&self, req: &SignCertRequestPayload) -> QuorumRpcMessage {
let signer_id = *self.key_package.identifier();
let session_id = req.session_id;
let expected_msg = req.cert_tbs.to_canonical_bytes();
if req.signing_package.message() != expected_msg.as_slice() {
return QuorumRpcMessage::SignCertReject {
session_id,
signer_id,
reason: "SigningPackage payload mismatch with CertTbs".into(),
};
}
if req.cert_tbs.not_after <= req.cert_tbs.not_before
|| (req.cert_tbs.not_after - req.cert_tbs.not_before) > self.policy.max_ttl_secs
{
return QuorumRpcMessage::SignCertReject {
session_id,
signer_id,
reason: "Certificate TTL exceeds signer policy maximum".into(),
};
}
let (nonces, replenish) = {
let mut vault = match self.vault.lock() {
Ok(guard) => guard,
Err(poisoned) => poisoned.into_inner(),
};
let Some(n) = vault.take_once(&req.slot_id) else {
return QuorumRpcMessage::SignCertReject {
session_id,
signer_id,
reason: "NonceSlotId not found, already consumed, or stale boot epoch".into(),
};
};
let rep = vault
.preprocess_batch(self.key_package.signing_share(), 1)
.pop();
(n, rep)
};
match frost::round2::sign(&req.signing_package, &nonces, &self.key_package) {
Ok(signature_share) => {
QuorumRpcMessage::SignCertResponse(Box::new(SignCertResponsePayload {
session_id,
signer_id,
signature_share,
replenish_commitment: replenish,
}))
}
Err(e) => QuorumRpcMessage::SignCertReject {
session_id,
signer_id,
reason: format!("FROST round2 sign error: {e}"),
},
}
}
}