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_cosign::{SignedCosign, Cosigning};
use serai_coordinator_substrate::{CanonicalEventStream, EphemeralEventStream};
use serai_coordinator_substrate::{CanonicalEventStream, EphemeralEventStream, SignSlashReport};
use serai_coordinator_tributary::Transaction;
mod db;
@@ -148,6 +148,8 @@ async fn main() {
prune_tributary_db(to_cleanup);
// Drain the cosign intents created for this set
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
@@ -198,7 +200,6 @@ async fn main() {
};
// Spawn the Substrate scanners
// TODO: SignSlashReport
let (substrate_task_def, substrate_task) = Task::new();
let (substrate_canonical_task_def, substrate_canonical_task) = Task::new();
tokio::spawn(

View File

@@ -2,6 +2,7 @@ use core::{future::Future, time::Duration};
use std::sync::Arc;
use zeroize::Zeroizing;
use rand_core::OsRng;
use blake2::{digest::typenum::U32, Digest, Blake2s};
use ciphersuite::{Ciphersuite, Ristretto};
@@ -12,14 +13,14 @@ use serai_db::{DbTxn, Db as DbTrait};
use scale::Encode;
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 message_queue::{Service, Metadata, client::MessageQueue};
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_p2p::P2p;
@@ -27,9 +28,9 @@ use crate::Db;
/// Provides Cosign/Cosigned Transactions onto the Tributary.
pub(crate) struct ProvideCosignCosignedTransactionsTask<CD: DbTrait, TD: DbTrait, P: P2p> {
pub(crate) db: CD,
pub(crate) set: NewSetInformation,
pub(crate) tributary: Tributary<TD, Transaction, P>,
db: CD,
set: NewSetInformation,
tributary: Tributary<TD, Transaction, P>,
}
impl<CD: DbTrait, TD: DbTrait, P: P2p> ContinuallyRan
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.
pub(crate) struct TributaryProcessorMessagesTask<TD: DbTrait> {
pub(crate) tributary_db: TD,
pub(crate) set: ValidatorSet,
pub(crate) message_queue: Arc<MessageQueue>,
tributary_db: TD,
set: ValidatorSet,
message_queue: Arc<MessageQueue>,
}
impl<TD: DbTrait> ContinuallyRan for TributaryProcessorMessagesTask<TD> {
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.
async fn scan_on_new_block<CD: DbTrait, TD: DbTrait, P: P2p>(
db: CD,
@@ -183,8 +229,14 @@ async fn scan_on_new_block<CD: DbTrait, TD: DbTrait, P: P2p>(
/// Spawn a Tributary.
///
/// This will spawn the Tributary, the Tributary scan task, forward the messages from the scan task
/// to the message queue, provide Cosign/Cosigned transactions, and inform the P2P network.
/// This will:
/// - 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>(
db: Db,
message_queue: Arc<MessageQueue>,
@@ -219,10 +271,16 @@ pub(crate) async fn spawn_tributary<P: P2p>(
// Spawn the Tributary
let tributary_db = crate::db::tributary_db(set.set);
let tributary =
Tributary::new(tributary_db.clone(), genesis, start_time, serai_key, tributary_validators, p2p)
.await
.unwrap();
let tributary = Tributary::new(
tributary_db.clone(),
genesis,
start_time,
serai_key.clone(),
tributary_validators,
p2p,
)
.await
.unwrap();
let reader = tributary.reader();
// Inform the P2P network
@@ -256,12 +314,25 @@ pub(crate) async fn spawn_tributary<P: P2p>(
// Spawn the scan task
let (scan_tributary_task_def, scan_tributary_task) = Task::new();
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
// dropped, it will be too
.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
// 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
@@ -271,6 +342,6 @@ pub(crate) async fn spawn_tributary<P: P2p>(
set.set,
tributary,
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 {
panic!("AcceptedHandover event wasn't a AcceptedHandover event: {accepted_handover:?}");
};
crate::SignSlashReport::send(&mut txn, set);
crate::SignSlashReport::send(&mut txn, *set);
}
txn.commit();

View File

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

View File

@@ -184,8 +184,8 @@ create_db!(
// The last handled tributary block's (number, hash)
LastHandledTributaryBlock: (set: ValidatorSet) -> (u64, [u8; 32]),
// The slash points a validator has accrued, with u64::MAX representing a fatal slash.
SlashPoints: (set: ValidatorSet, validator: SeraiAddress) -> u64,
// The slash points a validator has accrued, with u32::MAX representing a fatal slash.
SlashPoints: (set: ValidatorSet, validator: SeraiAddress) -> u32,
// The latest Substrate block to cosign.
LatestSubstrateBlockToCosign: (set: ValidatorSet) -> [u8; 32],
@@ -316,7 +316,7 @@ impl TributaryDb {
reason: &str,
) {
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(
@@ -324,7 +324,7 @@ impl TributaryDb {
set: ValidatorSet,
validator: SeraiAddress,
) -> 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)]

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 ciphersuite::{
group::{ff::Field, GroupEncoding},
group::{ff::Field, Group, GroupEncoding},
Ciphersuite, Ristretto,
};
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
#[derive(Clone, PartialEq, Eq, Debug, BorshSerialize, BorshDeserialize)]
pub enum Transaction {