Coordinator Cleanup (#481)

* Move logic for evaluating if a cosign should occur to its own file

Cleans it up and makes it more robust.

* Have expected_next_batch return an error instead of retrying

While convenient to offer an error-free implementation, it potentially caused
very long lived lock acquisitions in handle_processor_message.

* Unify and clean DkgConfirmer and DkgRemoval

Does so via adding a new file for the common code, SigningProtocol.

Modifies from_cache to return the preprocess with the machine, as there's no
reason not to. Also removes an unused Result around the type.

Clarifies the security around deterministic nonces, removing them for
saved-to-disk cached preprocesses. The cached preprocesses are encrypted as the
DB is not a proper secret store.

Moves arguments always present in the protocol from function arguments into the
struct itself.

Removes the horribly ugly code in DkgRemoval, fixing multiple issues present
with it which would cause it to fail on use.

* Set SeraiBlockNumber in cosign.rs as it's used by the cosigning protocol

* Remove unnecessary Clone from lambdas in coordinator

* Remove the EventDb from Tributary scanner

We used per-Transaction DB TXNs so on error, we don't have to rescan the entire
block yet only the rest of it. We prevented scanning multiple transactions by
tracking which we already had.

This is over-engineered and not worth it.

* Implement borsh for HasEvents, removing the manual encoding

* Merge DkgConfirmer and DkgRemoval into signing_protocol.rs

Fixes a bug in DkgConfirmer which would cause it to improperly handle indexes
if any validator had multiple key shares.

* Strictly type DataSpecification's Label

* Correct threshold_i_map_to_keys_and_musig_i_map

It didn't include the participant's own index and accordingly was offset.

* Create TributaryBlockHandler

This struct contains all variables prior passed to handle_block and stops them
from being passed around again and again.

This also ensures fatal_slash is only called while handling a block, as needed
as it expects to operate under perfect consensus.

* Inline accumulate, store confirmation nonces with shares

Inlining accumulate makes sense due to the amount of data accumulate needed to
be passed.

Storing confirmation nonces with shares ensures that both are available or
neither. Prior, one could be yet the other may not have been (requiring an
assert in runtime to ensure we didn't bungle it somehow).

* Create helper functions for handling DkgRemoval/SubstrateSign/Sign Tributary TXs

* Move Label into SignData

All of our transactions which use SignData end up with the same common usage
pattern for Label, justifying this.

Removes 3 transactions, explicitly de-duplicating their handlers.

* Remove CurrentlyCompletingKeyPair for the non-contextual DkgKeyPair

* Remove the manual read/write for TributarySpec for borsh

This struct doesn't have any optimizations booned by the manual impl. Using
borsh reduces our scope.

* Use temporary variables to further minimize LoC in tributary handler

* Remove usage of tuples for non-trivial Tributary transactions

* Remove serde from dkg

serde could be used to deserialize intenrally inconsistent objects which could
lead to panics or faults.

The BorshDeserialize derives have been replaced with a manual implementation
which won't produce inconsistent objects.

* Abstract Future generics using new trait definitions in coordinator

* Move published_signed_transaction to tributary/mod.rs to reduce the size of main.rs

* Split coordinator/src/tributary/mod.rs into spec.rs and transaction.rs
This commit is contained in:
Luke Parker 2023-12-10 20:21:44 -05:00 committed by GitHub
parent 6caf45ea1d
commit 11fdb6da1d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
34 changed files with 2531 additions and 2992 deletions

2
Cargo.lock generated
View file

@ -1571,7 +1571,6 @@ dependencies = [
"multiexp",
"rand_core",
"schnorr-signatures",
"serde",
"std-shims",
"thiserror",
"zeroize",
@ -7343,7 +7342,6 @@ dependencies = [
"log",
"modular-frost",
"parity-scale-codec",
"rand_chacha",
"rand_core",
"schnorr-signatures",
"serai-client",

View file

@ -358,7 +358,7 @@ impl SignMachine<Transaction> for TransactionSignMachine {
_: (),
_: ThresholdKeys<Secp256k1>,
_: CachedPreprocess,
) -> Result<Self, FrostError> {
) -> (Self, Self::Preprocess) {
unimplemented!(
"Bitcoin transactions don't support caching their preprocesses due to {}",
"being already bound to a specific transaction"

View file

@ -226,7 +226,7 @@ impl SignMachine<Transaction> for TransactionSignMachine {
);
}
fn from_cache(_: (), _: ThresholdKeys<Ed25519>, _: CachedPreprocess) -> Result<Self, FrostError> {
fn from_cache(_: (), _: ThresholdKeys<Ed25519>, _: CachedPreprocess) -> (Self, Self::Preprocess) {
unimplemented!(
"Monero transactions don't support caching their preprocesses due to {}",
"being already bound to a specific transaction"

View file

@ -45,6 +45,7 @@ macro_rules! create_db {
pub struct $field_name;
impl $field_name {
pub fn key($($arg: $arg_type),*) -> Vec<u8> {
use scale::Encode;
$crate::serai_db_key(
stringify!($db_name).as_bytes(),
stringify!($field_name).as_bytes(),

View file

@ -18,7 +18,6 @@ async-trait = { version = "0.1", default-features = false }
zeroize = { version = "^1.5", default-features = false, features = ["std"] }
rand_core = { version = "0.6", default-features = false, features = ["std"] }
rand_chacha = { version = "0.3", default-features = false, features = ["std"] }
blake2 = { version = "0.10", default-features = false, features = ["std"] }
@ -38,7 +37,7 @@ message-queue = { package = "serai-message-queue", path = "../message-queue" }
tributary = { package = "tributary-chain", path = "./tributary" }
sp-application-crypto = { git = "https://github.com/serai-dex/substrate", default-features = false, features = ["std"] }
serai-client = { path = "../substrate/client", default-features = false, features = ["serai"] }
serai-client = { path = "../substrate/client", default-features = false, features = ["serai", "borsh"] }
hex = { version = "0.4", default-features = false, features = ["std"] }
borsh = { version = "1", default-features = false, features = ["std", "derive", "de_strict_order"] }

View file

@ -4,6 +4,7 @@ use blake2::{
};
use scale::Encode;
use borsh::{BorshSerialize, BorshDeserialize};
use serai_client::{
primitives::NetworkId,
validator_sets::primitives::{Session, ValidatorSet},
@ -20,7 +21,6 @@ create_db!(
HandledMessageDb: (network: NetworkId) -> u64,
ActiveTributaryDb: () -> Vec<u8>,
RetiredTributaryDb: (set: ValidatorSet) -> (),
SignedTransactionDb: (order: &[u8], nonce: u32) -> Vec<u8>,
FirstPreprocessDb: (
network: NetworkId,
id_type: RecognizedIdType,
@ -43,7 +43,7 @@ impl ActiveTributaryDb {
let mut tributaries = vec![];
while !bytes_ref.is_empty() {
tributaries.push(TributarySpec::read(&mut bytes_ref).unwrap());
tributaries.push(TributarySpec::deserialize_reader(&mut bytes_ref).unwrap());
}
(bytes, tributaries)
@ -57,7 +57,7 @@ impl ActiveTributaryDb {
}
}
spec.write(&mut existing_bytes).unwrap();
spec.serialize(&mut existing_bytes).unwrap();
ActiveTributaryDb::set(txn, &existing_bytes);
}
@ -72,28 +72,13 @@ impl ActiveTributaryDb {
let mut bytes = vec![];
for active in active {
active.write(&mut bytes).unwrap();
active.serialize(&mut bytes).unwrap();
}
Self::set(txn, &bytes);
RetiredTributaryDb::set(txn, set, &());
}
}
impl SignedTransactionDb {
pub fn take_signed_transaction(
txn: &mut impl DbTxn,
order: &[u8],
nonce: u32,
) -> Option<Transaction> {
let res = SignedTransactionDb::get(txn, order, nonce)
.map(|bytes| Transaction::read(&mut bytes.as_slice()).unwrap());
if res.is_some() {
Self::del(txn, order, nonce);
}
res
}
}
impl FirstPreprocessDb {
pub fn save_first_preprocess(
txn: &mut impl DbTxn,

View file

@ -31,12 +31,12 @@ use tokio::{
time::sleep,
};
use ::tributary::{
ProvidedError, TransactionKind, TransactionError, TransactionTrait, Block, Tributary,
};
use ::tributary::{ProvidedError, TransactionKind, TransactionTrait, Block, Tributary};
mod tributary;
use crate::tributary::{TributarySpec, SignData, Transaction, scanner::RecognizedIdType, PlanIds};
use crate::tributary::{
TributarySpec, Label, SignData, Transaction, scanner::RecognizedIdType, PlanIds,
};
mod db;
use db::*;
@ -126,48 +126,6 @@ async fn add_tributary<D: Db, Pro: Processors, P: P2p>(
.unwrap();
}
async fn publish_signed_transaction<D: Db, P: P2p>(
txn: &mut D::Transaction<'_>,
tributary: &Tributary<D, Transaction, P>,
tx: Transaction,
) {
log::debug!("publishing transaction {}", hex::encode(tx.hash()));
let (order, signer) = if let TransactionKind::Signed(order, signed) = tx.kind() {
let signer = signed.signer;
// Safe as we should deterministically create transactions, meaning if this is already on-disk,
// it's what we're saving now
SignedTransactionDb::set(txn, &order, signed.nonce, &tx.serialize());
(order, signer)
} else {
panic!("non-signed transaction passed to publish_signed_transaction");
};
// If we're trying to publish 5, when the last transaction published was 3, this will delay
// publication until the point in time we publish 4
while let Some(tx) = SignedTransactionDb::take_signed_transaction(
txn,
&order,
tributary
.next_nonce(&signer, &order)
.await
.expect("we don't have a nonce, meaning we aren't a participant on this tributary"),
) {
// We need to return a proper error here to enable that, due to a race condition around
// multiple publications
match tributary.add_transaction(tx.clone()).await {
Ok(_) => {}
// Some asynchonicity if InvalidNonce, assumed safe to deterministic nonces
Err(TransactionError::InvalidNonce) => {
log::warn!("publishing TX {tx:?} returned InvalidNonce. was it already added?")
}
Err(e) => panic!("created an invalid transaction: {e:?}"),
}
}
}
// TODO: Find a better pattern for this
static HANDOVER_VERIFY_QUEUE_LOCK: OnceLock<Mutex<()>> = OnceLock::new();
@ -317,7 +275,9 @@ async fn handle_processor_message<D: Db, P: P2p>(
BatchDb::set(&mut txn, batch.batch.network, batch.batch.id, &batch.clone());
// Get the next-to-execute batch ID
let mut next = substrate::get_expected_next_batch(serai, network).await;
let Ok(mut next) = substrate::expected_next_batch(serai, network).await else {
return false;
};
// Since we have a new batch, publish all batches yet to be published to Serai
// This handles the edge-case where batch n+1 is signed before batch n is
@ -329,7 +289,10 @@ async fn handle_processor_message<D: Db, P: P2p>(
while let Some(batch) = batches.pop_front() {
// If this Batch should no longer be published, continue
if substrate::get_expected_next_batch(serai, network).await > batch.batch.id {
let Ok(expected_next_batch) = substrate::expected_next_batch(serai, network).await else {
return false;
};
if expected_next_batch > batch.batch.id {
continue;
}
@ -398,7 +361,11 @@ async fn handle_processor_message<D: Db, P: P2p>(
let txs = match msg.msg.clone() {
ProcessorMessage::KeyGen(inner_msg) => match inner_msg {
key_gen::ProcessorMessage::Commitments { id, commitments } => {
vec![Transaction::DkgCommitments(id.attempt, commitments, Transaction::empty_signed())]
vec![Transaction::DkgCommitments {
attempt: id.attempt,
commitments,
signed: Transaction::empty_signed(),
}]
}
key_gen::ProcessorMessage::InvalidCommitments { id: _, faulty } => {
// This doesn't need the ID since it's a Provided transaction which everyone will provide
@ -411,7 +378,7 @@ async fn handle_processor_message<D: Db, P: P2p>(
}
key_gen::ProcessorMessage::Shares { id, mut shares } => {
// Create a MuSig-based machine to inform Substrate of this key generation
let nonces = crate::tributary::dkg_confirmation_nonces(key, spec, id.attempt);
let nonces = crate::tributary::dkg_confirmation_nonces(key, spec, &mut txn, id.attempt);
let our_i = spec
.i(pub_key)
@ -449,7 +416,7 @@ async fn handle_processor_message<D: Db, P: P2p>(
// As for the safety of calling error_generating_key_pair, the processor is presumed
// to only send InvalidShare or GeneratedKeyPair for a given attempt
let mut txs = if let Some(faulty) =
crate::tributary::error_generating_key_pair::<_>(&txn, key, spec, id.attempt)
crate::tributary::error_generating_key_pair(&mut txn, key, spec, id.attempt)
{
vec![Transaction::RemoveParticipant(faulty)]
} else {
@ -480,7 +447,11 @@ async fn handle_processor_message<D: Db, P: P2p>(
match share {
Ok(share) => {
vec![Transaction::DkgConfirmed(id.attempt, share, Transaction::empty_signed())]
vec![Transaction::DkgConfirmed {
attempt: id.attempt,
confirmation_share: share,
signed: Transaction::empty_signed(),
}]
}
Err(p) => {
vec![Transaction::RemoveParticipant(p)]
@ -511,18 +482,20 @@ async fn handle_processor_message<D: Db, P: P2p>(
vec![]
} else {
vec![Transaction::SignPreprocess(SignData {
vec![Transaction::Sign(SignData {
plan: id.id,
attempt: id.attempt,
label: Label::Preprocess,
data: preprocesses,
signed: Transaction::empty_signed(),
})]
}
}
sign::ProcessorMessage::Share { id, shares } => {
vec![Transaction::SignShare(SignData {
vec![Transaction::Sign(SignData {
plan: id.id,
attempt: id.attempt,
label: Label::Share,
data: shares,
signed: Transaction::empty_signed(),
})]
@ -555,9 +528,10 @@ async fn handle_processor_message<D: Db, P: P2p>(
vec![]
}
coordinator::ProcessorMessage::CosignPreprocess { id, preprocesses } => {
vec![Transaction::SubstratePreprocess(SignData {
vec![Transaction::SubstrateSign(SignData {
plan: id.id,
attempt: id.attempt,
label: Label::Preprocess,
data: preprocesses.into_iter().map(Into::into).collect(),
signed: Transaction::empty_signed(),
})]
@ -586,13 +560,13 @@ async fn handle_processor_message<D: Db, P: P2p>(
preprocesses.into_iter().map(Into::into).collect(),
);
let intended = Transaction::Batch(
block.0,
match id.id {
let intended = Transaction::Batch {
block: block.0,
batch: match id.id {
SubstrateSignableId::Batch(id) => id,
_ => panic!("BatchPreprocess did not contain Batch ID"),
},
);
};
// If this is the new key's first Batch, only create this TX once we verify all
// all prior published `Batch`s
@ -649,18 +623,20 @@ async fn handle_processor_message<D: Db, P: P2p>(
res
}
} else {
vec![Transaction::SubstratePreprocess(SignData {
vec![Transaction::SubstrateSign(SignData {
plan: id.id,
attempt: id.attempt,
label: Label::Preprocess,
data: preprocesses.into_iter().map(Into::into).collect(),
signed: Transaction::empty_signed(),
})]
}
}
coordinator::ProcessorMessage::SubstrateShare { id, shares } => {
vec![Transaction::SubstrateShare(SignData {
vec![Transaction::SubstrateSign(SignData {
plan: id.id,
attempt: id.attempt,
label: Label::Share,
data: shares.into_iter().map(|share| share.to_vec()).collect(),
signed: Transaction::empty_signed(),
})]
@ -706,7 +682,7 @@ async fn handle_processor_message<D: Db, P: P2p>(
}
TransactionKind::Signed(_, _) => {
tx.sign(&mut OsRng, genesis, key);
publish_signed_transaction(&mut txn, tributary, tx).await;
tributary::publish_signed_transaction(&mut txn, tributary, tx).await;
}
}
}
@ -1079,16 +1055,18 @@ pub async fn run<D: Db, Pro: Processors, P: P2p>(
};
let mut tx = match id_type {
RecognizedIdType::Batch => Transaction::SubstratePreprocess(SignData {
RecognizedIdType::Batch => Transaction::SubstrateSign(SignData {
data: get_preprocess(&raw_db, id_type, &id).await,
plan: SubstrateSignableId::Batch(id.as_slice().try_into().unwrap()),
label: Label::Preprocess,
attempt: 0,
signed: Transaction::empty_signed(),
}),
RecognizedIdType::Plan => Transaction::SignPreprocess(SignData {
RecognizedIdType::Plan => Transaction::Sign(SignData {
data: get_preprocess(&raw_db, id_type, &id).await,
plan: id.try_into().unwrap(),
label: Label::Preprocess,
attempt: 0,
signed: Transaction::empty_signed(),
}),
@ -1119,7 +1097,7 @@ pub async fn run<D: Db, Pro: Processors, P: P2p>(
// TODO: Should this not take a txn accordingly? It's best practice to take a txn, yet
// taking a txn fails to declare its achieved independence
let mut txn = raw_db.txn();
publish_signed_transaction(&mut txn, tributary, tx).await;
tributary::publish_signed_transaction(&mut txn, tributary, tx).await;
txn.commit();
break;
}

View file

@ -12,57 +12,48 @@
ensure any block needing cosigned is consigned within a reasonable amount of time.
*/
use core::{ops::Deref, time::Duration};
use std::{
sync::Arc,
collections::{HashSet, HashMap},
};
use zeroize::Zeroizing;
use ciphersuite::{group::GroupEncoding, Ciphersuite, Ristretto};
use ciphersuite::{Ciphersuite, Ristretto};
use borsh::{BorshSerialize, BorshDeserialize};
use scale::{Encode, Decode};
use serai_client::{
SeraiError, Block, Serai, TemporalSerai,
primitives::{BlockHash, NetworkId},
validator_sets::{
primitives::{Session, ValidatorSet, KeyPair, amortize_excess_key_shares},
ValidatorSetsEvent,
},
in_instructions::InInstructionsEvent,
coins::CoinsEvent,
SeraiError, Serai,
primitives::NetworkId,
validator_sets::primitives::{Session, ValidatorSet},
};
use serai_db::*;
use processor_messages::SubstrateContext;
use tokio::{sync::mpsc, time::sleep};
use crate::{
Db,
processors::Processors,
tributary::{TributarySpec, SeraiBlockNumber},
};
use crate::{Db, substrate::in_set, tributary::SeraiBlockNumber};
// 5 minutes, expressed in blocks
// TODO: Pull a constant for block time
const COSIGN_DISTANCE: u64 = 5 * 60 / 6;
#[derive(Clone, Copy, PartialEq, Eq, Debug, BorshSerialize, BorshDeserialize)]
enum HasEvents {
KeyGen,
Yes,
No,
}
create_db!(
SubstrateCosignDb {
CosignTriggered: () -> (),
IntendedCosign: () -> (u64, Option<u64>),
BlockHasEvents: (block: u64) -> u8,
BlockHasEvents: (block: u64) -> HasEvents,
LatestCosignedBlock: () -> u64,
}
);
impl IntendedCosign {
// Sets the intended to cosign block, clearing the prior value entirely.
pub fn set_intended_cosign(txn: &mut impl DbTxn, intended: u64) {
Self::set(txn, &(intended, None::<u64>));
}
// Sets the cosign skipped since the last intended to cosign block.
pub fn set_skipped_cosign(txn: &mut impl DbTxn, skipped: u64) {
let (intended, prior_skipped) = Self::get(txn).unwrap();
assert!(prior_skipped.is_none());
@ -89,12 +80,6 @@ impl CosignTransactions {
}
}
#[derive(Clone, Copy, PartialEq, Eq, Debug, Encode, Decode)]
enum HasEvents {
KeyGen,
Yes,
No,
}
async fn block_has_events(
txn: &mut impl DbTxn,
serai: &Serai,
@ -122,45 +107,112 @@ async fn block_has_events(
let has_events = if has_no_events { HasEvents::No } else { HasEvents::Yes };
let has_events = has_events.encode();
assert_eq!(has_events.len(), 1);
BlockHasEvents::set(txn, block, &has_events[0]);
BlockHasEvents::set(txn, block, &has_events);
Ok(HasEvents::Yes)
}
Some(code) => Ok(HasEvents::decode(&mut [code].as_slice()).unwrap()),
Some(code) => Ok(code),
}
}
async fn potentially_cosign_block(
txn: &mut impl DbTxn,
serai: &Serai,
block: u64,
skipped_block: Option<u64>,
window_end_exclusive: u64,
) -> Result<bool, SeraiError> {
// The following code regarding marking cosigned if prior block is cosigned expects this block to
// not be zero
// While we could perform this check there, there's no reason not to optimize the entire function
// as such
if block == 0 {
return Ok(false);
}
let block_has_events = block_has_events(txn, serai, block).await?;
// If this block had no events and immediately follows a cosigned block, mark it as cosigned
if (block_has_events == HasEvents::No) &&
(LatestCosignedBlock::latest_cosigned_block(txn) == (block - 1))
{
LatestCosignedBlock::set(txn, &block);
}
// If we skipped a block, we're supposed to sign it plus the COSIGN_DISTANCE if no other blocks
// trigger a cosigning protocol covering it
// This means there will be the maximum delay allowed from a block needing cosigning occuring
// and a cosign for it triggering
let maximally_latent_cosign_block =
skipped_block.map(|skipped_block| skipped_block + COSIGN_DISTANCE);
// If this block is within the window,
if block < window_end_exclusive {
// and set a key, cosign it
if block_has_events == HasEvents::KeyGen {
IntendedCosign::set_intended_cosign(txn, block);
// Carry skipped if it isn't included by cosigning this block
if let Some(skipped) = skipped_block {
if skipped > block {
IntendedCosign::set_skipped_cosign(txn, block);
}
}
return Ok(true);
}
} else if (Some(block) == maximally_latent_cosign_block) || (block_has_events != HasEvents::No) {
// Since this block was outside the window and had events/was maximally latent, cosign it
IntendedCosign::set_intended_cosign(txn, block);
return Ok(true);
}
Ok(false)
}
/*
Advances the cosign protocol as should be done per the latest block.
A block is considered cosigned if:
A) It was cosigned
B) It's the parent of a cosigned block
C) It immediately follows a cosigned block and has no events requiring cosigning (TODO)
C) It immediately follows a cosigned block and has no events requiring cosigning
This only actually performs advancement within a limited bound (generally until it finds a block
which should be cosigned). Accordingly, it is necessary to call multiple times even if
`latest_number` doesn't change.
*/
async fn advance_cosign_protocol(db: &mut impl Db, serai: &Serai, latest_number: u64) -> Result<(), ()> {
let Some((last_intended_to_cosign_block, mut skipped_block)) = IntendedCosign::get(&txn) else {
pub async fn advance_cosign_protocol(
db: &mut impl Db,
key: &Zeroizing<<Ristretto as Ciphersuite>::F>,
serai: &Serai,
latest_number: u64,
) -> Result<(), SeraiError> {
let mut txn = db.txn();
IntendedCosign::set_intended_cosign(&mut txn, 1);
txn.commit();
return Ok(());
const INITIAL_INTENDED_COSIGN: u64 = 1;
let (last_intended_to_cosign_block, mut skipped_block) = {
let intended_cosign = IntendedCosign::get(&txn);
// If we haven't prior intended to cosign a block, set the intended cosign to 1
if let Some(intended_cosign) = intended_cosign {
intended_cosign
} else {
IntendedCosign::set_intended_cosign(&mut txn, INITIAL_INTENDED_COSIGN);
IntendedCosign::get(&txn).unwrap()
}
};
// "windows" refers to the window of blocks where even if there's a block which should be
// cosigned, it won't be due to proximity due to the prior cosign
let mut window_end_exclusive = last_intended_to_cosign_block + COSIGN_DISTANCE;
// If we've never triggered a cosign, don't skip any cosigns based on proximity
if last_intended_to_cosign_block == INITIAL_INTENDED_COSIGN {
window_end_exclusive = 0;
}
// If we haven't flagged skipped, and a block within the distance had events, flag the first
// such block as skipped
let mut distance_end_exclusive = last_intended_to_cosign_block + COSIGN_DISTANCE;
// If we've never triggered a cosign, don't skip any cosigns
if CosignTriggered::get(&txn).is_none() {
distance_end_exclusive = 0;
}
// Check all blocks within the window to see if they should be cosigned
// If so, we're skipping them and need to flag them as skipped so that once the window closes, we
// do cosign them
// We only perform this check if we haven't already marked a block as skipped since the cosign
// the skipped block will cause will cosign all other blocks within this window
if skipped_block.is_none() {
for b in (last_intended_to_cosign_block + 1) .. distance_end_exclusive {
if b > latest_number {
break;
}
for b in (last_intended_to_cosign_block + 1) .. window_end_exclusive.min(latest_number) {
if block_has_events(&mut txn, serai, b).await? == HasEvents::Yes {
skipped_block = Some(b);
log::debug!("skipping cosigning {b} due to proximity to prior cosign");
@ -170,58 +222,39 @@ if skipped_block.is_none() {
}
}
let mut has_no_cosigners = None;
let mut cosign = vec![];
// A block which should be cosigned
let mut to_cosign = None;
// A list of sets which are cosigning, along with a boolean of if we're in the set
let mut cosigning = vec![];
// Block we should cosign no matter what if no prior blocks qualified for cosigning
let maximally_latent_cosign_block =
skipped_block.map(|skipped_block| skipped_block + COSIGN_DISTANCE);
for block in (last_intended_to_cosign_block + 1) ..= latest_number {
let actual_block = serai
.finalized_block_by_number(block)
.await?
.expect("couldn't get block which should've been finalized");
// Save the block number for this block, as needed by the cosigner to perform cosigning
SeraiBlockNumber::set(&mut txn, actual_block.hash(), &block);
let mut set = false;
let block_has_events = block_has_events(&mut txn, serai, block).await?;
// If this block is within the distance,
if block < distance_end_exclusive {
// and set a key, cosign it
if block_has_events == HasEvents::KeyGen {
IntendedCosign::set_intended_cosign(&mut txn, block);
set = true;
// Carry skipped if it isn't included by cosigning this block
if let Some(skipped) = skipped_block {
if skipped > block {
IntendedCosign::set_skipped_cosign(&mut txn, block);
}
}
}
} else if (Some(block) == maximally_latent_cosign_block) ||
(block_has_events != HasEvents::No)
if potentially_cosign_block(&mut txn, serai, block, skipped_block, window_end_exclusive).await?
{
// Since this block was outside the distance and had events/was maximally latent, cosign it
IntendedCosign::set_intended_cosign(&mut txn, block);
set = true;
}
to_cosign = Some((block, actual_block.hash()));
if set {
// Get the keys as of the prior block
// That means if this block is setting new keys (which won't lock in until we process this
// block), we won't freeze up waiting for the yet-to-be-processed keys to sign this block
// If this key sets new keys, the coordinator won't acknowledge so until we process this
// block
// We won't process this block until its co-signed
// Using the keys of the prior block ensures this deadlock isn't reached
let serai = serai.as_of(actual_block.header.parent_hash.into());
has_no_cosigners = Some(actual_block.clone());
for network in serai_client::primitives::NETWORKS {
// Get the latest session to have set keys
let set_with_keys = {
let Some(latest_session) = serai.validator_sets().session(network).await? else {
continue;
};
let prior_session = Session(latest_session.0.saturating_sub(1));
let set_with_keys = if serai
if serai
.validator_sets()
.keys(ValidatorSet { network, session: prior_session })
.await?
@ -234,31 +267,33 @@ for block in (last_intended_to_cosign_block + 1) ..= latest_number {
continue;
}
set
}
};
// Since this is a valid cosigner, don't flag this block as having no cosigners
has_no_cosigners = None;
log::debug!("{:?} will be cosigning {block}", set_with_keys.network);
if in_set(key, &serai, set_with_keys).await?.unwrap() {
cosign.push((set_with_keys, block, actual_block.hash()));
}
cosigning.push((set_with_keys, in_set(key, &serai, set_with_keys).await?.unwrap()));
}
break;
}
}
if let Some((number, hash)) = to_cosign {
// If this block doesn't have cosigners, yet does have events, automatically mark it as
// cosigned
if let Some(has_no_cosigners) = has_no_cosigners {
log::debug!("{} had no cosigners available, marking as cosigned", has_no_cosigners.number());
LatestCosignedBlock::set(&mut txn, &has_no_cosigners.number());
if cosigning.is_empty() {
log::debug!("{} had no cosigners available, marking as cosigned", number);
LatestCosignedBlock::set(&mut txn, &number);
} else {
CosignTriggered::set(&mut txn, &());
for (set, block, hash) in cosign {
log::debug!("cosigning {block} with {:?} {:?}", set.network, set.session);
CosignTransactions::append_cosign(&mut txn, set, block, hash);
for (set, in_set) in cosigning {
if in_set {
log::debug!("cosigning {number} with {:?} {:?}", set.network, set.session);
CosignTransactions::append_cosign(&mut txn, set, number, hash);
}
}
}
}
txn.commit();
Ok(())
}

View file

@ -1,61 +1,32 @@
use scale::Encode;
use serai_client::{
primitives::NetworkId,
validator_sets::primitives::{Session, ValidatorSet},
};
use serai_client::primitives::NetworkId;
pub use serai_db::*;
mod inner_db {
use super::*;
create_db!(
SubstrateDb {
CosignTriggered: () -> (),
IntendedCosign: () -> (u64, Option<u64>),
BlockHasEvents: (block: u64) -> u8,
LatestCosignedBlock: () -> u64,
NextBlock: () -> u64,
EventDb: (id: &[u8], index: u32) -> (),
HandledEvent: (block: [u8; 32]) -> u32,
BatchInstructionsHashDb: (network: NetworkId, id: u32) -> [u8; 32]
}
);
}
pub use inner_db::{NextBlock, BatchInstructionsHashDb};
impl IntendedCosign {
pub fn set_intended_cosign(txn: &mut impl DbTxn, intended: u64) {
Self::set(txn, &(intended, None::<u64>));
pub struct HandledEvent;
impl HandledEvent {
fn next_to_handle_event(getter: &impl Get, block: [u8; 32]) -> u32 {
inner_db::HandledEvent::get(getter, block).map(|last| last + 1).unwrap_or(0)
}
pub fn set_skipped_cosign(txn: &mut impl DbTxn, skipped: u64) {
let (intended, prior_skipped) = Self::get(txn).unwrap();
assert!(prior_skipped.is_none());
Self::set(txn, &(intended, Some(skipped)));
}
}
impl LatestCosignedBlock {
pub fn latest_cosigned_block(getter: &impl Get) -> u64 {
Self::get(getter).unwrap_or_default().max(1)
}
}
impl EventDb {
pub fn is_unhandled(getter: &impl Get, id: &[u8], index: u32) -> bool {
Self::get(getter, id, index).is_none()
}
pub fn handle_event(txn: &mut impl DbTxn, id: &[u8], index: u32) {
assert!(Self::is_unhandled(txn, id, index));
Self::set(txn, id, index, &());
}
}
db_channel! {
SubstrateDbChannels {
CosignTransactions: (network: NetworkId) -> (Session, u64, [u8; 32]),
}
}
impl CosignTransactions {
// Append a cosign transaction.
pub fn append_cosign(txn: &mut impl DbTxn, set: ValidatorSet, number: u64, hash: [u8; 32]) {
CosignTransactions::send(txn, set.network, &(set.session, number, hash))
pub fn is_unhandled(getter: &impl Get, block: [u8; 32], event_id: u32) -> bool {
let next = Self::next_to_handle_event(getter, block);
assert!(next >= event_id);
next == event_id
}
pub fn handle_event(txn: &mut impl DbTxn, block: [u8; 32], index: u32) {
assert!(Self::next_to_handle_event(txn, block) == index);
inner_db::HandledEvent::set(txn, block, &index);
}
}

View file

@ -8,12 +8,11 @@ use zeroize::Zeroizing;
use ciphersuite::{group::GroupEncoding, Ciphersuite, Ristretto};
use scale::{Encode, Decode};
use serai_client::{
SeraiError, Block, Serai, TemporalSerai,
primitives::{BlockHash, NetworkId},
validator_sets::{
primitives::{Session, ValidatorSet, KeyPair, amortize_excess_key_shares},
primitives::{ValidatorSet, KeyPair, amortize_excess_key_shares},
ValidatorSetsEvent,
},
in_instructions::InInstructionsEvent,
@ -26,15 +25,14 @@ use processor_messages::SubstrateContext;
use tokio::{sync::mpsc, time::sleep};
use crate::{
Db,
processors::Processors,
tributary::{TributarySpec, SeraiBlockNumber},
};
use crate::{Db, processors::Processors, tributary::TributarySpec};
mod db;
pub use db::*;
mod cosign;
pub use cosign::*;
async fn in_set(
key: &Zeroizing<<Ristretto as Ciphersuite>::F>,
serai: &TemporalSerai<'_>,
@ -110,7 +108,7 @@ async fn handle_new_set<D: Db>(
new_tributary_spec.send(spec).unwrap();
} else {
log::info!("not present in set {:?}", set);
log::info!("not present in new set {:?}", set);
}
Ok(())
@ -147,8 +145,8 @@ async fn handle_key_gen<Pro: Processors>(
Ok(())
}
async fn handle_batch_and_burns<D: Db, Pro: Processors>(
db: &mut D,
async fn handle_batch_and_burns<Pro: Processors>(
txn: &mut impl DbTxn,
processors: &Pro,
serai: &Serai,
block: &Block,
@ -178,9 +176,7 @@ async fn handle_batch_and_burns<D: Db, Pro: Processors>(
{
network_had_event(&mut burns, &mut batches, network);
let mut txn = db.txn();
BatchInstructionsHashDb::set(&mut txn, network, id, &instructions_hash);
txn.commit();
BatchInstructionsHashDb::set(txn, network, id, &instructions_hash);
// Make sure this is the only Batch event for this network in this Block
assert!(batch_block.insert(network, network_block).is_none());
@ -257,8 +253,8 @@ async fn handle_block<D: Db, Pro: Processors>(
for new_set in serai.as_of(hash).validator_sets().new_set_events().await? {
// Individually mark each event as handled so on reboot, we minimize duplicates
// Additionally, if the Serai connection also fails 1/100 times, this means a block with 1000
// events will successfully be incrementally handled (though the Serai connection should be
// stable)
// events will successfully be incrementally handled
// (though the Serai connection should be stable, making this unnecessary)
let ValidatorSetsEvent::NewSet { set } = new_set else {
panic!("NewSet event wasn't NewSet: {new_set:?}");
};
@ -269,11 +265,11 @@ async fn handle_block<D: Db, Pro: Processors>(
continue;
}
if EventDb::is_unhandled(db, &hash, event_id) {
if HandledEvent::is_unhandled(db, hash, event_id) {
log::info!("found fresh new set event {:?}", new_set);
let mut txn = db.txn();
handle_new_set::<D>(&mut txn, key, new_tributary_spec, serai, &block, set).await?;
EventDb::handle_event(&mut txn, &hash, event_id);
HandledEvent::handle_event(&mut txn, hash, event_id);
txn.commit();
}
event_id += 1;
@ -281,7 +277,7 @@ async fn handle_block<D: Db, Pro: Processors>(
// If a key pair was confirmed, inform the processor
for key_gen in serai.as_of(hash).validator_sets().key_gen_events().await? {
if EventDb::is_unhandled(db, &hash, event_id) {
if HandledEvent::is_unhandled(db, hash, event_id) {
log::info!("found fresh key gen event {:?}", key_gen);
if let ValidatorSetsEvent::KeyGen { set, key_pair } = key_gen {
handle_key_gen(processors, serai, &block, set, key_pair).await?;
@ -289,7 +285,7 @@ async fn handle_block<D: Db, Pro: Processors>(
panic!("KeyGen event wasn't KeyGen: {key_gen:?}");
}
let mut txn = db.txn();
EventDb::handle_event(&mut txn, &hash, event_id);
HandledEvent::handle_event(&mut txn, hash, event_id);
txn.commit();
}
event_id += 1;
@ -304,28 +300,26 @@ async fn handle_block<D: Db, Pro: Processors>(
continue;
}
if EventDb::is_unhandled(db, &hash, event_id) {
if HandledEvent::is_unhandled(db, hash, event_id) {
log::info!("found fresh set retired event {:?}", retired_set);
let mut txn = db.txn();
crate::ActiveTributaryDb::retire_tributary(&mut txn, set);
tributary_retired.send(set).unwrap();
EventDb::handle_event(&mut txn, &hash, event_id);
HandledEvent::handle_event(&mut txn, hash, event_id);
txn.commit();
}
event_id += 1;
}
// Finally, tell the processor of acknowledged blocks/burns
// This uses a single event as. unlike prior events which individually executed code, all
// This uses a single event as unlike prior events which individually executed code, all
// following events share data collection
// This does break the uniqueness of (hash, event_id) -> one event, yet
// (network, (hash, event_id)) remains valid as a unique ID for an event
if EventDb::is_unhandled(db, &hash, event_id) {
handle_batch_and_burns(db, processors, serai, &block).await?;
}
if HandledEvent::is_unhandled(db, hash, event_id) {
let mut txn = db.txn();
EventDb::handle_event(&mut txn, &hash, event_id);
handle_batch_and_burns(&mut txn, processors, serai, &block).await?;
HandledEvent::handle_event(&mut txn, hash, event_id);
txn.commit();
}
Ok(())
}
@ -342,181 +336,8 @@ async fn handle_new_blocks<D: Db, Pro: Processors>(
// Check if there's been a new Substrate block
let latest_number = serai.latest_finalized_block().await?.number();
// TODO: If this block directly builds off a cosigned block *and* doesn't contain events, mark
// cosigned,
{
// If:
// A) This block has events and it's been at least X blocks since the last cosign or
// B) This block doesn't have events but it's been X blocks since a skipped block which did
// have events or
// C) This block key gens (which changes who the cosigners are)
// cosign this block.
const COSIGN_DISTANCE: u64 = 5 * 60 / 6; // 5 minutes, expressed in blocks
#[derive(Clone, Copy, PartialEq, Eq, Debug, Encode, Decode)]
enum HasEvents {
KeyGen,
Yes,
No,
}
async fn block_has_events(
txn: &mut impl DbTxn,
serai: &Serai,
block: u64,
) -> Result<HasEvents, SeraiError> {
let cached = BlockHasEvents::get(txn, block);
match cached {
None => {
let serai = serai.as_of(
serai
.finalized_block_by_number(block)
.await?
.expect("couldn't get block which should've been finalized")
.hash(),
);
if !serai.validator_sets().key_gen_events().await?.is_empty() {
return Ok(HasEvents::KeyGen);
}
let has_no_events = serai.coins().burn_with_instruction_events().await?.is_empty() &&
serai.in_instructions().batch_events().await?.is_empty() &&
serai.validator_sets().new_set_events().await?.is_empty() &&
serai.validator_sets().set_retired_events().await?.is_empty();
let has_events = if has_no_events { HasEvents::No } else { HasEvents::Yes };
let has_events = has_events.encode();
assert_eq!(has_events.len(), 1);
BlockHasEvents::set(txn, block, &has_events[0]);
Ok(HasEvents::Yes)
}
Some(code) => Ok(HasEvents::decode(&mut [code].as_slice()).unwrap()),
}
}
let mut txn = db.txn();
let Some((last_intended_to_cosign_block, mut skipped_block)) = IntendedCosign::get(&txn) else {
IntendedCosign::set_intended_cosign(&mut txn, 1);
txn.commit();
return Ok(());
};
// If we haven't flagged skipped, and a block within the distance had events, flag the first
// such block as skipped
let mut distance_end_exclusive = last_intended_to_cosign_block + COSIGN_DISTANCE;
// If we've never triggered a cosign, don't skip any cosigns
if CosignTriggered::get(&txn).is_none() {
distance_end_exclusive = 0;
}
if skipped_block.is_none() {
for b in (last_intended_to_cosign_block + 1) .. distance_end_exclusive {
if b > latest_number {
break;
}
if block_has_events(&mut txn, serai, b).await? == HasEvents::Yes {
skipped_block = Some(b);
log::debug!("skipping cosigning {b} due to proximity to prior cosign");
IntendedCosign::set_skipped_cosign(&mut txn, b);
break;
}
}
}
let mut has_no_cosigners = None;
let mut cosign = vec![];
// Block we should cosign no matter what if no prior blocks qualified for cosigning
let maximally_latent_cosign_block =
skipped_block.map(|skipped_block| skipped_block + COSIGN_DISTANCE);
for block in (last_intended_to_cosign_block + 1) ..= latest_number {
let actual_block = serai
.finalized_block_by_number(block)
.await?
.expect("couldn't get block which should've been finalized");
SeraiBlockNumber::set(&mut txn, actual_block.hash(), &block);
let mut set = false;
let block_has_events = block_has_events(&mut txn, serai, block).await?;
// If this block is within the distance,
if block < distance_end_exclusive {
// and set a key, cosign it
if block_has_events == HasEvents::KeyGen {
IntendedCosign::set_intended_cosign(&mut txn, block);
set = true;
// Carry skipped if it isn't included by cosigning this block
if let Some(skipped) = skipped_block {
if skipped > block {
IntendedCosign::set_skipped_cosign(&mut txn, block);
}
}
}
} else if (Some(block) == maximally_latent_cosign_block) ||
(block_has_events != HasEvents::No)
{
// Since this block was outside the distance and had events/was maximally latent, cosign it
IntendedCosign::set_intended_cosign(&mut txn, block);
set = true;
}
if set {
// Get the keys as of the prior block
// That means if this block is setting new keys (which won't lock in until we process this
// block), we won't freeze up waiting for the yet-to-be-processed keys to sign this block
let serai = serai.as_of(actual_block.header.parent_hash.into());
has_no_cosigners = Some(actual_block.clone());
for network in serai_client::primitives::NETWORKS {
// Get the latest session to have set keys
let Some(latest_session) = serai.validator_sets().session(network).await? else {
continue;
};
let prior_session = Session(latest_session.0.saturating_sub(1));
let set_with_keys = if serai
.validator_sets()
.keys(ValidatorSet { network, session: prior_session })
.await?
.is_some()
{
ValidatorSet { network, session: prior_session }
} else {
let set = ValidatorSet { network, session: latest_session };
if serai.validator_sets().keys(set).await?.is_none() {
continue;
}
set
};
// Since this is a valid cosigner, don't flag this block as having no cosigners
has_no_cosigners = None;
log::debug!("{:?} will be cosigning {block}", set_with_keys.network);
if in_set(key, &serai, set_with_keys).await?.unwrap() {
cosign.push((set_with_keys, block, actual_block.hash()));
}
}
break;
}
}
// If this block doesn't have cosigners, yet does have events, automatically mark it as
// cosigned
if let Some(has_no_cosigners) = has_no_cosigners {
log::debug!("{} had no cosigners available, marking as cosigned", has_no_cosigners.number());
LatestCosignedBlock::set(&mut txn, &has_no_cosigners.number());
} else {
CosignTriggered::set(&mut txn, &());
for (set, block, hash) in cosign {
log::debug!("cosigning {block} with {:?} {:?}", set.network, set.session);
CosignTransactions::append_cosign(&mut txn, set, block, hash);
}
}
txn.commit();
}
// Advance the cosigning protocol
advance_cosign_protocol(db, key, serai, latest_number).await?;
// Reduce to the latest cosigned block
let latest_number = latest_number.min(LatestCosignedBlock::latest_cosigned_block(db));
@ -526,24 +347,19 @@ async fn handle_new_blocks<D: Db, Pro: Processors>(
}
for b in *next_block ..= latest_number {
log::info!("found substrate block {b}");
handle_block(
db,
key,
new_tributary_spec,
tributary_retired,
processors,
serai,
serai
let block = serai
.finalized_block_by_number(b)
.await?
.expect("couldn't get block before the latest finalized block"),
)
.await?;
.expect("couldn't get block before the latest finalized block");
log::info!("handling substrate block {b}");
handle_block(db, key, new_tributary_spec, tributary_retired, processors, serai, block).await?;
*next_block += 1;
let mut txn = db.txn();
NextBlock::set(&mut txn, next_block);
txn.commit();
log::info!("handled substrate block {b}");
}
@ -578,6 +394,7 @@ pub async fn scan_task<D: Db, Pro: Processors>(
};
*/
// TODO: Restore the above subscription-based system
// That would require moving serai-client from HTTP to websockets
let new_substrate_block_notifier = {
let serai = &serai;
move |next_substrate_block| async move {
@ -648,22 +465,25 @@ pub async fn scan_task<D: Db, Pro: Processors>(
}
/// Gets the expected ID for the next Batch.
pub(crate) async fn get_expected_next_batch(serai: &Serai, network: NetworkId) -> u32 {
let mut first = true;
loop {
if !first {
log::error!("{} {network:?}", "couldn't connect to Serai node to get the next batch ID for",);
sleep(Duration::from_secs(5)).await;
///
/// Will log an error and apply a slight sleep on error, letting the caller simply immediately
/// retry.
pub(crate) async fn expected_next_batch(
serai: &Serai,
network: NetworkId,
) -> Result<u32, SeraiError> {
async fn expected_next_batch_inner(serai: &Serai, network: NetworkId) -> Result<u32, SeraiError> {
let serai = serai.as_of_latest_finalized_block().await?;
let last = serai.in_instructions().last_batch_for_network(network).await?;
Ok(if let Some(last) = last { last + 1 } else { 0 })
}
match expected_next_batch_inner(serai, network).await {
Ok(next) => Ok(next),
Err(e) => {
log::error!("couldn't get the expected next batch from substrate: {e:?}");
sleep(Duration::from_millis(100)).await;
Err(e)
}
first = false;
let Ok(serai) = serai.as_of_latest_finalized_block().await else {
continue;
};
let Ok(last) = serai.in_instructions().last_batch_for_network(network).await else {
continue;
};
break if let Some(last) = last { last + 1 } else { 0 };
}
}

View file

@ -13,7 +13,7 @@ use ciphersuite::{
};
use sp_application_crypto::sr25519;
use borsh::BorshDeserialize;
use serai_client::{
primitives::NetworkId,
validator_sets::primitives::{Session, ValidatorSet},
@ -58,21 +58,26 @@ pub fn new_spec<R: RngCore + CryptoRng>(
.collect::<Vec<_>>();
let res = TributarySpec::new(serai_block, start_time, set, set_participants);
assert_eq!(TributarySpec::read::<&[u8]>(&mut res.serialize().as_ref()).unwrap(), res);
assert_eq!(
TributarySpec::deserialize_reader(&mut borsh::to_vec(&res).unwrap().as_slice()).unwrap(),
res,
);
res
}
pub async fn new_tributaries(
keys: &[Zeroizing<<Ristretto as Ciphersuite>::F>],
spec: &TributarySpec,
) -> Vec<(LocalP2p, Tributary<MemDb, Transaction, LocalP2p>)> {
) -> Vec<(MemDb, LocalP2p, Tributary<MemDb, Transaction, LocalP2p>)> {
let p2p = LocalP2p::new(keys.len());
let mut res = vec![];
for (i, key) in keys.iter().enumerate() {
let db = MemDb::new();
res.push((
db.clone(),
p2p[i].clone(),
Tributary::<_, Transaction, _>::new(
MemDb::new(),
db,
spec.genesis(),
spec.start_time(),
key.clone(),
@ -152,7 +157,11 @@ async fn tributary_test() {
let keys = new_keys(&mut OsRng);
let spec = new_spec(&mut OsRng, &keys);
let mut tributaries = new_tributaries(&keys, &spec).await;
let mut tributaries = new_tributaries(&keys, &spec)
.await
.into_iter()
.map(|(_, p2p, tributary)| (p2p, tributary))
.collect::<Vec<_>>();
let mut blocks = 0;
let mut last_block = spec.genesis();

View file

@ -8,7 +8,7 @@ use ciphersuite::{group::GroupEncoding, Ciphersuite, Ristretto};
use frost::Participant;
use sp_runtime::traits::Verify;
use serai_client::validator_sets::primitives::KeyPair;
use serai_client::validator_sets::primitives::{ValidatorSet, KeyPair};
use tokio::time::sleep;
@ -34,10 +34,18 @@ use crate::{
#[tokio::test]
async fn dkg_test() {
env_logger::init();
let keys = new_keys(&mut OsRng);
let spec = new_spec(&mut OsRng, &keys);
let tributaries = new_tributaries(&keys, &spec).await;
let full_tributaries = new_tributaries(&keys, &spec).await;
let mut dbs = vec![];
let mut tributaries = vec![];
for (db, p2p, tributary) in full_tributaries {
dbs.push(db);
tributaries.push((p2p, tributary));
}
// Run the tributaries in the background
tokio::spawn(run_tributaries(tributaries.clone()));
@ -49,8 +57,11 @@ async fn dkg_test() {
let mut commitments = vec![0; 256];
OsRng.fill_bytes(&mut commitments);
let mut tx =
Transaction::DkgCommitments(attempt, vec![commitments], Transaction::empty_signed());
let mut tx = Transaction::DkgCommitments {
attempt,
commitments: vec![commitments],
signed: Transaction::empty_signed(),
};
tx.sign(&mut OsRng, spec.genesis(), key);
txs.push(tx);
}
@ -71,7 +82,7 @@ async fn dkg_test() {
.iter()
.enumerate()
.map(|(i, tx)| {
if let Transaction::DkgCommitments(_, commitments, _) = tx {
if let Transaction::DkgCommitments { commitments, .. } = tx {
(Participant::new((i + 1).try_into().unwrap()).unwrap(), commitments[0].clone())
} else {
panic!("txs had non-commitments");
@ -80,20 +91,20 @@ async fn dkg_test() {
.collect();
async fn new_processors(
db: &mut MemDb,
key: &Zeroizing<<Ristretto as Ciphersuite>::F>,
spec: &TributarySpec,
tributary: &Tributary<MemDb, Transaction, LocalP2p>,
) -> (MemDb, MemProcessors) {
let mut scanner_db = MemDb::new();
) -> MemProcessors {
let processors = MemProcessors::new();
handle_new_blocks::<_, _, _, _, _, _, _, _, LocalP2p>(
&mut scanner_db,
handle_new_blocks::<_, _, _, _, _, LocalP2p>(
db,
key,
|_, _, _, _| async {
&|_, _, _, _| async {
panic!("provided TX caused recognized_id to be called in new_processors")
},
&processors,
|_, _, _| async { panic!("test tried to publish a new Serai TX in new_processors") },
&|_, _, _| async { panic!("test tried to publish a new Serai TX in new_processors") },
&|_| async {
panic!(
"test tried to publish a new Tributary TX from handle_application_tx in new_processors"
@ -103,11 +114,11 @@ async fn dkg_test() {
&tributary.reader(),
)
.await;
(scanner_db, processors)
processors
}
// Instantiate a scanner and verify it has nothing to report
let (mut scanner_db, processors) = new_processors(&keys[0], &spec, &tributaries[0].1).await;
let processors = new_processors(&mut dbs[0], &keys[0], &spec, &tributaries[0].1).await;
assert!(processors.0.read().await.is_empty());
// Publish the last commitment
@ -117,14 +128,14 @@ async fn dkg_test() {
sleep(Duration::from_secs(Tributary::<MemDb, Transaction, LocalP2p>::block_time().into())).await;
// Verify the scanner emits a KeyGen::Commitments message
handle_new_blocks::<_, _, _, _, _, _, _, _, LocalP2p>(
&mut scanner_db,
handle_new_blocks::<_, _, _, _, _, LocalP2p>(
&mut dbs[0],
&keys[0],
|_, _, _, _| async {
&|_, _, _, _| async {
panic!("provided TX caused recognized_id to be called after Commitments")
},
&processors,
|_, _, _| async { panic!("test tried to publish a new Serai TX after Commitments") },
&|_, _, _| async { panic!("test tried to publish a new Serai TX after Commitments") },
&|_| async {
panic!(
"test tried to publish a new Tributary TX from handle_application_tx after Commitments"
@ -151,8 +162,8 @@ async fn dkg_test() {
}
// Verify all keys exhibit this scanner behavior
for (i, key) in keys.iter().enumerate() {
let (_, processors) = new_processors(key, &spec, &tributaries[i].1).await;
for (i, key) in keys.iter().enumerate().skip(1) {
let processors = new_processors(&mut dbs[i], key, &spec, &tributaries[i].1).await;
let mut msgs = processors.0.write().await;
assert_eq!(msgs.len(), 1);
let msgs = msgs.get_mut(&spec.set().network).unwrap();
@ -182,12 +193,14 @@ async fn dkg_test() {
}
}
let mut txn = dbs[k].txn();
let mut tx = Transaction::DkgShares {
attempt,
shares,
confirmation_nonces: crate::tributary::dkg_confirmation_nonces(key, &spec, 0),
confirmation_nonces: crate::tributary::dkg_confirmation_nonces(key, &spec, &mut txn, 0),
signed: Transaction::empty_signed(),
};
txn.commit();
tx.sign(&mut OsRng, spec.genesis(), key);
txs.push(tx);
}
@ -201,14 +214,14 @@ async fn dkg_test() {
}
// With just 4 sets of shares, nothing should happen yet
handle_new_blocks::<_, _, _, _, _, _, _, _, LocalP2p>(
&mut scanner_db,
handle_new_blocks::<_, _, _, _, _, LocalP2p>(
&mut dbs[0],
&keys[0],
|_, _, _, _| async {
&|_, _, _, _| async {
panic!("provided TX caused recognized_id to be called after some shares")
},
&processors,
|_, _, _| async { panic!("test tried to publish a new Serai TX after some shares") },
&|_, _, _| async { panic!("test tried to publish a new Serai TX after some shares") },
&|_| async {
panic!(
"test tried to publish a new Tributary TX from handle_application_tx after some shares"
@ -254,28 +267,30 @@ async fn dkg_test() {
};
// Any scanner which has handled the prior blocks should only emit the new event
handle_new_blocks::<_, _, _, _, _, _, _, _, LocalP2p>(
&mut scanner_db,
&keys[0],
|_, _, _, _| async { panic!("provided TX caused recognized_id to be called after shares") },
for (i, key) in keys.iter().enumerate() {
handle_new_blocks::<_, _, _, _, _, LocalP2p>(
&mut dbs[i],
key,
&|_, _, _, _| async { panic!("provided TX caused recognized_id to be called after shares") },
&processors,
|_, _, _| async { panic!("test tried to publish a new Serai TX") },
&|_, _, _| async { panic!("test tried to publish a new Serai TX") },
&|_| async { panic!("test tried to publish a new Tributary TX from handle_application_tx") },
&spec,
&tributaries[0].1.reader(),
&tributaries[i].1.reader(),
)
.await;
{
let mut msgs = processors.0.write().await;
assert_eq!(msgs.len(), 1);
let msgs = msgs.get_mut(&spec.set().network).unwrap();
assert_eq!(msgs.pop_front().unwrap(), shares_for(0));
assert_eq!(msgs.pop_front().unwrap(), shares_for(i));
assert!(msgs.is_empty());
}
}
// Yet new scanners should emit all events
for (i, key) in keys.iter().enumerate() {
let (_, processors) = new_processors(key, &spec, &tributaries[i].1).await;
let processors = new_processors(&mut MemDb::new(), key, &spec, &tributaries[i].1).await;
let mut msgs = processors.0.write().await;
assert_eq!(msgs.len(), 1);
let msgs = msgs.get_mut(&spec.set().network).unwrap();
@ -302,17 +317,16 @@ async fn dkg_test() {
let mut txs = vec![];
for (i, key) in keys.iter().enumerate() {
let attempt = 0;
let mut scanner_db = &mut scanner_db;
let (mut local_scanner_db, _) = new_processors(key, &spec, &tributaries[0].1).await;
if i != 0 {
scanner_db = &mut local_scanner_db;
}
let mut txn = scanner_db.txn();
let mut txn = dbs[i].txn();
let share =
crate::tributary::generated_key_pair::<MemDb>(&mut txn, key, &spec, &key_pair, 0).unwrap();
txn.commit();
let mut tx = Transaction::DkgConfirmed(attempt, share, Transaction::empty_signed());
let mut tx = Transaction::DkgConfirmed {
attempt,
confirmation_share: share,
signed: Transaction::empty_signed(),
};
tx.sign(&mut OsRng, spec.genesis(), key);
txs.push(tx);
}
@ -325,14 +339,14 @@ async fn dkg_test() {
}
// The scanner should successfully try to publish a transaction with a validly signed signature
handle_new_blocks::<_, _, _, _, _, _, _, _, LocalP2p>(
&mut scanner_db,
handle_new_blocks::<_, _, _, _, _, LocalP2p>(
&mut dbs[0],
&keys[0],
|_, _, _, _| async {
&|_, _, _, _| async {
panic!("provided TX caused recognized_id to be called after DKG confirmation")
},
&processors,
|set, tx_type, tx| {
&|set: ValidatorSet, tx_type, tx: serai_client::Transaction| {
assert_eq!(tx_type, PstTxType::SetKeys);
let spec = spec.clone();

View file

@ -27,7 +27,11 @@ async fn handle_p2p_test() {
let keys = new_keys(&mut OsRng);
let spec = new_spec(&mut OsRng, &keys);
let mut tributaries = new_tributaries(&keys, &spec).await;
let mut tributaries = new_tributaries(&keys, &spec)
.await
.into_iter()
.map(|(_, p2p, tributary)| (p2p, tributary))
.collect::<Vec<_>>();
let mut tributary_senders = vec![];
let mut tributary_arcs = vec![];

View file

@ -7,7 +7,7 @@ use processor_messages::coordinator::SubstrateSignableId;
use tributary::{ReadWrite, tests::random_signed_with_nonce};
use crate::tributary::{SignData, Transaction};
use crate::tributary::{Label, SignData, Transaction};
mod chain;
pub use chain::*;
@ -34,11 +34,12 @@ fn random_vec<R: RngCore>(rng: &mut R, limit: usize) -> Vec<u8> {
fn random_sign_data<R: RngCore, Id: Clone + PartialEq + Eq + Debug + Encode + Decode>(
rng: &mut R,
plan: Id,
nonce: u32,
label: Label,
) -> SignData<Id> {
SignData {
plan,
attempt: random_u32(&mut OsRng),
label,
data: {
let mut res = vec![];
@ -48,7 +49,7 @@ fn random_sign_data<R: RngCore, Id: Clone + PartialEq + Eq + Debug + Encode + De
res
},
signed: random_signed_with_nonce(&mut OsRng, nonce),
signed: random_signed_with_nonce(&mut OsRng, label.nonce()),
}
}
@ -87,7 +88,7 @@ fn serialize_sign_data() {
fn test_read_write<Id: Clone + PartialEq + Eq + Debug + Encode + Decode>(value: SignData<Id>) {
let mut buf = vec![];
value.write(&mut buf).unwrap();
assert_eq!(value, SignData::read(&mut buf.as_slice(), value.signed.nonce).unwrap())
assert_eq!(value, SignData::read(&mut buf.as_slice()).unwrap())
}
let mut plan = [0; 3];
@ -95,28 +96,28 @@ fn serialize_sign_data() {
test_read_write(random_sign_data::<_, _>(
&mut OsRng,
plan,
u32::try_from(OsRng.next_u64() >> 32).unwrap(),
if (OsRng.next_u64() % 2) == 0 { Label::Preprocess } else { Label::Share },
));
let mut plan = [0; 5];
OsRng.fill_bytes(&mut plan);
test_read_write(random_sign_data::<_, _>(
&mut OsRng,
plan,
u32::try_from(OsRng.next_u64() >> 32).unwrap(),
if (OsRng.next_u64() % 2) == 0 { Label::Preprocess } else { Label::Share },
));
let mut plan = [0; 8];
OsRng.fill_bytes(&mut plan);
test_read_write(random_sign_data::<_, _>(
&mut OsRng,
plan,
u32::try_from(OsRng.next_u64() >> 32).unwrap(),
if (OsRng.next_u64() % 2) == 0 { Label::Preprocess } else { Label::Share },
));
let mut plan = [0; 24];
OsRng.fill_bytes(&mut plan);
test_read_write(random_sign_data::<_, _>(
&mut OsRng,
plan,
u32::try_from(OsRng.next_u64() >> 32).unwrap(),
if (OsRng.next_u64() % 2) == 0 { Label::Preprocess } else { Label::Share },
));
}
@ -134,11 +135,11 @@ fn serialize_transaction() {
OsRng.fill_bytes(&mut temp);
commitments.push(temp);
}
test_read_write(Transaction::DkgCommitments(
random_u32(&mut OsRng),
test_read_write(Transaction::DkgCommitments {
attempt: random_u32(&mut OsRng),
commitments,
random_signed_with_nonce(&mut OsRng, 0),
));
signed: random_signed_with_nonce(&mut OsRng, 0),
});
}
{
@ -192,25 +193,25 @@ fn serialize_transaction() {
});
}
test_read_write(Transaction::DkgConfirmed(
random_u32(&mut OsRng),
{
test_read_write(Transaction::DkgConfirmed {
attempt: random_u32(&mut OsRng),
confirmation_share: {
let mut share = [0; 32];
OsRng.fill_bytes(&mut share);
share
},
random_signed_with_nonce(&mut OsRng, 2),
));
signed: random_signed_with_nonce(&mut OsRng, 2),
});
{
let mut key = [0; 32];
OsRng.fill_bytes(&mut key);
test_read_write(Transaction::DkgRemovalPreprocess(random_sign_data(&mut OsRng, key, 0)));
test_read_write(Transaction::DkgRemoval(random_sign_data(&mut OsRng, key, Label::Preprocess)));
}
{
let mut key = [0; 32];
OsRng.fill_bytes(&mut key);
test_read_write(Transaction::DkgRemovalShare(random_sign_data(&mut OsRng, key, 1)));
test_read_write(Transaction::DkgRemoval(random_sign_data(&mut OsRng, key, Label::Share)));
}
{
@ -224,38 +225,38 @@ fn serialize_transaction() {
OsRng.fill_bytes(&mut block);
let mut batch = [0; 5];
OsRng.fill_bytes(&mut batch);
test_read_write(Transaction::Batch(block, batch));
test_read_write(Transaction::Batch { block, batch });
}
test_read_write(Transaction::SubstrateBlock(OsRng.next_u64()));
{
let mut plan = [0; 5];
OsRng.fill_bytes(&mut plan);
test_read_write(Transaction::SubstratePreprocess(random_sign_data(
test_read_write(Transaction::SubstrateSign(random_sign_data(
&mut OsRng,
SubstrateSignableId::Batch(plan),
0,
Label::Preprocess,
)));
}
{
let mut plan = [0; 5];
OsRng.fill_bytes(&mut plan);
test_read_write(Transaction::SubstrateShare(random_sign_data(
test_read_write(Transaction::SubstrateSign(random_sign_data(
&mut OsRng,
SubstrateSignableId::Batch(plan),
1,
Label::Share,
)));
}
{
let mut plan = [0; 32];
OsRng.fill_bytes(&mut plan);
test_read_write(Transaction::SignPreprocess(random_sign_data(&mut OsRng, plan, 0)));
test_read_write(Transaction::Sign(random_sign_data(&mut OsRng, plan, Label::Preprocess)));
}
{
let mut plan = [0; 32];
OsRng.fill_bytes(&mut plan);
test_read_write(Transaction::SignShare(random_sign_data(&mut OsRng, plan, 1)));
test_read_write(Transaction::Sign(random_sign_data(&mut OsRng, plan, Label::Share)));
}
{

View file

@ -31,7 +31,11 @@ async fn sync_test() {
// Ensure this can have a node fail
assert!(spec.n() > spec.t());
let mut tributaries = new_tributaries(&keys, &spec).await;
let mut tributaries = new_tributaries(&keys, &spec)
.await
.into_iter()
.map(|(_, p2p, tributary)| (p2p, tributary))
.collect::<Vec<_>>();
// Keep a Tributary back, effectively having it offline
let syncer_key = keys.pop().unwrap();

View file

@ -23,7 +23,11 @@ async fn tx_test() {
let keys = new_keys(&mut OsRng);
let spec = new_spec(&mut OsRng, &keys);
let tributaries = new_tributaries(&keys, &spec).await;
let tributaries = new_tributaries(&keys, &spec)
.await
.into_iter()
.map(|(_, p2p, tributary)| (p2p, tributary))
.collect::<Vec<_>>();
// Run the tributaries in the background
tokio::spawn(run_tributaries(tributaries.clone()));
@ -39,8 +43,11 @@ async fn tx_test() {
// Create the TX with a null signature so we can get its sig hash
let block_before_tx = tributaries[sender].1.tip().await;
let mut tx =
Transaction::DkgCommitments(attempt, vec![commitments.clone()], Transaction::empty_signed());
let mut tx = Transaction::DkgCommitments {
attempt,
commitments: vec![commitments.clone()],
signed: Transaction::empty_signed(),
};
tx.sign(&mut OsRng, spec.genesis(), &key);
assert_eq!(tributaries[sender].1.add_transaction(tx.clone()).await, Ok(true));

View file

@ -1,23 +1,23 @@
use core::ops::Deref;
use std::collections::HashMap;
use zeroize::Zeroizing;
use ciphersuite::{Ciphersuite, Ristretto, group::GroupEncoding};
use scale::Encode;
use frost::Participant;
use serai_client::validator_sets::primitives::KeyPair;
use processor_messages::coordinator::SubstrateSignableId;
use scale::{Encode, Decode};
pub use serai_db::*;
use crate::tributary::TributarySpec;
use tributary::ReadWrite;
#[derive(Clone, Copy, PartialEq, Eq, Debug, Encode, Decode)]
use crate::tributary::{Label, Transaction};
#[derive(Clone, Copy, PartialEq, Eq, Debug, Encode)]
pub enum Topic {
Dkg,
DkgConfirmation,
DkgRemoval([u8; 32]),
SubstrateSign(SubstrateSignableId),
Sign([u8; 32]),
@ -27,7 +27,7 @@ pub enum Topic {
#[derive(Clone, Copy, PartialEq, Eq, Debug, Encode)]
pub struct DataSpecification {
pub topic: Topic,
pub label: &'static str,
pub label: Label,
pub attempt: u32,
}
@ -42,9 +42,9 @@ pub enum Accumulation {
}
create_db!(
NewTributary {
Tributary {
SeraiBlockNumber: (hash: [u8; 32]) -> u64,
LastBlock: (genesis: [u8; 32]) -> [u8; 32],
LastHandledBlock: (genesis: [u8; 32]) -> [u8; 32],
FatalSlashes: (genesis: [u8; 32]) -> Vec<[u8; 32]>,
FatallySlashed: (genesis: [u8; 32], account: [u8; 32]) -> (),
DkgShare: (genesis: [u8; 32], from: u16, to: u16) -> Vec<u8>,
@ -52,12 +52,13 @@ create_db!(
ConfirmationNonces: (genesis: [u8; 32], attempt: u32) -> HashMap<Participant, Vec<u8>>,
RemovalNonces:
(genesis: [u8; 32], removing: [u8; 32], attempt: u32) -> HashMap<Participant, Vec<u8>>,
CurrentlyCompletingKeyPair: (genesis: [u8; 32]) -> KeyPair,
DkgKeyPair: (genesis: [u8; 32], attempt: u32) -> KeyPair,
DkgCompleted: (genesis: [u8; 32]) -> (),
AttemptDb: (genesis: [u8; 32], topic: &Topic) -> u32,
DataReceived: (genesis: [u8; 32], data_spec: &DataSpecification) -> u16,
DataDb: (genesis: [u8; 32], data_spec: &DataSpecification, signer_bytes: &[u8; 32]) -> Vec<u8>,
EventDb: (id: [u8; 32], index: u32) -> (),
SignedTransactionDb: (order: &[u8], nonce: u32) -> Vec<u8>,
}
);
@ -84,78 +85,24 @@ impl AttemptDb {
pub fn attempt(getter: &impl Get, genesis: [u8; 32], topic: Topic) -> Option<u32> {
let attempt = Self::get(getter, genesis, &topic);
// Don't require explicit recognition of the Dkg topic as it starts when the chain does
if attempt.is_none() && (topic == Topic::Dkg) {
if attempt.is_none() && ((topic == Topic::Dkg) || (topic == Topic::DkgConfirmation)) {
return Some(0);
}
attempt
}
}
impl DataDb {
pub fn accumulate(
impl SignedTransactionDb {
pub fn take_signed_transaction(
txn: &mut impl DbTxn,
our_key: &Zeroizing<<Ristretto as Ciphersuite>::F>,
spec: &TributarySpec,
data_spec: &DataSpecification,
signer: <Ristretto as Ciphersuite>::G,
data: &Vec<u8>,
) -> Accumulation {
let genesis = spec.genesis();
if Self::get(txn, genesis, data_spec, &signer.to_bytes()).is_some() {
panic!("accumulating data for a participant multiple times");
order: &[u8],
nonce: u32,
) -> Option<Transaction> {
let res = SignedTransactionDb::get(txn, order, nonce)
.map(|bytes| Transaction::read(&mut bytes.as_slice()).unwrap());
if res.is_some() {
Self::del(txn, order, nonce);
}
let signer_shares = {
let signer_i =
spec.i(signer).expect("transaction signed by a non-validator for this tributary");
u16::from(signer_i.end) - u16::from(signer_i.start)
};
let prior_received = DataReceived::get(txn, genesis, data_spec).unwrap_or_default();
let now_received = prior_received + signer_shares;
DataReceived::set(txn, genesis, data_spec, &now_received);
DataDb::set(txn, genesis, data_spec, &signer.to_bytes(), data);
// If we have all the needed commitments/preprocesses/shares, tell the processor
let needed = if data_spec.topic == Topic::Dkg { spec.n() } else { spec.t() };
if (prior_received < needed) && (now_received >= needed) {
return Accumulation::Ready({
let mut data = HashMap::new();
for validator in spec.validators().iter().map(|validator| validator.0) {
data.insert(
spec.i(validator).unwrap().start,
if let Some(data) = Self::get(txn, genesis, data_spec, &validator.to_bytes()) {
data
} else {
continue;
},
);
}
assert_eq!(data.len(), usize::from(needed));
// Remove our own piece of data, if we were involved
if data
.remove(
&spec
.i(Ristretto::generator() * our_key.deref())
.expect("handling a message for a Tributary we aren't part of")
.start,
)
.is_some()
{
DataSet::Participating(data)
} else {
DataSet::NotParticipating
}
});
}
Accumulation::NotReady
}
}
impl EventDb {
pub fn handle_event(txn: &mut impl DbTxn, id: [u8; 32], index: u32) {
assert!(Self::get(txn, id, index).is_none());
Self::set(txn, id, index, &());
res
}
}

View file

@ -1,207 +0,0 @@
use std::collections::HashMap;
use zeroize::Zeroizing;
use rand_core::SeedableRng;
use rand_chacha::ChaCha20Rng;
use transcript::{Transcript, RecommendedTranscript};
use ciphersuite::{Ciphersuite, Ristretto};
use frost::{
FrostError,
dkg::{Participant, musig::musig},
sign::*,
};
use frost_schnorrkel::Schnorrkel;
use serai_client::validator_sets::primitives::{KeyPair, musig_context, set_keys_message};
use crate::tributary::TributarySpec;
/*
The following confirms the results of the DKG performed by the Processors onto Substrate.
This is done by a signature over the generated key pair by the validators' MuSig-aggregated
public key. The MuSig-aggregation achieves on-chain efficiency and prevents on-chain censorship
of individual validator's DKG results by the Serai validator set.
Since we're using the validators public keys, as needed for their being the root of trust, the
coordinator must perform the signing. This is distinct from all other group-signing operations
which are generally done by the processor.
Instead of maintaining state, the following rebuilds the full state on every call. This is deemed
acceptable re: performance as:
1) The DKG confirmation is only done upon the start of the Tributary.
2) This is an O(n) algorithm.
3) The size of the validator set is bounded by MAX_KEY_SHARES_PER_SET.
Accordingly, this should be infrequently ran and of tolerable algorithmic complexity.
As for safety, it is explicitly unsafe to reuse nonces across signing sessions. This is in
contradiction with our rebuilding which is dependent on deterministic nonces. Safety is derived
from the deterministic nonces being context-bound under a BFT protocol. The flow is as follows:
1) Derive a deterministic nonce by hashing the private key, Tributary parameters, and attempt.
2) Publish the nonces' commitments, receiving everyone elses *and the DKG shares determining the
message to be signed*.
3) Sign and publish the signature share.
In order for nonce re-use to occur, the received nonce commitments, or the received DKG shares,
would have to be distinct and sign would have to be called again.
Before we act on any received messages, they're ordered and finalized by a BFT algorithm. The
only way to operate on distinct received messages would be if:
1) A logical flaw exists, letting new messages over write prior messages
2) A reorganization occured from chain A to chain B, and with it, different messages
Reorganizations are not supported, as BFT is assumed by the presence of a BFT algorithm. While
a significant amount of processes may be byzantine, leading to BFT being broken, that still will
not trigger a reorganization. The only way to move to a distinct chain, with distinct messages,
would be by rebuilding the local process entirely (this time following chain B).
Accordingly, safety follows if:
1) The local view of received messages is static
2) The local process doesn't rebuild after a byzantine fault produces multiple blockchains
We assume the former. We can prevent the latter (TODO) by:
1) Defining a per-build entropy, used so long as a DB is used.
2) Checking the initially used commitments for the DKG align with the per-build entropy.
If a rebuild occurs, which is the only way we could follow a distinct blockchain, our entropy
will change (preventing nonce reuse).
This will allow a validator to still participate in DKGs within a single build, even if they have
spontaneous reboots, and on collapse triggering a rebuild, they don't lose safety.
TODO: We also need to review how we're handling Processor preprocesses and likely implement the
same on-chain-preprocess-matches-presumed-preprocess check before publishing shares.
*/
pub(crate) struct DkgConfirmer;
impl DkgConfirmer {
// Convert the passed in HashMap, which uses the validators' start index for their `s` threshold
// shares, to the indexes needed for MuSig
fn from_threshold_i_to_musig_i(
spec: &TributarySpec,
mut old_map: HashMap<Participant, Vec<u8>>,
) -> HashMap<Participant, Vec<u8>> {
let mut new_map = HashMap::new();
for (new_i, validator) in spec.validators().into_iter().enumerate() {
let threshold_i = spec.i(validator.0).unwrap();
if let Some(value) = old_map.remove(&threshold_i.start) {
new_map.insert(Participant::new(u16::try_from(new_i + 1).unwrap()).unwrap(), value);
}
}
new_map
}
fn preprocess_internal(
spec: &TributarySpec,
key: &Zeroizing<<Ristretto as Ciphersuite>::F>,
attempt: u32,
) -> (AlgorithmSignMachine<Ristretto, Schnorrkel>, [u8; 64]) {
let validators = spec.validators().iter().map(|val| val.0).collect::<Vec<_>>();
let context = musig_context(spec.set());
let mut chacha = ChaCha20Rng::from_seed({
let mut entropy_transcript = RecommendedTranscript::new(b"DkgConfirmer Entropy");
entropy_transcript.append_message(b"spec", spec.serialize());
entropy_transcript.append_message(b"key", Zeroizing::new(key.to_bytes()));
entropy_transcript.append_message(b"attempt", attempt.to_le_bytes());
Zeroizing::new(entropy_transcript).rng_seed(b"preprocess")
});
let (machine, preprocess) = AlgorithmMachine::new(
Schnorrkel::new(b"substrate"),
musig(&context, key, &validators)
.expect("confirming the DKG for a set we aren't in/validator present multiple times")
.into(),
)
.preprocess(&mut chacha);
(machine, preprocess.serialize().try_into().unwrap())
}
// Get the preprocess for this confirmation.
pub(crate) fn preprocess(
spec: &TributarySpec,
key: &Zeroizing<<Ristretto as Ciphersuite>::F>,
attempt: u32,
) -> [u8; 64] {
Self::preprocess_internal(spec, key, attempt).1
}
fn share_internal(
spec: &TributarySpec,
key: &Zeroizing<<Ristretto as Ciphersuite>::F>,
attempt: u32,
preprocesses: HashMap<Participant, Vec<u8>>,
key_pair: &KeyPair,
) -> Result<(AlgorithmSignatureMachine<Ristretto, Schnorrkel>, [u8; 32]), Participant> {
let machine = Self::preprocess_internal(spec, key, attempt).0;
let preprocesses = Self::from_threshold_i_to_musig_i(spec, preprocesses)
.into_iter()
.map(|(p, preprocess)| {
machine
.read_preprocess(&mut preprocess.as_slice())
.map(|preprocess| (p, preprocess))
.map_err(|_| p)
})
.collect::<Result<HashMap<_, _>, _>>()?;
let (machine, share) = machine
.sign(preprocesses, &set_keys_message(&spec.set(), key_pair))
.map_err(|e| match e {
FrostError::InternalError(e) => unreachable!("FrostError::InternalError {e}"),
FrostError::InvalidParticipant(_, _) |
FrostError::InvalidSigningSet(_) |
FrostError::InvalidParticipantQuantity(_, _) |
FrostError::DuplicatedParticipant(_) |
FrostError::MissingParticipant(_) => unreachable!("{e:?}"),
FrostError::InvalidPreprocess(p) | FrostError::InvalidShare(p) => p,
})?;
Ok((machine, share.serialize().try_into().unwrap()))
}
// Get the share for this confirmation, if the preprocesses are valid.
pub(crate) fn share(
spec: &TributarySpec,
key: &Zeroizing<<Ristretto as Ciphersuite>::F>,
attempt: u32,
preprocesses: HashMap<Participant, Vec<u8>>,
key_pair: &KeyPair,
) -> Result<[u8; 32], Participant> {
Self::share_internal(spec, key, attempt, preprocesses, key_pair).map(|(_, share)| share)
}
pub(crate) fn complete(
spec: &TributarySpec,
key: &Zeroizing<<Ristretto as Ciphersuite>::F>,
attempt: u32,
preprocesses: HashMap<Participant, Vec<u8>>,
key_pair: &KeyPair,
shares: HashMap<Participant, Vec<u8>>,
) -> Result<[u8; 64], Participant> {
let machine = Self::share_internal(spec, key, attempt, preprocesses, key_pair)
.expect("trying to complete a machine which failed to preprocess")
.0;
let shares = Self::from_threshold_i_to_musig_i(spec, shares)
.into_iter()
.map(|(p, share)| {
machine.read_share(&mut share.as_slice()).map(|share| (p, share)).map_err(|_| p)
})
.collect::<Result<HashMap<_, _>, _>>()?;
let signature = machine.complete(shares).map_err(|e| match e {
FrostError::InternalError(e) => unreachable!("FrostError::InternalError {e}"),
FrostError::InvalidParticipant(_, _) |
FrostError::InvalidSigningSet(_) |
FrostError::InvalidParticipantQuantity(_, _) |
FrostError::DuplicatedParticipant(_) |
FrostError::MissingParticipant(_) => unreachable!("{e:?}"),
FrostError::InvalidPreprocess(p) | FrostError::InvalidShare(p) => p,
})?;
Ok(signature.to_bytes())
}
}

View file

@ -1,241 +0,0 @@
use core::ops::Deref;
use std::collections::HashMap;
use zeroize::Zeroizing;
use rand_core::SeedableRng;
use rand_chacha::ChaCha20Rng;
use transcript::{Transcript, RecommendedTranscript};
use ciphersuite::{
group::{Group, GroupEncoding},
Ciphersuite, Ristretto,
};
use frost::{
FrostError,
dkg::{Participant, musig::musig},
sign::*,
};
use frost_schnorrkel::Schnorrkel;
use serai_client::{
Public, SeraiAddress,
validator_sets::primitives::{musig_context, remove_participant_message},
};
use crate::tributary::TributarySpec;
/*
The following is a clone of DkgConfirmer modified for DKG removals.
The notable difference is this uses a MuSig key of the first `t` participants to respond with
preprocesses, not all `n` participants.
TODO: Exact same commentary on seeded RNGs. The following can drop its seeded RNG if cached
preprocesses are used to carry the preprocess between machines
*/
pub(crate) struct DkgRemoval;
impl DkgRemoval {
// Convert the passed in HashMap, which uses the validators' start index for their `s` threshold
// shares, to the indexes needed for MuSig
fn from_threshold_i_to_musig_i(
mut old_map: HashMap<[u8; 32], Vec<u8>>,
) -> HashMap<Participant, Vec<u8>> {
let mut new_map = HashMap::new();
let mut participating = old_map.keys().cloned().collect::<Vec<_>>();
participating.sort();
for (i, participating) in participating.into_iter().enumerate() {
new_map.insert(
Participant::new(u16::try_from(i + 1).unwrap()).unwrap(),
old_map.remove(&participating).unwrap(),
);
}
new_map
}
fn preprocess_rng(
spec: &TributarySpec,
key: &Zeroizing<<Ristretto as Ciphersuite>::F>,
attempt: u32,
) -> ChaCha20Rng {
ChaCha20Rng::from_seed({
let mut entropy_transcript = RecommendedTranscript::new(b"DkgRemoval Entropy");
entropy_transcript.append_message(b"spec", spec.serialize());
entropy_transcript.append_message(b"key", Zeroizing::new(key.to_bytes()));
entropy_transcript.append_message(b"attempt", attempt.to_le_bytes());
Zeroizing::new(entropy_transcript).rng_seed(b"preprocess")
})
}
fn preprocess_internal(
spec: &TributarySpec,
key: &Zeroizing<<Ristretto as Ciphersuite>::F>,
attempt: u32,
participants: Option<&[<Ristretto as Ciphersuite>::G]>,
) -> (Option<AlgorithmSignMachine<Ristretto, Schnorrkel>>, [u8; 64]) {
// TODO: Diversify this among DkgConfirmer/DkgRemoval?
let context = musig_context(spec.set());
let (_, preprocess) = AlgorithmMachine::new(
Schnorrkel::new(b"substrate"),
// Preprocess with our key alone as we don't know the signing set yet
musig(&context, key, &[<Ristretto as Ciphersuite>::G::generator() * key.deref()])
.expect("couldn't get the MuSig key of our key alone")
.into(),
)
.preprocess(&mut Self::preprocess_rng(spec, key, attempt));
let machine = if let Some(participants) = participants {
let (machine, actual_preprocess) = AlgorithmMachine::new(
Schnorrkel::new(b"substrate"),
// Preprocess with our key alone as we don't know the signing set yet
musig(&context, key, participants)
.expect(
"couldn't create a MuSig key for the DKG removal we're supposedly participating in",
)
.into(),
)
.preprocess(&mut Self::preprocess_rng(spec, key, attempt));
// Doesn't use assert_eq due to lack of Debug
assert!(preprocess == actual_preprocess);
Some(machine)
} else {
None
};
(machine, preprocess.serialize().try_into().unwrap())
}
// Get the preprocess for this confirmation.
pub(crate) fn preprocess(
spec: &TributarySpec,
key: &Zeroizing<<Ristretto as Ciphersuite>::F>,
attempt: u32,
) -> [u8; 64] {
Self::preprocess_internal(spec, key, attempt, None).1
}
fn share_internal(
spec: &TributarySpec,
key: &Zeroizing<<Ristretto as Ciphersuite>::F>,
attempt: u32,
mut preprocesses: HashMap<Participant, Vec<u8>>,
removed: [u8; 32],
) -> Result<(AlgorithmSignatureMachine<Ristretto, Schnorrkel>, [u8; 32]), Participant> {
// TODO: Remove this ugly blob
let preprocesses = {
let mut preprocesses_participants = preprocesses.keys().cloned().collect::<Vec<_>>();
preprocesses_participants.sort();
let mut actual_keys = vec![];
let spec_validators = spec.validators();
for participant in &preprocesses_participants {
for (validator, _) in &spec_validators {
if participant == &spec.i(*validator).unwrap().start {
actual_keys.push(*validator);
}
}
}
let mut new_preprocesses = HashMap::new();
for (participant, actual_key) in
preprocesses_participants.into_iter().zip(actual_keys.into_iter())
{
new_preprocesses.insert(actual_key, preprocesses.remove(&participant).unwrap());
}
new_preprocesses
};
let participants = preprocesses.keys().cloned().collect::<Vec<_>>();
let preprocesses = Self::from_threshold_i_to_musig_i(
preprocesses.into_iter().map(|(key, preprocess)| (key.to_bytes(), preprocess)).collect(),
);
let machine = Self::preprocess_internal(spec, key, attempt, Some(&participants)).0.unwrap();
let preprocesses = preprocesses
.into_iter()
.map(|(p, preprocess)| {
machine
.read_preprocess(&mut preprocess.as_slice())
.map(|preprocess| (p, preprocess))
.map_err(|_| p)
})
.collect::<Result<HashMap<_, _>, _>>()?;
let (machine, share) = machine
.sign(preprocesses, &remove_participant_message(&spec.set(), Public(removed)))
.map_err(|e| match e {
FrostError::InternalError(e) => unreachable!("FrostError::InternalError {e}"),
FrostError::InvalidParticipant(_, _) |
FrostError::InvalidSigningSet(_) |
FrostError::InvalidParticipantQuantity(_, _) |
FrostError::DuplicatedParticipant(_) |
FrostError::MissingParticipant(_) => unreachable!("{e:?}"),
FrostError::InvalidPreprocess(p) | FrostError::InvalidShare(p) => p,
})?;
Ok((machine, share.serialize().try_into().unwrap()))
}
// Get the share for this confirmation, if the preprocesses are valid.
pub(crate) fn share(
spec: &TributarySpec,
key: &Zeroizing<<Ristretto as Ciphersuite>::F>,
attempt: u32,
preprocesses: HashMap<Participant, Vec<u8>>,
removed: [u8; 32],
) -> Result<[u8; 32], Participant> {
Self::share_internal(spec, key, attempt, preprocesses, removed).map(|(_, share)| share)
}
pub(crate) fn complete(
spec: &TributarySpec,
key: &Zeroizing<<Ristretto as Ciphersuite>::F>,
attempt: u32,
preprocesses: HashMap<Participant, Vec<u8>>,
removed: [u8; 32],
mut shares: HashMap<Participant, Vec<u8>>,
) -> Result<(Vec<SeraiAddress>, [u8; 64]), Participant> {
// TODO: Remove this ugly blob
let shares = {
let mut shares_participants = shares.keys().cloned().collect::<Vec<_>>();
shares_participants.sort();
let mut actual_keys = vec![];
let spec_validators = spec.validators();
for participant in &shares_participants {
for (validator, _) in &spec_validators {
if participant == &spec.i(*validator).unwrap().start {
actual_keys.push(*validator);
}
}
}
let mut new_shares = HashMap::new();
for (participant, actual_key) in shares_participants.into_iter().zip(actual_keys.into_iter())
{
new_shares.insert(actual_key.to_bytes(), shares.remove(&participant).unwrap());
}
new_shares
};
let mut signers = shares.keys().cloned().map(SeraiAddress).collect::<Vec<_>>();
signers.sort();
let machine = Self::share_internal(spec, key, attempt, preprocesses, removed)
.expect("trying to complete a machine which failed to preprocess")
.0;
let shares = Self::from_threshold_i_to_musig_i(shares)
.into_iter()
.map(|(p, share)| {
machine.read_share(&mut share.as_slice()).map(|share| (p, share)).map_err(|_| p)
})
.collect::<Result<HashMap<_, _>, _>>()?;
let signature = machine.complete(shares).map_err(|e| match e {
FrostError::InternalError(e) => unreachable!("FrostError::InternalError {e}"),
FrostError::InvalidParticipant(_, _) |
FrostError::InvalidSigningSet(_) |
FrostError::InvalidParticipantQuantity(_, _) |
FrostError::DuplicatedParticipant(_) |
FrostError::MissingParticipant(_) => unreachable!("{e:?}"),
FrostError::InvalidPreprocess(p) | FrostError::InvalidShare(p) => p,
})?;
Ok((signers, signature.to_bytes()))
}
}

File diff suppressed because it is too large Load diff

View file

@ -1,852 +1,63 @@
use core::{
ops::{Deref, Range},
fmt::Debug,
};
use std::io::{self, Read, Write};
use zeroize::Zeroizing;
use rand_core::{RngCore, CryptoRng};
use blake2::{Digest, Blake2s256};
use transcript::{Transcript, RecommendedTranscript};
use ciphersuite::{
group::{ff::Field, GroupEncoding},
Ciphersuite, Ristretto,
};
use schnorr::SchnorrSignature;
use frost::Participant;
use scale::{Encode, Decode};
use processor_messages::coordinator::SubstrateSignableId;
use serai_client::{
primitives::{NetworkId, PublicKey},
validator_sets::primitives::{Session, ValidatorSet},
};
#[rustfmt::skip]
use tributary::{
ReadWrite,
transaction::{Signed, TransactionError, TransactionKind, Transaction as TransactionTrait},
TRANSACTION_SIZE_LIMIT,
transaction::{TransactionError, TransactionKind, Transaction as TransactionTrait},
Tributary,
};
mod db;
pub use db::*;
mod dkg_confirmer;
mod dkg_removal;
mod spec;
pub use spec::TributarySpec;
mod transaction;
pub use transaction::{Label, SignData, Transaction};
mod signing_protocol;
mod handle;
pub use handle::*;
pub mod scanner;
#[derive(Clone, PartialEq, Eq, Debug)]
pub struct TributarySpec {
serai_block: [u8; 32],
start_time: u64,
set: ValidatorSet,
validators: Vec<(<Ristretto as Ciphersuite>::G, u16)>,
}
impl TributarySpec {
pub fn new(
serai_block: [u8; 32],
start_time: u64,
set: ValidatorSet,
set_participants: Vec<(PublicKey, u16)>,
) -> TributarySpec {
let mut validators = vec![];
for (participant, shares) in set_participants {
let participant = <Ristretto as Ciphersuite>::read_G::<&[u8]>(&mut participant.0.as_ref())
.expect("invalid key registered as participant");
validators.push((participant, shares));
}
Self { serai_block, start_time, set, validators }
}
pub fn set(&self) -> ValidatorSet {
self.set
}
pub fn genesis(&self) -> [u8; 32] {
// Calculate the genesis for this Tributary
let mut genesis = RecommendedTranscript::new(b"Serai Tributary Genesis");
// This locks it to a specific Serai chain
genesis.append_message(b"serai_block", self.serai_block);
genesis.append_message(b"session", self.set.session.0.to_le_bytes());
genesis.append_message(b"network", self.set.network.encode());
let genesis = genesis.challenge(b"genesis");
let genesis_ref: &[u8] = genesis.as_ref();
genesis_ref[.. 32].try_into().unwrap()
}
pub fn start_time(&self) -> u64 {
self.start_time
}
pub fn n(&self) -> u16 {
self.validators.iter().map(|(_, weight)| weight).sum()
}
pub fn t(&self) -> u16 {
((2 * self.n()) / 3) + 1
}
pub fn i(&self, key: <Ristretto as Ciphersuite>::G) -> Option<Range<Participant>> {
let mut i = 1;
for (validator, weight) in &self.validators {
if validator == &key {
return Some(Range {
start: Participant::new(i).unwrap(),
end: Participant::new(i + weight).unwrap(),
});
}
i += weight;
}
None
}
pub fn validators(&self) -> Vec<(<Ristretto as Ciphersuite>::G, u64)> {
self.validators.iter().map(|(validator, weight)| (*validator, u64::from(*weight))).collect()
}
pub fn write<W: Write>(&self, writer: &mut W) -> io::Result<()> {
writer.write_all(&self.serai_block)?;
writer.write_all(&self.start_time.to_le_bytes())?;
writer.write_all(&self.set.session.0.to_le_bytes())?;
let network_encoded = self.set.network.encode();
assert_eq!(network_encoded.len(), 1);
writer.write_all(&network_encoded)?;
writer.write_all(&u32::try_from(self.validators.len()).unwrap().to_le_bytes())?;
for validator in &self.validators {
writer.write_all(&validator.0.to_bytes())?;
writer.write_all(&validator.1.to_le_bytes())?;
}
Ok(())
}
pub fn serialize(&self) -> Vec<u8> {
let mut res = vec![];
self.write(&mut res).unwrap();
res
}
pub fn read<R: Read>(reader: &mut R) -> io::Result<Self> {
let mut serai_block = [0; 32];
reader.read_exact(&mut serai_block)?;
let mut start_time = [0; 8];
reader.read_exact(&mut start_time)?;
let start_time = u64::from_le_bytes(start_time);
let mut session = [0; 4];
reader.read_exact(&mut session)?;
let session = Session(u32::from_le_bytes(session));
let mut network = [0; 1];
reader.read_exact(&mut network)?;
let network =
NetworkId::decode(&mut &network[..]).map_err(|_| io::Error::other("invalid network"))?;
let mut validators_len = [0; 4];
reader.read_exact(&mut validators_len)?;
let validators_len = usize::try_from(u32::from_le_bytes(validators_len)).unwrap();
let mut validators = Vec::with_capacity(validators_len);
for _ in 0 .. validators_len {
let key = Ristretto::read_G(reader)?;
let mut weight = [0; 2];
reader.read_exact(&mut weight)?;
validators.push((key, u16::from_le_bytes(weight)));
}
Ok(Self { serai_block, start_time, set: ValidatorSet { session, network }, validators })
}
}
#[derive(Clone, PartialEq, Eq)]
pub struct SignData<Id: Clone + PartialEq + Eq + Debug + Encode + Decode> {
pub plan: Id,
pub attempt: u32,
pub data: Vec<Vec<u8>>,
pub signed: Signed,
}
impl<Id: Clone + PartialEq + Eq + Debug + Encode + Decode> Debug for SignData<Id> {
fn fmt(&self, fmt: &mut core::fmt::Formatter<'_>) -> Result<(), core::fmt::Error> {
fmt
.debug_struct("SignData")
.field("id", &hex::encode(self.plan.encode()))
.field("attempt", &self.attempt)
.field("signer", &hex::encode(self.signed.signer.to_bytes()))
.finish_non_exhaustive()
}
}
impl<Id: Clone + PartialEq + Eq + Debug + Encode + Decode> SignData<Id> {
pub(crate) fn read<R: io::Read>(reader: &mut R, nonce: u32) -> io::Result<Self> {
let plan = Id::decode(&mut scale::IoReader(&mut *reader))
.map_err(|_| io::Error::other("invalid plan in SignData"))?;
let mut attempt = [0; 4];
reader.read_exact(&mut attempt)?;
let attempt = u32::from_le_bytes(attempt);
let data = {
let mut data_pieces = [0];
reader.read_exact(&mut data_pieces)?;
if data_pieces[0] == 0 {
Err(io::Error::other("zero pieces of data in SignData"))?;
}
let mut all_data = vec![];
for _ in 0 .. data_pieces[0] {
let mut data_len = [0; 2];
reader.read_exact(&mut data_len)?;
let mut data = vec![0; usize::from(u16::from_le_bytes(data_len))];
reader.read_exact(&mut data)?;
all_data.push(data);
}
all_data
};
let signed = Signed::read_without_nonce(reader, nonce)?;
Ok(SignData { plan, attempt, data, signed })
}
pub(crate) fn write<W: io::Write>(&self, writer: &mut W) -> io::Result<()> {
writer.write_all(&self.plan.encode())?;
writer.write_all(&self.attempt.to_le_bytes())?;
writer.write_all(&[u8::try_from(self.data.len()).unwrap()])?;
for data in &self.data {
if data.len() > u16::MAX.into() {
// Currently, the largest individual preprocess is a Monero transaction
// It provides 4 commitments per input (128 bytes), a 64-byte proof for them, along with a
// key image and proof (96 bytes)
// Even with all of that, we could support 227 inputs in a single TX
// Monero is limited to ~120 inputs per TX
//
// Bitcoin has a much higher input count of 520, yet it only uses 64 bytes per preprocess
Err(io::Error::other("signing data exceeded 65535 bytes"))?;
}
writer.write_all(&u16::try_from(data.len()).unwrap().to_le_bytes())?;
writer.write_all(data)?;
}
self.signed.write_without_nonce(writer)
}
}
#[derive(Clone, PartialEq, Eq)]
pub enum Transaction {
RemoveParticipant(Participant),
// Once this completes successfully, no more instances should be created.
DkgCommitments(u32, Vec<Vec<u8>>, Signed),
DkgShares {
attempt: u32,
// Sending Participant, Receiving Participant, Share
shares: Vec<Vec<Vec<u8>>>,
confirmation_nonces: [u8; 64],
signed: Signed,
},
InvalidDkgShare {
attempt: u32,
accuser: Participant,
faulty: Participant,
blame: Option<Vec<u8>>,
signed: Signed,
},
DkgConfirmed(u32, [u8; 32], Signed),
DkgRemovalPreprocess(SignData<[u8; 32]>),
DkgRemovalShare(SignData<[u8; 32]>),
// Co-sign a Substrate block.
CosignSubstrateBlock([u8; 32]),
// When we have synchrony on a batch, we can allow signing it
// TODO (never?): This is less efficient compared to an ExternalBlock provided transaction,
// which would be binding over the block hash and automatically achieve synchrony on all
// relevant batches. ExternalBlock was removed for this due to complexity around the pipeline
// with the current processor, yet it would still be an improvement.
Batch([u8; 32], [u8; 5]),
// When a Serai block is finalized, with the contained batches, we can allow the associated plan
// IDs
SubstrateBlock(u64),
SubstratePreprocess(SignData<SubstrateSignableId>),
SubstrateShare(SignData<SubstrateSignableId>),
SignPreprocess(SignData<[u8; 32]>),
SignShare(SignData<[u8; 32]>),
// This is defined as an Unsigned transaction in order to de-duplicate SignCompleted amongst
// reporters (who should all report the same thing)
// We do still track the signer in order to prevent a single signer from publishing arbitrarily
// many TXs without penalty
// Here, they're denoted as the first_signer, as only the signer of the first TX to be included
// with this pairing will be remembered on-chain
SignCompleted {
plan: [u8; 32],
tx_hash: Vec<u8>,
first_signer: <Ristretto as Ciphersuite>::G,
signature: SchnorrSignature<Ristretto>,
},
}
impl Debug for Transaction {
fn fmt(&self, fmt: &mut core::fmt::Formatter<'_>) -> Result<(), core::fmt::Error> {
match self {
Transaction::RemoveParticipant(participant) => fmt
.debug_struct("Transaction::RemoveParticipant")
.field("participant", participant)
.finish(),
Transaction::DkgCommitments(attempt, _, signed) => fmt
.debug_struct("Transaction::DkgCommitments")
.field("attempt", attempt)
.field("signer", &hex::encode(signed.signer.to_bytes()))
.finish_non_exhaustive(),
Transaction::DkgShares { attempt, signed, .. } => fmt
.debug_struct("Transaction::DkgShares")
.field("attempt", attempt)
.field("signer", &hex::encode(signed.signer.to_bytes()))
.finish_non_exhaustive(),
Transaction::InvalidDkgShare { attempt, accuser, faulty, .. } => fmt
.debug_struct("Transaction::InvalidDkgShare")
.field("attempt", attempt)
.field("accuser", accuser)
.field("faulty", faulty)
.finish_non_exhaustive(),
Transaction::DkgConfirmed(attempt, _, signed) => fmt
.debug_struct("Transaction::DkgConfirmed")
.field("attempt", attempt)
.field("signer", &hex::encode(signed.signer.to_bytes()))
.finish_non_exhaustive(),
Transaction::DkgRemovalPreprocess(sign_data) => {
fmt.debug_struct("Transaction::DkgRemovalPreprocess").field("sign_data", sign_data).finish()
}
Transaction::DkgRemovalShare(sign_data) => {
fmt.debug_struct("Transaction::DkgRemovalShare").field("sign_data", sign_data).finish()
}
Transaction::CosignSubstrateBlock(block) => fmt
.debug_struct("Transaction::CosignSubstrateBlock")
.field("block", &hex::encode(block))
.finish(),
Transaction::Batch(block, batch) => fmt
.debug_struct("Transaction::Batch")
.field("block", &hex::encode(block))
.field("batch", &hex::encode(batch))
.finish(),
Transaction::SubstrateBlock(block) => {
fmt.debug_struct("Transaction::SubstrateBlock").field("block", block).finish()
}
Transaction::SubstratePreprocess(sign_data) => {
fmt.debug_struct("Transaction::SubstratePreprocess").field("sign_data", sign_data).finish()
}
Transaction::SubstrateShare(sign_data) => {
fmt.debug_struct("Transaction::SubstrateShare").field("sign_data", sign_data).finish()
}
Transaction::SignPreprocess(sign_data) => {
fmt.debug_struct("Transaction::SignPreprocess").field("sign_data", sign_data).finish()
}
Transaction::SignShare(sign_data) => {
fmt.debug_struct("Transaction::SignShare").field("sign_data", sign_data).finish()
}
Transaction::SignCompleted { plan, tx_hash, .. } => fmt
.debug_struct("Transaction::SignCompleted")
.field("plan", &hex::encode(plan))
.field("tx_hash", &hex::encode(tx_hash))
.finish_non_exhaustive(),
}
}
}
impl ReadWrite for Transaction {
fn read<R: io::Read>(reader: &mut R) -> io::Result<Self> {
let mut kind = [0];
reader.read_exact(&mut kind)?;
match kind[0] {
0 => Ok(Transaction::RemoveParticipant({
let mut participant = [0; 2];
reader.read_exact(&mut participant)?;
Participant::new(u16::from_le_bytes(participant))
.ok_or_else(|| io::Error::other("invalid participant in RemoveParticipant"))?
})),
1 => {
let mut attempt = [0; 4];
reader.read_exact(&mut attempt)?;
let attempt = u32::from_le_bytes(attempt);
let commitments = {
let mut commitments_len = [0; 1];
reader.read_exact(&mut commitments_len)?;
let commitments_len = usize::from(commitments_len[0]);
if commitments_len == 0 {
Err(io::Error::other("zero commitments in DkgCommitments"))?;
}
let mut each_commitments_len = [0; 2];
reader.read_exact(&mut each_commitments_len)?;
let each_commitments_len = usize::from(u16::from_le_bytes(each_commitments_len));
if (commitments_len * each_commitments_len) > TRANSACTION_SIZE_LIMIT {
Err(io::Error::other(
"commitments present in transaction exceeded transaction size limit",
))?;
}
let mut commitments = vec![vec![]; commitments_len];
for commitments in &mut commitments {
*commitments = vec![0; each_commitments_len];
reader.read_exact(commitments)?;
}
commitments
};
let signed = Signed::read_without_nonce(reader, 0)?;
Ok(Transaction::DkgCommitments(attempt, commitments, signed))
}
2 => {
let mut attempt = [0; 4];
reader.read_exact(&mut attempt)?;
let attempt = u32::from_le_bytes(attempt);
let shares = {
let mut share_quantity = [0; 1];
reader.read_exact(&mut share_quantity)?;
let mut key_share_quantity = [0; 1];
reader.read_exact(&mut key_share_quantity)?;
let mut share_len = [0; 2];
reader.read_exact(&mut share_len)?;
let share_len = usize::from(u16::from_le_bytes(share_len));
let mut all_shares = vec![];
for _ in 0 .. share_quantity[0] {
let mut shares = vec![];
for _ in 0 .. key_share_quantity[0] {
let mut share = vec![0; share_len];
reader.read_exact(&mut share)?;
shares.push(share);
}
all_shares.push(shares);
}
all_shares
};
let mut confirmation_nonces = [0; 64];
reader.read_exact(&mut confirmation_nonces)?;
let signed = Signed::read_without_nonce(reader, 1)?;
Ok(Transaction::DkgShares { attempt, shares, confirmation_nonces, signed })
}
3 => {
let mut attempt = [0; 4];
reader.read_exact(&mut attempt)?;
let attempt = u32::from_le_bytes(attempt);
let mut accuser = [0; 2];
reader.read_exact(&mut accuser)?;
let accuser = Participant::new(u16::from_le_bytes(accuser))
.ok_or_else(|| io::Error::other("invalid participant in InvalidDkgShare"))?;
let mut faulty = [0; 2];
reader.read_exact(&mut faulty)?;
let faulty = Participant::new(u16::from_le_bytes(faulty))
.ok_or_else(|| io::Error::other("invalid participant in InvalidDkgShare"))?;
let mut blame_len = [0; 2];
reader.read_exact(&mut blame_len)?;
let mut blame = vec![0; u16::from_le_bytes(blame_len).into()];
reader.read_exact(&mut blame)?;
// This shares a nonce with DkgConfirmed as only one is expected
let signed = Signed::read_without_nonce(reader, 2)?;
Ok(Transaction::InvalidDkgShare {
attempt,
accuser,
faulty,
blame: Some(blame).filter(|blame| !blame.is_empty()),
signed,
})
}
4 => {
let mut attempt = [0; 4];
reader.read_exact(&mut attempt)?;
let attempt = u32::from_le_bytes(attempt);
let mut confirmation_share = [0; 32];
reader.read_exact(&mut confirmation_share)?;
let signed = Signed::read_without_nonce(reader, 2)?;
Ok(Transaction::DkgConfirmed(attempt, confirmation_share, signed))
}
5 => SignData::read(reader, 0).map(Transaction::DkgRemovalPreprocess),
6 => SignData::read(reader, 1).map(Transaction::DkgRemovalShare),
7 => {
let mut block = [0; 32];
reader.read_exact(&mut block)?;
Ok(Transaction::CosignSubstrateBlock(block))
}
8 => {
let mut block = [0; 32];
reader.read_exact(&mut block)?;
let mut batch = [0; 5];
reader.read_exact(&mut batch)?;
Ok(Transaction::Batch(block, batch))
}
9 => {
let mut block = [0; 8];
reader.read_exact(&mut block)?;
Ok(Transaction::SubstrateBlock(u64::from_le_bytes(block)))
}
10 => SignData::read(reader, 0).map(Transaction::SubstratePreprocess),
11 => SignData::read(reader, 1).map(Transaction::SubstrateShare),
12 => SignData::read(reader, 0).map(Transaction::SignPreprocess),
13 => SignData::read(reader, 1).map(Transaction::SignShare),
14 => {
let mut plan = [0; 32];
reader.read_exact(&mut plan)?;
let mut tx_hash_len = [0];
reader.read_exact(&mut tx_hash_len)?;
let mut tx_hash = vec![0; usize::from(tx_hash_len[0])];
reader.read_exact(&mut tx_hash)?;
let first_signer = Ristretto::read_G(reader)?;
let signature = SchnorrSignature::<Ristretto>::read(reader)?;
Ok(Transaction::SignCompleted { plan, tx_hash, first_signer, signature })
}
_ => Err(io::Error::other("invalid transaction type")),
}
}
fn write<W: io::Write>(&self, writer: &mut W) -> io::Result<()> {
match self {
Transaction::RemoveParticipant(i) => {
writer.write_all(&[0])?;
writer.write_all(&u16::from(*i).to_le_bytes())
}
Transaction::DkgCommitments(attempt, commitments, signed) => {
writer.write_all(&[1])?;
writer.write_all(&attempt.to_le_bytes())?;
if commitments.is_empty() {
Err(io::Error::other("zero commitments in DkgCommitments"))?
}
writer.write_all(&[u8::try_from(commitments.len()).unwrap()])?;
for commitments_i in commitments {
if commitments_i.len() != commitments[0].len() {
Err(io::Error::other("commitments of differing sizes in DkgCommitments"))?
}
}
writer.write_all(&u16::try_from(commitments[0].len()).unwrap().to_le_bytes())?;
for commitments in commitments {
writer.write_all(commitments)?;
}
signed.write_without_nonce(writer)
}
Transaction::DkgShares { attempt, shares, confirmation_nonces, signed } => {
writer.write_all(&[2])?;
writer.write_all(&attempt.to_le_bytes())?;
// `shares` is a Vec which is supposed to map to a HashMap<Pariticpant, Vec<u8>>. Since we
// bound participants to 150, this conversion is safe if a valid in-memory transaction.
writer.write_all(&[u8::try_from(shares.len()).unwrap()])?;
// This assumes at least one share is being sent to another party
writer.write_all(&[u8::try_from(shares[0].len()).unwrap()])?;
let share_len = shares[0][0].len();
// For BLS12-381 G2, this would be:
// - A 32-byte share
// - A 96-byte ephemeral key
// - A 128-byte signature
// Hence why this has to be u16
writer.write_all(&u16::try_from(share_len).unwrap().to_le_bytes())?;
for these_shares in shares {
assert_eq!(these_shares.len(), shares[0].len(), "amount of sent shares was variable");
for share in these_shares {
assert_eq!(share.len(), share_len, "sent shares were of variable length");
writer.write_all(share)?;
}
}
writer.write_all(confirmation_nonces)?;
signed.write_without_nonce(writer)
}
Transaction::InvalidDkgShare { attempt, accuser, faulty, blame, signed } => {
writer.write_all(&[3])?;
writer.write_all(&attempt.to_le_bytes())?;
writer.write_all(&u16::from(*accuser).to_le_bytes())?;
writer.write_all(&u16::from(*faulty).to_le_bytes())?;
// Flattens Some(vec![]) to None on the expectation no actual blame will be 0-length
assert!(blame.as_ref().map(|blame| blame.len()).unwrap_or(1) != 0);
let blame_len =
u16::try_from(blame.as_ref().unwrap_or(&vec![]).len()).expect("blame exceeded 64 KB");
writer.write_all(&blame_len.to_le_bytes())?;
writer.write_all(blame.as_ref().unwrap_or(&vec![]))?;
signed.write_without_nonce(writer)
}
Transaction::DkgConfirmed(attempt, share, signed) => {
writer.write_all(&[4])?;
writer.write_all(&attempt.to_le_bytes())?;
writer.write_all(share)?;
signed.write_without_nonce(writer)
}
Transaction::DkgRemovalPreprocess(data) => {
writer.write_all(&[5])?;
data.write(writer)
}
Transaction::DkgRemovalShare(data) => {
writer.write_all(&[6])?;
data.write(writer)
}
Transaction::CosignSubstrateBlock(block) => {
writer.write_all(&[7])?;
writer.write_all(block)
}
Transaction::Batch(block, batch) => {
writer.write_all(&[8])?;
writer.write_all(block)?;
writer.write_all(batch)
}
Transaction::SubstrateBlock(block) => {
writer.write_all(&[9])?;
writer.write_all(&block.to_le_bytes())
}
Transaction::SubstratePreprocess(data) => {
writer.write_all(&[10])?;
data.write(writer)
}
Transaction::SubstrateShare(data) => {
writer.write_all(&[11])?;
data.write(writer)
}
Transaction::SignPreprocess(data) => {
writer.write_all(&[12])?;
data.write(writer)
}
Transaction::SignShare(data) => {
writer.write_all(&[13])?;
data.write(writer)
}
Transaction::SignCompleted { plan, tx_hash, first_signer, signature } => {
writer.write_all(&[14])?;
writer.write_all(plan)?;
writer
.write_all(&[u8::try_from(tx_hash.len()).expect("tx hash length exceed 255 bytes")])?;
writer.write_all(tx_hash)?;
writer.write_all(&first_signer.to_bytes())?;
signature.write(writer)
}
}
}
}
impl TransactionTrait for Transaction {
fn kind(&self) -> TransactionKind<'_> {
match self {
Transaction::RemoveParticipant(_) => TransactionKind::Provided("remove"),
Transaction::DkgCommitments(attempt, _, signed) => {
TransactionKind::Signed((b"dkg", attempt).encode(), signed)
}
Transaction::DkgShares { attempt, signed, .. } => {
TransactionKind::Signed((b"dkg", attempt).encode(), signed)
}
Transaction::InvalidDkgShare { attempt, signed, .. } => {
TransactionKind::Signed((b"dkg", attempt).encode(), signed)
}
Transaction::DkgConfirmed(attempt, _, signed) => {
TransactionKind::Signed((b"dkg", attempt).encode(), signed)
}
Transaction::DkgRemovalPreprocess(data) => {
TransactionKind::Signed((b"dkg_removal", data.plan, data.attempt).encode(), &data.signed)
}
Transaction::DkgRemovalShare(data) => {
TransactionKind::Signed((b"dkg_removal", data.plan, data.attempt).encode(), &data.signed)
}
Transaction::CosignSubstrateBlock(_) => TransactionKind::Provided("cosign"),
Transaction::Batch(_, _) => TransactionKind::Provided("batch"),
Transaction::SubstrateBlock(_) => TransactionKind::Provided("serai"),
Transaction::SubstratePreprocess(data) => {
TransactionKind::Signed((b"substrate", data.plan, data.attempt).encode(), &data.signed)
}
Transaction::SubstrateShare(data) => {
TransactionKind::Signed((b"substrate", data.plan, data.attempt).encode(), &data.signed)
}
Transaction::SignPreprocess(data) => {
TransactionKind::Signed((b"sign", data.plan, data.attempt).encode(), &data.signed)
}
Transaction::SignShare(data) => {
TransactionKind::Signed((b"sign", data.plan, data.attempt).encode(), &data.signed)
}
Transaction::SignCompleted { .. } => TransactionKind::Unsigned,
}
}
fn hash(&self) -> [u8; 32] {
let mut tx = self.serialize();
if let TransactionKind::Signed(_, signed) = self.kind() {
// Make sure the part we're cutting off is the signature
assert_eq!(tx.drain((tx.len() - 64) ..).collect::<Vec<_>>(), signed.signature.serialize());
}
Blake2s256::digest([b"Coordinator Tributary Transaction".as_slice(), &tx].concat()).into()
}
fn verify(&self) -> Result<(), TransactionError> {
if let Transaction::SubstrateShare(data) = self {
for data in &data.data {
if data.len() != 32 {
Err(TransactionError::InvalidContent)?;
}
}
}
if let Transaction::SignCompleted { first_signer, signature, .. } = self {
if !signature.verify(*first_signer, self.sign_completed_challenge()) {
Err(TransactionError::InvalidContent)?;
}
}
Ok(())
}
}
impl Transaction {
// Used to initially construct transactions so we can then get sig hashes and perform signing
pub fn empty_signed() -> Signed {
Signed {
signer: Ristretto::generator(),
nonce: 0,
signature: SchnorrSignature::<Ristretto> {
R: Ristretto::generator(),
s: <Ristretto as Ciphersuite>::F::ZERO,
},
}
}
// Sign a transaction
pub fn sign<R: RngCore + CryptoRng>(
&mut self,
rng: &mut R,
genesis: [u8; 32],
key: &Zeroizing<<Ristretto as Ciphersuite>::F>,
pub async fn publish_signed_transaction<D: Db, P: crate::P2p>(
txn: &mut D::Transaction<'_>,
tributary: &Tributary<D, Transaction, P>,
tx: Transaction,
) {
fn signed(tx: &mut Transaction) -> (u32, &mut Signed) {
let nonce = match tx {
Transaction::RemoveParticipant(_) => panic!("signing RemoveParticipant"),
log::debug!("publishing transaction {}", hex::encode(tx.hash()));
Transaction::DkgCommitments(_, _, _) => 0,
Transaction::DkgShares { .. } => 1,
Transaction::InvalidDkgShare { .. } => 2,
Transaction::DkgConfirmed(_, _, _) => 2,
let (order, signer) = if let TransactionKind::Signed(order, signed) = tx.kind() {
let signer = signed.signer;
Transaction::DkgRemovalPreprocess(_) => 0,
Transaction::DkgRemovalShare(_) => 1,
// Safe as we should deterministically create transactions, meaning if this is already on-disk,
// it's what we're saving now
SignedTransactionDb::set(txn, &order, signed.nonce, &tx.serialize());
Transaction::CosignSubstrateBlock(_) => panic!("signing CosignSubstrateBlock"),
Transaction::Batch(_, _) => panic!("signing Batch"),
Transaction::SubstrateBlock(_) => panic!("signing SubstrateBlock"),
Transaction::SubstratePreprocess(_) => 0,
Transaction::SubstrateShare(_) => 1,
Transaction::SignPreprocess(_) => 0,
Transaction::SignShare(_) => 1,
Transaction::SignCompleted { .. } => panic!("signing SignCompleted"),
(order, signer)
} else {
panic!("non-signed transaction passed to publish_signed_transaction");
};
(
nonce,
match tx {
Transaction::RemoveParticipant(_) => panic!("signing RemoveParticipant"),
Transaction::DkgCommitments(_, _, ref mut signed) => signed,
Transaction::DkgShares { ref mut signed, .. } => signed,
Transaction::InvalidDkgShare { ref mut signed, .. } => signed,
Transaction::DkgConfirmed(_, _, ref mut signed) => signed,
Transaction::DkgRemovalPreprocess(ref mut data) => &mut data.signed,
Transaction::DkgRemovalShare(ref mut data) => &mut data.signed,
Transaction::CosignSubstrateBlock(_) => panic!("signing CosignSubstrateBlock"),
Transaction::Batch(_, _) => panic!("signing Batch"),
Transaction::SubstrateBlock(_) => panic!("signing SubstrateBlock"),
Transaction::SubstratePreprocess(ref mut data) => &mut data.signed,
Transaction::SubstrateShare(ref mut data) => &mut data.signed,
Transaction::SignPreprocess(ref mut data) => &mut data.signed,
Transaction::SignShare(ref mut data) => &mut data.signed,
Transaction::SignCompleted { .. } => panic!("signing SignCompleted"),
},
)
// If we're trying to publish 5, when the last transaction published was 3, this will delay
// publication until the point in time we publish 4
while let Some(tx) = SignedTransactionDb::take_signed_transaction(
txn,
&order,
tributary
.next_nonce(&signer, &order)
.await
.expect("we don't have a nonce, meaning we aren't a participant on this tributary"),
) {
// We need to return a proper error here to enable that, due to a race condition around
// multiple publications
match tributary.add_transaction(tx.clone()).await {
Ok(_) => {}
// Some asynchonicity if InvalidNonce, assumed safe to deterministic nonces
Err(TransactionError::InvalidNonce) => {
log::warn!("publishing TX {tx:?} returned InvalidNonce. was it already added?")
}
let (nonce, signed_ref) = signed(self);
signed_ref.signer = Ristretto::generator() * key.deref();
signed_ref.nonce = nonce;
let sig_nonce = Zeroizing::new(<Ristretto as Ciphersuite>::F::random(rng));
signed(self).1.signature.R = <Ristretto as Ciphersuite>::generator() * sig_nonce.deref();
let sig_hash = self.sig_hash(genesis);
signed(self).1.signature = SchnorrSignature::<Ristretto>::sign(key, sig_nonce, sig_hash);
}
pub fn sign_completed_challenge(&self) -> <Ristretto as Ciphersuite>::F {
if let Transaction::SignCompleted { plan, tx_hash, first_signer, signature } = self {
let mut transcript =
RecommendedTranscript::new(b"Coordinator Tributary Transaction SignCompleted");
transcript.append_message(b"plan", plan);
transcript.append_message(b"tx_hash", tx_hash);
transcript.append_message(b"signer", first_signer.to_bytes());
transcript.append_message(b"nonce", signature.R.to_bytes());
Ristretto::hash_to_F(b"SignCompleted signature", &transcript.challenge(b"challenge"))
} else {
panic!("sign_completed_challenge called on transaction which wasn't SignCompleted")
Err(e) => panic!("created an invalid transaction: {e:?}"),
}
}
}

View file

@ -1,9 +1,12 @@
use core::{future::Future, time::Duration};
use core::{marker::PhantomData, future::Future, time::Duration};
use std::sync::Arc;
use rand_core::OsRng;
use zeroize::Zeroizing;
use ciphersuite::{Ciphersuite, Ristretto};
use ciphersuite::{group::GroupEncoding, Ciphersuite, Ristretto};
use frost::Participant;
use tokio::sync::broadcast;
@ -22,9 +25,11 @@ use tributary::{
use crate::{
Db,
tributary::handle::{fatal_slash, handle_application_tx},
processors::Processors,
tributary::{TributarySpec, Transaction, LastBlock, EventDb},
tributary::{
TributarySpec, Label, SignData, Transaction, Topic, AttemptDb, LastHandledBlock,
FatallySlashed, DkgCompleted, signing_protocol::DkgRemoval,
},
P2p,
};
@ -34,13 +39,31 @@ pub enum RecognizedIdType {
Plan,
}
pub(crate) trait RIDTrait<FRid>:
Clone + Fn(ValidatorSet, [u8; 32], RecognizedIdType, Vec<u8>) -> FRid
{
#[async_trait::async_trait]
pub trait RIDTrait {
async fn recognized_id(
&self,
set: ValidatorSet,
genesis: [u8; 32],
kind: RecognizedIdType,
id: Vec<u8>,
);
}
impl<FRid, F: Clone + Fn(ValidatorSet, [u8; 32], RecognizedIdType, Vec<u8>) -> FRid> RIDTrait<FRid>
for F
#[async_trait::async_trait]
impl<
FRid: Send + Future<Output = ()>,
F: Sync + Fn(ValidatorSet, [u8; 32], RecognizedIdType, Vec<u8>) -> FRid,
> RIDTrait for F
{
async fn recognized_id(
&self,
set: ValidatorSet,
genesis: [u8; 32],
kind: RecognizedIdType,
id: Vec<u8>,
) {
(self)(set, genesis, kind, id).await
}
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
@ -49,42 +72,123 @@ pub enum PstTxType {
RemoveParticipant([u8; 32]),
}
// Handle a specific Tributary block
#[allow(clippy::too_many_arguments)]
async fn handle_block<
D: Db,
Pro: Processors,
FPst: Future<Output = ()>,
PST: Clone + Fn(ValidatorSet, PstTxType, serai_client::Transaction) -> FPst,
FPtt: Future<Output = ()>,
PTT: Clone + Fn(Transaction) -> FPtt,
FRid: Future<Output = ()>,
RID: RIDTrait<FRid>,
P: P2p,
>(
db: &mut D,
key: &Zeroizing<<Ristretto as Ciphersuite>::F>,
recognized_id: RID,
processors: &Pro,
publish_serai_tx: PST,
publish_tributary_tx: &PTT,
spec: &TributarySpec,
block: Block<Transaction>,
#[async_trait::async_trait]
pub trait PSTTrait {
async fn publish_serai_tx(
&self,
set: ValidatorSet,
kind: PstTxType,
tx: serai_client::Transaction,
);
}
#[async_trait::async_trait]
impl<
FPst: Send + Future<Output = ()>,
F: Sync + Fn(ValidatorSet, PstTxType, serai_client::Transaction) -> FPst,
> PSTTrait for F
{
async fn publish_serai_tx(
&self,
set: ValidatorSet,
kind: PstTxType,
tx: serai_client::Transaction,
) {
log::info!("found block for Tributary {:?}", spec.set());
let hash = block.hash();
let mut event_id = 0;
#[allow(clippy::explicit_counter_loop)] // event_id isn't TX index. It just currently lines up
for tx in block.transactions {
if EventDb::get(db, hash, event_id).is_some() {
event_id += 1;
continue;
(self)(set, kind, tx).await
}
}
let mut txn = db.txn();
#[async_trait::async_trait]
pub trait PTTTrait {
async fn publish_tributary_tx(&self, tx: Transaction);
}
#[async_trait::async_trait]
impl<FPtt: Send + Future<Output = ()>, F: Sync + Fn(Transaction) -> FPtt> PTTTrait for F {
async fn publish_tributary_tx(&self, tx: Transaction) {
(self)(tx).await
}
}
pub struct TributaryBlockHandler<
'a,
T: DbTxn,
Pro: Processors,
PST: PSTTrait,
PTT: PTTTrait,
RID: RIDTrait,
P: P2p,
> {
pub txn: &'a mut T,
pub our_key: &'a Zeroizing<<Ristretto as Ciphersuite>::F>,
pub recognized_id: &'a RID,
pub processors: &'a Pro,
pub publish_serai_tx: &'a PST,
pub publish_tributary_tx: &'a PTT,
pub spec: &'a TributarySpec,
block: Block<Transaction>,
_p2p: PhantomData<P>,
}
impl<T: DbTxn, Pro: Processors, PST: PSTTrait, PTT: PTTTrait, RID: RIDTrait, P: P2p>
TributaryBlockHandler<'_, T, Pro, PST, PTT, RID, P>
{
pub async fn fatal_slash(&mut self, slashing: [u8; 32], reason: &str) {
let genesis = self.spec.genesis();
log::warn!("fatally slashing {}. reason: {}", hex::encode(slashing), reason);
FatallySlashed::set_fatally_slashed(self.txn, genesis, slashing);
// TODO: disconnect the node from network/ban from further participation in all Tributaries
// TODO: If during DKG, trigger a re-attempt
// Despite triggering a re-attempt, this DKG may still complete and may become in-use
// If during a DKG, remove the participant
if DkgCompleted::get(self.txn, genesis).is_none() {
AttemptDb::recognize_topic(self.txn, genesis, Topic::DkgRemoval(slashing));
let preprocess = (DkgRemoval {
spec: self.spec,
key: self.our_key,
txn: self.txn,
removing: slashing,
attempt: 0,
})
.preprocess();
let mut tx = Transaction::DkgRemoval(SignData {
plan: slashing,
attempt: 0,
label: Label::Preprocess,
data: vec![preprocess.to_vec()],
signed: Transaction::empty_signed(),
});
tx.sign(&mut OsRng, genesis, self.our_key);
self.publish_tributary_tx.publish_tributary_tx(tx).await;
}
}
// TODO: Once Substrate confirms a key, we need to rotate our validator set OR form a second
// Tributary post-DKG
// https://github.com/serai-dex/serai/issues/426
pub async fn fatal_slash_with_participant_index(&mut self, i: Participant, reason: &str) {
// Resolve from Participant to <Ristretto as Ciphersuite>::G
let i = u16::from(i);
let mut validator = None;
for (potential, _) in self.spec.validators() {
let v_i = self.spec.i(potential).unwrap();
if (u16::from(v_i.start) <= i) && (i < u16::from(v_i.end)) {
validator = Some(potential);
break;
}
}
let validator = validator.unwrap();
self.fatal_slash(validator.to_bytes(), reason).await;
}
async fn handle<D: Db>(mut self) {
log::info!("found block for Tributary {:?}", self.spec.set());
let transactions = self.block.transactions.clone();
for tx in transactions {
match tx {
TributaryTransaction::Tendermint(TendermintTx::SlashEvidence(ev)) => {
// Since the evidence is on the chain, it should already have been validated
@ -107,65 +211,42 @@ async fn handle_block<
},
);
// Since anything with evidence is fundamentally faulty behavior, not just temporal errors,
// mark the node as fatally slashed
fatal_slash::<D, _, _>(
&mut txn,
spec,
publish_tributary_tx,
key,
msgs.0.msg.sender,
&format!("invalid tendermint messages: {:?}", msgs),
)
// Since anything with evidence is fundamentally faulty behavior, not just temporal
// errors, mark the node as fatally slashed
self
.fatal_slash(msgs.0.msg.sender, &format!("invalid tendermint messages: {:?}", msgs))
.await;
}
TributaryTransaction::Application(tx) => {
handle_application_tx::<D, _, _, _, _, _, _, _>(
tx,
spec,
processors,
publish_serai_tx.clone(),
publish_tributary_tx,
key,
recognized_id.clone(),
&mut txn,
)
.await;
self.handle_application_tx(tx).await;
}
}
EventDb::handle_event(&mut txn, hash, event_id);
txn.commit();
event_id += 1;
}
// TODO: Trigger any necessary re-attempts
}
}
#[allow(clippy::too_many_arguments)]
pub(crate) async fn handle_new_blocks<
D: Db,
Pro: Processors,
FPst: Future<Output = ()>,
PST: Clone + Fn(ValidatorSet, PstTxType, serai_client::Transaction) -> FPst,
FPtt: Future<Output = ()>,
PTT: Clone + Fn(Transaction) -> FPtt,
FRid: Future<Output = ()>,
RID: RIDTrait<FRid>,
PST: PSTTrait,
PTT: PTTTrait,
RID: RIDTrait,
P: P2p,
>(
db: &mut D,
key: &Zeroizing<<Ristretto as Ciphersuite>::F>,
recognized_id: RID,
recognized_id: &RID,
processors: &Pro,
publish_serai_tx: PST,
publish_serai_tx: &PST,
publish_tributary_tx: &PTT,
spec: &TributarySpec,
tributary: &TributaryReader<D, Transaction>,
) {
let genesis = tributary.genesis();
let mut last_block = LastBlock::get(db, genesis).unwrap_or(genesis);
let mut last_block = LastHandledBlock::get(db, genesis).unwrap_or(genesis);
while let Some(next) = tributary.block_after(&last_block) {
let block = tributary.block(&next).unwrap();
@ -182,20 +263,22 @@ pub(crate) async fn handle_new_blocks<
}
}
handle_block::<_, _, _, _, _, _, _, _, P>(
db,
key,
recognized_id.clone(),
processors,
publish_serai_tx.clone(),
publish_tributary_tx,
let mut txn = db.txn();
(TributaryBlockHandler {
txn: &mut txn,
spec,
our_key: key,
recognized_id,
processors,
publish_serai_tx,
publish_tributary_tx,
block,
)
_p2p: PhantomData::<P>,
})
.handle::<D>()
.await;
last_block = next;
let mut txn = db.txn();
LastBlock::set(&mut txn, genesis, &next);
LastHandledBlock::set(&mut txn, genesis, &next);
txn.commit();
}
}
@ -204,8 +287,7 @@ pub(crate) async fn scan_tributaries_task<
D: Db,
Pro: Processors,
P: P2p,
FRid: Send + Future<Output = ()>,
RID: 'static + Send + Sync + RIDTrait<FRid>,
RID: 'static + Send + Sync + Clone + RIDTrait,
>(
raw_db: D,
key: Zeroizing<<Ristretto as Ciphersuite>::F>,
@ -240,12 +322,12 @@ pub(crate) async fn scan_tributaries_task<
// the next block occurs
let next_block_notification = tributary.next_block_notification().await;
handle_new_blocks::<_, _, _, _, _, _, _, _, P>(
handle_new_blocks::<_, _, _, _, _, P>(
&mut tributary_db,
&key,
recognized_id.clone(),
&recognized_id,
&processors,
|set, tx_type, tx| {
&|set, tx_type, tx| {
let serai = serai.clone();
async move {
loop {
@ -314,7 +396,7 @@ pub(crate) async fn scan_tributaries_task<
}
}
},
&|tx| {
&|tx: Transaction| {
let tributary = tributary.clone();
async move {
match tributary.add_transaction(tx.clone()).await {

View file

@ -0,0 +1,395 @@
/*
A MuSig-based signing protocol executed with the validators' keys.
This is used for confirming the results of a DKG on-chain, an operation requiring all validators,
and for removing another validator before the DKG completes, an operation requiring a
supermajority of validators.
Since we're using the validator's keys, as needed for their being the root of trust, the
coordinator must perform the signing. This is distinct from all other group-signing operations,
as they're all done by the processor.
The MuSig-aggregation achieves on-chain efficiency and enables a more secure design pattern.
While we could individually tack votes, that'd require logic to prevent voting multiple times and
tracking the accumulated votes. MuSig-aggregation simply requires checking the list is sorted and
the list's weight exceeds the threshold.
Instead of maintaining state in memory, a combination of the DB and re-execution are used. This
is deemed acceptable re: performance as:
1) This is only done prior to a DKG being confirmed on Substrate and is assumed infrequent.
2) This is an O(n) algorithm.
3) The size of the validator set is bounded by MAX_KEY_SHARES_PER_SET.
Accordingly, this should be tolerable.
As for safety, it is explicitly unsafe to reuse nonces across signing sessions. This raises
concerns regarding our re-execution which is dependent on fixed nonces. Safety is derived from
the nonces being context-bound under a BFT protocol. The flow is as follows:
1) Decide the nonce.
2) Publish the nonces' commitments, receiving everyone elses *and potentially the message to be
signed*.
3) Sign and publish the signature share.
In order for nonce re-use to occur, the received nonce commitments (or the message to be signed)
would have to be distinct and sign would have to be called again.
Before we act on any received messages, they're ordered and finalized by a BFT algorithm. The
only way to operate on distinct received messages would be if:
1) A logical flaw exists, letting new messages over write prior messages
2) A reorganization occured from chain A to chain B, and with it, different messages
Reorganizations are not supported, as BFT is assumed by the presence of a BFT algorithm. While
a significant amount of processes may be byzantine, leading to BFT being broken, that still will
not trigger a reorganization. The only way to move to a distinct chain, with distinct messages,
would be by rebuilding the local process (this time following chain B). Upon any complete
rebuild, we'd re-decide nonces, achieving safety. This does set a bound preventing partial
rebuilds which is accepted.
Additionally, to ensure a rebuilt service isn't flagged as malicious, we have to check the
commitments generated from the decided nonces are in fact its commitments on-chain (TODO).
TODO: We also need to review how we're handling Processor preprocesses and likely implement the
same on-chain-preprocess-matches-presumed-preprocess check before publishing shares.
*/
use core::ops::Deref;
use std::collections::HashMap;
use zeroize::{Zeroize, Zeroizing};
use rand_core::OsRng;
use blake2::{Digest, Blake2s256};
use ciphersuite::{
group::{ff::PrimeField, Group, GroupEncoding},
Ciphersuite, Ristretto,
};
use frost::{
FrostError,
dkg::{Participant, musig::musig},
ThresholdKeys,
sign::*,
};
use frost_schnorrkel::Schnorrkel;
use scale::Encode;
use serai_client::{
Public, SeraiAddress,
validator_sets::primitives::{
KeyPair, musig_context, set_keys_message, remove_participant_message,
},
};
use serai_db::*;
use crate::tributary::TributarySpec;
create_db!(
SigningProtocolDb {
CachedPreprocesses: (context: &impl Encode) -> [u8; 32]
}
);
struct SigningProtocol<'a, T: DbTxn, C: Encode> {
pub(crate) key: &'a Zeroizing<<Ristretto as Ciphersuite>::F>,
pub(crate) spec: &'a TributarySpec,
pub(crate) txn: &'a mut T,
pub(crate) context: C,
}
impl<T: DbTxn, C: Encode> SigningProtocol<'_, T, C> {
fn preprocess_internal(
&mut self,
participants: &[<Ristretto as Ciphersuite>::G],
) -> (AlgorithmSignMachine<Ristretto, Schnorrkel>, [u8; 64]) {
// Encrypt the cached preprocess as recovery of it will enable recovering the private key
// While the DB isn't expected to be arbitrarily readable, it isn't a proper secret store and
// shouldn't be trusted as one
let mut encryption_key = {
let mut encryption_key_preimage =
Zeroizing::new(b"Cached Preprocess Encryption Key".to_vec());
encryption_key_preimage.extend(self.context.encode());
let repr = Zeroizing::new(self.key.to_repr());
encryption_key_preimage.extend(repr.deref());
Blake2s256::digest(&encryption_key_preimage)
};
let encryption_key_slice: &mut [u8] = encryption_key.as_mut();
let algorithm = Schnorrkel::new(b"substrate");
let keys: ThresholdKeys<Ristretto> =
musig(&musig_context(self.spec.set()), self.key, participants)
.expect("signing for a set we aren't in/validator present multiple times")
.into();
if CachedPreprocesses::get(self.txn, &self.context).is_none() {
let (machine, _) =
AlgorithmMachine::new(algorithm.clone(), keys.clone()).preprocess(&mut OsRng);
let mut cache = machine.cache();
assert_eq!(cache.0.len(), 32);
#[allow(clippy::needless_range_loop)]
for b in 0 .. 32 {
cache.0[b] ^= encryption_key_slice[b];
}
CachedPreprocesses::set(self.txn, &self.context, &cache.0);
}
let cached = CachedPreprocesses::get(self.txn, &self.context).unwrap();
let mut cached: Zeroizing<[u8; 32]> = Zeroizing::new(cached);
#[allow(clippy::needless_range_loop)]
for b in 0 .. 32 {
cached[b] ^= encryption_key_slice[b];
}
encryption_key_slice.zeroize();
let (machine, preprocess) =
AlgorithmSignMachine::from_cache(algorithm, keys, CachedPreprocess(cached));
(machine, preprocess.serialize().try_into().unwrap())
}
fn share_internal(
&mut self,
participants: &[<Ristretto as Ciphersuite>::G],
mut serialized_preprocesses: HashMap<Participant, Vec<u8>>,
msg: &[u8],
) -> Result<(AlgorithmSignatureMachine<Ristretto, Schnorrkel>, [u8; 32]), Participant> {
let machine = self.preprocess_internal(participants).0;
let mut participants = serialized_preprocesses.keys().cloned().collect::<Vec<_>>();
participants.sort();
let mut preprocesses = HashMap::new();
for participant in participants {
preprocesses.insert(
participant,
machine
.read_preprocess(&mut serialized_preprocesses.remove(&participant).unwrap().as_slice())
.map_err(|_| participant)?,
);
}
let (machine, share) = machine.sign(preprocesses, msg).map_err(|e| match e {
FrostError::InternalError(e) => unreachable!("FrostError::InternalError {e}"),
FrostError::InvalidParticipant(_, _) |
FrostError::InvalidSigningSet(_) |
FrostError::InvalidParticipantQuantity(_, _) |
FrostError::DuplicatedParticipant(_) |
FrostError::MissingParticipant(_) => unreachable!("{e:?}"),
FrostError::InvalidPreprocess(p) | FrostError::InvalidShare(p) => p,
})?;
Ok((machine, share.serialize().try_into().unwrap()))
}
fn complete_internal(
&mut self,
machine: AlgorithmSignatureMachine<Ristretto, Schnorrkel>,
shares: HashMap<Participant, Vec<u8>>,
) -> Result<[u8; 64], Participant> {
let shares = shares
.into_iter()
.map(|(p, share)| {
machine.read_share(&mut share.as_slice()).map(|share| (p, share)).map_err(|_| p)
})
.collect::<Result<HashMap<_, _>, _>>()?;
let signature = machine.complete(shares).map_err(|e| match e {
FrostError::InternalError(e) => unreachable!("FrostError::InternalError {e}"),
FrostError::InvalidParticipant(_, _) |
FrostError::InvalidSigningSet(_) |
FrostError::InvalidParticipantQuantity(_, _) |
FrostError::DuplicatedParticipant(_) |
FrostError::MissingParticipant(_) => unreachable!("{e:?}"),
FrostError::InvalidPreprocess(p) | FrostError::InvalidShare(p) => p,
})?;
Ok(signature.to_bytes())
}
}
// Get the keys of the participants, noted by their threshold is, and return a new map indexed by
// the MuSig is.
//
// If sort_by_keys = true, the MuSig is will index the keys once sorted. Else, the MuSig is will
// index the validators in the order they've been defined.
fn threshold_i_map_to_keys_and_musig_i_map(
spec: &TributarySpec,
our_key: &Zeroizing<<Ristretto as Ciphersuite>::F>,
mut map: HashMap<Participant, Vec<u8>>,
sort_by_keys: bool,
) -> (Vec<<Ristretto as Ciphersuite>::G>, HashMap<Participant, Vec<u8>>) {
// Insert our own index so calculations aren't offset
let our_threshold_i =
spec.i(<Ristretto as Ciphersuite>::generator() * our_key.deref()).unwrap().start;
assert!(map.insert(our_threshold_i, vec![]).is_none());
let spec_validators = spec.validators();
let key_from_threshold_i = |threshold_i| {
for (key, _) in &spec_validators {
if threshold_i == spec.i(*key).unwrap().start {
return *key;
}
}
panic!("requested info for threshold i which doesn't exist")
};
let mut sorted = vec![];
let mut threshold_is = map.keys().cloned().collect::<Vec<_>>();
threshold_is.sort();
for threshold_i in threshold_is {
sorted.push((key_from_threshold_i(threshold_i), map.remove(&threshold_i).unwrap()));
}
if sort_by_keys {
// Substrate expects these signers to be sorted by key
sorted.sort_by(|(key1, _), (key2, _)| key1.to_bytes().cmp(&key2.to_bytes()));
}
// Now that signers are sorted, with their shares, create a map with the is needed for MuSig
let mut participants = vec![];
let mut map = HashMap::new();
for (raw_i, (key, share)) in sorted.into_iter().enumerate() {
let musig_i = u16::try_from(raw_i).unwrap() + 1;
participants.push(key);
map.insert(Participant::new(musig_i).unwrap(), share);
}
map.remove(&our_threshold_i).unwrap();
(participants, map)
}
pub(crate) struct DkgConfirmer<'a, T: DbTxn> {
pub(crate) key: &'a Zeroizing<<Ristretto as Ciphersuite>::F>,
pub(crate) spec: &'a TributarySpec,
pub(crate) txn: &'a mut T,
pub(crate) attempt: u32,
}
impl<T: DbTxn> DkgConfirmer<'_, T> {
fn signing_protocol(&mut self) -> SigningProtocol<'_, T, (&'static [u8; 12], u32)> {
let context = (b"DkgConfirmer", self.attempt);
SigningProtocol { key: self.key, spec: self.spec, txn: self.txn, context }
}
fn preprocess_internal(&mut self) -> (AlgorithmSignMachine<Ristretto, Schnorrkel>, [u8; 64]) {
let participants = self.spec.validators().iter().map(|val| val.0).collect::<Vec<_>>();
self.signing_protocol().preprocess_internal(&participants)
}
// Get the preprocess for this confirmation.
pub(crate) fn preprocess(&mut self) -> [u8; 64] {
self.preprocess_internal().1
}
fn share_internal(
&mut self,
preprocesses: HashMap<Participant, Vec<u8>>,
key_pair: &KeyPair,
) -> Result<(AlgorithmSignatureMachine<Ristretto, Schnorrkel>, [u8; 32]), Participant> {
let participants = self.spec.validators().iter().map(|val| val.0).collect::<Vec<_>>();
let preprocesses =
threshold_i_map_to_keys_and_musig_i_map(self.spec, self.key, preprocesses, false).1;
let msg = set_keys_message(&self.spec.set(), key_pair);
self.signing_protocol().share_internal(&participants, preprocesses, &msg)
}
// Get the share for this confirmation, if the preprocesses are valid.
pub(crate) fn share(
&mut self,
preprocesses: HashMap<Participant, Vec<u8>>,
key_pair: &KeyPair,
) -> Result<[u8; 32], Participant> {
self.share_internal(preprocesses, key_pair).map(|(_, share)| share)
}
pub(crate) fn complete(
&mut self,
preprocesses: HashMap<Participant, Vec<u8>>,
key_pair: &KeyPair,
shares: HashMap<Participant, Vec<u8>>,
) -> Result<[u8; 64], Participant> {
let shares = threshold_i_map_to_keys_and_musig_i_map(self.spec, self.key, shares, false).1;
let machine = self
.share_internal(preprocesses, key_pair)
.expect("trying to complete a machine which failed to preprocess")
.0;
self.signing_protocol().complete_internal(machine, shares)
}
}
pub(crate) struct DkgRemoval<'a, T: DbTxn> {
pub(crate) key: &'a Zeroizing<<Ristretto as Ciphersuite>::F>,
pub(crate) spec: &'a TributarySpec,
pub(crate) txn: &'a mut T,
pub(crate) removing: [u8; 32],
pub(crate) attempt: u32,
}
impl<T: DbTxn> DkgRemoval<'_, T> {
fn signing_protocol(&mut self) -> SigningProtocol<'_, T, (&'static [u8; 10], [u8; 32], u32)> {
let context = (b"DkgRemoval", self.removing, self.attempt);
SigningProtocol { key: self.key, spec: self.spec, txn: self.txn, context }
}
fn preprocess_internal(
&mut self,
participants: Option<&[<Ristretto as Ciphersuite>::G]>,
) -> (AlgorithmSignMachine<Ristretto, Schnorrkel>, [u8; 64]) {
// We won't know the participants when we first preprocess
// If we don't, we use our key alone as the participant
let just_us = [<Ristretto as Ciphersuite>::G::generator() * self.key.deref()];
let to_musig = if let Some(participants) = participants { participants } else { &just_us };
let (machine, preprocess) = self.signing_protocol().preprocess_internal(to_musig);
// If we're now specifying participants, confirm the commitments were the same
if participants.is_some() {
let (_, theoretical_preprocess) = self.signing_protocol().preprocess_internal(&just_us);
assert_eq!(theoretical_preprocess, preprocess);
}
(machine, preprocess)
}
// Get the preprocess for this confirmation.
pub(crate) fn preprocess(&mut self) -> [u8; 64] {
self.preprocess_internal(None).1
}
fn share_internal(
&mut self,
preprocesses: HashMap<Participant, Vec<u8>>,
) -> Result<(AlgorithmSignatureMachine<Ristretto, Schnorrkel>, [u8; 32]), Participant> {
let (participants, preprocesses) =
threshold_i_map_to_keys_and_musig_i_map(self.spec, self.key, preprocesses, true);
let msg = remove_participant_message(&self.spec.set(), Public(self.removing));
self.signing_protocol().share_internal(&participants, preprocesses, &msg)
}
// Get the share for this confirmation, if the preprocesses are valid.
pub(crate) fn share(
&mut self,
preprocesses: HashMap<Participant, Vec<u8>>,
) -> Result<[u8; 32], Participant> {
self.share_internal(preprocesses).map(|(_, share)| share)
}
pub(crate) fn complete(
&mut self,
preprocesses: HashMap<Participant, Vec<u8>>,
shares: HashMap<Participant, Vec<u8>>,
) -> Result<(Vec<SeraiAddress>, [u8; 64]), Participant> {
let (participants, shares) =
threshold_i_map_to_keys_and_musig_i_map(self.spec, self.key, shares, true);
let signers = participants.iter().map(|key| SeraiAddress(key.to_bytes())).collect::<Vec<_>>();
let machine = self
.share_internal(preprocesses)
.expect("trying to complete a machine which failed to preprocess")
.0;
let signature = self.signing_protocol().complete_internal(machine, shares)?;
Ok((signers, signature))
}
}

View file

@ -0,0 +1,116 @@
use core::{ops::Range, fmt::Debug};
use std::io;
use transcript::{Transcript, RecommendedTranscript};
use ciphersuite::{group::GroupEncoding, Ciphersuite, Ristretto};
use frost::Participant;
use scale::Encode;
use borsh::{BorshSerialize, BorshDeserialize};
use serai_client::{primitives::PublicKey, validator_sets::primitives::ValidatorSet};
fn borsh_serialize_validators<W: io::Write>(
validators: &Vec<(<Ristretto as Ciphersuite>::G, u16)>,
writer: &mut W,
) -> Result<(), io::Error> {
let len = u16::try_from(validators.len()).unwrap();
BorshSerialize::serialize(&len, writer)?;
for validator in validators {
BorshSerialize::serialize(&validator.0.to_bytes(), writer)?;
BorshSerialize::serialize(&validator.1, writer)?;
}
Ok(())
}
fn borsh_deserialize_validators<R: io::Read>(
reader: &mut R,
) -> Result<Vec<(<Ristretto as Ciphersuite>::G, u16)>, io::Error> {
let len: u16 = BorshDeserialize::deserialize_reader(reader)?;
let mut res = vec![];
for _ in 0 .. len {
let compressed: [u8; 32] = BorshDeserialize::deserialize_reader(reader)?;
let point = Option::from(<Ristretto as Ciphersuite>::G::from_bytes(&compressed))
.ok_or_else(|| io::Error::other("invalid point for validator"))?;
let weight: u16 = BorshDeserialize::deserialize_reader(reader)?;
res.push((point, weight));
}
Ok(res)
}
#[derive(Clone, PartialEq, Eq, Debug, BorshSerialize, BorshDeserialize)]
pub struct TributarySpec {
serai_block: [u8; 32],
start_time: u64,
set: ValidatorSet,
#[borsh(
serialize_with = "borsh_serialize_validators",
deserialize_with = "borsh_deserialize_validators"
)]
validators: Vec<(<Ristretto as Ciphersuite>::G, u16)>,
}
impl TributarySpec {
pub fn new(
serai_block: [u8; 32],
start_time: u64,
set: ValidatorSet,
set_participants: Vec<(PublicKey, u16)>,
) -> TributarySpec {
let mut validators = vec![];
for (participant, shares) in set_participants {
let participant = <Ristretto as Ciphersuite>::read_G::<&[u8]>(&mut participant.0.as_ref())
.expect("invalid key registered as participant");
validators.push((participant, shares));
}
Self { serai_block, start_time, set, validators }
}
pub fn set(&self) -> ValidatorSet {
self.set
}
pub fn genesis(&self) -> [u8; 32] {
// Calculate the genesis for this Tributary
let mut genesis = RecommendedTranscript::new(b"Serai Tributary Genesis");
// This locks it to a specific Serai chain
genesis.append_message(b"serai_block", self.serai_block);
genesis.append_message(b"session", self.set.session.0.to_le_bytes());
genesis.append_message(b"network", self.set.network.encode());
let genesis = genesis.challenge(b"genesis");
let genesis_ref: &[u8] = genesis.as_ref();
genesis_ref[.. 32].try_into().unwrap()
}
pub fn start_time(&self) -> u64 {
self.start_time
}
pub fn n(&self) -> u16 {
self.validators.iter().map(|(_, weight)| weight).sum()
}
pub fn t(&self) -> u16 {
((2 * self.n()) / 3) + 1
}
pub fn i(&self, key: <Ristretto as Ciphersuite>::G) -> Option<Range<Participant>> {
let mut i = 1;
for (validator, weight) in &self.validators {
if validator == &key {
return Some(Range {
start: Participant::new(i).unwrap(),
end: Participant::new(i + weight).unwrap(),
});
}
i += weight;
}
None
}
pub fn validators(&self) -> Vec<(<Ristretto as Ciphersuite>::G, u64)> {
self.validators.iter().map(|(validator, weight)| (*validator, u64::from(*weight))).collect()
}
}

View file

@ -0,0 +1,693 @@
use core::{ops::Deref, fmt::Debug};
use std::io;
use zeroize::Zeroizing;
use rand_core::{RngCore, CryptoRng};
use blake2::{Digest, Blake2s256};
use transcript::{Transcript, RecommendedTranscript};
use ciphersuite::{
group::{ff::Field, GroupEncoding},
Ciphersuite, Ristretto,
};
use schnorr::SchnorrSignature;
use frost::Participant;
use scale::{Encode, Decode};
use processor_messages::coordinator::SubstrateSignableId;
use tributary::{
TRANSACTION_SIZE_LIMIT, ReadWrite,
transaction::{Signed, TransactionError, TransactionKind, Transaction as TransactionTrait},
};
#[derive(Clone, Copy, PartialEq, Eq, Debug, Encode)]
pub enum Label {
Preprocess,
Share,
}
impl Label {
// TODO: Should nonces be u8 thanks to our use of topics?
pub fn nonce(&self) -> u32 {
match self {
Label::Preprocess => 0,
Label::Share => 1,
}
}
}
#[derive(Clone, PartialEq, Eq)]
pub struct SignData<Id: Clone + PartialEq + Eq + Debug + Encode + Decode> {
pub plan: Id,
pub attempt: u32,
pub label: Label,
pub data: Vec<Vec<u8>>,
pub signed: Signed,
}
impl<Id: Clone + PartialEq + Eq + Debug + Encode + Decode> Debug for SignData<Id> {
fn fmt(&self, fmt: &mut core::fmt::Formatter<'_>) -> Result<(), core::fmt::Error> {
fmt
.debug_struct("SignData")
.field("id", &hex::encode(self.plan.encode()))
.field("attempt", &self.attempt)
.field("label", &self.label)
.field("signer", &hex::encode(self.signed.signer.to_bytes()))
.finish_non_exhaustive()
}
}
impl<Id: Clone + PartialEq + Eq + Debug + Encode + Decode> SignData<Id> {
pub(crate) fn read<R: io::Read>(reader: &mut R) -> io::Result<Self> {
let plan = Id::decode(&mut scale::IoReader(&mut *reader))
.map_err(|_| io::Error::other("invalid plan in SignData"))?;
let mut attempt = [0; 4];
reader.read_exact(&mut attempt)?;
let attempt = u32::from_le_bytes(attempt);
let mut label = [0; 1];
reader.read_exact(&mut label)?;
let label = match label[0] {
0 => Label::Preprocess,
1 => Label::Share,
_ => Err(io::Error::other("invalid label in SignData"))?,
};
let data = {
let mut data_pieces = [0];
reader.read_exact(&mut data_pieces)?;
if data_pieces[0] == 0 {
Err(io::Error::other("zero pieces of data in SignData"))?;
}
let mut all_data = vec![];
for _ in 0 .. data_pieces[0] {
let mut data_len = [0; 2];
reader.read_exact(&mut data_len)?;
let mut data = vec![0; usize::from(u16::from_le_bytes(data_len))];
reader.read_exact(&mut data)?;
all_data.push(data);
}
all_data
};
let signed = Signed::read_without_nonce(reader, label.nonce())?;
Ok(SignData { plan, attempt, label, data, signed })
}
pub(crate) fn write<W: io::Write>(&self, writer: &mut W) -> io::Result<()> {
writer.write_all(&self.plan.encode())?;
writer.write_all(&self.attempt.to_le_bytes())?;
writer.write_all(&[match self.label {
Label::Preprocess => 0,
Label::Share => 1,
}])?;
writer.write_all(&[u8::try_from(self.data.len()).unwrap()])?;
for data in &self.data {
if data.len() > u16::MAX.into() {
// Currently, the largest individual preprocess is a Monero transaction
// It provides 4 commitments per input (128 bytes), a 64-byte proof for them, along with a
// key image and proof (96 bytes)
// Even with all of that, we could support 227 inputs in a single TX
// Monero is limited to ~120 inputs per TX
//
// Bitcoin has a much higher input count of 520, yet it only uses 64 bytes per preprocess
Err(io::Error::other("signing data exceeded 65535 bytes"))?;
}
writer.write_all(&u16::try_from(data.len()).unwrap().to_le_bytes())?;
writer.write_all(data)?;
}
self.signed.write_without_nonce(writer)
}
}
#[derive(Clone, PartialEq, Eq)]
pub enum Transaction {
RemoveParticipant(Participant),
// Once this completes successfully, no more instances should be created.
DkgCommitments {
attempt: u32,
commitments: Vec<Vec<u8>>,
signed: Signed,
},
DkgShares {
attempt: u32,
// Sending Participant, Receiving Participant, Share
shares: Vec<Vec<Vec<u8>>>,
confirmation_nonces: [u8; 64],
signed: Signed,
},
InvalidDkgShare {
attempt: u32,
accuser: Participant,
faulty: Participant,
blame: Option<Vec<u8>>,
signed: Signed,
},
DkgConfirmed {
attempt: u32,
confirmation_share: [u8; 32],
signed: Signed,
},
DkgRemoval(SignData<[u8; 32]>),
// Co-sign a Substrate block.
CosignSubstrateBlock([u8; 32]),
// When we have synchrony on a batch, we can allow signing it
// TODO (never?): This is less efficient compared to an ExternalBlock provided transaction,
// which would be binding over the block hash and automatically achieve synchrony on all
// relevant batches. ExternalBlock was removed for this due to complexity around the pipeline
// with the current processor, yet it would still be an improvement.
Batch {
block: [u8; 32],
batch: [u8; 5],
},
// When a Serai block is finalized, with the contained batches, we can allow the associated plan
// IDs
SubstrateBlock(u64),
SubstrateSign(SignData<SubstrateSignableId>),
Sign(SignData<[u8; 32]>),
// This is defined as an Unsigned transaction in order to de-duplicate SignCompleted amongst
// reporters (who should all report the same thing)
// We do still track the signer in order to prevent a single signer from publishing arbitrarily
// many TXs without penalty
// Here, they're denoted as the first_signer, as only the signer of the first TX to be included
// with this pairing will be remembered on-chain
SignCompleted {
plan: [u8; 32],
tx_hash: Vec<u8>,
first_signer: <Ristretto as Ciphersuite>::G,
signature: SchnorrSignature<Ristretto>,
},
}
impl Debug for Transaction {
fn fmt(&self, fmt: &mut core::fmt::Formatter<'_>) -> Result<(), core::fmt::Error> {
match self {
Transaction::RemoveParticipant(participant) => fmt
.debug_struct("Transaction::RemoveParticipant")
.field("participant", participant)
.finish(),
Transaction::DkgCommitments { attempt, commitments: _, signed } => fmt
.debug_struct("Transaction::DkgCommitments")
.field("attempt", attempt)
.field("signer", &hex::encode(signed.signer.to_bytes()))
.finish_non_exhaustive(),
Transaction::DkgShares { attempt, signed, .. } => fmt
.debug_struct("Transaction::DkgShares")
.field("attempt", attempt)
.field("signer", &hex::encode(signed.signer.to_bytes()))
.finish_non_exhaustive(),
Transaction::InvalidDkgShare { attempt, accuser, faulty, .. } => fmt
.debug_struct("Transaction::InvalidDkgShare")
.field("attempt", attempt)
.field("accuser", accuser)
.field("faulty", faulty)
.finish_non_exhaustive(),
Transaction::DkgConfirmed { attempt, confirmation_share: _, signed } => fmt
.debug_struct("Transaction::DkgConfirmed")
.field("attempt", attempt)
.field("signer", &hex::encode(signed.signer.to_bytes()))
.finish_non_exhaustive(),
Transaction::DkgRemoval(sign_data) => {
fmt.debug_struct("Transaction::DkgRemoval").field("sign_data", sign_data).finish()
}
Transaction::CosignSubstrateBlock(block) => fmt
.debug_struct("Transaction::CosignSubstrateBlock")
.field("block", &hex::encode(block))
.finish(),
Transaction::Batch { block, batch } => fmt
.debug_struct("Transaction::Batch")
.field("block", &hex::encode(block))
.field("batch", &hex::encode(batch))
.finish(),
Transaction::SubstrateBlock(block) => {
fmt.debug_struct("Transaction::SubstrateBlock").field("block", block).finish()
}
Transaction::SubstrateSign(sign_data) => {
fmt.debug_struct("Transaction::Substrate").field("sign_data", sign_data).finish()
}
Transaction::Sign(sign_data) => {
fmt.debug_struct("Transaction::Sign").field("sign_data", sign_data).finish()
}
Transaction::SignCompleted { plan, tx_hash, .. } => fmt
.debug_struct("Transaction::SignCompleted")
.field("plan", &hex::encode(plan))
.field("tx_hash", &hex::encode(tx_hash))
.finish_non_exhaustive(),
}
}
}
impl ReadWrite for Transaction {
fn read<R: io::Read>(reader: &mut R) -> io::Result<Self> {
let mut kind = [0];
reader.read_exact(&mut kind)?;
match kind[0] {
0 => Ok(Transaction::RemoveParticipant({
let mut participant = [0; 2];
reader.read_exact(&mut participant)?;
Participant::new(u16::from_le_bytes(participant))
.ok_or_else(|| io::Error::other("invalid participant in RemoveParticipant"))?
})),
1 => {
let mut attempt = [0; 4];
reader.read_exact(&mut attempt)?;
let attempt = u32::from_le_bytes(attempt);
let commitments = {
let mut commitments_len = [0; 1];
reader.read_exact(&mut commitments_len)?;
let commitments_len = usize::from(commitments_len[0]);
if commitments_len == 0 {
Err(io::Error::other("zero commitments in DkgCommitments"))?;
}
let mut each_commitments_len = [0; 2];
reader.read_exact(&mut each_commitments_len)?;
let each_commitments_len = usize::from(u16::from_le_bytes(each_commitments_len));
if (commitments_len * each_commitments_len) > TRANSACTION_SIZE_LIMIT {
Err(io::Error::other(
"commitments present in transaction exceeded transaction size limit",
))?;
}
let mut commitments = vec![vec![]; commitments_len];
for commitments in &mut commitments {
*commitments = vec![0; each_commitments_len];
reader.read_exact(commitments)?;
}
commitments
};
let signed = Signed::read_without_nonce(reader, 0)?;
Ok(Transaction::DkgCommitments { attempt, commitments, signed })
}
2 => {
let mut attempt = [0; 4];
reader.read_exact(&mut attempt)?;
let attempt = u32::from_le_bytes(attempt);
let shares = {
let mut share_quantity = [0; 1];
reader.read_exact(&mut share_quantity)?;
let mut key_share_quantity = [0; 1];
reader.read_exact(&mut key_share_quantity)?;
let mut share_len = [0; 2];
reader.read_exact(&mut share_len)?;
let share_len = usize::from(u16::from_le_bytes(share_len));
let mut all_shares = vec![];
for _ in 0 .. share_quantity[0] {
let mut shares = vec![];
for _ in 0 .. key_share_quantity[0] {
let mut share = vec![0; share_len];
reader.read_exact(&mut share)?;
shares.push(share);
}
all_shares.push(shares);
}
all_shares
};
let mut confirmation_nonces = [0; 64];
reader.read_exact(&mut confirmation_nonces)?;
let signed = Signed::read_without_nonce(reader, 1)?;
Ok(Transaction::DkgShares { attempt, shares, confirmation_nonces, signed })
}
3 => {
let mut attempt = [0; 4];
reader.read_exact(&mut attempt)?;
let attempt = u32::from_le_bytes(attempt);
let mut accuser = [0; 2];
reader.read_exact(&mut accuser)?;
let accuser = Participant::new(u16::from_le_bytes(accuser))
.ok_or_else(|| io::Error::other("invalid participant in InvalidDkgShare"))?;
let mut faulty = [0; 2];
reader.read_exact(&mut faulty)?;
let faulty = Participant::new(u16::from_le_bytes(faulty))
.ok_or_else(|| io::Error::other("invalid participant in InvalidDkgShare"))?;
let mut blame_len = [0; 2];
reader.read_exact(&mut blame_len)?;
let mut blame = vec![0; u16::from_le_bytes(blame_len).into()];
reader.read_exact(&mut blame)?;
// This shares a nonce with DkgConfirmed as only one is expected
let signed = Signed::read_without_nonce(reader, 2)?;
Ok(Transaction::InvalidDkgShare {
attempt,
accuser,
faulty,
blame: Some(blame).filter(|blame| !blame.is_empty()),
signed,
})
}
4 => {
let mut attempt = [0; 4];
reader.read_exact(&mut attempt)?;
let attempt = u32::from_le_bytes(attempt);
let mut confirmation_share = [0; 32];
reader.read_exact(&mut confirmation_share)?;
let signed = Signed::read_without_nonce(reader, 2)?;
Ok(Transaction::DkgConfirmed { attempt, confirmation_share, signed })
}
5 => SignData::read(reader).map(Transaction::DkgRemoval),
6 => {
let mut block = [0; 32];
reader.read_exact(&mut block)?;
Ok(Transaction::CosignSubstrateBlock(block))
}
7 => {
let mut block = [0; 32];
reader.read_exact(&mut block)?;
let mut batch = [0; 5];
reader.read_exact(&mut batch)?;
Ok(Transaction::Batch { block, batch })
}
8 => {
let mut block = [0; 8];
reader.read_exact(&mut block)?;
Ok(Transaction::SubstrateBlock(u64::from_le_bytes(block)))
}
9 => SignData::read(reader).map(Transaction::SubstrateSign),
10 => SignData::read(reader).map(Transaction::Sign),
11 => {
let mut plan = [0; 32];
reader.read_exact(&mut plan)?;
let mut tx_hash_len = [0];
reader.read_exact(&mut tx_hash_len)?;
let mut tx_hash = vec![0; usize::from(tx_hash_len[0])];
reader.read_exact(&mut tx_hash)?;
let first_signer = Ristretto::read_G(reader)?;
let signature = SchnorrSignature::<Ristretto>::read(reader)?;
Ok(Transaction::SignCompleted { plan, tx_hash, first_signer, signature })
}
_ => Err(io::Error::other("invalid transaction type")),
}
}
fn write<W: io::Write>(&self, writer: &mut W) -> io::Result<()> {
match self {
Transaction::RemoveParticipant(i) => {
writer.write_all(&[0])?;
writer.write_all(&u16::from(*i).to_le_bytes())
}
Transaction::DkgCommitments { attempt, commitments, signed } => {
writer.write_all(&[1])?;
writer.write_all(&attempt.to_le_bytes())?;
if commitments.is_empty() {
Err(io::Error::other("zero commitments in DkgCommitments"))?
}
writer.write_all(&[u8::try_from(commitments.len()).unwrap()])?;
for commitments_i in commitments {
if commitments_i.len() != commitments[0].len() {
Err(io::Error::other("commitments of differing sizes in DkgCommitments"))?
}
}
writer.write_all(&u16::try_from(commitments[0].len()).unwrap().to_le_bytes())?;
for commitments in commitments {
writer.write_all(commitments)?;
}
signed.write_without_nonce(writer)
}
Transaction::DkgShares { attempt, shares, confirmation_nonces, signed } => {
writer.write_all(&[2])?;
writer.write_all(&attempt.to_le_bytes())?;
// `shares` is a Vec which is supposed to map to a HashMap<Pariticpant, Vec<u8>>. Since we
// bound participants to 150, this conversion is safe if a valid in-memory transaction.
writer.write_all(&[u8::try_from(shares.len()).unwrap()])?;
// This assumes at least one share is being sent to another party
writer.write_all(&[u8::try_from(shares[0].len()).unwrap()])?;
let share_len = shares[0][0].len();
// For BLS12-381 G2, this would be:
// - A 32-byte share
// - A 96-byte ephemeral key
// - A 128-byte signature
// Hence why this has to be u16
writer.write_all(&u16::try_from(share_len).unwrap().to_le_bytes())?;
for these_shares in shares {
assert_eq!(these_shares.len(), shares[0].len(), "amount of sent shares was variable");
for share in these_shares {
assert_eq!(share.len(), share_len, "sent shares were of variable length");
writer.write_all(share)?;
}
}
writer.write_all(confirmation_nonces)?;
signed.write_without_nonce(writer)
}
Transaction::InvalidDkgShare { attempt, accuser, faulty, blame, signed } => {
writer.write_all(&[3])?;
writer.write_all(&attempt.to_le_bytes())?;
writer.write_all(&u16::from(*accuser).to_le_bytes())?;
writer.write_all(&u16::from(*faulty).to_le_bytes())?;
// Flattens Some(vec![]) to None on the expectation no actual blame will be 0-length
assert!(blame.as_ref().map(|blame| blame.len()).unwrap_or(1) != 0);
let blame_len =
u16::try_from(blame.as_ref().unwrap_or(&vec![]).len()).expect("blame exceeded 64 KB");
writer.write_all(&blame_len.to_le_bytes())?;
writer.write_all(blame.as_ref().unwrap_or(&vec![]))?;
signed.write_without_nonce(writer)
}
Transaction::DkgConfirmed { attempt, confirmation_share, signed } => {
writer.write_all(&[4])?;
writer.write_all(&attempt.to_le_bytes())?;
writer.write_all(confirmation_share)?;
signed.write_without_nonce(writer)
}
Transaction::DkgRemoval(data) => {
writer.write_all(&[5])?;
data.write(writer)
}
Transaction::CosignSubstrateBlock(block) => {
writer.write_all(&[6])?;
writer.write_all(block)
}
Transaction::Batch { block, batch } => {
writer.write_all(&[7])?;
writer.write_all(block)?;
writer.write_all(batch)
}
Transaction::SubstrateBlock(block) => {
writer.write_all(&[8])?;
writer.write_all(&block.to_le_bytes())
}
Transaction::SubstrateSign(data) => {
writer.write_all(&[9])?;
data.write(writer)
}
Transaction::Sign(data) => {
writer.write_all(&[10])?;
data.write(writer)
}
Transaction::SignCompleted { plan, tx_hash, first_signer, signature } => {
writer.write_all(&[11])?;
writer.write_all(plan)?;
writer
.write_all(&[u8::try_from(tx_hash.len()).expect("tx hash length exceed 255 bytes")])?;
writer.write_all(tx_hash)?;
writer.write_all(&first_signer.to_bytes())?;
signature.write(writer)
}
}
}
}
impl TransactionTrait for Transaction {
fn kind(&self) -> TransactionKind<'_> {
match self {
Transaction::RemoveParticipant(_) => TransactionKind::Provided("remove"),
Transaction::DkgCommitments { attempt, commitments: _, signed } => {
TransactionKind::Signed((b"dkg", attempt).encode(), signed)
}
Transaction::DkgShares { attempt, signed, .. } => {
TransactionKind::Signed((b"dkg", attempt).encode(), signed)
}
Transaction::InvalidDkgShare { attempt, signed, .. } => {
TransactionKind::Signed((b"dkg", attempt).encode(), signed)
}
Transaction::DkgConfirmed { attempt, signed, .. } => {
TransactionKind::Signed((b"dkg", attempt).encode(), signed)
}
Transaction::DkgRemoval(data) => {
TransactionKind::Signed((b"dkg_removal", data.plan, data.attempt).encode(), &data.signed)
}
Transaction::CosignSubstrateBlock(_) => TransactionKind::Provided("cosign"),
Transaction::Batch { .. } => TransactionKind::Provided("batch"),
Transaction::SubstrateBlock(_) => TransactionKind::Provided("serai"),
Transaction::SubstrateSign(data) => {
TransactionKind::Signed((b"substrate", data.plan, data.attempt).encode(), &data.signed)
}
Transaction::Sign(data) => {
TransactionKind::Signed((b"sign", data.plan, data.attempt).encode(), &data.signed)
}
Transaction::SignCompleted { .. } => TransactionKind::Unsigned,
}
}
fn hash(&self) -> [u8; 32] {
let mut tx = self.serialize();
if let TransactionKind::Signed(_, signed) = self.kind() {
// Make sure the part we're cutting off is the signature
assert_eq!(tx.drain((tx.len() - 64) ..).collect::<Vec<_>>(), signed.signature.serialize());
}
Blake2s256::digest([b"Coordinator Tributary Transaction".as_slice(), &tx].concat()).into()
}
fn verify(&self) -> Result<(), TransactionError> {
// TODO: Check DkgRemoval and SubstrateSign's lengths here
if let Transaction::SignCompleted { first_signer, signature, .. } = self {
if !signature.verify(*first_signer, self.sign_completed_challenge()) {
Err(TransactionError::InvalidContent)?;
}
}
Ok(())
}
}
impl Transaction {
// Used to initially construct transactions so we can then get sig hashes and perform signing
pub fn empty_signed() -> Signed {
Signed {
signer: Ristretto::generator(),
nonce: 0,
signature: SchnorrSignature::<Ristretto> {
R: Ristretto::generator(),
s: <Ristretto as Ciphersuite>::F::ZERO,
},
}
}
// Sign a transaction
pub fn sign<R: RngCore + CryptoRng>(
&mut self,
rng: &mut R,
genesis: [u8; 32],
key: &Zeroizing<<Ristretto as Ciphersuite>::F>,
) {
fn signed(tx: &mut Transaction) -> (u32, &mut Signed) {
let nonce = match tx {
Transaction::RemoveParticipant(_) => panic!("signing RemoveParticipant"),
Transaction::DkgCommitments { .. } => 0,
Transaction::DkgShares { .. } => 1,
Transaction::InvalidDkgShare { .. } => 2,
Transaction::DkgConfirmed { .. } => 2,
Transaction::DkgRemoval(data) => data.label.nonce(),
Transaction::CosignSubstrateBlock(_) => panic!("signing CosignSubstrateBlock"),
Transaction::Batch { .. } => panic!("signing Batch"),
Transaction::SubstrateBlock(_) => panic!("signing SubstrateBlock"),
Transaction::SubstrateSign(data) => data.label.nonce(),
Transaction::Sign(data) => data.label.nonce(),
Transaction::SignCompleted { .. } => panic!("signing SignCompleted"),
};
(
nonce,
match tx {
Transaction::RemoveParticipant(_) => panic!("signing RemoveParticipant"),
Transaction::DkgCommitments { ref mut signed, .. } => signed,
Transaction::DkgShares { ref mut signed, .. } => signed,
Transaction::InvalidDkgShare { ref mut signed, .. } => signed,
Transaction::DkgConfirmed { ref mut signed, .. } => signed,
Transaction::DkgRemoval(ref mut data) => &mut data.signed,
Transaction::CosignSubstrateBlock(_) => panic!("signing CosignSubstrateBlock"),
Transaction::Batch { .. } => panic!("signing Batch"),
Transaction::SubstrateBlock(_) => panic!("signing SubstrateBlock"),
Transaction::SubstrateSign(ref mut data) => &mut data.signed,
Transaction::Sign(ref mut data) => &mut data.signed,
Transaction::SignCompleted { .. } => panic!("signing SignCompleted"),
},
)
}
let (nonce, signed_ref) = signed(self);
signed_ref.signer = Ristretto::generator() * key.deref();
signed_ref.nonce = nonce;
let sig_nonce = Zeroizing::new(<Ristretto as Ciphersuite>::F::random(rng));
signed(self).1.signature.R = <Ristretto as Ciphersuite>::generator() * sig_nonce.deref();
let sig_hash = self.sig_hash(genesis);
signed(self).1.signature = SchnorrSignature::<Ristretto>::sign(key, sig_nonce, sig_hash);
}
pub fn sign_completed_challenge(&self) -> <Ristretto as Ciphersuite>::F {
if let Transaction::SignCompleted { plan, tx_hash, first_signer, signature } = self {
let mut transcript =
RecommendedTranscript::new(b"Coordinator Tributary Transaction SignCompleted");
transcript.append_message(b"plan", plan);
transcript.append_message(b"tx_hash", tx_hash);
transcript.append_message(b"signer", first_signer.to_bytes());
transcript.append_message(b"nonce", signature.R.to_bytes());
Ristretto::hash_to_F(b"SignCompleted signature", &transcript.challenge(b"challenge"))
} else {
panic!("sign_completed_challenge called on transaction which wasn't SignCompleted")
}
}
}

View file

@ -23,7 +23,6 @@ zeroize = { version = "^1.5", default-features = false, features = ["zeroize_der
std-shims = { version = "0.1", path = "../../common/std-shims", default-features = false }
borsh = { version = "1", default-features = false, features = ["derive", "de_strict_order"], optional = true }
serde = { version = "1", default-features = false, features = ["derive"], optional = true }
transcript = { package = "flexible-transcript", path = "../transcript", version = "^0.3.2", default-features = false, features = ["recommended"] }
chacha20 = { version = "0.9", default-features = false, features = ["zeroize"] }
@ -47,7 +46,6 @@ std = [
"std-shims/std",
"borsh?/std",
"serde?/std",
"transcript/std",
"chacha20/std",
@ -61,6 +59,5 @@ std = [
"dleq/serialize"
]
borsh = ["dep:borsh"]
serde = ["dep:serde"]
tests = ["rand_core/getrandom"]
default = ["std"]

View file

@ -31,8 +31,7 @@ pub mod tests;
/// The ID of a participant, defined as a non-zero u16.
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Zeroize)]
#[cfg_attr(feature = "borsh", derive(borsh::BorshSerialize, borsh::BorshDeserialize))]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "borsh", derive(borsh::BorshSerialize))]
pub struct Participant(pub(crate) u16);
impl Participant {
/// Create a new Participant identifier from a u16.
@ -118,6 +117,14 @@ mod lib {
Ciphersuite,
};
#[cfg(feature = "borsh")]
impl borsh::BorshDeserialize for Participant {
fn deserialize_reader<R: io::Read>(reader: &mut R) -> io::Result<Self> {
Participant::new(u16::deserialize_reader(reader)?)
.ok_or_else(|| io::Error::other("invalid participant"))
}
}
// Validate a map of values to have the expected included participants
pub(crate) fn validate_map<T, B: Clone + PartialEq + Eq + Debug>(
map: &HashMap<Participant, T>,
@ -147,8 +154,7 @@ mod lib {
/// Parameters for a multisig.
// These fields should not be made public as they should be static
#[derive(Clone, Copy, PartialEq, Eq, Debug, Zeroize)]
#[cfg_attr(feature = "borsh", derive(borsh::BorshSerialize, borsh::BorshDeserialize))]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "borsh", derive(borsh::BorshSerialize))]
pub struct ThresholdParams {
/// Participants needed to sign on behalf of the group.
pub(crate) t: u16,
@ -189,6 +195,16 @@ mod lib {
}
}
#[cfg(feature = "borsh")]
impl borsh::BorshDeserialize for ThresholdParams {
fn deserialize_reader<R: io::Read>(reader: &mut R) -> io::Result<Self> {
let t = u16::deserialize_reader(reader)?;
let n = u16::deserialize_reader(reader)?;
let i = Participant::deserialize_reader(reader)?;
ThresholdParams::new(t, n, i).map_err(|e| io::Error::other(format!("{e:?}")))
}
}
/// Calculate the lagrange coefficient for a signing set.
pub fn lagrange<F: PrimeField>(i: Participant, included: &[Participant]) -> F {
let i_f = F::from(u64::from(u16::from(i)));

View file

@ -224,13 +224,15 @@ pub trait SignMachine<S>: Send + Sync + Sized {
/// security as your private key share.
fn cache(self) -> CachedPreprocess;
/// Create a sign machine from a cached preprocess. After this, the preprocess must be deleted so
/// it's never reused. Any reuse would cause the signer to leak their secret share.
/// Create a sign machine from a cached preprocess.
/// After this, the preprocess must be deleted so it's never reused. Any reuse will presumably
/// cause the signer to leak their secret share.
fn from_cache(
params: Self::Params,
keys: Self::Keys,
cache: CachedPreprocess,
) -> Result<Self, FrostError>;
) -> (Self, Self::Preprocess);
/// Read a Preprocess message. Despite taking self, this does not save the preprocess.
/// It must be externally cached and passed into sign.
@ -277,9 +279,8 @@ impl<C: Curve, A: Algorithm<C>> SignMachine<A::Signature> for AlgorithmSignMachi
algorithm: A,
keys: ThresholdKeys<C>,
cache: CachedPreprocess,
) -> Result<Self, FrostError> {
let (machine, _) = AlgorithmMachine::new(algorithm, keys).seeded_preprocess(cache);
Ok(machine)
) -> (Self, Self::Preprocess) {
AlgorithmMachine::new(algorithm, keys).seeded_preprocess(cache)
}
fn read_preprocess<R: Read>(&self, reader: &mut R) -> io::Result<Self::Preprocess> {

View file

@ -183,7 +183,7 @@ pub fn sign<R: RngCore + CryptoRng, M: PreprocessMachine>(
let cache = machines.remove(&i).unwrap().cache();
machines.insert(
i,
M::SignMachine::from_cache(params.clone(), keys.remove(&i).unwrap(), cache).unwrap(),
M::SignMachine::from_cache(params.clone(), keys.remove(&i).unwrap(), cache).0,
);
}
}

View file

@ -16,7 +16,6 @@ use frost_schnorrkel::Schnorrkel;
use log::{info, warn};
use scale::Encode;
use serai_client::validator_sets::primitives::Session;
use messages::coordinator::*;

View file

@ -16,7 +16,6 @@ use frost::{
use log::info;
use scale::Encode;
use serai_client::validator_sets::primitives::{Session, KeyPair};
use messages::key_gen::*;

View file

@ -10,7 +10,6 @@ use frost::{
use log::{info, debug, warn, error};
use scale::Encode;
use serai_client::validator_sets::primitives::Session;
use messages::sign::*;

View file

@ -34,5 +34,19 @@ serai-signals-primitives = { path = "../signals/primitives", version = "0.1" }
frame-support = { git = "https://github.com/serai-dex/substrate" }
[features]
borsh = ["dep:borsh"]
serde = ["dep:serde"]
borsh = [
"dep:borsh",
"serai-primitives/borsh",
"serai-coins-primitives/borsh",
"serai-validator-sets-primitives/borsh",
"serai-in-instructions-primitives/borsh",
"serai-signals-primitives/borsh",
]
serde = [
"dep:serde",
"serai-primitives/serde",
"serai-coins-primitives/serde",
"serai-validator-sets-primitives/serde",
"serai-in-instructions-primitives/serde",
"serai-signals-primitives/serde",
]

View file

@ -54,6 +54,7 @@ serai-docker-tests = { path = "../../tests/docker" }
[features]
serai = ["thiserror", "serde", "serde_json", "sp-core", "sp-runtime", "frame-system", "simple-request"]
borsh = ["serai-abi/borsh"]
networks = []
bitcoin = ["networks", "dep:bitcoin"]