mirror of
https://github.com/serai-dex/serai.git
synced 2025-12-08 12:19:24 +00:00
Add tasks to publish data onto Serai
This commit is contained in:
1
Cargo.lock
generated
1
Cargo.lock
generated
@@ -8385,6 +8385,7 @@ dependencies = [
|
||||
name = "serai-coordinator-substrate"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"bitvec",
|
||||
"borsh",
|
||||
"futures",
|
||||
"log",
|
||||
|
||||
@@ -30,7 +30,7 @@ schnorr = { package = "schnorr-signatures", path = "../crypto/schnorr", default-
|
||||
frost = { package = "modular-frost", path = "../crypto/frost" }
|
||||
frost-schnorrkel = { path = "../crypto/schnorrkel" }
|
||||
|
||||
scale = { package = "parity-scale-codec", version = "3", default-features = false, features = ["std", "derive"] }
|
||||
scale = { package = "parity-scale-codec", version = "3", default-features = false, features = ["std", "derive", "bit-vec"] }
|
||||
|
||||
zalloc = { path = "../common/zalloc" }
|
||||
serai-db = { path = "../common/db" }
|
||||
|
||||
@@ -39,8 +39,6 @@ mod p2p {
|
||||
pub use serai_coordinator_libp2p_p2p::Libp2p;
|
||||
}
|
||||
|
||||
mod serai;
|
||||
|
||||
// Use a zeroizing allocator for this entire application
|
||||
// While secrets should already be zeroized, the presence of secret keys in a networked application
|
||||
// (at increased risk of OOB reads) justifies the performance hit in case any secrets weren't
|
||||
@@ -227,10 +225,10 @@ async fn handle_processor_messages(
|
||||
SignedCosigns::send(&mut txn, &cosign);
|
||||
}
|
||||
messages::coordinator::ProcessorMessage::SignedBatch { batch } => {
|
||||
todo!("TODO Save to DB, have task read from DB and publish to Serai")
|
||||
todo!("TODO PublishBatchTask")
|
||||
}
|
||||
messages::coordinator::ProcessorMessage::SignedSlashReport { session, signature } => {
|
||||
todo!("TODO Save to DB, have task read from DB and publish to Serai")
|
||||
todo!("TODO PublishSlashReportTask")
|
||||
}
|
||||
},
|
||||
messages::ProcessorMessage::Substrate(msg) => match msg {
|
||||
|
||||
@@ -18,7 +18,9 @@ rustdoc-args = ["--cfg", "docsrs"]
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
scale = { package = "parity-scale-codec", version = "3", default-features = false, features = ["std", "derive"] }
|
||||
bitvec = { version = "1", default-features = false, features = ["std"] }
|
||||
|
||||
scale = { package = "parity-scale-codec", version = "3", default-features = false, features = ["std", "derive", "bit-vec"] }
|
||||
borsh = { version = "1", default-features = false, features = ["std", "derive", "de_strict_order"] }
|
||||
serai-client = { path = "../../substrate/client", version = "0.1", default-features = false, features = ["serai", "borsh"] }
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Serai Coordinate Substrate Scanner
|
||||
# Serai Coordinator Substrate
|
||||
|
||||
This is the scanner of the Serai blockchain for the purposes of Serai's coordinator.
|
||||
This crate manages the Serai coordinators's interactions with Serai's Substrate blockchain.
|
||||
|
||||
Two event streams are defined:
|
||||
|
||||
@@ -12,3 +12,9 @@ Two event streams are defined:
|
||||
The canonical event stream is available without provision of a validator's public key. The ephemeral
|
||||
event stream requires provision of a validator's public key. Both are ordered within themselves, yet
|
||||
there are no ordering guarantees across the two.
|
||||
|
||||
Additionally, a collection of tasks are defined to publish data onto Serai:
|
||||
|
||||
- `SetKeysTask`, which sets the keys generated via DKGs onto Serai.
|
||||
- `PublishBatchTask`, which publishes `Batch`s onto Serai.
|
||||
- `PublishSlashReportTask`, which publishes `SlashReport`s onto Serai.
|
||||
|
||||
@@ -6,8 +6,10 @@ use scale::{Encode, Decode};
|
||||
use borsh::{io, BorshSerialize, BorshDeserialize};
|
||||
|
||||
use serai_client::{
|
||||
primitives::{PublicKey, NetworkId},
|
||||
validator_sets::primitives::ValidatorSet,
|
||||
primitives::{NetworkId, PublicKey, Signature, SeraiAddress},
|
||||
validator_sets::primitives::{Session, ValidatorSet, KeyPair},
|
||||
in_instructions::primitives::SignedBatch,
|
||||
Transaction,
|
||||
};
|
||||
|
||||
use serai_db::*;
|
||||
@@ -17,6 +19,13 @@ pub use canonical::CanonicalEventStream;
|
||||
mod ephemeral;
|
||||
pub use ephemeral::EphemeralEventStream;
|
||||
|
||||
mod set_keys;
|
||||
pub use set_keys::SetKeysTask;
|
||||
mod publish_batch;
|
||||
pub use publish_batch::PublishBatchTask;
|
||||
mod publish_slash_report;
|
||||
pub use publish_slash_report::PublishSlashReportTask;
|
||||
|
||||
fn borsh_serialize_validators<W: io::Write>(
|
||||
validators: &Vec<(PublicKey, u16)>,
|
||||
writer: &mut W,
|
||||
@@ -53,11 +62,7 @@ pub struct NewSetInformation {
|
||||
}
|
||||
|
||||
mod _public_db {
|
||||
use serai_client::{primitives::NetworkId, validator_sets::primitives::ValidatorSet};
|
||||
|
||||
use serai_db::*;
|
||||
|
||||
use crate::NewSetInformation;
|
||||
use super::*;
|
||||
|
||||
db_channel!(
|
||||
CoordinatorSubstrate {
|
||||
@@ -68,6 +73,18 @@ mod _public_db {
|
||||
NewSet: () -> NewSetInformation,
|
||||
// Potentially relevant sign slash report, from an ephemeral event stream
|
||||
SignSlashReport: (set: ValidatorSet) -> (),
|
||||
|
||||
// Signed batches to publish onto the Serai network
|
||||
SignedBatches: (network: NetworkId) -> SignedBatch,
|
||||
}
|
||||
);
|
||||
|
||||
create_db!(
|
||||
CoordinatorSubstrate {
|
||||
// Keys to set on the Serai network
|
||||
Keys: (network: NetworkId) -> (Session, Vec<u8>),
|
||||
// Slash reports to publish onto the Serai network
|
||||
SlashReports: (network: NetworkId) -> (Session, Vec<u8>),
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -118,3 +135,94 @@ impl SignSlashReport {
|
||||
_public_db::SignSlashReport::try_recv(txn, set)
|
||||
}
|
||||
}
|
||||
|
||||
/// The keys to set on Serai.
|
||||
pub struct Keys;
|
||||
impl Keys {
|
||||
/// Set the keys to report for a validator set.
|
||||
///
|
||||
/// This only saves the most recent keys as only a single session is eligible to have its keys
|
||||
/// reported at once.
|
||||
pub fn set(
|
||||
txn: &mut impl DbTxn,
|
||||
set: ValidatorSet,
|
||||
key_pair: KeyPair,
|
||||
signature_participants: bitvec::vec::BitVec<u8, bitvec::order::Lsb0>,
|
||||
signature: Signature,
|
||||
) {
|
||||
// If we have a more recent pair of keys, don't write this historic one
|
||||
if let Some((existing_session, _)) = _public_db::Keys::get(txn, set.network) {
|
||||
if existing_session.0 >= set.session.0 {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
let tx = serai_client::validator_sets::SeraiValidatorSets::set_keys(
|
||||
set.network,
|
||||
key_pair,
|
||||
signature_participants,
|
||||
signature,
|
||||
);
|
||||
_public_db::Keys::set(txn, set.network, &(set.session, tx.encode()));
|
||||
}
|
||||
pub(crate) fn take(txn: &mut impl DbTxn, network: NetworkId) -> Option<(Session, Transaction)> {
|
||||
let (session, tx) = _public_db::Keys::take(txn, network)?;
|
||||
Some((session, <_>::decode(&mut tx.as_slice()).unwrap()))
|
||||
}
|
||||
}
|
||||
|
||||
/// The signed batches to publish onto Serai.
|
||||
pub struct SignedBatches;
|
||||
impl SignedBatches {
|
||||
/// Send a `SignedBatch` to publish onto Serai.
|
||||
///
|
||||
/// These will be published sequentially. Out-of-order sending risks hanging the task.
|
||||
pub fn send(txn: &mut impl DbTxn, batch: &SignedBatch) {
|
||||
_public_db::SignedBatches::send(txn, batch.batch.network, batch);
|
||||
}
|
||||
pub(crate) fn try_recv(txn: &mut impl DbTxn, network: NetworkId) -> Option<SignedBatch> {
|
||||
_public_db::SignedBatches::try_recv(txn, network)
|
||||
}
|
||||
}
|
||||
|
||||
/// The slash report was invalid.
|
||||
#[derive(Debug)]
|
||||
pub struct InvalidSlashReport;
|
||||
|
||||
/// The slash reports to publish onto Serai.
|
||||
pub struct SlashReports;
|
||||
impl SlashReports {
|
||||
/// Set the slashes to report for a validator set.
|
||||
///
|
||||
/// This only saves the most recent slashes as only a single session is eligible to have its
|
||||
/// slashes reported at once.
|
||||
///
|
||||
/// Returns Err if the slashes are invalid. Returns Ok if the slashes weren't detected as
|
||||
/// invalid. Slashes may be considered invalid by the Serai blockchain later even if not detected
|
||||
/// as invalid here.
|
||||
pub fn set(
|
||||
txn: &mut impl DbTxn,
|
||||
set: ValidatorSet,
|
||||
slashes: Vec<(SeraiAddress, u32)>,
|
||||
signature: Signature,
|
||||
) -> Result<(), InvalidSlashReport> {
|
||||
// If we have a more recent slash report, don't write this historic one
|
||||
if let Some((existing_session, _)) = _public_db::SlashReports::get(txn, set.network) {
|
||||
if existing_session.0 >= set.session.0 {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
let tx = serai_client::validator_sets::SeraiValidatorSets::report_slashes(
|
||||
set.network,
|
||||
slashes.try_into().map_err(|_| InvalidSlashReport)?,
|
||||
signature,
|
||||
);
|
||||
_public_db::SlashReports::set(txn, set.network, &(set.session, tx.encode()));
|
||||
Ok(())
|
||||
}
|
||||
pub(crate) fn take(txn: &mut impl DbTxn, network: NetworkId) -> Option<(Session, Transaction)> {
|
||||
let (session, tx) = _public_db::SlashReports::take(txn, network)?;
|
||||
Some((session, <_>::decode(&mut tx.as_slice()).unwrap()))
|
||||
}
|
||||
}
|
||||
|
||||
66
coordinator/substrate/src/publish_batch.rs
Normal file
66
coordinator/substrate/src/publish_batch.rs
Normal file
@@ -0,0 +1,66 @@
|
||||
use core::future::Future;
|
||||
use std::sync::Arc;
|
||||
|
||||
use serai_db::{DbTxn, Db};
|
||||
|
||||
use serai_client::{primitives::NetworkId, SeraiError, Serai};
|
||||
|
||||
use serai_task::ContinuallyRan;
|
||||
|
||||
use crate::SignedBatches;
|
||||
|
||||
/// Publish `SignedBatch`s from `SignedBatches` onto Serai.
|
||||
pub struct PublishBatchTask<D: Db> {
|
||||
db: D,
|
||||
serai: Arc<Serai>,
|
||||
network: NetworkId,
|
||||
}
|
||||
|
||||
impl<D: Db> PublishBatchTask<D> {
|
||||
/// Create a task to publish `SignedBatch`s onto Serai.
|
||||
///
|
||||
/// Returns None if `network == NetworkId::Serai`.
|
||||
// TODO: ExternalNetworkId
|
||||
pub fn new(db: D, serai: Arc<Serai>, network: NetworkId) -> Option<Self> {
|
||||
if network == NetworkId::Serai {
|
||||
None?
|
||||
};
|
||||
Some(Self { db, serai, network })
|
||||
}
|
||||
}
|
||||
|
||||
impl<D: Db> ContinuallyRan for PublishBatchTask<D> {
|
||||
type Error = SeraiError;
|
||||
|
||||
fn run_iteration(&mut self) -> impl Send + Future<Output = Result<bool, Self::Error>> {
|
||||
async move {
|
||||
let mut made_progress = false;
|
||||
|
||||
loop {
|
||||
let mut txn = self.db.txn();
|
||||
let Some(batch) = SignedBatches::try_recv(&mut txn, self.network) else {
|
||||
// No batch to publish at this time
|
||||
break;
|
||||
};
|
||||
|
||||
// Publish this Batch if it hasn't already been published
|
||||
let serai = self.serai.as_of_latest_finalized_block().await?;
|
||||
let last_batch = serai.in_instructions().last_batch_for_network(self.network).await?;
|
||||
if last_batch < Some(batch.batch.id) {
|
||||
// This stream of Batches *should* be sequential within the larger context of the Serai
|
||||
// coordinator. In this library, we use a more relaxed definition and don't assert
|
||||
// sequence. This does risk hanging the task, if Batch #n+1 is sent before Batch #n, but
|
||||
// that is a documented fault of the `SignedBatches` API.
|
||||
self
|
||||
.serai
|
||||
.publish(&serai_client::in_instructions::SeraiInInstructions::execute_batch(batch))
|
||||
.await?;
|
||||
}
|
||||
|
||||
txn.commit();
|
||||
made_progress = true;
|
||||
}
|
||||
Ok(made_progress)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,25 +1,28 @@
|
||||
use core::future::Future;
|
||||
use std::sync::Arc;
|
||||
|
||||
use serai_db::{Get, DbTxn, Db as DbTrait, create_db};
|
||||
use serai_db::{DbTxn, Db};
|
||||
|
||||
use scale::Decode;
|
||||
use serai_client::{primitives::NetworkId, validator_sets::primitives::Session, Serai};
|
||||
|
||||
use serai_task::ContinuallyRan;
|
||||
|
||||
create_db! {
|
||||
CoordinatorSerai {
|
||||
SlashReports: (network: NetworkId) -> (Session, Vec<u8>),
|
||||
use crate::SlashReports;
|
||||
|
||||
/// Publish slash reports from `SlashReports` onto Serai.
|
||||
pub struct PublishSlashReportTask<D: Db> {
|
||||
db: D,
|
||||
serai: Arc<Serai>,
|
||||
}
|
||||
|
||||
impl<D: Db> PublishSlashReportTask<D> {
|
||||
/// Create a task to publish slash reports onto Serai.
|
||||
pub fn new(db: D, serai: Arc<Serai>) -> Self {
|
||||
Self { db, serai }
|
||||
}
|
||||
}
|
||||
|
||||
/// Publish `SlashReport`s from `SlashReports` onto Serai.
|
||||
pub struct PublishSlashReportTask<CD: DbTrait> {
|
||||
db: CD,
|
||||
serai: Arc<Serai>,
|
||||
}
|
||||
impl<CD: DbTrait> ContinuallyRan for PublishSlashReportTask<CD> {
|
||||
impl<D: Db> ContinuallyRan for PublishSlashReportTask<D> {
|
||||
type Error = String;
|
||||
|
||||
fn run_iteration(&mut self) -> impl Send + Future<Output = Result<bool, Self::Error>> {
|
||||
@@ -35,7 +38,6 @@ impl<CD: DbTrait> ContinuallyRan for PublishSlashReportTask<CD> {
|
||||
// No slash report to publish
|
||||
continue;
|
||||
};
|
||||
let slash_report = serai_client::Transaction::decode(&mut slash_report.as_slice()).unwrap();
|
||||
|
||||
let serai =
|
||||
self.serai.as_of_latest_finalized_block().await.map_err(|e| format!("{e:?}"))?;
|
||||
@@ -48,7 +50,7 @@ impl<CD: DbTrait> ContinuallyRan for PublishSlashReportTask<CD> {
|
||||
let session_after_slash_report_retired =
|
||||
current_session > Some(session_after_slash_report.0);
|
||||
if session_after_slash_report_retired {
|
||||
// Commit the txn to drain this SlashReport from the database and not try it again later
|
||||
// Commit the txn to drain this slash report from the database and not try it again later
|
||||
txn.commit();
|
||||
continue;
|
||||
}
|
||||
@@ -57,7 +59,7 @@ impl<CD: DbTrait> ContinuallyRan for PublishSlashReportTask<CD> {
|
||||
// We already checked the current session wasn't greater, and they're not equal
|
||||
assert!(current_session < Some(session_after_slash_report.0));
|
||||
// This would mean the Serai node is resyncing and is behind where it prior was
|
||||
Err("have a SlashReport for a session Serai has yet to retire".to_string())?;
|
||||
Err("have a slash report for a session Serai has yet to retire".to_string())?;
|
||||
}
|
||||
|
||||
// If this session which should publish a slash report already has, move on
|
||||
@@ -68,14 +70,6 @@ impl<CD: DbTrait> ContinuallyRan for PublishSlashReportTask<CD> {
|
||||
continue;
|
||||
};
|
||||
|
||||
/*
|
||||
let tx = serai_client::SeraiValidatorSets::report_slashes(
|
||||
network,
|
||||
slash_report,
|
||||
signature.clone(),
|
||||
);
|
||||
*/
|
||||
|
||||
match self.serai.publish(&slash_report).await {
|
||||
Ok(()) => {
|
||||
txn.commit();
|
||||
88
coordinator/substrate/src/set_keys.rs
Normal file
88
coordinator/substrate/src/set_keys.rs
Normal file
@@ -0,0 +1,88 @@
|
||||
use core::future::Future;
|
||||
use std::sync::Arc;
|
||||
|
||||
use serai_db::{DbTxn, Db};
|
||||
|
||||
use serai_client::{primitives::NetworkId, validator_sets::primitives::ValidatorSet, Serai};
|
||||
|
||||
use serai_task::ContinuallyRan;
|
||||
|
||||
use crate::Keys;
|
||||
|
||||
/// Set keys from `Keys` on Serai.
|
||||
pub struct SetKeysTask<D: Db> {
|
||||
db: D,
|
||||
serai: Arc<Serai>,
|
||||
}
|
||||
|
||||
impl<D: Db> SetKeysTask<D> {
|
||||
/// Create a task to publish slash reports onto Serai.
|
||||
pub fn new(db: D, serai: Arc<Serai>) -> Self {
|
||||
Self { db, serai }
|
||||
}
|
||||
}
|
||||
|
||||
impl<D: Db> ContinuallyRan for SetKeysTask<D> {
|
||||
type Error = String;
|
||||
|
||||
fn run_iteration(&mut self) -> impl Send + Future<Output = Result<bool, Self::Error>> {
|
||||
async move {
|
||||
let mut made_progress = false;
|
||||
for network in serai_client::primitives::NETWORKS {
|
||||
if network == NetworkId::Serai {
|
||||
continue;
|
||||
};
|
||||
|
||||
let mut txn = self.db.txn();
|
||||
let Some((session, keys)) = Keys::take(&mut txn, network) else {
|
||||
// No keys to set
|
||||
continue;
|
||||
};
|
||||
|
||||
let serai =
|
||||
self.serai.as_of_latest_finalized_block().await.map_err(|e| format!("{e:?}"))?;
|
||||
let serai = serai.validator_sets();
|
||||
let current_session = serai.session(network).await.map_err(|e| format!("{e:?}"))?;
|
||||
let current_session = current_session.map(|session| session.0);
|
||||
// Only attempt to set these keys if this isn't a retired session
|
||||
if Some(session.0) < current_session {
|
||||
// Commit the txn to take these keys from the database and not try it again later
|
||||
txn.commit();
|
||||
continue;
|
||||
}
|
||||
|
||||
if Some(session.0) != current_session {
|
||||
// We already checked the current session wasn't greater, and they're not equal
|
||||
assert!(current_session < Some(session.0));
|
||||
// This would mean the Serai node is resyncing and is behind where it prior was
|
||||
Err("have a keys for a session Serai has yet to start".to_string())?;
|
||||
}
|
||||
|
||||
// If this session already has had its keys set, move on
|
||||
if serai
|
||||
.keys(ValidatorSet { network, session })
|
||||
.await
|
||||
.map_err(|e| format!("{e:?}"))?
|
||||
.is_some()
|
||||
{
|
||||
txn.commit();
|
||||
continue;
|
||||
};
|
||||
|
||||
match self.serai.publish(&keys).await {
|
||||
Ok(()) => {
|
||||
txn.commit();
|
||||
made_progress = true;
|
||||
}
|
||||
// This could be specific to this TX (such as an already in mempool error) and it may be
|
||||
// worthwhile to continue iteration with the other pending slash reports. We assume this
|
||||
// error ephemeral and that the latency incurred for this ephemeral error to resolve is
|
||||
// miniscule compared to the window reasonable to set the keys. That makes this a
|
||||
// non-issue.
|
||||
Err(e) => Err(format!("couldn't publish set keys transaction: {e:?}"))?,
|
||||
}
|
||||
}
|
||||
Ok(made_progress)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -206,7 +206,7 @@ impl<'a, TD: Db, TDT: DbTxn, P: P2p> ScanBlock<'a, TD, TDT, P> {
|
||||
}
|
||||
Transaction::DkgConfirmationShare { attempt, share, signed } => {
|
||||
// Accumulate the shares into our own FROST attempt manager
|
||||
todo!("TODO")
|
||||
todo!("TODO: SetKeysTask")
|
||||
}
|
||||
|
||||
Transaction::Cosign { substrate_block_hash } => {
|
||||
|
||||
@@ -238,6 +238,8 @@ impl<'a> SeraiValidatorSets<'a> {
|
||||
|
||||
pub fn report_slashes(
|
||||
network: NetworkId,
|
||||
// TODO: This bounds a maximum length but takes more space than just publishing all the u32s
|
||||
// (50 * (32 + 4)) > (150 * 4)
|
||||
slashes: sp_runtime::BoundedVec<
|
||||
(SeraiAddress, u32),
|
||||
sp_core::ConstU32<{ primitives::MAX_KEY_SHARES_PER_SET / 3 }>,
|
||||
|
||||
Reference in New Issue
Block a user