mirror of
https://github.com/serai-dex/serai.git
synced 2025-12-09 04:39:24 +00:00
add reasons to slash evidence (#414)
* add reasons to slash evidence * fix CI failing * Remove unnecessary clones .encode() takes &self * InvalidVr to InvalidValidRound * Unrelated to this PR: Clarify reasoning/potentials behind dropping evidence * Clarify prevotes in SlashEvidence test * Replace use of read_to_end * Restore decode_signed_message --------- Co-authored-by: Luke Parker <lukeparker5132@gmail.com>
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
use std::io;
|
||||
|
||||
use scale::Decode;
|
||||
use scale::{Encode, Decode, IoReader};
|
||||
|
||||
use blake2::{Digest, Blake2s256};
|
||||
|
||||
@@ -19,53 +19,24 @@ use tendermint::{
|
||||
ext::{Network, Commit, RoundNumber, SignatureScheme},
|
||||
};
|
||||
|
||||
pub use tendermint::Evidence;
|
||||
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
#[derive(Clone, PartialEq, Eq, Debug)]
|
||||
pub enum TendermintTx {
|
||||
SlashEvidence(Vec<u8>),
|
||||
SlashEvidence(Evidence),
|
||||
}
|
||||
|
||||
impl ReadWrite for TendermintTx {
|
||||
fn read<R: io::Read>(reader: &mut R) -> io::Result<Self> {
|
||||
let mut kind = [0];
|
||||
reader.read_exact(&mut kind)?;
|
||||
match kind[0] {
|
||||
0 => {
|
||||
let mut len = [0; 4];
|
||||
reader.read_exact(&mut len)?;
|
||||
let mut len =
|
||||
usize::try_from(u32::from_le_bytes(len)).expect("running on a 16-bit system?");
|
||||
|
||||
let mut data = vec![];
|
||||
|
||||
// Read chunk-by-chunk so a claimed 4 GB length doesn't cause a 4 GB allocation
|
||||
// While we could check the length is sane, that'd require we know what a sane length is
|
||||
// We'd also have to maintain that length's sanity even as other parts of the codebase,
|
||||
// and even entire crates, change
|
||||
// This is fine as it'll eventually hit the P2P message size limit, yet doesn't require
|
||||
// knowing it nor does it make any assumptions
|
||||
const CHUNK_LEN: usize = 1024;
|
||||
let mut chunk = [0; CHUNK_LEN];
|
||||
while len > 0 {
|
||||
let to_read = len.min(CHUNK_LEN);
|
||||
data.reserve(to_read);
|
||||
reader.read_exact(&mut chunk[.. to_read])?;
|
||||
data.extend(&chunk[.. to_read]);
|
||||
len -= to_read;
|
||||
}
|
||||
Ok(TendermintTx::SlashEvidence(data))
|
||||
}
|
||||
_ => Err(io::Error::new(io::ErrorKind::Other, "invalid transaction type")),
|
||||
}
|
||||
Evidence::decode(&mut IoReader(reader))
|
||||
.map(TendermintTx::SlashEvidence)
|
||||
.map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "invalid evidence format"))
|
||||
}
|
||||
|
||||
fn write<W: io::Write>(&self, writer: &mut W) -> io::Result<()> {
|
||||
match self {
|
||||
TendermintTx::SlashEvidence(ev) => {
|
||||
writer.write_all(&[0])?;
|
||||
writer.write_all(&u32::try_from(ev.len()).unwrap().to_le_bytes())?;
|
||||
writer.write_all(ev)
|
||||
}
|
||||
TendermintTx::SlashEvidence(ev) => writer.write_all(&ev.encode()),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -92,13 +63,23 @@ impl Transaction for TendermintTx {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn decode_evidence<N: Network>(
|
||||
mut ev: &[u8],
|
||||
) -> Result<(SignedMessageFor<N>, Option<SignedMessageFor<N>>), TransactionError> {
|
||||
<(SignedMessageFor<N>, Option<SignedMessageFor<N>>)>::decode(&mut ev).map_err(|_| {
|
||||
dbg!("failed to decode");
|
||||
TransactionError::InvalidContent
|
||||
})
|
||||
pub fn decode_signed_message<N: Network>(
|
||||
mut data: &[u8],
|
||||
) -> Result<SignedMessageFor<N>, TransactionError> {
|
||||
SignedMessageFor::<N>::decode(&mut data).map_err(|_| TransactionError::InvalidContent)
|
||||
}
|
||||
|
||||
fn decode_and_verify_signed_message<N: Network>(
|
||||
data: &[u8],
|
||||
schema: &N::SignatureScheme,
|
||||
) -> Result<SignedMessageFor<N>, TransactionError> {
|
||||
let msg = decode_signed_message::<N>(data)?;
|
||||
|
||||
// verify that evidence messages are signed correctly
|
||||
if !msg.verify_signature(schema) {
|
||||
Err(TransactionError::InvalidSignature)?
|
||||
}
|
||||
Ok(msg)
|
||||
}
|
||||
|
||||
// TODO: Move this into tendermint-machine
|
||||
@@ -115,70 +96,59 @@ pub(crate) fn verify_tendermint_tx<N: Network>(
|
||||
match tx {
|
||||
// TODO: Only allow one evidence per validator, since evidence is fatal
|
||||
TendermintTx::SlashEvidence(ev) => {
|
||||
let (first, second) = decode_evidence::<N>(ev)?;
|
||||
match ev {
|
||||
Evidence::ConflictingMessages(first, second) => {
|
||||
let first = decode_and_verify_signed_message::<N>(first, &schema)?.msg;
|
||||
let second = decode_and_verify_signed_message::<N>(second, &schema)?.msg;
|
||||
|
||||
// verify that evidence messages are signed correctly
|
||||
if !first.verify_signature(&schema) {
|
||||
Err(TransactionError::InvalidSignature)?
|
||||
}
|
||||
let first = first.msg;
|
||||
// Make sure they're distinct messages, from the same sender, within the same block
|
||||
if (first == second) || (first.sender != second.sender) || (first.block != second.block) {
|
||||
Err(TransactionError::InvalidContent)?;
|
||||
}
|
||||
|
||||
if let Some(second) = second {
|
||||
if !second.verify_signature(&schema) {
|
||||
Err(TransactionError::InvalidSignature)?
|
||||
// Distinct messages within the same step
|
||||
if !((first.round == second.round) && (first.data.step() == second.data.step())) {
|
||||
Err(TransactionError::InvalidContent)?;
|
||||
}
|
||||
}
|
||||
let second = second.msg;
|
||||
Evidence::ConflictingPrecommit(first, second) => {
|
||||
let first = decode_and_verify_signed_message::<N>(first, &schema)?.msg;
|
||||
let second = decode_and_verify_signed_message::<N>(second, &schema)?.msg;
|
||||
|
||||
// 2 types of evidence here
|
||||
// 1- multiple distinct messages for the same block + round + step
|
||||
// 2- precommitted to multiple blocks
|
||||
if (first.sender != second.sender) || (first.block != second.block) {
|
||||
Err(TransactionError::InvalidContent)?;
|
||||
}
|
||||
|
||||
// Make sure they're distinct messages, from the same sender, within the same block
|
||||
if (first == second) || (first.sender != second.sender) || (first.block != second.block) {
|
||||
Err(TransactionError::InvalidContent)?;
|
||||
}
|
||||
|
||||
// Distinct messages within the same step
|
||||
if (first.round == second.round) && (first.data.step() == second.data.step()) {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// check whether messages are precommits to different blocks
|
||||
// The inner signatures don't need to be verified since the outer signatures were
|
||||
// While the inner signatures may be invalid, that would've yielded a invalid precommit
|
||||
// signature slash instead of distinct precommit slash
|
||||
if let Data::Precommit(Some((h1, _))) = first.data {
|
||||
if let Data::Precommit(Some((h2, _))) = second.data {
|
||||
if h1 == h2 {
|
||||
Err(TransactionError::InvalidContent)?;
|
||||
// check whether messages are precommits to different blocks
|
||||
// The inner signatures don't need to be verified since the outer signatures were
|
||||
// While the inner signatures may be invalid, that would've yielded a invalid precommit
|
||||
// signature slash instead of distinct precommit slash
|
||||
if let Data::Precommit(Some((h1, _))) = first.data {
|
||||
if let Data::Precommit(Some((h2, _))) = second.data {
|
||||
if h1 == h2 {
|
||||
Err(TransactionError::InvalidContent)?;
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// No fault identified
|
||||
Err(TransactionError::InvalidContent)?
|
||||
}
|
||||
Evidence::InvalidPrecommit(msg) => {
|
||||
let msg = decode_and_verify_signed_message::<N>(msg, &schema)?.msg;
|
||||
|
||||
// No fault identified
|
||||
Err(TransactionError::InvalidContent)?
|
||||
}
|
||||
|
||||
// 2 types of evidence can be here
|
||||
// 1- invalid commit signature
|
||||
// 2- vr number that was greater than or equal to the current round
|
||||
match &first.data {
|
||||
Data::Proposal(vr, _) => {
|
||||
// check the vr
|
||||
if vr.is_none() || vr.unwrap().0 < first.round.0 {
|
||||
let Data::Precommit(Some((id, sig))) = &msg.data else {
|
||||
Err(TransactionError::InvalidContent)?
|
||||
}
|
||||
}
|
||||
Data::Precommit(Some((id, sig))) => {
|
||||
};
|
||||
// TODO: We need to be passed in the genesis time to handle this edge case
|
||||
if first.block.0 == 0 {
|
||||
if msg.block.0 == 0 {
|
||||
todo!("invalid precommit signature on first block")
|
||||
}
|
||||
|
||||
// get the last commit
|
||||
// TODO: Why do we use u32 when Tendermint uses u64?
|
||||
let prior_commit = match u32::try_from(first.block.0 - 1) {
|
||||
let prior_commit = match u32::try_from(msg.block.0 - 1) {
|
||||
Ok(n) => match commit(n) {
|
||||
Some(c) => c,
|
||||
// If we have yet to sync the block in question, we will return InvalidContent based
|
||||
@@ -193,16 +163,25 @@ pub(crate) fn verify_tendermint_tx<N: Network>(
|
||||
|
||||
// calculate the end time till the msg round
|
||||
let mut last_end_time = CanonicalInstant::new(prior_commit.end_time);
|
||||
for r in 0 ..= first.round.0 {
|
||||
for r in 0 ..= msg.round.0 {
|
||||
last_end_time = RoundData::<N>::new(RoundNumber(r), last_end_time).end_time();
|
||||
}
|
||||
|
||||
// verify that the commit was actually invalid
|
||||
if schema.verify(first.sender, &commit_msg(last_end_time.canonical(), id.as_ref()), sig) {
|
||||
if schema.verify(msg.sender, &commit_msg(last_end_time.canonical(), id.as_ref()), sig) {
|
||||
Err(TransactionError::InvalidContent)?
|
||||
}
|
||||
}
|
||||
Evidence::InvalidValidRound(msg) => {
|
||||
let msg = decode_and_verify_signed_message::<N>(msg, &schema)?.msg;
|
||||
|
||||
let Data::Proposal(Some(vr), _) = &msg.data else {
|
||||
Err(TransactionError::InvalidContent)?
|
||||
};
|
||||
if vr.0 < msg.round.0 {
|
||||
Err(TransactionError::InvalidContent)?
|
||||
}
|
||||
}
|
||||
_ => Err(TransactionError::InvalidContent)?,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user