mirror of
https://github.com/serai-dex/serai.git
synced 2024-12-23 03:59:22 +00:00
Finish binding Tendermint, bar the P2P layer
This commit is contained in:
parent
997dd611d5
commit
03a6470a5b
7 changed files with 192 additions and 37 deletions
|
@ -21,8 +21,14 @@ transcript = { package = "flexible-transcript", path = "../../crypto/transcript"
|
|||
ciphersuite = { package = "ciphersuite", path = "../../crypto/ciphersuite", features = ["ristretto"] }
|
||||
schnorr = { package = "schnorr-signatures", path = "../../crypto/schnorr" }
|
||||
|
||||
hex = "0.4"
|
||||
log = "0.4"
|
||||
|
||||
scale = { package = "parity-scale-codec", version = "3", features = ["derive"] }
|
||||
tendermint = { package = "tendermint-machine", path = "./tendermint" }
|
||||
|
||||
tokio = { version = "1", features = ["macros", "sync", "time", "rt"] }
|
||||
|
||||
[dev-dependencies]
|
||||
zeroize = "^1.5"
|
||||
rand_core = "0.6"
|
||||
|
|
|
@ -48,7 +48,7 @@ impl ReadWrite for BlockHeader {
|
|||
}
|
||||
|
||||
impl BlockHeader {
|
||||
fn hash(&self) -> [u8; 32] {
|
||||
pub fn hash(&self) -> [u8; 32] {
|
||||
Blake2s256::digest([b"tributary_block".as_ref(), &self.serialize()].concat()).into()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -55,12 +55,8 @@ impl<T: Transaction> Blockchain<T> {
|
|||
}
|
||||
|
||||
/// Add a block.
|
||||
#[must_use]
|
||||
pub fn add_block(&mut self, block: &Block<T>) -> bool {
|
||||
// TODO: Handle desyncs re: provided transactions properly
|
||||
if self.verify_block(block).is_err() {
|
||||
return false;
|
||||
}
|
||||
pub fn add_block(&mut self, block: &Block<T>) -> Result<(), BlockError> {
|
||||
self.verify_block(block)?;
|
||||
|
||||
// None of the following assertions should be reachable since we verified the block
|
||||
self.tip = block.hash();
|
||||
|
@ -85,6 +81,6 @@ impl<T: Transaction> Blockchain<T> {
|
|||
}
|
||||
}
|
||||
|
||||
true
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,7 @@
|
|||
use core::ops::Deref;
|
||||
use std::{sync::Arc, collections::HashMap};
|
||||
|
||||
use async_trait::async_trait;
|
||||
|
||||
use subtle::ConstantTimeEq;
|
||||
use zeroize::{Zeroize, Zeroizing};
|
||||
|
@ -14,7 +17,18 @@ use ciphersuite::{
|
|||
};
|
||||
use schnorr::SchnorrSignature;
|
||||
|
||||
use tendermint::ext::{Signer as SignerTrait, SignatureScheme as SignatureSchemeTrait};
|
||||
use scale::{Encode, Decode};
|
||||
use tendermint::{
|
||||
SignedMessageFor,
|
||||
ext::{
|
||||
BlockNumber, RoundNumber, Signer as SignerTrait, SignatureScheme, Weights, Block as BlockTrait,
|
||||
BlockError as TendermintBlockError, Commit, Network as NetworkTrait,
|
||||
},
|
||||
};
|
||||
|
||||
use tokio::time::{Duration, sleep};
|
||||
|
||||
use crate::{ReadWrite, Transaction, TransactionError, BlockHeader, Block, BlockError, Blockchain};
|
||||
|
||||
fn challenge(
|
||||
genesis: [u8; 32],
|
||||
|
@ -37,7 +51,7 @@ struct Signer {
|
|||
key: Zeroizing<<Ristretto as Ciphersuite>::F>,
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
#[async_trait]
|
||||
impl SignerTrait for Signer {
|
||||
type ValidatorId = [u8; 32];
|
||||
type Signature = [u8; 64];
|
||||
|
@ -85,19 +99,25 @@ impl SignerTrait for Signer {
|
|||
}
|
||||
|
||||
#[derive(Clone, PartialEq, Eq, Debug)]
|
||||
struct SignatureScheme {
|
||||
struct Validators {
|
||||
genesis: [u8; 32],
|
||||
weight: u64,
|
||||
weights: HashMap<[u8; 32], u64>,
|
||||
robin: Vec<[u8; 32]>,
|
||||
}
|
||||
|
||||
impl SignatureSchemeTrait for SignatureScheme {
|
||||
impl SignatureScheme for Validators {
|
||||
type ValidatorId = [u8; 32];
|
||||
type Signature = [u8; 64];
|
||||
// TODO: Use half-aggregation.
|
||||
type AggregateSignature = Vec<[u8; 64]>;
|
||||
type Signer = Signer;
|
||||
type Signer = Arc<Signer>;
|
||||
|
||||
#[must_use]
|
||||
fn verify(&self, validator: Self::ValidatorId, msg: &[u8], sig: &Self::Signature) -> bool {
|
||||
if !self.weights.contains_key(&validator) {
|
||||
return false;
|
||||
}
|
||||
let Ok(validator_point) = Ristretto::read_G::<&[u8]>(&mut validator.as_ref()) else {
|
||||
return false;
|
||||
};
|
||||
|
@ -126,3 +146,122 @@ impl SignatureSchemeTrait for SignatureScheme {
|
|||
true
|
||||
}
|
||||
}
|
||||
|
||||
impl Weights for Validators {
|
||||
type ValidatorId = [u8; 32];
|
||||
|
||||
fn total_weight(&self) -> u64 {
|
||||
self.weight
|
||||
}
|
||||
fn weight(&self, validator: Self::ValidatorId) -> u64 {
|
||||
self.weights[&validator]
|
||||
}
|
||||
fn proposer(&self, block: BlockNumber, round: RoundNumber) -> Self::ValidatorId {
|
||||
let block = usize::try_from(block.0).unwrap();
|
||||
let round = usize::try_from(round.0).unwrap();
|
||||
// If multiple rounds are used, a naive block + round would cause the same index to be chosen
|
||||
// in quick succesion.
|
||||
// Accordingly, if we use additional rounds, jump halfway around.
|
||||
// While this is still game-able, it's not explicitly reusing indexes immediately after each
|
||||
// other.
|
||||
self.robin
|
||||
[(block + (if round == 0 { 0 } else { round + (self.robin.len() / 2) })) % self.robin.len()]
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq, Eq, Debug, Encode, Decode)]
|
||||
struct TendermintBlock(Vec<u8>);
|
||||
impl BlockTrait for TendermintBlock {
|
||||
type Id = [u8; 32];
|
||||
fn id(&self) -> Self::Id {
|
||||
BlockHeader::read::<&[u8]>(&mut self.0.as_ref()).unwrap().hash()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq, Eq, Debug)]
|
||||
struct Network<T: Transaction> {
|
||||
genesis: [u8; 32],
|
||||
signer: Arc<Signer>,
|
||||
validators: Arc<Validators>,
|
||||
blockchain: Blockchain<T>,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<T: Transaction> NetworkTrait for Network<T> {
|
||||
type ValidatorId = [u8; 32];
|
||||
type SignatureScheme = Arc<Validators>;
|
||||
type Weights = Arc<Validators>;
|
||||
type Block = TendermintBlock;
|
||||
|
||||
const BLOCK_PROCESSING_TIME: u32 = 3;
|
||||
const LATENCY_TIME: u32 = 1;
|
||||
|
||||
fn signer(&self) -> Arc<Signer> {
|
||||
self.signer.clone()
|
||||
}
|
||||
fn signature_scheme(&self) -> Arc<Validators> {
|
||||
self.validators.clone()
|
||||
}
|
||||
fn weights(&self) -> Arc<Validators> {
|
||||
self.validators.clone()
|
||||
}
|
||||
|
||||
async fn broadcast(&mut self, _msg: SignedMessageFor<Self>) {
|
||||
todo!()
|
||||
}
|
||||
async fn slash(&mut self, validator: Self::ValidatorId) {
|
||||
log::error!(
|
||||
"validator {} was slashed on tributary {}",
|
||||
hex::encode(validator),
|
||||
hex::encode(self.genesis)
|
||||
);
|
||||
}
|
||||
|
||||
async fn validate(&mut self, block: &Self::Block) -> Result<(), TendermintBlockError> {
|
||||
let block =
|
||||
Block::read::<&[u8]>(&mut block.0.as_ref()).map_err(|_| TendermintBlockError::Fatal)?;
|
||||
self.blockchain.verify_block(&block).map_err(|e| match e {
|
||||
BlockError::TransactionError(TransactionError::MissingProvided(_)) => {
|
||||
TendermintBlockError::Temporal
|
||||
}
|
||||
_ => TendermintBlockError::Fatal,
|
||||
})
|
||||
}
|
||||
|
||||
async fn add_block(
|
||||
&mut self,
|
||||
block: Self::Block,
|
||||
_commit: Commit<Self::SignatureScheme>,
|
||||
) -> Option<Self::Block> {
|
||||
let invalid_block = || {
|
||||
// There's a fatal flaw in the code, it's behind a hard fork, or the validators turned
|
||||
// malicious
|
||||
// All justify a halt to then achieve social consensus from
|
||||
// TODO: Under multiple validator sets, a small validator set turning malicious knocks
|
||||
// off the entire network. That's an unacceptable DoS.
|
||||
panic!("validators added invalid block to tributary {}", hex::encode(self.genesis));
|
||||
};
|
||||
|
||||
let Ok(block) = Block::read::<&[u8]>(&mut block.0.as_ref()) else {
|
||||
return invalid_block();
|
||||
};
|
||||
|
||||
loop {
|
||||
match self.blockchain.add_block(&block) {
|
||||
Ok(()) => break,
|
||||
Err(BlockError::TransactionError(TransactionError::MissingProvided(hash))) => {
|
||||
log::error!(
|
||||
"missing provided transaction {} which other validators on tributary {} had",
|
||||
hex::encode(hash),
|
||||
hex::encode(self.genesis)
|
||||
);
|
||||
sleep(Duration::from_secs(30)).await;
|
||||
}
|
||||
_ => return invalid_block(),
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Handle the commit and return the next proposal
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -35,7 +35,7 @@ fn block_addition() {
|
|||
assert_eq!(block.header.parent, genesis);
|
||||
assert_eq!(block.header.transactions, [0; 32]);
|
||||
blockchain.verify_block(&block).unwrap();
|
||||
assert!(blockchain.add_block(&block));
|
||||
assert!(blockchain.add_block(&block).is_ok());
|
||||
assert_eq!(blockchain.tip(), block.hash());
|
||||
}
|
||||
|
||||
|
@ -155,7 +155,7 @@ fn signed_transaction() {
|
|||
|
||||
// Verify and add the block
|
||||
blockchain.verify_block(&block).unwrap();
|
||||
assert!(blockchain.add_block(&block));
|
||||
assert!(blockchain.add_block(&block).is_ok());
|
||||
assert_eq!(blockchain.tip(), block.hash());
|
||||
};
|
||||
|
||||
|
@ -194,11 +194,11 @@ fn provided_transaction() {
|
|||
blockchain.verify_block(&block).unwrap();
|
||||
|
||||
// add_block should work for verified blocks
|
||||
assert!(blockchain.add_block(&block));
|
||||
assert!(blockchain.add_block(&block).is_ok());
|
||||
|
||||
let block = Block::new(blockchain.tip(), &txs, HashMap::new());
|
||||
// The provided transaction should no longer considered provided, causing this error
|
||||
assert!(blockchain.verify_block(&block).is_err());
|
||||
// add_block should fail for unverified provided transactions if told to add them
|
||||
assert!(!blockchain.add_block(&block));
|
||||
assert!(blockchain.add_block(&block).is_err());
|
||||
}
|
||||
|
|
|
@ -15,12 +15,18 @@ use crate::ReadWrite;
|
|||
|
||||
#[derive(Clone, PartialEq, Eq, Debug, Error)]
|
||||
pub enum TransactionError {
|
||||
/// This transaction was perceived as invalid against the current state.
|
||||
#[error("transaction temporally invalid")]
|
||||
Temporal,
|
||||
/// This transaction is definitively invalid.
|
||||
#[error("transaction definitively invalid")]
|
||||
Fatal,
|
||||
/// A provided transaction wasn't locally provided.
|
||||
#[error("provided transaction wasn't locally provided")]
|
||||
MissingProvided([u8; 32]),
|
||||
/// This transaction's signer isn't a participant.
|
||||
#[error("invalid signer")]
|
||||
InvalidSigner,
|
||||
/// This transaction's nonce isn't the prior nonce plus one.
|
||||
#[error("invalid nonce")]
|
||||
InvalidNonce,
|
||||
/// This transaction's signature is invalid.
|
||||
#[error("invalid signature")]
|
||||
InvalidSignature,
|
||||
}
|
||||
|
||||
/// Data for a signed transaction.
|
||||
|
@ -100,24 +106,25 @@ pub(crate) fn verify_transaction<T: Transaction>(
|
|||
|
||||
match tx.kind() {
|
||||
TransactionKind::Provided => {
|
||||
if !locally_provided.remove(&tx.hash()) {
|
||||
Err(TransactionError::Temporal)?;
|
||||
let hash = tx.hash();
|
||||
if !locally_provided.remove(&hash) {
|
||||
Err(TransactionError::MissingProvided(hash))?;
|
||||
}
|
||||
}
|
||||
TransactionKind::Unsigned => {}
|
||||
TransactionKind::Signed(Signed { signer, nonce, signature }) => {
|
||||
if let Some(next_nonce) = next_nonces.get(signer) {
|
||||
if nonce != next_nonce {
|
||||
Err(TransactionError::Temporal)?;
|
||||
Err(TransactionError::InvalidNonce)?;
|
||||
}
|
||||
} else {
|
||||
// Not a participant
|
||||
Err(TransactionError::Fatal)?;
|
||||
Err(TransactionError::InvalidSigner)?;
|
||||
}
|
||||
|
||||
// TODO: Use Schnorr half-aggregation and a batch verification here
|
||||
if !signature.verify(*signer, tx.sig_hash(genesis)) {
|
||||
Err(TransactionError::Fatal)?;
|
||||
Err(TransactionError::InvalidSignature)?;
|
||||
}
|
||||
|
||||
next_nonces.insert(*signer, nonce + 1);
|
||||
|
|
|
@ -241,23 +241,30 @@ pub trait Network: Send + Sync {
|
|||
commit.validators.iter().map(|v| weights.weight(*v)).sum::<u64>() >= weights.threshold()
|
||||
}
|
||||
|
||||
/// Broadcast a message to the other validators. If authenticated channels have already been
|
||||
/// established, this will double-authenticate. Switching to unauthenticated channels in a system
|
||||
/// already providing authenticated channels is not recommended as this is a minor, temporal
|
||||
/// inefficiency while downgrading channels may have wider implications.
|
||||
/// Broadcast a message to the other validators.
|
||||
///
|
||||
/// If authenticated channels have already been established, this will double-authenticate.
|
||||
/// Switching to unauthenticated channels in a system already providing authenticated channels is
|
||||
/// not recommended as this is a minor, temporal inefficiency, while downgrading channels may
|
||||
/// have wider implications.
|
||||
async fn broadcast(&mut self, msg: SignedMessageFor<Self>);
|
||||
|
||||
/// Trigger a slash for the validator in question who was definitively malicious.
|
||||
///
|
||||
/// The exact process of triggering a slash is undefined and left to the network as a whole.
|
||||
async fn slash(&mut self, validator: Self::ValidatorId);
|
||||
|
||||
/// Validate a block.
|
||||
async fn validate(&mut self, block: &Self::Block) -> Result<(), BlockError>;
|
||||
/// Add a block, returning the proposal for the next one. It's possible a block, which was never
|
||||
/// validated or even failed validation, may be passed here if a supermajority of validators did
|
||||
/// consider it valid and created a commit for it. This deviates from the paper which will have a
|
||||
/// local node refuse to decide on a block it considers invalid. This library acknowledges the
|
||||
/// network did decide on it, leaving handling of it to the network, and outside of this scope.
|
||||
|
||||
/// Add a block, returning the proposal for the next one.
|
||||
///
|
||||
/// It's possible a block, which was never validated or even failed validation, may be passed
|
||||
/// here if a supermajority of validators did consider it valid and created a commit for it.
|
||||
///
|
||||
/// This deviates from the paper which will have a local node refuse to decide on a block it
|
||||
/// considers invalid. This library acknowledges the network did decide on it, leaving handling
|
||||
/// of it to the network, and outside of this scope.
|
||||
async fn add_block(
|
||||
&mut self,
|
||||
block: Self::Block,
|
||||
|
|
Loading…
Reference in a new issue