Finish binding Tendermint, bar the P2P layer

This commit is contained in:
Luke Parker 2023-04-12 18:04:28 -04:00
parent 997dd611d5
commit 03a6470a5b
No known key found for this signature in database
7 changed files with 192 additions and 37 deletions

View file

@ -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"

View file

@ -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()
}
}

View file

@ -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(())
}
}

View file

@ -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!()
}
}

View file

@ -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());
}

View file

@ -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);

View file

@ -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,