-
Notifications
You must be signed in to change notification settings - Fork 8
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add blockchain crate defining basic emulated blockchain structure (#9)
The big missing parat are rewards and slashing. For rewards we probably need to keep track who generated signatures (including valid signatures submitted within some period after quorum has been reached). With slashing there are many questions to answer: do we and if so how do we remove slashed validator from validators set.
- Loading branch information
Showing
14 changed files
with
1,902 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
[package] | ||
name = "blockchain" | ||
authors = ["Michal Nazarewicz <[email protected]>"] | ||
version = "0.0.0" | ||
edition = "2021" | ||
|
||
[dependencies] | ||
borsh.workspace = true | ||
derive_more.workspace = true | ||
|
||
lib = { workspace = true, features = ["borsh"] } | ||
|
||
[dev-dependencies] | ||
lib = { workspace = true, features = ["borsh", "test_utils"] } | ||
rand.workspace = true | ||
stdx.workspace = true | ||
|
||
[features] | ||
std = [] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,243 @@ | ||
use lib::hash::CryptoHash; | ||
|
||
use crate::epoch; | ||
use crate::height::{BlockHeight, HostHeight}; | ||
use crate::validators::PubKey; | ||
|
||
type Result<T, E = borsh::maybestd::io::Error> = core::result::Result<T, E>; | ||
|
||
/// A single block of the emulated blockchain. | ||
/// | ||
/// Emulated block’s height and timestamp are taken directly from the host | ||
/// chain. Emulated blocks don’t have their own timestamps. | ||
/// | ||
/// A block is uniquely identified by its hash which can be obtained via | ||
/// [`Block::calc_hash`]. | ||
/// | ||
/// Each block belongs to an epoch (identifier by `epoch_id`) which describes | ||
/// set of validators which can sign the block. A new epoch is introduced by | ||
/// setting `next_epoch` field; epoch becomes current one starting from the | ||
/// following block. | ||
#[derive( | ||
Clone, Debug, PartialEq, Eq, borsh::BorshSerialize, borsh::BorshDeserialize, | ||
)] | ||
pub struct Block<PK> { | ||
/// Version of the structure. At the moment always zero byte. | ||
version: crate::common::VersionZero, | ||
|
||
/// Hash of the previous block. | ||
pub prev_block_hash: CryptoHash, | ||
/// Height of the emulated blockchain’s block. | ||
pub block_height: BlockHeight, | ||
/// Height of the host blockchain’s block in which this block was created. | ||
pub host_height: HostHeight, | ||
/// Timestamp of the host blockchani’s block in which this block was created. | ||
pub host_timestamp: u64, | ||
/// Hash of the root node of the state trie, i.e. the commitment | ||
/// of the state. | ||
pub state_root: CryptoHash, | ||
|
||
/// Hash of the block in which current epoch has been defined. | ||
/// | ||
/// Epoch determines validators set signing each block. If epoch is about | ||
/// to change, the new epoch is defined in `next_epoch` field. Then, the | ||
/// very next block will use current’s block hash as `epoch_id`. | ||
pub epoch_id: CryptoHash, | ||
|
||
/// If present, epoch *the next* block will belong to. | ||
pub next_epoch: Option<epoch::Epoch<PK>>, | ||
} | ||
|
||
/// Error while generating new block. | ||
#[derive(Clone, Copy, Debug, PartialEq, Eq)] | ||
pub enum GenerateError { | ||
/// Host height went backwards. | ||
BadHostHeight, | ||
/// Host timestamp went backwards. | ||
BadHostTimestamp, | ||
} | ||
|
||
impl<PK: PubKey> Block<PK> { | ||
/// Returns whether the block is a valid genesis block. | ||
pub fn is_genesis(&self) -> bool { | ||
self.prev_block_hash == CryptoHash::DEFAULT && | ||
self.epoch_id == CryptoHash::DEFAULT | ||
} | ||
|
||
/// Calculates hash of the block. | ||
pub fn calc_hash(&self) -> CryptoHash { | ||
let mut builder = CryptoHash::builder(); | ||
borsh::to_writer(&mut builder, self).unwrap(); | ||
builder.build() | ||
} | ||
|
||
/// Sign the block using provided signer function. | ||
pub fn sign( | ||
&self, | ||
// TODO(mina86): Consider using signature::Signer. | ||
signer: impl FnOnce(&[u8]) -> Result<PK::Signature>, | ||
) -> Result<PK::Signature> { | ||
borsh::to_vec(self).and_then(|vec| signer(vec.as_slice())) | ||
} | ||
|
||
#[cfg(test)] | ||
fn verify(&self, pk: &PK, signature: &PK::Signature) -> bool { | ||
crate::validators::Signature::verify(signature, &self.calc_hash(), pk) | ||
} | ||
|
||
/// Constructs next block. | ||
/// | ||
/// Returns a new block with `self` as the previous block. Verifies that | ||
/// `host_height` and `host_timestamp` don’t go backwards but otherwise they | ||
/// can increase by any amount. The new block will have `block_height` | ||
/// incremented by one. | ||
pub fn generate_next( | ||
&self, | ||
host_height: HostHeight, | ||
host_timestamp: u64, | ||
state_root: CryptoHash, | ||
next_epoch: Option<epoch::Epoch<PK>>, | ||
) -> Result<Self, GenerateError> { | ||
if host_height <= self.host_height { | ||
return Err(GenerateError::BadHostHeight); | ||
} else if host_timestamp <= self.host_timestamp { | ||
return Err(GenerateError::BadHostTimestamp); | ||
} | ||
|
||
let prev_block_hash = self.calc_hash(); | ||
// If self defines a new epoch than the new block starts a new epoch | ||
// with epoch id equal to self’s block hash. Otherwise, epoch doesn’t | ||
// change and the new block uses the same epoch id as self. | ||
let epoch_id = match self.next_epoch.is_some() { | ||
false => self.epoch_id.clone(), | ||
true => prev_block_hash.clone(), | ||
}; | ||
Ok(Self { | ||
version: crate::common::VersionZero, | ||
prev_block_hash, | ||
block_height: self.block_height.next(), | ||
host_height, | ||
host_timestamp, | ||
state_root, | ||
epoch_id, | ||
next_epoch, | ||
}) | ||
} | ||
|
||
/// Constructs a new genesis block. | ||
/// | ||
/// A genesis block is identified by previous block hash and epoch id both | ||
/// being all-zero hash. | ||
pub fn generate_genesis( | ||
block_height: BlockHeight, | ||
host_height: HostHeight, | ||
host_timestamp: u64, | ||
state_root: CryptoHash, | ||
next_epoch: epoch::Epoch<PK>, | ||
) -> Result<Self, GenerateError> { | ||
Ok(Self { | ||
version: crate::common::VersionZero, | ||
prev_block_hash: CryptoHash::DEFAULT, | ||
block_height, | ||
host_height, | ||
host_timestamp, | ||
state_root, | ||
epoch_id: CryptoHash::DEFAULT, | ||
next_epoch: Some(next_epoch), | ||
}) | ||
} | ||
} | ||
|
||
#[test] | ||
fn test_block_generation() { | ||
use crate::validators::{MockPubKey, MockSignature}; | ||
|
||
// Generate a genesis block and test it’s behaviour. | ||
let genesis_hash = "Zq3s+b7x6R8tKV1iQtByAWqlDMXVVD9tSDOlmuLH7wI="; | ||
let genesis_hash = CryptoHash::from_base64(genesis_hash).unwrap(); | ||
|
||
let genesis = Block::generate_genesis( | ||
BlockHeight::from(0), | ||
HostHeight::from(42), | ||
24, | ||
CryptoHash::test(66), | ||
epoch::Epoch::test(&[(0, 10), (1, 10)]), | ||
) | ||
.unwrap(); | ||
|
||
assert!(genesis.is_genesis()); | ||
|
||
let mut block = genesis.clone(); | ||
block.prev_block_hash = genesis_hash.clone(); | ||
assert!(!block.is_genesis()); | ||
|
||
let mut block = genesis.clone(); | ||
block.epoch_id = genesis_hash.clone(); | ||
assert!(!block.is_genesis()); | ||
|
||
assert_eq!(genesis_hash, genesis.calc_hash()); | ||
assert_ne!(genesis_hash, block.calc_hash()); | ||
|
||
let pk = MockPubKey(77); | ||
let signature = | ||
genesis.sign(|msg| Ok(MockSignature::new(msg, pk))).unwrap(); | ||
assert_eq!(MockSignature(1722674425, pk), signature); | ||
assert!(genesis.verify(&pk, &signature)); | ||
assert!(!genesis.verify(&MockPubKey(88), &signature)); | ||
assert!(!genesis.verify(&pk, &MockSignature(0, pk))); | ||
|
||
let mut block = genesis.clone(); | ||
block.host_timestamp += 1; | ||
assert_ne!(genesis_hash, block.calc_hash()); | ||
assert!(!block.verify(&pk, &signature)); | ||
|
||
// Try creating invalid next block. | ||
assert_eq!( | ||
Err(GenerateError::BadHostHeight), | ||
genesis.generate_next( | ||
HostHeight::from(42), | ||
100, | ||
CryptoHash::test(99), | ||
None | ||
) | ||
); | ||
assert_eq!( | ||
Err(GenerateError::BadHostTimestamp), | ||
genesis.generate_next( | ||
HostHeight::from(43), | ||
24, | ||
CryptoHash::test(99), | ||
None | ||
) | ||
); | ||
|
||
// Create next block and test its behaviour. | ||
let block = genesis | ||
.generate_next(HostHeight::from(50), 50, CryptoHash::test(99), None) | ||
.unwrap(); | ||
assert!(!block.is_genesis()); | ||
assert_eq!(BlockHeight::from(1), block.block_height); | ||
assert_eq!(genesis_hash, block.prev_block_hash); | ||
assert_eq!(genesis_hash, block.epoch_id); | ||
let hash = "uv7IaNMkac36VYAD/RNtDF14wY/DXxlxzsS2Qi+d4uw="; | ||
let hash = CryptoHash::from_base64(hash).unwrap(); | ||
assert_eq!(hash, block.calc_hash()); | ||
|
||
// Create next block within and introduce a new epoch. | ||
let epoch = Some(epoch::Epoch::test(&[(0, 20), (1, 10)])); | ||
let block = block | ||
.generate_next(HostHeight::from(60), 60, CryptoHash::test(99), epoch) | ||
.unwrap(); | ||
assert_eq!(hash, block.prev_block_hash); | ||
assert_eq!(genesis_hash, block.epoch_id); | ||
let hash = "JWVBe5GotaDzyClzBuArPLjcAQTRElMCxvstyZ0bMtM="; | ||
let hash = CryptoHash::from_base64(hash).unwrap(); | ||
assert_eq!(hash, block.calc_hash()); | ||
|
||
// Create next block which belongs to the new epoch. | ||
let block = block | ||
.generate_next(HostHeight::from(65), 65, CryptoHash::test(99), None) | ||
.unwrap(); | ||
assert_eq!(hash, block.prev_block_hash); | ||
assert_eq!(hash, block.epoch_id); | ||
} |
Oops, something went wrong.