Publish SlashReport onto the Tributary

This commit is contained in:
Luke Parker
2025-01-11 06:51:55 -05:00
parent e731b546ab
commit 74106b025f
7 changed files with 124 additions and 30 deletions

View File

@@ -17,7 +17,7 @@ use message_queue::{Service, client::MessageQueue};
use serai_task::{Task, TaskHandle, ContinuallyRan}; use serai_task::{Task, TaskHandle, ContinuallyRan};
use serai_cosign::{SignedCosign, Cosigning}; use serai_cosign::{SignedCosign, Cosigning};
use serai_coordinator_substrate::{CanonicalEventStream, EphemeralEventStream}; use serai_coordinator_substrate::{CanonicalEventStream, EphemeralEventStream, SignSlashReport};
use serai_coordinator_tributary::Transaction; use serai_coordinator_tributary::Transaction;
mod db; mod db;
@@ -148,6 +148,8 @@ async fn main() {
prune_tributary_db(to_cleanup); prune_tributary_db(to_cleanup);
// Drain the cosign intents created for this set // Drain the cosign intents created for this set
while !Cosigning::<Db>::intended_cosigns(&mut txn, to_cleanup).is_empty() {} while !Cosigning::<Db>::intended_cosigns(&mut txn, to_cleanup).is_empty() {}
// Remove the SignSlashReport notification
SignSlashReport::try_recv(&mut txn, to_cleanup);
} }
// Remove retired Tributaries from ActiveTributaries // Remove retired Tributaries from ActiveTributaries
@@ -198,7 +200,6 @@ async fn main() {
}; };
// Spawn the Substrate scanners // Spawn the Substrate scanners
// TODO: SignSlashReport
let (substrate_task_def, substrate_task) = Task::new(); let (substrate_task_def, substrate_task) = Task::new();
let (substrate_canonical_task_def, substrate_canonical_task) = Task::new(); let (substrate_canonical_task_def, substrate_canonical_task) = Task::new();
tokio::spawn( tokio::spawn(

View File

@@ -2,6 +2,7 @@ use core::{future::Future, time::Duration};
use std::sync::Arc; use std::sync::Arc;
use zeroize::Zeroizing; use zeroize::Zeroizing;
use rand_core::OsRng;
use blake2::{digest::typenum::U32, Digest, Blake2s}; use blake2::{digest::typenum::U32, Digest, Blake2s};
use ciphersuite::{Ciphersuite, Ristretto}; use ciphersuite::{Ciphersuite, Ristretto};
@@ -12,14 +13,14 @@ use serai_db::{DbTxn, Db as DbTrait};
use scale::Encode; use scale::Encode;
use serai_client::validator_sets::primitives::ValidatorSet; use serai_client::validator_sets::primitives::ValidatorSet;
use tributary_sdk::{ProvidedError, Tributary}; use tributary_sdk::{TransactionError, ProvidedError, Tributary};
use serai_task::{Task, TaskHandle, ContinuallyRan}; use serai_task::{Task, TaskHandle, ContinuallyRan};
use message_queue::{Service, Metadata, client::MessageQueue}; use message_queue::{Service, Metadata, client::MessageQueue};
use serai_cosign::Cosigning; use serai_cosign::Cosigning;
use serai_coordinator_substrate::NewSetInformation; use serai_coordinator_substrate::{NewSetInformation, SignSlashReport};
use serai_coordinator_tributary::{Transaction, ProcessorMessages, ScanTributaryTask}; use serai_coordinator_tributary::{Transaction, ProcessorMessages, ScanTributaryTask};
use serai_coordinator_p2p::P2p; use serai_coordinator_p2p::P2p;
@@ -27,9 +28,9 @@ use crate::Db;
/// Provides Cosign/Cosigned Transactions onto the Tributary. /// Provides Cosign/Cosigned Transactions onto the Tributary.
pub(crate) struct ProvideCosignCosignedTransactionsTask<CD: DbTrait, TD: DbTrait, P: P2p> { pub(crate) struct ProvideCosignCosignedTransactionsTask<CD: DbTrait, TD: DbTrait, P: P2p> {
pub(crate) db: CD, db: CD,
pub(crate) set: NewSetInformation, set: NewSetInformation,
pub(crate) tributary: Tributary<TD, Transaction, P>, tributary: Tributary<TD, Transaction, P>,
} }
impl<CD: DbTrait, TD: DbTrait, P: P2p> ContinuallyRan impl<CD: DbTrait, TD: DbTrait, P: P2p> ContinuallyRan
for ProvideCosignCosignedTransactionsTask<CD, TD, P> for ProvideCosignCosignedTransactionsTask<CD, TD, P>
@@ -128,9 +129,9 @@ impl<CD: DbTrait, TD: DbTrait, P: P2p> ContinuallyRan
/// Takes the messages from ScanTributaryTask and publishes them to the message-queue. /// Takes the messages from ScanTributaryTask and publishes them to the message-queue.
pub(crate) struct TributaryProcessorMessagesTask<TD: DbTrait> { pub(crate) struct TributaryProcessorMessagesTask<TD: DbTrait> {
pub(crate) tributary_db: TD, tributary_db: TD,
pub(crate) set: ValidatorSet, set: ValidatorSet,
pub(crate) message_queue: Arc<MessageQueue>, message_queue: Arc<MessageQueue>,
} }
impl<TD: DbTrait> ContinuallyRan for TributaryProcessorMessagesTask<TD> { impl<TD: DbTrait> ContinuallyRan for TributaryProcessorMessagesTask<TD> {
fn run_iteration(&mut self) -> impl Send + Future<Output = Result<bool, String>> { fn run_iteration(&mut self) -> impl Send + Future<Output = Result<bool, String>> {
@@ -155,6 +156,51 @@ impl<TD: DbTrait> ContinuallyRan for TributaryProcessorMessagesTask<TD> {
} }
} }
/// Checks for the notification to sign a slash report and does so if present.
pub(crate) struct SignSlashReportTask<CD: DbTrait, TD: DbTrait, P: P2p> {
db: CD,
tributary_db: TD,
tributary: Tributary<TD, Transaction, P>,
set: NewSetInformation,
key: Zeroizing<<Ristretto as Ciphersuite>::F>,
}
impl<CD: DbTrait, TD: DbTrait, P: P2p> ContinuallyRan for SignSlashReportTask<CD, TD, P> {
fn run_iteration(&mut self) -> impl Send + Future<Output = Result<bool, String>> {
async move {
let mut txn = self.db.txn();
let Some(()) = SignSlashReport::try_recv(&mut txn, self.set.set) else { return Ok(false) };
// Fetch the slash report for this Tributary
let mut tx =
serai_coordinator_tributary::slash_report_transaction(&self.tributary_db, &self.set);
tx.sign(&mut OsRng, self.tributary.genesis(), &self.key);
let res = self.tributary.add_transaction(tx.clone()).await;
match &res {
// Fresh publication, already published
Ok(true | false) => {}
Err(
TransactionError::TooLargeTransaction |
TransactionError::InvalidSigner |
TransactionError::InvalidNonce |
TransactionError::InvalidSignature |
TransactionError::InvalidContent,
) => {
panic!("created an invalid SlashReport transaction, tx: {tx:?}, err: {res:?}");
}
// We've published too many transactions recently
// Drop this txn to try to publish it again later on a future iteration
Err(TransactionError::TooManyInMempool) => return Ok(false),
// This isn't a Provided transaction so this should never be hit
Err(TransactionError::ProvidedAddedToMempool) => unreachable!(),
}
txn.commit();
Ok(true)
}
}
}
/// Run the scan task whenever the Tributary adds a new block. /// Run the scan task whenever the Tributary adds a new block.
async fn scan_on_new_block<CD: DbTrait, TD: DbTrait, P: P2p>( async fn scan_on_new_block<CD: DbTrait, TD: DbTrait, P: P2p>(
db: CD, db: CD,
@@ -183,8 +229,14 @@ async fn scan_on_new_block<CD: DbTrait, TD: DbTrait, P: P2p>(
/// Spawn a Tributary. /// Spawn a Tributary.
/// ///
/// This will spawn the Tributary, the Tributary scan task, forward the messages from the scan task /// This will:
/// to the message queue, provide Cosign/Cosigned transactions, and inform the P2P network. /// - Spawn the Tributary
/// - Inform the P2P network of the Tributary
/// - Spawn the ScanTributaryTask
/// - Spawn the ProvideCosignCosignedTransactionsTask
/// - Spawn the TributaryProcessorMessagesTask
/// - Spawn the SignSlashReportTask
/// - Iterate the scan task whenever a new block occurs (not just on the standard interval)
pub(crate) async fn spawn_tributary<P: P2p>( pub(crate) async fn spawn_tributary<P: P2p>(
db: Db, db: Db,
message_queue: Arc<MessageQueue>, message_queue: Arc<MessageQueue>,
@@ -219,10 +271,16 @@ pub(crate) async fn spawn_tributary<P: P2p>(
// Spawn the Tributary // Spawn the Tributary
let tributary_db = crate::db::tributary_db(set.set); let tributary_db = crate::db::tributary_db(set.set);
let tributary = let tributary = Tributary::new(
Tributary::new(tributary_db.clone(), genesis, start_time, serai_key, tributary_validators, p2p) tributary_db.clone(),
.await genesis,
.unwrap(); start_time,
serai_key.clone(),
tributary_validators,
p2p,
)
.await
.unwrap();
let reader = tributary.reader(); let reader = tributary.reader();
// Inform the P2P network // Inform the P2P network
@@ -256,12 +314,25 @@ pub(crate) async fn spawn_tributary<P: P2p>(
// Spawn the scan task // Spawn the scan task
let (scan_tributary_task_def, scan_tributary_task) = Task::new(); let (scan_tributary_task_def, scan_tributary_task) = Task::new();
tokio::spawn( tokio::spawn(
ScanTributaryTask::<_, _, P>::new(db.clone(), tributary_db, &set, reader) ScanTributaryTask::<_, _, P>::new(db.clone(), tributary_db.clone(), &set, reader)
// This is the only handle for this TributaryProcessorMessagesTask, so when this task is // This is the only handle for this TributaryProcessorMessagesTask, so when this task is
// dropped, it will be too // dropped, it will be too
.continually_run(scan_tributary_task_def, vec![scan_tributary_messages_task]), .continually_run(scan_tributary_task_def, vec![scan_tributary_messages_task]),
); );
// Spawn the sign slash report task
let (sign_slash_report_task_def, sign_slash_report_task) = Task::new();
tokio::spawn(
(SignSlashReportTask {
db: db.clone(),
tributary_db,
tributary: tributary.clone(),
set: set.clone(),
key: serai_key,
})
.continually_run(sign_slash_report_task_def, vec![]),
);
// Whenever a new block occurs, immediately run the scan task // Whenever a new block occurs, immediately run the scan task
// This function also preserves the ProvideCosignCosignedTransactionsTask handle until the // This function also preserves the ProvideCosignCosignedTransactionsTask handle until the
// Tributary is retired, ensuring it isn't dropped prematurely and that the task don't run ad // Tributary is retired, ensuring it isn't dropped prematurely and that the task don't run ad
@@ -271,6 +342,6 @@ pub(crate) async fn spawn_tributary<P: P2p>(
set.set, set.set,
tributary, tributary,
scan_tributary_task, scan_tributary_task,
vec![provide_cosign_cosigned_transactions_task], vec![provide_cosign_cosigned_transactions_task, sign_slash_report_task],
)); ));
} }

View File

@@ -234,7 +234,7 @@ impl<D: Db> ContinuallyRan for EphemeralEventStream<D> {
else { else {
panic!("AcceptedHandover event wasn't a AcceptedHandover event: {accepted_handover:?}"); panic!("AcceptedHandover event wasn't a AcceptedHandover event: {accepted_handover:?}");
}; };
crate::SignSlashReport::send(&mut txn, set); crate::SignSlashReport::send(&mut txn, *set);
} }
txn.commit(); txn.commit();

View File

@@ -66,8 +66,8 @@ mod _public_db {
// Relevant new set, from an ephemeral event stream // Relevant new set, from an ephemeral event stream
NewSet: () -> NewSetInformation, NewSet: () -> NewSetInformation,
// Relevant sign slash report, from an ephemeral event stream // Potentially relevant sign slash report, from an ephemeral event stream
SignSlashReport: () -> ValidatorSet, SignSlashReport: (set: ValidatorSet) -> (),
} }
); );
} }
@@ -109,12 +109,12 @@ impl NewSet {
/// notifications for all relevant validator sets will be included. /// notifications for all relevant validator sets will be included.
pub struct SignSlashReport; pub struct SignSlashReport;
impl SignSlashReport { impl SignSlashReport {
pub(crate) fn send(txn: &mut impl DbTxn, set: &ValidatorSet) { pub(crate) fn send(txn: &mut impl DbTxn, set: ValidatorSet) {
_public_db::SignSlashReport::send(txn, set); _public_db::SignSlashReport::send(txn, set, &());
} }
/// Try to receive a notification to sign a slash report, returning `None` if there is none to /// Try to receive a notification to sign a slash report, returning `None` if there is none to
/// receive. /// receive.
pub fn try_recv(txn: &mut impl DbTxn) -> Option<ValidatorSet> { pub fn try_recv(txn: &mut impl DbTxn, set: ValidatorSet) -> Option<()> {
_public_db::SignSlashReport::try_recv(txn) _public_db::SignSlashReport::try_recv(txn, set)
} }
} }

View File

@@ -184,8 +184,8 @@ create_db!(
// The last handled tributary block's (number, hash) // The last handled tributary block's (number, hash)
LastHandledTributaryBlock: (set: ValidatorSet) -> (u64, [u8; 32]), LastHandledTributaryBlock: (set: ValidatorSet) -> (u64, [u8; 32]),
// The slash points a validator has accrued, with u64::MAX representing a fatal slash. // The slash points a validator has accrued, with u32::MAX representing a fatal slash.
SlashPoints: (set: ValidatorSet, validator: SeraiAddress) -> u64, SlashPoints: (set: ValidatorSet, validator: SeraiAddress) -> u32,
// The latest Substrate block to cosign. // The latest Substrate block to cosign.
LatestSubstrateBlockToCosign: (set: ValidatorSet) -> [u8; 32], LatestSubstrateBlockToCosign: (set: ValidatorSet) -> [u8; 32],
@@ -316,7 +316,7 @@ impl TributaryDb {
reason: &str, reason: &str,
) { ) {
log::warn!("{validator} fatally slashed: {reason}"); log::warn!("{validator} fatally slashed: {reason}");
SlashPoints::set(txn, set, validator, &u64::MAX); SlashPoints::set(txn, set, validator, &u32::MAX);
} }
pub(crate) fn is_fatally_slashed( pub(crate) fn is_fatally_slashed(
@@ -324,7 +324,7 @@ impl TributaryDb {
set: ValidatorSet, set: ValidatorSet,
validator: SeraiAddress, validator: SeraiAddress,
) -> bool { ) -> bool {
SlashPoints::get(getter, set, validator).unwrap_or(0) == u64::MAX SlashPoints::get(getter, set, validator).unwrap_or(0) == u32::MAX
} }
#[allow(clippy::too_many_arguments)] #[allow(clippy::too_many_arguments)]

View File

@@ -511,3 +511,13 @@ impl<CD: Db, TD: Db, P: P2p> ContinuallyRan for ScanTributaryTask<CD, TD, P> {
} }
} }
} }
/// Create the Transaction::SlashReport to publish per the local view.
pub fn slash_report_transaction(getter: &impl Get, set: &NewSetInformation) -> Transaction {
let mut slash_points = Vec::with_capacity(set.validators.len());
for (validator, _weight) in set.validators.iter().copied() {
let validator = SeraiAddress::from(validator);
slash_points.push(SlashPoints::get(getter, set.set, validator).unwrap_or(0));
}
Transaction::SlashReport { slash_points, signed: Signed::default() }
}

View File

@@ -6,7 +6,7 @@ use rand_core::{RngCore, CryptoRng};
use blake2::{digest::typenum::U32, Digest, Blake2b}; use blake2::{digest::typenum::U32, Digest, Blake2b};
use ciphersuite::{ use ciphersuite::{
group::{ff::Field, GroupEncoding}, group::{ff::Field, Group, GroupEncoding},
Ciphersuite, Ristretto, Ciphersuite, Ristretto,
}; };
use schnorr::SchnorrSignature; use schnorr::SchnorrSignature;
@@ -80,6 +80,18 @@ impl Signed {
} }
} }
impl Default for Signed {
fn default() -> Self {
Self {
signer: <Ristretto as Ciphersuite>::G::identity(),
signature: SchnorrSignature {
R: <Ristretto as Ciphersuite>::G::identity(),
s: <Ristretto as Ciphersuite>::F::ZERO,
},
}
}
}
/// The Tributary transaction definition used by Serai /// The Tributary transaction definition used by Serai
#[derive(Clone, PartialEq, Eq, Debug, BorshSerialize, BorshDeserialize)] #[derive(Clone, PartialEq, Eq, Debug, BorshSerialize, BorshDeserialize)]
pub enum Transaction { pub enum Transaction {