Route blame between Processor and Coordinator (#427)

* Have processor report errors during the DKG to the coordinator

* Add RemoveParticipant, InvalidDkgShare to coordinator

* Route DKG blame around coordinator

* Allow public construction of AdditionalBlameMachine

Necessary for upcoming work on handling DKG blame in the processor and
coordinator.

Additionally fixes a publicly reachable panic when commitments parsed with one
ThresholdParams are used in a machine using another set of ThresholdParams.

Renames InvalidProofOfKnowledge to InvalidCommitments.

* Remove unused error from dleq

* Implement support for VerifyBlame in the processor

* Have coordinator send the processor share message relevant to Blame

* Remove desync between processors reporting InvalidShare and ones reporting GeneratedKeyPair

* Route blame on sign between processor and coordinator

Doesn't yet act on it in coordinator.

* Move txn usage as needed for stable Rust to build

* Correct InvalidDkgShare serialization
This commit is contained in:
Luke Parker
2023-11-12 07:24:41 -05:00
committed by GitHub
parent d015ee96a3
commit 54f1929078
18 changed files with 931 additions and 281 deletions

View File

@@ -87,11 +87,14 @@ impl<D: Db> TributaryDb<D> {
fn fatal_slashes_key(genesis: [u8; 32]) -> Vec<u8> {
Self::tributary_key(b"fatal_slashes", genesis)
}
fn fatally_slashed_key(account: [u8; 32]) -> Vec<u8> {
Self::tributary_key(b"fatally_slashed", account)
fn fatally_slashed_key(genesis: [u8; 32], account: [u8; 32]) -> Vec<u8> {
Self::tributary_key(b"fatally_slashed", (genesis, account).encode())
}
pub fn is_fatally_slashed<G: Get>(getter: &G, genesis: [u8; 32], account: [u8; 32]) -> bool {
getter.get(Self::fatally_slashed_key(genesis, account)).is_some()
}
pub fn set_fatally_slashed(txn: &mut D::Transaction<'_>, genesis: [u8; 32], account: [u8; 32]) {
txn.put(Self::fatally_slashed_key(account), []);
txn.put(Self::fatally_slashed_key(genesis, account), []);
let key = Self::fatal_slashes_key(genesis);
let mut existing = txn.get(&key).unwrap_or(vec![]);
@@ -105,6 +108,27 @@ impl<D: Db> TributaryDb<D> {
txn.put(key, existing);
}
fn share_for_blame_key(genesis: &[u8], from: Participant, to: Participant) -> Vec<u8> {
Self::tributary_key(b"share_for_blame", (genesis, u16::from(from), u16::from(to)).encode())
}
pub fn save_share_for_blame(
txn: &mut D::Transaction<'_>,
genesis: &[u8],
from: Participant,
to: Participant,
share: &[u8],
) {
txn.put(Self::share_for_blame_key(genesis, from, to), share);
}
pub fn share_for_blame<G: Get>(
getter: &G,
genesis: &[u8],
from: Participant,
to: Participant,
) -> Option<Vec<u8>> {
getter.get(Self::share_for_blame_key(genesis, from, to))
}
// The plan IDs associated with a Substrate block
fn plan_ids_key(genesis: &[u8], block: u64) -> Vec<u8> {
Self::tributary_key(b"plan_ids", [genesis, block.to_le_bytes().as_ref()].concat())

View File

@@ -1,20 +1,20 @@
use core::{ops::Deref, future::Future};
use std::collections::HashMap;
use zeroize::Zeroizing;
use zeroize::{Zeroize, Zeroizing};
use ciphersuite::{group::GroupEncoding, Ciphersuite, Ristretto};
use frost::dkg::Participant;
use scale::{Encode, Decode};
use serai_client::{
Signature,
Public, Signature,
validator_sets::primitives::{ValidatorSet, KeyPair},
subxt::utils::Encoded,
SeraiValidatorSets,
};
use tributary::Signed;
use tributary::{Signed, TransactionKind, TransactionTrait};
use processor_messages::{
key_gen::{self, KeyGenId},
@@ -22,7 +22,7 @@ use processor_messages::{
sign::{self, SignId},
};
use serai_db::Db;
use serai_db::{Get, Db};
use crate::{
processors::Processors,
@@ -56,7 +56,33 @@ pub fn dkg_confirmation_nonces(
DkgConfirmer::preprocess(spec, key, attempt)
}
#[allow(clippy::needless_pass_by_ref_mut)]
// If there's an error generating a key pair, return any errors which would've occured when
// executing the DkgConfirmer in order to stay in sync with those who did.
//
// The caller must ensure only error_generating_key_pair or generated_key_pair is called for a
// given attempt.
pub fn error_generating_key_pair<D: Db, G: Get>(
getter: &G,
key: &Zeroizing<<Ristretto as Ciphersuite>::F>,
spec: &TributarySpec,
attempt: u32,
) -> Option<Participant> {
let preprocesses =
TributaryDb::<D>::confirmation_nonces(getter, spec.genesis(), attempt).unwrap();
// Sign a key pair which can't be valid
// (0xff used as 0 would be the Ristretto identity point, 0-length for the network key)
let key_pair = (Public([0xff; 32]), vec![0xffu8; 0].try_into().unwrap());
match DkgConfirmer::share(spec, key, attempt, preprocesses, &key_pair) {
Ok(mut share) => {
// Zeroize the share to ensure it's not accessed
share.zeroize();
None
}
Err(p) => Some(p),
}
}
pub fn generated_key_pair<D: Db>(
txn: &mut D::Transaction<'_>,
key: &Zeroizing<<Ristretto as Ciphersuite>::F>,
@@ -69,7 +95,7 @@ pub fn generated_key_pair<D: Db>(
DkgConfirmer::share(spec, key, attempt, preprocesses, key_pair)
}
pub(crate) fn fatal_slash<D: Db>(
pub(super) fn fatal_slash<D: Db>(
txn: &mut D::Transaction<'_>,
genesis: [u8; 32],
account: [u8; 32],
@@ -78,6 +104,33 @@ pub(crate) fn fatal_slash<D: Db>(
log::warn!("fatally slashing {}. reason: {}", hex::encode(account), reason);
TributaryDb::<D>::set_fatally_slashed(txn, genesis, account);
// TODO: disconnect the node from network/ban from further participation in all Tributaries
// TODO: If during DKG, trigger a re-attempt
}
// 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
fn fatal_slash_with_participant_index<D: Db>(
spec: &TributarySpec,
txn: &mut <D as Db>::Transaction<'_>,
i: Participant,
reason: &str,
) {
// Resolve from Participant to <Ristretto as Ciphersuite>::G
let i = u16::from(i);
let mut validator = None;
for (potential, _) in spec.validators() {
let v_i = 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();
fatal_slash::<D>(txn, spec.genesis(), validator.to_bytes(), reason);
}
pub(crate) async fn handle_application_tx<
@@ -98,6 +151,15 @@ pub(crate) async fn handle_application_tx<
) {
let genesis = spec.genesis();
// Don't handle transactions from fatally slashed participants
// TODO: Because fatally slashed participants can still publish onto the blockchain, they have
// a notable DoS ability
if let TransactionKind::Signed(signed) = tx.kind() {
if TributaryDb::<D>::is_fatally_slashed(txn, genesis, signed.signer.to_bytes()) {
return;
}
}
let handle = |txn: &mut <D as Db>::Transaction<'_>,
data_spec: &DataSpecification,
bytes: Vec<u8>,
@@ -178,6 +240,9 @@ pub(crate) async fn handle_application_tx<
}
match tx {
Transaction::RemoveParticipant(i) => {
fatal_slash_with_participant_index::<D>(spec, txn, i, "RemoveParticipant Provided TX")
}
Transaction::DkgCommitments(attempt, commitments, signed) => {
let Ok(_) = check_sign_data_len::<D>(txn, spec, signed.signer, commitments.len()) else {
return;
@@ -230,7 +295,28 @@ pub(crate) async fn handle_application_tx<
}
}
// Only save our share's bytes
// Save each share as needed for blame
{
let from = spec.i(signed.signer).unwrap();
for (to, shares) in shares.iter().enumerate() {
// 0-indexed (the enumeration) to 1-indexed (Participant)
let mut to = u16::try_from(to).unwrap() + 1;
// Adjust for the omission of the sender's own shares
if to >= u16::from(from.start) {
to += u16::from(from.end) - u16::from(from.start);
}
let to = Participant::new(to).unwrap();
for (sender_share, share) in shares.iter().enumerate() {
let from =
Participant::new(u16::from(from.start) + u16::try_from(sender_share).unwrap())
.unwrap();
TributaryDb::<D>::save_share_for_blame(txn, &genesis, from, to, share);
}
}
}
// Filter down to only our share's bytes for handle
let our_i = spec
.i(Ristretto::generator() * key.deref())
.expect("in a tributary we're not a validator for");
@@ -327,6 +413,49 @@ pub(crate) async fn handle_application_tx<
}
}
// TODO: Only accept one of either InvalidDkgShare/DkgConfirmed per signer
// TODO: Ban self-accusals
Transaction::InvalidDkgShare { attempt, accuser, faulty, blame, signed } => {
let range = spec.i(signed.signer).unwrap();
if (u16::from(accuser) < u16::from(range.start)) ||
(u16::from(range.end) <= u16::from(accuser))
{
fatal_slash::<D>(
txn,
genesis,
signed.signer.to_bytes(),
"accused with a Participant index which wasn't theirs",
);
return;
}
if !((u16::from(range.start) <= u16::from(faulty)) &&
(u16::from(faulty) < u16::from(range.end)))
{
fatal_slash::<D>(
txn,
genesis,
signed.signer.to_bytes(),
"accused self of having an InvalidDkgShare",
);
return;
}
let share = TributaryDb::<D>::share_for_blame(txn, &genesis, accuser, faulty).unwrap();
processors
.send(
spec.set().network,
key_gen::CoordinatorMessage::VerifyBlame {
id: KeyGenId { set: spec.set(), attempt },
accuser,
accused: faulty,
share,
blame,
},
)
.await;
}
Transaction::DkgConfirmed(attempt, shares, signed) => {
match handle(
txn,
@@ -347,11 +476,14 @@ pub(crate) async fn handle_application_tx<
"(including us) fires DkgConfirmed, yet no confirming key pair"
)
});
let Ok(sig) = DkgConfirmer::complete(spec, key, attempt, preprocesses, &key_pair, shares)
else {
// TODO: Full slash
todo!();
};
let sig =
match DkgConfirmer::complete(spec, key, attempt, preprocesses, &key_pair, shares) {
Ok(sig) => sig,
Err(p) => {
fatal_slash_with_participant_index::<D>(spec, txn, p, "invalid DkgConfirmer share");
return;
}
};
publish_serai_tx(
spec.set(),

View File

@@ -233,6 +233,8 @@ impl<const N: usize> ReadWrite for SignData<N> {
#[derive(Clone, PartialEq, Eq, Debug)]
pub enum Transaction {
RemoveParticipant(Participant),
// Once this completes successfully, no more instances should be created.
DkgCommitments(u32, Vec<Vec<u8>>, Signed),
DkgShares {
@@ -242,6 +244,13 @@ pub enum Transaction {
confirmation_nonces: [u8; 64],
signed: Signed,
},
InvalidDkgShare {
attempt: u32,
accuser: Participant,
faulty: Participant,
blame: Option<Vec<u8>>,
signed: Signed,
},
DkgConfirmed(u32, [u8; 32], Signed),
// When we have synchrony on a batch, we can allow signing it
@@ -279,7 +288,15 @@ impl ReadWrite for Transaction {
reader.read_exact(&mut kind)?;
match kind[0] {
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::new(io::ErrorKind::Other, "invalid participant in RemoveParticipant")
})?
})),
1 => {
let mut attempt = [0; 4];
reader.read_exact(&mut attempt)?;
let attempt = u32::from_le_bytes(attempt);
@@ -314,7 +331,7 @@ impl ReadWrite for Transaction {
Ok(Transaction::DkgCommitments(attempt, commitments, signed))
}
1 => {
2 => {
let mut attempt = [0; 4];
reader.read_exact(&mut attempt)?;
let attempt = u32::from_le_bytes(attempt);
@@ -351,7 +368,40 @@ impl ReadWrite for Transaction {
Ok(Transaction::DkgShares { attempt, shares, confirmation_nonces, signed })
}
2 => {
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::new(io::ErrorKind::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::new(io::ErrorKind::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)?;
let signed = Signed::read(reader)?;
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);
@@ -364,7 +414,7 @@ impl ReadWrite for Transaction {
Ok(Transaction::DkgConfirmed(attempt, confirmation_share, signed))
}
3 => {
5 => {
let mut block = [0; 32];
reader.read_exact(&mut block)?;
let mut batch = [0; 5];
@@ -372,19 +422,19 @@ impl ReadWrite for Transaction {
Ok(Transaction::Batch(block, batch))
}
4 => {
6 => {
let mut block = [0; 8];
reader.read_exact(&mut block)?;
Ok(Transaction::SubstrateBlock(u64::from_le_bytes(block)))
}
5 => SignData::read(reader).map(Transaction::BatchPreprocess),
6 => SignData::read(reader).map(Transaction::BatchShare),
7 => SignData::read(reader).map(Transaction::BatchPreprocess),
8 => SignData::read(reader).map(Transaction::BatchShare),
7 => SignData::read(reader).map(Transaction::SignPreprocess),
8 => SignData::read(reader).map(Transaction::SignShare),
9 => SignData::read(reader).map(Transaction::SignPreprocess),
10 => SignData::read(reader).map(Transaction::SignShare),
9 => {
11 => {
let mut plan = [0; 32];
reader.read_exact(&mut plan)?;
@@ -405,8 +455,13 @@ impl ReadWrite for Transaction {
fn write<W: io::Write>(&self, writer: &mut W) -> io::Result<()> {
match self {
Transaction::DkgCommitments(attempt, commitments, signed) => {
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::new(io::ErrorKind::Other, "zero commitments in DkgCommitments"))?
@@ -428,7 +483,7 @@ impl ReadWrite for Transaction {
}
Transaction::DkgShares { attempt, shares, confirmation_nonces, signed } => {
writer.write_all(&[1])?;
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
@@ -456,43 +511,59 @@ impl ReadWrite for Transaction {
signed.write(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(writer)
}
Transaction::DkgConfirmed(attempt, share, signed) => {
writer.write_all(&[2])?;
writer.write_all(&[4])?;
writer.write_all(&attempt.to_le_bytes())?;
writer.write_all(share)?;
signed.write(writer)
}
Transaction::Batch(block, batch) => {
writer.write_all(&[3])?;
writer.write_all(&[5])?;
writer.write_all(block)?;
writer.write_all(batch)
}
Transaction::SubstrateBlock(block) => {
writer.write_all(&[4])?;
writer.write_all(&[6])?;
writer.write_all(&block.to_le_bytes())
}
Transaction::BatchPreprocess(data) => {
writer.write_all(&[5])?;
writer.write_all(&[7])?;
data.write(writer)
}
Transaction::BatchShare(data) => {
writer.write_all(&[6])?;
writer.write_all(&[8])?;
data.write(writer)
}
Transaction::SignPreprocess(data) => {
writer.write_all(&[7])?;
writer.write_all(&[9])?;
data.write(writer)
}
Transaction::SignShare(data) => {
writer.write_all(&[8])?;
writer.write_all(&[10])?;
data.write(writer)
}
Transaction::SignCompleted { plan, tx_hash, first_signer, signature } => {
writer.write_all(&[9])?;
writer.write_all(&[11])?;
writer.write_all(plan)?;
writer
.write_all(&[u8::try_from(tx_hash.len()).expect("tx hash length exceed 255 bytes")])?;
@@ -507,8 +578,11 @@ impl ReadWrite for Transaction {
impl TransactionTrait for Transaction {
fn kind(&self) -> TransactionKind<'_> {
match self {
Transaction::RemoveParticipant(_) => TransactionKind::Provided("remove"),
Transaction::DkgCommitments(_, _, signed) => TransactionKind::Signed(signed),
Transaction::DkgShares { signed, .. } => TransactionKind::Signed(signed),
Transaction::InvalidDkgShare { signed, .. } => TransactionKind::Signed(signed),
Transaction::DkgConfirmed(_, _, signed) => TransactionKind::Signed(signed),
Transaction::Batch(_, _) => TransactionKind::Provided("batch"),
@@ -574,8 +648,11 @@ impl Transaction {
) {
fn signed(tx: &mut Transaction) -> &mut Signed {
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::Batch(_, _) => panic!("signing Batch"),

View File

@@ -85,6 +85,8 @@ impl<D: Db> NonceDecider<D> {
pub fn nonce<G: Get>(getter: &G, genesis: [u8; 32], tx: &Transaction) -> Option<Option<u32>> {
match tx {
Transaction::RemoveParticipant(_) => None,
Transaction::DkgCommitments(attempt, _, _) => {
assert_eq!(*attempt, 0);
Some(Some(0))
@@ -93,6 +95,12 @@ impl<D: Db> NonceDecider<D> {
assert_eq!(*attempt, 0);
Some(Some(1))
}
// InvalidDkgShare and DkgConfirmed share a nonce due to the expected existence of only one
// on-chain
Transaction::InvalidDkgShare { attempt, .. } => {
assert_eq!(*attempt, 0);
Some(Some(2))
}
Transaction::DkgConfirmed(attempt, _, _) => {
assert_eq!(*attempt, 0);
Some(Some(2))