mirror of
https://github.com/serai-dex/serai.git
synced 2025-12-08 12:19:24 +00:00
This helps identify where the various functionalities are used, or rather, not used. The `Ciphersuite` trait present in `patches/ciphersuite`, facilitating the entire FCMP++ tree, only requires the markers _and_ canonical point decoding. I've opened a PR to upstream such a trait into `group` (https://github.com/zkcrypto/group/pull/68). `WrappedGroup` is still justified for as long as `Group::generator` exists. Moving `::generator()` to its own trait, on an independent structure (upstream) would be massively appreciated. @tarcieri also wanted to update from `fn generator()` to `const GENERATOR`, which would encourage further discussion on https://github.com/zkcrypto/group/issues/32 and https://github.com/zkcrypto/group/issues/45, which have been stagnant. The `Id` trait is occasionally used yet really should be first off the chopping block. Finally, `WithPreferredHash` is only actually used around a third of the time, which more than justifies it being a separate trait. --- Updates `dalek_ff_group::Scalar` to directly re-export `curve25519_dalek::Scalar`, as without issue. `dalek_ff_group::RistrettoPoint` also could be replaced with an export of `curve25519_dalek::RistrettoPoint`, yet the coordinator relies on how we implemented `Hash` on it for the hell of it so it isn't worth it at this time. `dalek_ff_group::EdwardsPoint` can't be replaced for an re-export of `curve25519_dalek::SubgroupPoint` as it doesn't implement `zeroize`, `subtle` traits within a released, non-yanked version. Relevance to https://github.com/serai-dex/serai/issues/201 and https://github.com/dalek-cryptography/curve25519-dalek/issues/811#issuecomment-3247732746. Also updates the `Ristretto` ciphersuite to prefer `Blake2b-512` over `SHA2-512`. In order to maintain compliance with FROST's IETF standard, `modular-frost` defines its own ciphersuite for Ristretto which still uses `SHA2-512`.
301 lines
10 KiB
Rust
301 lines
10 KiB
Rust
use std::sync::Arc;
|
|
|
|
use zeroize::Zeroizing;
|
|
use rand::{RngCore, rngs::OsRng};
|
|
|
|
use dalek_ff_group::Ristretto;
|
|
use ciphersuite::*;
|
|
|
|
use scale::Encode;
|
|
|
|
use tendermint::{
|
|
time::CanonicalInstant,
|
|
round::RoundData,
|
|
Data, commit_msg, Evidence,
|
|
ext::{RoundNumber, Commit, Signer as SignerTrait},
|
|
};
|
|
|
|
use serai_db::MemDb;
|
|
|
|
use crate::{
|
|
ReadWrite,
|
|
tendermint::{
|
|
tx::{TendermintTx, verify_tendermint_tx},
|
|
TendermintBlock, Signer, Validators, TendermintNetwork,
|
|
},
|
|
tests::{
|
|
p2p::DummyP2p, SignedTransaction, random_evidence_tx, tendermint_meta, signed_from_data,
|
|
},
|
|
};
|
|
|
|
type N = TendermintNetwork<MemDb, SignedTransaction, DummyP2p>;
|
|
|
|
#[tokio::test]
|
|
async fn serialize_tendermint() {
|
|
// make a tendermint tx with random evidence
|
|
let (_, signer, _, _) = tendermint_meta().await;
|
|
let tx = random_evidence_tx::<N>(signer.into(), TendermintBlock(vec![])).await;
|
|
let res = TendermintTx::read::<&[u8]>(&mut tx.serialize().as_ref()).unwrap();
|
|
assert_eq!(res, tx);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn invalid_valid_round() {
|
|
// signer
|
|
let (_, signer, signer_id, validators) = tendermint_meta().await;
|
|
let commit = |_: u64| -> Option<Commit<Arc<Validators>>> {
|
|
Some(Commit::<Arc<Validators>> { end_time: 0, validators: vec![], signature: vec![] })
|
|
};
|
|
|
|
let valid_round_tx = |valid_round| {
|
|
let signer = signer.clone();
|
|
async move {
|
|
let data = Data::Proposal(valid_round, TendermintBlock(vec![]));
|
|
let signed = signed_from_data::<N>(signer.clone().into(), signer_id, 0, 0, data).await;
|
|
(signed.clone(), TendermintTx::SlashEvidence(Evidence::InvalidValidRound(signed.encode())))
|
|
}
|
|
};
|
|
|
|
// This should be invalid evidence if a valid valid round is specified
|
|
let (_, tx) = valid_round_tx(None).await;
|
|
assert!(verify_tendermint_tx::<N>(&tx, &validators, commit).is_err());
|
|
|
|
// If an invalid valid round is specified (>= current), this should be invalid evidence
|
|
let (mut signed, tx) = valid_round_tx(Some(RoundNumber(0))).await;
|
|
|
|
// should pass
|
|
verify_tendermint_tx::<N>(&tx, &validators, commit).unwrap();
|
|
|
|
// change the signature
|
|
let mut random_sig = [0u8; 64];
|
|
OsRng.fill_bytes(&mut random_sig);
|
|
signed.sig = random_sig;
|
|
let tx = TendermintTx::SlashEvidence(Evidence::InvalidValidRound(signed.encode()));
|
|
|
|
// should fail
|
|
assert!(verify_tendermint_tx::<N>(&tx, &validators, commit).is_err());
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn invalid_precommit_signature() {
|
|
let (_, signer, signer_id, validators) = tendermint_meta().await;
|
|
let commit = |i: u64| -> Option<Commit<Arc<Validators>>> {
|
|
assert_eq!(i, 0);
|
|
Some(Commit::<Arc<Validators>> { end_time: 0, validators: vec![], signature: vec![] })
|
|
};
|
|
|
|
let precommit = |precommit| {
|
|
let signer = signer.clone();
|
|
async move {
|
|
let signed =
|
|
signed_from_data::<N>(signer.clone().into(), signer_id, 1, 0, Data::Precommit(precommit))
|
|
.await;
|
|
(signed.clone(), TendermintTx::SlashEvidence(Evidence::InvalidPrecommit(signed.encode())))
|
|
}
|
|
};
|
|
|
|
// Empty Precommit should fail.
|
|
assert!(verify_tendermint_tx::<N>(&precommit(None).await.1, &validators, commit).is_err());
|
|
|
|
// valid precommit signature should fail.
|
|
let block_id = [0x22u8; 32];
|
|
let last_end_time =
|
|
RoundData::<N>::new(RoundNumber(0), CanonicalInstant::new(commit(0).unwrap().end_time))
|
|
.end_time();
|
|
let commit_msg = commit_msg(last_end_time.canonical(), block_id.as_ref());
|
|
|
|
assert!(verify_tendermint_tx::<N>(
|
|
&precommit(Some((block_id, signer.clone().sign(&commit_msg).await))).await.1,
|
|
&validators,
|
|
commit
|
|
)
|
|
.is_err());
|
|
|
|
// any other signature can be used as evidence.
|
|
{
|
|
let (mut signed, tx) = precommit(Some((block_id, signer.sign(&[]).await))).await;
|
|
verify_tendermint_tx::<N>(&tx, &validators, commit).unwrap();
|
|
|
|
// So long as we can authenticate where it came from
|
|
let mut random_sig = [0u8; 64];
|
|
OsRng.fill_bytes(&mut random_sig);
|
|
signed.sig = random_sig;
|
|
let tx = TendermintTx::SlashEvidence(Evidence::InvalidPrecommit(signed.encode()));
|
|
assert!(verify_tendermint_tx::<N>(&tx, &validators, commit).is_err());
|
|
}
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn evidence_with_prevote() {
|
|
let (_, signer, signer_id, validators) = tendermint_meta().await;
|
|
let commit = |_: u64| -> Option<Commit<Arc<Validators>>> {
|
|
Some(Commit::<Arc<Validators>> { end_time: 0, validators: vec![], signature: vec![] })
|
|
};
|
|
|
|
let prevote = |block_id| {
|
|
let signer = signer.clone();
|
|
async move {
|
|
// it should fail for all reasons.
|
|
let mut txs = vec![];
|
|
txs.push(TendermintTx::SlashEvidence(Evidence::InvalidPrecommit(
|
|
signed_from_data::<N>(signer.clone().into(), signer_id, 0, 0, Data::Prevote(block_id))
|
|
.await
|
|
.encode(),
|
|
)));
|
|
txs.push(TendermintTx::SlashEvidence(Evidence::InvalidValidRound(
|
|
signed_from_data::<N>(signer.clone().into(), signer_id, 0, 0, Data::Prevote(block_id))
|
|
.await
|
|
.encode(),
|
|
)));
|
|
// Since these require a second message, provide this one again
|
|
// ConflictingMessages can be fired for actually conflicting Prevotes however
|
|
txs.push(TendermintTx::SlashEvidence(Evidence::ConflictingMessages(
|
|
signed_from_data::<N>(signer.clone().into(), signer_id, 0, 0, Data::Prevote(block_id))
|
|
.await
|
|
.encode(),
|
|
signed_from_data::<N>(signer.clone().into(), signer_id, 0, 0, Data::Prevote(block_id))
|
|
.await
|
|
.encode(),
|
|
)));
|
|
txs
|
|
}
|
|
};
|
|
|
|
// No prevote message alone should be valid as slash evidence at this time
|
|
for prevote in prevote(None).await {
|
|
assert!(verify_tendermint_tx::<N>(&prevote, &validators, commit).is_err());
|
|
}
|
|
for prevote in prevote(Some([0x22u8; 32])).await {
|
|
assert!(verify_tendermint_tx::<N>(&prevote, &validators, commit).is_err());
|
|
}
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn conflicting_msgs_evidence_tx() {
|
|
let (genesis, signer, signer_id, validators) = tendermint_meta().await;
|
|
let commit = |i: u64| -> Option<Commit<Arc<Validators>>> {
|
|
assert_eq!(i, 0);
|
|
Some(Commit::<Arc<Validators>> { end_time: 0, validators: vec![], signature: vec![] })
|
|
};
|
|
|
|
// Block b, round n
|
|
let signed_for_b_r = |block, round, data| {
|
|
let signer = signer.clone();
|
|
async move { signed_from_data::<N>(signer.clone().into(), signer_id, block, round, data).await }
|
|
};
|
|
|
|
// Proposal
|
|
{
|
|
// non-conflicting data should fail
|
|
let signed_1 = signed_for_b_r(0, 0, Data::Proposal(None, TendermintBlock(vec![0x11]))).await;
|
|
let tx = TendermintTx::SlashEvidence(Evidence::ConflictingMessages(
|
|
signed_1.encode(),
|
|
signed_1.encode(),
|
|
));
|
|
assert!(verify_tendermint_tx::<N>(&tx, &validators, commit).is_err());
|
|
|
|
// conflicting data should pass
|
|
let signed_2 = signed_for_b_r(0, 0, Data::Proposal(None, TendermintBlock(vec![0x22]))).await;
|
|
let tx = TendermintTx::SlashEvidence(Evidence::ConflictingMessages(
|
|
signed_1.encode(),
|
|
signed_2.encode(),
|
|
));
|
|
verify_tendermint_tx::<N>(&tx, &validators, commit).unwrap();
|
|
|
|
// Except if it has a distinct round number, as we don't check cross-round conflicts
|
|
// (except for Precommit)
|
|
let signed_2 = signed_for_b_r(0, 1, Data::Proposal(None, TendermintBlock(vec![0x22]))).await;
|
|
let tx = TendermintTx::SlashEvidence(Evidence::ConflictingMessages(
|
|
signed_1.encode(),
|
|
signed_2.encode(),
|
|
));
|
|
verify_tendermint_tx::<N>(&tx, &validators, commit).unwrap_err();
|
|
|
|
// Proposals for different block numbers should also fail as evidence
|
|
let signed_2 = signed_for_b_r(1, 0, Data::Proposal(None, TendermintBlock(vec![0x22]))).await;
|
|
let tx = TendermintTx::SlashEvidence(Evidence::ConflictingMessages(
|
|
signed_1.encode(),
|
|
signed_2.encode(),
|
|
));
|
|
verify_tendermint_tx::<N>(&tx, &validators, commit).unwrap_err();
|
|
}
|
|
|
|
// Prevote
|
|
{
|
|
// non-conflicting data should fail
|
|
let signed_1 = signed_for_b_r(0, 0, Data::Prevote(Some([0x11; 32]))).await;
|
|
let tx = TendermintTx::SlashEvidence(Evidence::ConflictingMessages(
|
|
signed_1.encode(),
|
|
signed_1.encode(),
|
|
));
|
|
assert!(verify_tendermint_tx::<N>(&tx, &validators, commit).is_err());
|
|
|
|
// conflicting data should pass
|
|
let signed_2 = signed_for_b_r(0, 0, Data::Prevote(Some([0x22; 32]))).await;
|
|
let tx = TendermintTx::SlashEvidence(Evidence::ConflictingMessages(
|
|
signed_1.encode(),
|
|
signed_2.encode(),
|
|
));
|
|
verify_tendermint_tx::<N>(&tx, &validators, commit).unwrap();
|
|
|
|
// Except if it has a distinct round number, as we don't check cross-round conflicts
|
|
// (except for Precommit)
|
|
let signed_2 = signed_for_b_r(0, 1, Data::Prevote(Some([0x22; 32]))).await;
|
|
let tx = TendermintTx::SlashEvidence(Evidence::ConflictingMessages(
|
|
signed_1.encode(),
|
|
signed_2.encode(),
|
|
));
|
|
verify_tendermint_tx::<N>(&tx, &validators, commit).unwrap_err();
|
|
|
|
// Proposals for different block numbers should also fail as evidence
|
|
let signed_2 = signed_for_b_r(1, 0, Data::Prevote(Some([0x22; 32]))).await;
|
|
let tx = TendermintTx::SlashEvidence(Evidence::ConflictingMessages(
|
|
signed_1.encode(),
|
|
signed_2.encode(),
|
|
));
|
|
verify_tendermint_tx::<N>(&tx, &validators, commit).unwrap_err();
|
|
}
|
|
|
|
// msgs from different senders should fail
|
|
{
|
|
let signed_1 = signed_for_b_r(0, 0, Data::Proposal(None, TendermintBlock(vec![0x11]))).await;
|
|
|
|
let signer_2 =
|
|
Signer::new(genesis, Zeroizing::new(<Ristretto as WrappedGroup>::F::random(&mut OsRng)));
|
|
let signed_id_2 = signer_2.validator_id().await.unwrap();
|
|
let signed_2 = signed_from_data::<N>(
|
|
signer_2.into(),
|
|
signed_id_2,
|
|
0,
|
|
0,
|
|
Data::Proposal(None, TendermintBlock(vec![0x22])),
|
|
)
|
|
.await;
|
|
|
|
let tx = TendermintTx::SlashEvidence(Evidence::ConflictingMessages(
|
|
signed_1.encode(),
|
|
signed_2.encode(),
|
|
));
|
|
|
|
// update schema so that we don't fail due to invalid signature
|
|
let signer_pub = <Ristretto as GroupIo>::read_G::<&[u8]>(&mut signer_id.as_slice()).unwrap();
|
|
let signer_pub_2 =
|
|
<Ristretto as GroupIo>::read_G::<&[u8]>(&mut signed_id_2.as_slice()).unwrap();
|
|
let validators =
|
|
Arc::new(Validators::new(genesis, vec![(signer_pub, 1), (signer_pub_2, 1)]).unwrap());
|
|
|
|
assert!(verify_tendermint_tx::<N>(&tx, &validators, commit).is_err());
|
|
}
|
|
|
|
// msgs with different steps should fail
|
|
{
|
|
let signed_1 = signed_for_b_r(0, 0, Data::Proposal(None, TendermintBlock(vec![]))).await;
|
|
let signed_2 = signed_for_b_r(0, 0, Data::Prevote(None)).await;
|
|
let tx = TendermintTx::SlashEvidence(Evidence::ConflictingMessages(
|
|
signed_1.encode(),
|
|
signed_2.encode(),
|
|
));
|
|
assert!(verify_tendermint_tx::<N>(&tx, &validators, commit).is_err());
|
|
}
|
|
}
|