Files
serai/processor/signers/src/batch/mod.rs

213 lines
6.9 KiB
Rust
Raw Normal View History

use core::future::Future;
use std::collections::HashSet;
use blake2::{digest::typenum::U32, Digest, Blake2b};
2025-08-25 09:17:29 -04:00
use ciphersuite::group::GroupEncoding;
Smash the singular `Ciphersuite` trait into multiple This helps identify where the various functionalities are used, or rather, not used. The `Ciphersuite` trait present in `patches/ciphersuite`, facilitating the entire FCMP++ tree, only requires the markers _and_ canonical point decoding. I've opened a PR to upstream such a trait into `group` (https://github.com/zkcrypto/group/pull/68). `WrappedGroup` is still justified for as long as `Group::generator` exists. Moving `::generator()` to its own trait, on an independent structure (upstream) would be massively appreciated. @tarcieri also wanted to update from `fn generator()` to `const GENERATOR`, which would encourage further discussion on https://github.com/zkcrypto/group/issues/32 and https://github.com/zkcrypto/group/issues/45, which have been stagnant. The `Id` trait is occasionally used yet really should be first off the chopping block. Finally, `WithPreferredHash` is only actually used around a third of the time, which more than justifies it being a separate trait. --- Updates `dalek_ff_group::Scalar` to directly re-export `curve25519_dalek::Scalar`, as without issue. `dalek_ff_group::RistrettoPoint` also could be replaced with an export of `curve25519_dalek::RistrettoPoint`, yet the coordinator relies on how we implemented `Hash` on it for the hell of it so it isn't worth it at this time. `dalek_ff_group::EdwardsPoint` can't be replaced for an re-export of `curve25519_dalek::SubgroupPoint` as it doesn't implement `zeroize`, `subtle` traits within a released, non-yanked version. Relevance to https://github.com/serai-dex/serai/issues/201 and https://github.com/dalek-cryptography/curve25519-dalek/issues/811#issuecomment-3247732746. Also updates the `Ristretto` ciphersuite to prefer `Blake2b-512` over `SHA2-512`. In order to maintain compliance with FROST's IETF standard, `modular-frost` defines its own ciphersuite for Ristretto which still uses `SHA2-512`.
2025-09-03 12:25:37 -04:00
use frost::{dkg::ThresholdKeys, curve::Ristretto};
use serai_primitives::{validator_sets::Session, instructions::SignedBatch};
use serai_db::{Get, DbTxn, Db};
use messages::sign::VariantSignId;
use primitives::task::{DoesNotError, ContinuallyRan};
use scanner::{BatchesToSign, AcknowledgedBatches};
use frost_attempt_manager::*;
use crate::{
db::{CoordinatorToBatchSignerMessages, BatchSignerToCoordinatorMessages},
WrappedSchnorrkelMachine,
};
mod db;
use db::*;
pub(crate) fn last_acknowledged_batch(getter: &impl Get) -> Option<u32> {
LastAcknowledgedBatch::get(getter)
}
pub(crate) fn signed_batch(getter: &impl Get, id: u32) -> Option<SignedBatch> {
SignedBatches::get(getter, id)
}
// Fetches batches to sign and signs them.
pub(crate) struct BatchSignerTask<D: Db, E: GroupEncoding> {
db: D,
session: Session,
external_key: E,
keys: Vec<ThresholdKeys<Ristretto>>,
active_signing_protocols: HashSet<[u8; 32]>,
attempt_manager: AttemptManager<D, WrappedSchnorrkelMachine>,
}
impl<D: Db, E: GroupEncoding> BatchSignerTask<D, E> {
pub(crate) fn new(
db: D,
session: Session,
external_key: E,
keys: Vec<ThresholdKeys<Ristretto>>,
) -> Self {
let mut active_signing_protocols = HashSet::new();
let mut attempt_manager = AttemptManager::new(
db.clone(),
session,
keys.first().expect("creating a batch signer with 0 keys").params().i(),
);
// Re-register all active signing protocols
for id in ActiveSigningProtocols::get(&db, session).unwrap_or(vec![]) {
active_signing_protocols.insert(id);
let batch = Batches::get(&db, id).unwrap();
let mut machines = Vec::with_capacity(keys.len());
for keys in &keys {
// TODO: Fetch the context for this from a constant instead of re-defining it
machines.push(WrappedSchnorrkelMachine::new(
keys.clone(),
b"substrate",
batch.publish_batch_message(),
));
}
attempt_manager.register(VariantSignId::Batch(id), machines);
}
Self { db, session, external_key, keys, active_signing_protocols, attempt_manager }
}
}
impl<D: Db, E: Send + GroupEncoding> ContinuallyRan for BatchSignerTask<D, E> {
type Error = DoesNotError;
fn run_iteration(&mut self) -> impl Send + Future<Output = Result<bool, Self::Error>> {
async move {
let mut iterated = false;
// Check for new batches to sign
loop {
let mut txn = self.db.txn();
let Some(batch) = BatchesToSign::try_recv(&mut txn, &self.external_key) else {
break;
};
iterated = true;
// Save this to the database as a transaction to sign
let batch_hash = <[u8; 32]>::from(Blake2b::<U32>::digest(borsh::to_vec(&batch).unwrap()));
self.active_signing_protocols.insert(batch_hash);
ActiveSigningProtocols::set(
&mut txn,
self.session,
&self.active_signing_protocols.iter().copied().collect(),
);
BatchHash::set(&mut txn, batch.id(), &batch_hash);
Batches::set(&mut txn, batch_hash, &batch);
let mut machines = Vec::with_capacity(self.keys.len());
for keys in &self.keys {
// TODO: Also fetch the constant here
machines.push(WrappedSchnorrkelMachine::new(
keys.clone(),
b"substrate",
batch.publish_batch_message(),
));
}
for msg in self.attempt_manager.register(VariantSignId::Batch(batch_hash), machines) {
BatchSignerToCoordinatorMessages::send(&mut txn, self.session, &msg);
}
txn.commit();
}
// Check for acknowledged Batches (meaning we should no longer sign for these Batches)
loop {
let mut txn = self.db.txn();
let batch_hash = {
let Some(batch_id) = AcknowledgedBatches::try_recv(&mut txn, &self.external_key) else {
break;
};
/*
We may have yet to register this signing protocol.
While `BatchesToSign` is populated before `AcknowledgedBatches`, we could theoretically
have `BatchesToSign` populated with a new batch _while iterating over
`AcknowledgedBatches`_, and then have `AcknowledgedBatched` populated. In that edge
case, we will see the acknowledgement notification before we see the transaction.
In such a case, we break (dropping the txn, re-queueing the acknowledgement
notification). On the task's next iteration, we'll process the Batch from
`BatchesToSign` and be able to make progress.
*/
let Some(batch_hash) = BatchHash::take(&mut txn, batch_id) else {
drop(txn);
break;
};
batch_hash
};
let batch =
Batches::take(&mut txn, batch_hash).expect("BatchHash populated but not Batches");
iterated = true;
// Update the last acknowledged Batch
{
let last_acknowledged = LastAcknowledgedBatch::get(&txn);
if Some(batch.id()) > last_acknowledged {
LastAcknowledgedBatch::set(&mut txn, &batch.id());
}
}
// Remove this as an active signing protocol
assert!(self.active_signing_protocols.remove(&batch_hash));
ActiveSigningProtocols::set(
&mut txn,
self.session,
&self.active_signing_protocols.iter().copied().collect(),
);
// Clean up SignedBatches
SignedBatches::del(&mut txn, batch.id());
// We retire with a txn so we either successfully flag this Batch as acknowledged, and
// won't re-register it (making this retire safe), or we don't flag it, meaning we will
// re-register it, yet that's safe as we have yet to retire it
self.attempt_manager.retire(&mut txn, VariantSignId::Batch(batch_hash));
txn.commit();
}
// Handle any messages sent to us
loop {
let mut txn = self.db.txn();
let Some(msg) = CoordinatorToBatchSignerMessages::try_recv(&mut txn, self.session) else {
break;
};
iterated = true;
match self.attempt_manager.handle(msg) {
Response::Messages(msgs) => {
for msg in msgs {
BatchSignerToCoordinatorMessages::send(&mut txn, self.session, &msg);
}
}
Response::Signature { id, signature } => {
let VariantSignId::Batch(id) = id else { panic!("BatchSignerTask signed a non-Batch") };
let batch =
Batches::get(&txn, id).expect("signed a Batch we didn't save to the database");
let signed_batch = SignedBatch { batch, signature: signature.into() };
SignedBatches::set(&mut txn, signed_batch.batch.id(), &signed_batch);
}
}
txn.commit();
}
Ok(iterated)
}
}
}