validator: validate blocks based on their version

This commit is contained in:
aggstam
2023-10-03 21:45:28 +03:00
parent 95434701f4
commit 6ad2a19bac
16 changed files with 545 additions and 252 deletions

View File

@@ -159,8 +159,10 @@ async fn realmain(args: Args, ex: Arc<smol::Executor<'static>>) -> Result<()> {
let genesis_block = BlockInfo::default();
let genesis_txs_total = genesis_txs_total(&genesis_block.txs)?;
let time_keeper = TimeKeeper::new(genesis_block.header.timestamp, 10, 90, 0);
// TODO: add miner threads arg
let config = ValidatorConfig::new(
time_keeper,
Some(40),
genesis_block,
genesis_txs_total,
vec![],

View File

@@ -16,8 +16,10 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
use darkfi::{
blockchain::BlockInfo, system::sleep, util::time::Timestamp, validator::pow::PoWModule, Result,
use darkfi::{blockchain::BlockInfo, system::sleep, util::time::Timestamp, Result};
use darkfi_sdk::{
blockchain::{expected_reward, PidOutput, PreviousSlot, Slot},
pasta::{group::ff::Field, pallas},
};
use log::info;
use smol::channel::Receiver;
@@ -57,40 +59,55 @@ pub async fn miner_task(node: &Darkfid, stop_signal: &Receiver<()>) -> Result<()
/// Miner loop
async fn miner_loop(node: &Darkfid, stop_signal: &Receiver<()>) -> Result<()> {
// TODO: add miner threads arg
// Generate a PoW module
let mut module = PoWModule::new(node.validator.read().await.blockchain.clone(), None, Some(90));
// Miner loop
let timekeeper = node.validator.read().await.consensus.time_keeper.clone();
loop {
// TODO: consensus should generate next block, along with its difficulty,
// derived from best fork
// Retrieve last block
let last = node.validator.read().await.blockchain.last_block()?;
let last_hash = last.hash()?;
// Generate next PoW slot
let last_slot = last.slots.last().unwrap().clone();
let id = last_slot.id + 1;
let producers = 1;
let previous = PreviousSlot::new(
producers,
vec![last_hash],
vec![last.header.previous],
last_slot.pid.error,
);
let pid = PidOutput::default();
let total_tokens = last_slot.total_tokens + last_slot.reward;
let reward = expected_reward(id);
let slot = Slot::new(id, previous, pid, pallas::Base::ZERO, total_tokens, reward);
// Mine next block
let difficulty = module.next_difficulty();
let mut next_block = BlockInfo::default();
next_block.header.version = 0;
next_block.header.previous = last.hash()?;
next_block.header.height = last.header.height + 1;
next_block.header.previous = last_hash;
next_block.header.height = slot.id;
next_block.header.epoch = timekeeper.slot_epoch(next_block.header.height);
next_block.header.timestamp = Timestamp::current_time();
module.mine_block(&mut next_block, stop_signal)?;
next_block.slots = vec![slot];
node.validator.read().await.consensus.module.mine_block(&mut next_block, stop_signal)?;
// Verify it
module.verify_block(&next_block)?;
node.validator.read().await.consensus.module.verify_current_block(&next_block)?;
// Generate stuff before pushing block to blockchain
let timestamp = next_block.header.timestamp.0;
let message = BlockInfoMessage::from(&next_block);
// Append block to blockchain
node.validator.write().await.add_blocks(&[next_block]).await?;
let mut lock = node.validator.write().await;
lock.add_blocks(&[next_block]).await?;
// Update PoW module
let difficulty = lock.consensus.module.next_difficulty();
lock.consensus.module.append(timestamp, &difficulty);
// Broadcast block
node.sync_p2p.broadcast(&message).await;
// Update PoW module
module.append(timestamp, &difficulty);
}
}

View File

@@ -28,7 +28,7 @@ fn forks() -> Result<()> {
let blockchain = Blockchain::new(&sled::Config::new().temporary(true).open()?)?;
// Create a fork
let fork = Fork::new(&blockchain)?;
let fork = Fork::new(&blockchain, Some(90))?;
// Add a dummy record to fork
fork.overlay.lock().unwrap().order.insert(&[0], &[record0])?;

View File

@@ -29,7 +29,7 @@ use darkfi::{
};
use darkfi_contract_test_harness::{vks, Holder, TestHarness};
use darkfi_sdk::{
blockchain::{expected_reward, PidOutput, PreviousSlot, Slot},
blockchain::{expected_reward, PidOutput, PreviousSlot, Slot, POS_START},
pasta::{group::ff::Field, pallas},
};
use url::Url;
@@ -42,6 +42,7 @@ use crate::{
};
pub struct HarnessConfig {
pub pow_target: Option<usize>,
pub testing_node: bool,
pub alice_initial: u64,
pub bob_initial: u64,
@@ -81,6 +82,7 @@ impl Harness {
let time_keeper = TimeKeeper::new(genesis_block.header.timestamp, 10, 90, 0);
let validator_config = ValidatorConfig::new(
time_keeper,
config.pow_target,
genesis_block,
genesis_txs_total,
vec![],
@@ -134,8 +136,8 @@ impl Harness {
let alice = &self.alice.validator.read().await;
let bob = &self.bob.validator.read().await;
alice.validate_blockchain(genesis_txs_total, vec![]).await?;
bob.validate_blockchain(genesis_txs_total, vec![]).await?;
alice.validate_blockchain(genesis_txs_total, vec![], self.config.pow_target).await?;
bob.validate_blockchain(genesis_txs_total, vec![], self.config.pow_target).await?;
let alice_blockchain_len = alice.blockchain.len();
assert_eq!(alice_blockchain_len, bob.blockchain.len());
@@ -160,7 +162,7 @@ impl Harness {
Ok(())
}
pub async fn generate_next_block(
pub async fn generate_next_pos_block(
&self,
previous: &BlockInfo,
slots_count: usize,
@@ -171,7 +173,7 @@ impl Harness {
let mut slots = Vec::with_capacity(slots_count);
let mut previous_slot = previous.slots.last().unwrap().clone();
for i in 0..slots_count {
let id = previous_slot.id + 1;
let id = if previous_slot.id < POS_START { POS_START } else { previous_slot.id + 1 };
// First slot in the sequence has (at least) 1 previous slot producer
let producers = if i == 0 { 1 } else { 0 };
let previous = PreviousSlot::new(
@@ -195,10 +197,11 @@ impl Harness {
timestamp.add(1);
// Generate header
let height = slots.last().unwrap().id;
let header = Header::new(
previous_hash,
previous.header.epoch,
slots.last().unwrap().id,
self.alice.validator.read().await.consensus.time_keeper.slot_epoch(height),
height,
timestamp,
previous.header.nonce,
);

View File

@@ -28,21 +28,23 @@ use harness::{generate_node, Harness, HarnessConfig};
mod forks;
async fn sync_blocks_real(ex: Arc<Executor<'static>>) -> Result<()> {
async fn sync_pos_blocks_real(ex: Arc<Executor<'static>>) -> Result<()> {
init_logger();
// Initialize harness in testing mode
let config = HarnessConfig { testing_node: true, alice_initial: 1000, bob_initial: 500 };
let pow_target = Some(90);
let config =
HarnessConfig { testing_node: true, pow_target, alice_initial: 1000, bob_initial: 500 };
let th = Harness::new(config, &ex).await?;
// Retrieve genesis block
let previous = th.alice.validator.read().await.blockchain.last_block()?;
// Generate next block
let block1 = th.generate_next_block(&previous, 1).await?;
let block1 = th.generate_next_pos_block(&previous, 1).await?;
// Generate next block, with 4 empty slots inbetween
let block2 = th.generate_next_block(&block1, 5).await?;
let block2 = th.generate_next_pos_block(&block1, 5).await?;
// Add it to nodes
th.add_blocks(&vec![block1, block2]).await?;
@@ -65,7 +67,7 @@ async fn sync_blocks_real(ex: Arc<Executor<'static>>) -> Result<()> {
let genesis_txs_total = th.config.alice_initial + th.config.bob_initial;
let alice = &th.alice.validator.read().await;
let charlie = &charlie.validator.read().await;
charlie.validate_blockchain(genesis_txs_total, vec![]).await?;
charlie.validate_blockchain(genesis_txs_total, vec![], pow_target).await?;
assert_eq!(alice.blockchain.len(), charlie.blockchain.len());
assert_eq!(alice.blockchain.slots.len(), charlie.blockchain.slots.len());
@@ -74,14 +76,14 @@ async fn sync_blocks_real(ex: Arc<Executor<'static>>) -> Result<()> {
}
#[test]
fn sync_blocks() -> Result<()> {
fn sync_pos_blocks() -> Result<()> {
let ex = Arc::new(Executor::new());
let (signal, shutdown) = smol::channel::unbounded::<()>();
easy_parallel::Parallel::new().each(0..4, |_| smol::block_on(ex.run(shutdown.recv()))).finish(
|| {
smol::block_on(async {
sync_blocks_real(ex.clone()).await.unwrap();
sync_pos_blocks_real(ex.clone()).await.unwrap();
drop(signal);
})
},

View File

@@ -139,19 +139,16 @@ impl Blockchain {
trees.push(self.order.0.clone());
batches.push(blocks_order_batch);
// Store extra stuff based on block version
if block.header.version > 0 {
// Store block slots uids vector
let slots = block.slots.iter().map(|x| x.id).collect();
let blocks_slots_bactch = self.blocks_slots.insert_batch(&block_hash_vec, &[&slots])?;
trees.push(self.blocks_slots.0.clone());
batches.push(blocks_slots_bactch);
// Store block slots uids vector
let slots = block.slots.iter().map(|x| x.id).collect();
let blocks_slots_bactch = self.blocks_slots.insert_batch(&block_hash_vec, &[&slots])?;
trees.push(self.blocks_slots.0.clone());
batches.push(blocks_slots_bactch);
// Store block slots
let slots_batch = self.slots.insert_batch(&block.slots)?;
trees.push(self.slots.0.clone());
batches.push(slots_batch);
}
// Store block slots
let slots_batch = self.slots.insert_batch(&block.slots)?;
trees.push(self.slots.0.clone());
batches.push(slots_batch);
// Perform an atomic transaction over the trees and apply the batches.
self.atomic_write(&trees, &batches)?;
@@ -173,22 +170,19 @@ impl Blockchain {
return Ok(false)
}
// Check extra stuff based on block version
if block.header.version > 0 {
// Check if we have block slots uids vector
let slots = match self.blocks_slots.get(&[blockhash], true) {
Ok(v) => v[0].clone().unwrap(),
Err(_) => return Ok(false),
};
let provided_block_slots: Vec<u64> = block.slots.iter().map(|x| x.id).collect();
if slots != provided_block_slots {
return Ok(false)
}
// Check if we have block slots uids vector
let slots = match self.blocks_slots.get(&[blockhash], true) {
Ok(v) => v[0].clone().unwrap(),
Err(_) => return Ok(false),
};
let provided_block_slots: Vec<u64> = block.slots.iter().map(|x| x.id).collect();
if slots != provided_block_slots {
return Ok(false)
}
// Check if we have all slots
if self.slots.get(&slots, true).is_err() {
return Ok(false)
}
// Check if we have all slots
if self.slots.get(&slots, true).is_err() {
return Ok(false)
}
// Check provided info produces the same hash
@@ -216,14 +210,10 @@ impl Blockchain {
let txs = self.transactions.get(&block.txs, true)?;
let txs = txs.iter().map(|x| x.clone().unwrap()).collect();
// Retrieve extra stuff based on block version
let mut block_slots = vec![];
if header.version > 0 {
let slots = self.blocks_slots.get(&[block.hash()], true)?;
let slots = slots[0].clone().unwrap();
let slots = self.slots.get(&slots, true)?;
block_slots = slots.iter().map(|x| x.clone().unwrap()).collect();
}
let slots = self.blocks_slots.get(&[block.hash()], true)?;
let slots = slots[0].clone().unwrap();
let slots = self.slots.get(&slots, true)?;
let block_slots = slots.iter().map(|x| x.clone().unwrap()).collect();
let info = BlockInfo::new(header, txs, block.signature, block_slots);
ret.push(info);
@@ -491,15 +481,12 @@ impl BlockchainOverlay {
// Store block order
self.order.insert(&[block.header.height], &block_hash_vec)?;
// Store extra stuff based on block version
if block.header.version > 0 {
// Store block slots uids vector
let slots = block.slots.iter().map(|x| x.id).collect();
self.blocks_slots.insert(&block_hash_vec, &[&slots])?;
// Store block slots uids vector
let slots = block.slots.iter().map(|x| x.id).collect();
self.blocks_slots.insert(&block_hash_vec, &[&slots])?;
// Store block slots
self.slots.insert(&block.slots)?;
}
// Store block slots
self.slots.insert(&block.slots)?;
Ok(block_hash)
}
@@ -518,22 +505,19 @@ impl BlockchainOverlay {
return Ok(false)
}
// Check extra stuff based on block version
if block.header.version > 0 {
// Check if we have block slots uids vector
let slots = match self.blocks_slots.get(&[blockhash], true) {
Ok(v) => v[0].clone().unwrap(),
Err(_) => return Ok(false),
};
let provided_block_slots: Vec<u64> = block.slots.iter().map(|x| x.id).collect();
if slots != provided_block_slots {
return Ok(false)
}
// Check if we have block slots uids vector
let slots = match self.blocks_slots.get(&[blockhash], true) {
Ok(v) => v[0].clone().unwrap(),
Err(_) => return Ok(false),
};
let provided_block_slots: Vec<u64> = block.slots.iter().map(|x| x.id).collect();
if slots != provided_block_slots {
return Ok(false)
}
// Check if we have all slots
if self.slots.get(&slots, true).is_err() {
return Ok(false)
}
// Check if we have all slots
if self.slots.get(&slots, true).is_err() {
return Ok(false)
}
// Check provided info produces the same hash
@@ -561,14 +545,10 @@ impl BlockchainOverlay {
let txs = self.transactions.get(&block.txs, true)?;
let txs = txs.iter().map(|x| x.clone().unwrap()).collect();
// Retrieve extra stuff based on block version
let mut block_slots = vec![];
if header.version > 0 {
let slots = self.blocks_slots.get(&[block.hash()], true)?;
let slots = slots[0].clone().unwrap();
let slots = self.slots.get(&slots, true)?;
block_slots = slots.iter().map(|x| x.clone().unwrap()).collect();
}
let slots = self.blocks_slots.get(&[block.hash()], true)?;
let slots = slots[0].clone().unwrap();
let slots = self.slots.get(&slots, true)?;
let block_slots = slots.iter().map(|x| x.clone().unwrap()).collect();
let info = BlockInfo::new(header, txs, block.signature, block_slots);
ret.push(info);

View File

@@ -111,7 +111,7 @@ impl TestHarness {
let timer = Instant::now();
wallet.validator.read().await.add_test_producer_transaction(tx, slot, true).await?;
wallet.validator.read().await.add_test_producer_transaction(tx, slot, 2, true).await?;
wallet.consensus_staked_merkle_tree.append(MerkleNode::from(params.output.coin.inner()));
tx_action_benchmark.verify_times.push(timer.elapsed());
@@ -174,7 +174,7 @@ impl TestHarness {
.validator
.read()
.await
.add_test_producer_transaction(tx, slot, true)
.add_test_producer_transaction(tx, slot, 2, true)
.await
.is_err());
tx_action_benchmark.verify_times.push(timer.elapsed());

View File

@@ -157,6 +157,7 @@ impl Wallet {
let time_keeper = TimeKeeper::new(genesis_block.header.timestamp, 10, 90, 0);
let config = ValidatorConfig::new(
time_keeper,
Some(90),
genesis_block.clone(),
0,
faucet_pubkeys.to_vec(),

View File

@@ -100,7 +100,12 @@ impl TestHarness {
self.tx_action_benchmarks.get_mut(&TxAction::MoneyPoWReward).unwrap();
let timer = Instant::now();
wallet.validator.read().await.add_test_producer_transaction(tx, block_height, true).await?;
wallet
.validator
.read()
.await
.add_test_producer_transaction(tx, block_height, 1, true)
.await?;
wallet.money_merkle_tree.append(MerkleNode::from(params.output.coin.inner()));
tx_action_benchmark.verify_times.push(timer.elapsed());
@@ -122,7 +127,7 @@ impl TestHarness {
.validator
.read()
.await
.add_test_producer_transaction(tx, block_height, true)
.add_test_producer_transaction(tx, block_height, 1, true)
.await
.is_err());
tx_action_benchmark.verify_times.push(timer.elapsed());

View File

@@ -354,6 +354,9 @@ pub enum Error {
#[error("Block {0} is invalid")]
BlockIsInvalid(String),
#[error("Block version {0} is invalid")]
BlockVersionIsInvalid(u8),
#[error("Block {0} already in database")]
BlockAlreadyExists(String),

View File

@@ -29,7 +29,7 @@ use crate::{
blockchain::{BlockInfo, Blockchain, BlockchainOverlay, BlockchainOverlayPtr, Header},
tx::Transaction,
util::time::{TimeKeeper, Timestamp},
validator::{pid::slot_pid_output, verify_block, verify_transactions},
validator::{pid::slot_pid_output, pow::PoWModule, verify_block, verify_transactions},
Error, Result,
};
@@ -42,25 +42,38 @@ pub struct Consensus {
pub blockchain: Blockchain,
/// Helper structure to calculate time related operations
pub time_keeper: TimeKeeper,
/// Currently configured PoW target
pub pow_target: Option<usize>,
/// Node is participating to consensus
pub participating: bool,
/// Last slot node check for finalization
pub checked_finalization: u64,
/// Fork chains containing block proposals
pub forks: Vec<Fork>,
// TODO: remove this once everything goes through forks
/// Canonical blockchain PoW module state
pub module: PoWModule,
/// Flag to enable testing mode
pub testing_mode: bool,
}
impl Consensus {
/// Generate a new Consensus state.
pub fn new(blockchain: Blockchain, time_keeper: TimeKeeper, testing_mode: bool) -> Self {
pub fn new(
blockchain: Blockchain,
time_keeper: TimeKeeper,
pow_target: Option<usize>,
testing_mode: bool,
) -> Self {
let module = PoWModule::new(blockchain.clone(), None, pow_target);
Self {
blockchain,
time_keeper,
pow_target,
participating: false,
checked_finalization: 0,
forks: vec![],
module,
testing_mode,
}
}
@@ -72,7 +85,7 @@ impl Consensus {
// If no forks exist, create a new one as a basis to extend
if self.forks.is_empty() {
self.forks.push(Fork::new(&self.blockchain)?);
self.forks.push(Fork::new(&self.blockchain, self.pow_target)?);
}
// Grab previous slot information
@@ -238,6 +251,7 @@ impl Consensus {
if verify_block(
&fork.overlay,
&time_keeper,
&fork.module,
&proposal.block,
&previous,
expected_reward,
@@ -251,6 +265,11 @@ impl Consensus {
return Err(Error::BlockIsInvalid(proposal.hash.to_string()))
};
// Update PoW module
if proposal.block.header.version == 1 {
fork.module.append(proposal.block.header.timestamp.0, &fork.module.next_difficulty());
}
// If a fork index was found, replace forks with the mutated one,
// otherwise push the new fork.
fork.proposals.push(proposal.hash);
@@ -299,7 +318,7 @@ impl Consensus {
return Err(Error::ExtendedChainIndexNotFound)
}
return Ok((Fork::new(&self.blockchain)?, None))
return Ok((Fork::new(&self.blockchain, self.pow_target)?, None))
}
let (f_index, p_index) = found.unwrap();
@@ -310,7 +329,7 @@ impl Consensus {
}
// Rebuild fork
let mut fork = Fork::new(&self.blockchain)?;
let mut fork = Fork::new(&self.blockchain, self.pow_target)?;
fork.proposals = original_fork.proposals[..p_index + 1].to_vec();
// Retrieve proposals blocks from original fork
@@ -334,6 +353,7 @@ impl Consensus {
if verify_block(
&fork.overlay,
&time_keeper,
&fork.module,
block,
previous,
expected_reward,
@@ -347,6 +367,11 @@ impl Consensus {
return Err(Error::BlockIsInvalid(block.hash()?.to_string()))
};
// Update PoW module
if block.header.version == 1 {
fork.module.append(block.header.timestamp.0, &fork.module.next_difficulty());
}
// Use last inserted block as next iteration previous
previous = block;
}
@@ -444,6 +469,8 @@ impl From<Proposal> for BlockInfo {
pub struct Fork {
/// Overlay cache over canonical Blockchain
pub overlay: BlockchainOverlayPtr,
/// Current PoW module state,
pub module: PoWModule,
/// Fork proposal hashes sequence
pub proposals: Vec<blake3::Hash>,
/// Hot/live slots
@@ -453,11 +480,12 @@ pub struct Fork {
}
impl Fork {
pub fn new(blockchain: &Blockchain) -> Result<Self> {
pub fn new(blockchain: &Blockchain, pow_target: Option<usize>) -> Result<Self> {
let module = PoWModule::new(blockchain.clone(), None, pow_target);
let mempool =
blockchain.get_pending_txs()?.iter().map(|tx| blake3::hash(&serialize(tx))).collect();
let overlay = BlockchainOverlay::new(blockchain)?;
Ok(Self { overlay, proposals: vec![], slots: vec![], mempool })
Ok(Self { overlay, module, proposals: vec![], slots: vec![], mempool })
}
/// Auxiliary function to retrieve last proposal
@@ -573,10 +601,11 @@ impl Fork {
/// overlay pointer have been updated to the cloned one.
pub fn full_clone(&self) -> Result<Self> {
let overlay = self.overlay.lock().unwrap().full_clone()?;
let module = self.module.clone();
let proposals = self.proposals.clone();
let slots = self.slots.clone();
let mempool = self.mempool.clone();
Ok(Self { overlay, proposals, slots, mempool })
Ok(Self { overlay, module, proposals, slots, mempool })
}
}

View File

@@ -40,6 +40,7 @@ use consensus::Consensus;
/// DarkFi PoW module
pub mod pow;
use pow::PoWModule;
/// DarkFi consensus PID controller
pub mod pid;
@@ -65,6 +66,8 @@ pub mod float_10;
pub struct ValidatorConfig {
/// Helper structure to calculate time related operations
pub time_keeper: TimeKeeper,
/// Currently configured PoW target
pub pow_target: Option<usize>,
/// Genesis block
pub genesis_block: BlockInfo,
/// Total amount of minted tokens in genesis block
@@ -78,12 +81,20 @@ pub struct ValidatorConfig {
impl ValidatorConfig {
pub fn new(
time_keeper: TimeKeeper,
pow_target: Option<usize>,
genesis_block: BlockInfo,
genesis_txs_total: u64,
faucet_pubkeys: Vec<PublicKey>,
testing_mode: bool,
) -> Self {
Self { time_keeper, genesis_block, genesis_txs_total, faucet_pubkeys, testing_mode }
Self {
time_keeper,
pow_target,
genesis_block,
genesis_txs_total,
faucet_pubkeys,
testing_mode,
}
}
}
@@ -132,7 +143,8 @@ impl Validator {
overlay.lock().unwrap().overlay.lock().unwrap().apply()?;
info!(target: "validator::new", "Initializing Consensus");
let consensus = Consensus::new(blockchain.clone(), config.time_keeper, testing_mode);
let consensus =
Consensus::new(blockchain.clone(), config.time_keeper, config.pow_target, testing_mode);
// Create the actual state
let state =
@@ -296,8 +308,9 @@ impl Validator {
// Retrieve last block
let mut previous = &overlay.lock().unwrap().last_block()?;
// Create a time keeper to validate each block
// Create a time keeper and a PoW module to validate each block
let mut time_keeper = self.consensus.time_keeper.clone();
let mut module = self.consensus.module.clone();
// Keep track of all blocks transactions to remove them from pending txs store
let mut removed_txs = vec![];
@@ -314,6 +327,7 @@ impl Validator {
if verify_block(
&overlay,
&time_keeper,
&module,
block,
previous,
expected_reward,
@@ -327,6 +341,11 @@ impl Validator {
return Err(Error::BlockIsInvalid(block.hash()?.to_string()))
};
// Update PoW module
if block.header.version == 1 {
module.append(block.header.timestamp.0, &module.next_difficulty());
}
// Store block transactions
for tx in &block.txs {
removed_txs.push(tx.clone());
@@ -343,6 +362,9 @@ impl Validator {
self.blockchain.remove_pending_txs(&removed_txs)?;
self.purge_pending_txs().await?;
// Update PoW module
self.consensus.module = module;
Ok(())
}
@@ -407,6 +429,7 @@ impl Validator {
&self,
tx: &Transaction,
verifying_slot: u64,
block_version: u8,
write: bool,
) -> Result<()> {
debug!(target: "validator::add_test_producer_transaction", "Instantiating BlockchainOverlay");
@@ -422,7 +445,8 @@ impl Validator {
// Verify transaction
let mut erroneous_txs = vec![];
if let Err(e) = verify_producer_transaction(&overlay, &time_keeper, tx).await {
if let Err(e) = verify_producer_transaction(&overlay, &time_keeper, tx, block_version).await
{
warn!(target: "validator::add_test_producer_transaction", "Transaction verification failed: {}", e);
erroneous_txs.push(tx.clone());
}
@@ -453,6 +477,7 @@ impl Validator {
&self,
genesis_txs_total: u64,
faucet_pubkeys: Vec<PublicKey>,
pow_target: Option<usize>,
) -> Result<()> {
let blocks = self.blockchain.get_all()?;
@@ -469,8 +494,9 @@ impl Validator {
// Set previous
let mut previous = &blocks[0];
// Create a time keeper to validate each block
// Create a time keeper and a PoW module to validate each block
let mut time_keeper = self.consensus.time_keeper.clone();
let mut module = PoWModule::new(blockchain.clone(), None, pow_target);
// Deploy native wasm contracts
deploy_native_contracts(&overlay, &time_keeper, &faucet_pubkeys)?;
@@ -490,6 +516,7 @@ impl Validator {
if verify_block(
&overlay,
&time_keeper,
&module,
block,
previous,
expected_reward,
@@ -503,6 +530,11 @@ impl Validator {
return Err(Error::BlockIsInvalid(block.hash()?.to_string()))
};
// Update PoW module
if block.header.version == 1 {
module.append(block.header.timestamp.0, &module.next_difficulty());
}
// Use last inserted block as next iteration previous
previous = block;
}

View File

@@ -73,6 +73,7 @@ const CUT_BEGIN: usize = 60;
const CUT_END: usize = 660;
/// Default target block time, in seconds
const DIFFICULTY_TARGET: usize = 20;
// TODO: maybe add more difficulty targets (testnet, mainnet, etc)
/// How many most recent blocks to use to verify new blocks' timestamp
const BLOCKCHAIN_TIMESTAMP_CHECK_WINDOW: usize = 60;
/// Time limit in the future of what blocks can be
@@ -175,12 +176,18 @@ impl PoWModule {
difficulty == &self.next_difficulty()
}
/// Verify provided block timestamp is valid and matches certain criteria
pub fn verify_timestamp(&self, timestamp: u64) -> bool {
/// Verify provided block timestamp is not far in the future and
/// check its valid acorrding to current timestamps median
pub fn verify_current_timestamp(&self, timestamp: u64) -> bool {
if timestamp > Timestamp::current_time().0 + BLOCK_FUTURE_TIME_LIMIT {
return false
}
self.verify_timestamp_by_median(timestamp)
}
/// Verify provided block timestamp is valid and matches certain criteria
pub fn verify_timestamp_by_median(&self, timestamp: u64) -> bool {
// If not enough blocks, no proper median yet, return true
if self.timestamps.len() < BLOCKCHAIN_TIMESTAMP_CHECK_WINDOW {
return true
@@ -192,17 +199,17 @@ impl PoWModule {
timestamp >= median(timestamps)
}
/// Verify provided block timestamp and difficulty pair
pub fn verify_pair(&self, timestamp: u64, difficulty: &BigUint) {
assert!(self.verify_timestamp(timestamp));
assert!(self.verify_difficulty(difficulty));
/// Verify provided block timestamp and hash
pub fn verify_current_block(&self, block: &BlockInfo) -> Result<()> {
// First we verify the block's timestamp
assert!(self.verify_current_timestamp(block.header.timestamp.0));
// Then we verify the block's hash
self.verify_block_hash(block)
}
/// Verify provided block corresponds to next mine target
pub fn verify_block(&self, block: &BlockInfo) -> Result<()> {
// First we verify the block's timestamp
assert!(self.verify_timestamp(block.header.timestamp.0));
pub fn verify_block_hash(&self, block: &BlockInfo) -> Result<()> {
// Then we verify the proof of work:
let verifier_setup = Instant::now();
@@ -421,7 +428,7 @@ mod tests {
module.mine_block(&mut next_block, &recvr)?;
// Verify it
module.verify_block(&next_block)?;
module.verify_current_block(&next_block)?;
Ok(())
}

View File

@@ -17,88 +17,252 @@
*/
use darkfi_sdk::{
blockchain::{expected_reward, Slot},
pasta::pallas,
blockchain::{block_version, expected_reward, Slot},
pasta::{group::ff::Field, pallas},
};
use crate::{
blockchain::{BlockInfo, Blockchain},
validator::pid::slot_pid_output,
validator::{pid::slot_pid_output, pow::PoWModule},
Error, Result,
};
/// A block is considered valid when the following rules apply:
/// 1. Parent hash is equal to the hash of the previous block
/// 2. Timestamp increments previous block timestamp
/// 3. Slot increments previous block slot
/// 4. Slots vector is not empty and all its slots are valid
/// 5. Slot is the same as the slots vector last slot id
/// Additional validity rules can be applied.
pub fn validate_block(block: &BlockInfo, previous: &BlockInfo, expected_reward: u64) -> Result<()> {
let error = Err(Error::BlockIsInvalid(block.hash()?.to_string()));
let previous_hash = previous.hash()?;
// Check previous hash (1)
if block.header.previous != previous_hash {
return error
}
// Check timestamps are incremental (2)
if block.header.timestamp <= previous.header.timestamp {
return error
}
// Check heights are incremental (3)
if block.header.height <= previous.header.height {
return error
}
// Check extra stuff based on block version
if block.header.version > 0 {
// Verify slots (4)
if block.slots.is_empty() {
return error
}
// Retrieve previous block last slot
let mut previous_slot = previous.slots.last().unwrap();
// Check if empty slots existed
if block.slots.len() > 1 {
// All slots exluding the last one must have reward value set to 0.
// Slots must already be in correct order (sorted by id).
for slot in &block.slots[..block.slots.len() - 1] {
validate_slot(
slot,
previous_slot,
&previous_hash,
&previous.header.previous,
&previous.header.nonce,
0,
)?;
previous_slot = slot;
}
}
validate_slot(
block.slots.last().unwrap(),
previous_slot,
&previous_hash,
&previous.header.previous,
&previous.header.nonce,
expected_reward,
)?;
// Check block height is the last slot id (5)
if block.slots.last().unwrap().id != block.header.height {
return error
}
/// Validate provided block, using its previous, based on its version.
pub fn validate_block(
block: &BlockInfo,
previous: &BlockInfo,
expected_reward: u64,
module: &PoWModule,
) -> Result<()> {
// TODO: verify block validations work as expected on versions change(cutoff)
match block_version(block.header.height) {
1 => validate_pow_block(block, previous, expected_reward, module)?,
2 => validate_pos_block(block, previous, expected_reward)?,
_ => return Err(Error::BlockVersionIsInvalid(block.header.version)),
}
Ok(())
}
/// A slot is considered valid when the following rules apply:
/// A PoW block is considered valid when the following rules apply:
/// 1. Block version is equal to 1
/// 2. Parent hash is equal to the hash of the previous block
/// 3. Block heigh increments previous block height by 1
/// 4. Timestamp is valid based on PoWModule validation
/// 5. Block hash is valid based on PoWModule validation
/// 6. Slots vector contains a single valid slot
/// 7. Block height is the same as the slots vector last slot id
/// Additional validity rules can be applied.
pub fn validate_pow_block(
block: &BlockInfo,
previous: &BlockInfo,
expected_reward: u64,
module: &PoWModule,
) -> Result<()> {
let error = Err(Error::BlockIsInvalid(block.hash()?.to_string()));
// Check block version (1)
if block.header.version != 1 {
return error
}
// Check previous hash (2)
let previous_hash = previous.hash()?;
if block.header.previous != previous_hash {
return error
}
// Check heights are incremental (3)
if block.header.height != previous.header.height + 1 {
return error
}
// Check timestamp validity (4)
if !module.verify_timestamp_by_median(block.header.timestamp.0) {
return error
}
// Check block hash corresponds to next one (5)
module.verify_block_hash(block)?;
// Verify slots vector is empty (6)
if block.slots.len() != 1 {
return error
}
// Retrieve previous block last slot
let previous_slot = previous.slots.last().unwrap();
// Validate last slot
let last_slot = block.slots.last().unwrap();
validate_pow_slot(
last_slot,
previous_slot,
&previous_hash,
&previous.header.previous,
expected_reward,
)?;
// Check block height is the last slot id (7)
if last_slot.id != block.header.height {
return error
}
Ok(())
}
/// A PoW slot is considered valid when the following rules apply:
/// 1. Id increments previous slot id by 1
/// 2. Forks extend previous block hash
/// 3. Forks follow previous block sequence
/// 4. Slot total tokens represent the total network tokens
/// up until this slot
/// 5. Slot previous error value correspond to previous slot one
/// 6. Slot previous has only 1 producer(the miner)
/// 7. PID output for this slot is correct(zero)
/// 8. Slot last eta is the expected one(zero)
/// 9. Slot reward value is the expected one
/// Additional validity rules can be applied.
pub fn validate_pow_slot(
slot: &Slot,
previous: &Slot,
previous_block_hash: &blake3::Hash,
previous_block_sequence: &blake3::Hash,
expected_reward: u64,
) -> Result<()> {
let error = Err(Error::SlotIsInvalid(slot.id));
// Check slots are incremental (1)
if slot.id != previous.id + 1 {
return error
}
// Check previous block hash (2)
if !slot.previous.last_hashes.contains(previous_block_hash) {
return error
}
// Check previous block sequence (3)
if !slot.previous.second_to_last_hashes.contains(previous_block_sequence) {
return error
}
// Check total tokens (4)
if slot.total_tokens != previous.total_tokens + previous.reward {
return error
}
// Check previous slot error (5)
if slot.previous.error != previous.pid.error {
return error
}
// Check previous slot producers (6)
if slot.previous.producers != 1 {
return error
}
// Check PID output for this slot (7)
if (slot.pid.f, slot.pid.error, slot.pid.sigma1, slot.pid.sigma2) !=
(0.0, 0.0, pallas::Base::ZERO, pallas::Base::ZERO)
{
return error
}
// Check eta is the expected one (8)
if slot.last_eta != pallas::Base::ZERO {
return error
}
// Check reward is the expected one (9)
if slot.reward != expected_reward {
return error
}
Ok(())
}
/// A PoS block is considered valid when the following rules apply:
/// 1. Block version is equal to 2
/// 2. Parent hash is equal to the hash of the previous block
/// 3. Timestamp increments previous block timestamp
/// 4. Slot increments previous block slot
/// 5. Slots vector is not empty and all its slots are valid
/// 6. Slot is the same as the slots vector last slot id
/// Additional validity rules can be applied.
pub fn validate_pos_block(
block: &BlockInfo,
previous: &BlockInfo,
expected_reward: u64,
) -> Result<()> {
let error = Err(Error::BlockIsInvalid(block.hash()?.to_string()));
// Check block version (1)
if block.header.version != 2 {
return error
}
// Check previous hash (2)
let previous_hash = previous.hash()?;
if block.header.previous != previous_hash {
return error
}
// Check timestamps are incremental (3)
if block.header.timestamp <= previous.header.timestamp {
return error
}
// Check heights are incremental (4)
if block.header.height <= previous.header.height {
return error
}
// Verify slots (5)
if block.slots.is_empty() {
return error
}
// Retrieve previous block last slot
let mut previous_slot = previous.slots.last().unwrap();
// Check if empty slots existed
if block.slots.len() > 1 {
// All slots exluding the last one must have reward value set to 0.
// Slots must already be in correct order (sorted by id).
for slot in &block.slots[..block.slots.len() - 1] {
validate_pos_slot(
slot,
previous_slot,
&previous_hash,
&previous.header.previous,
&previous.header.nonce,
0,
)?;
previous_slot = slot;
}
}
// Validate last slot
let last_slot = block.slots.last().unwrap();
validate_pos_slot(
last_slot,
previous_slot,
&previous_hash,
&previous.header.previous,
&previous.header.nonce,
expected_reward,
)?;
// Check block height is the last slot id (6)
if last_slot.id != block.header.height {
return error
}
Ok(())
}
/// A PoS slot is considered valid when the following rules apply:
/// 1. Id increments previous slot id
/// 2. Forks extend previous block hash
/// 3. Forks follow previous block sequence
@@ -109,7 +273,7 @@ pub fn validate_block(block: &BlockInfo, previous: &BlockInfo, expected_reward:
/// 7. Slot last eta is the expected one
/// 8. Slot reward value is the expected one
/// Additional validity rules can be applied.
pub fn validate_slot(
pub fn validate_pos_slot(
slot: &Slot,
previous: &Slot,
previous_block_hash: &blake3::Hash,
@@ -151,7 +315,7 @@ pub fn validate_slot(
return error
}
// Check reward is the expected one (7)
// Check eta is the expected one (7)
if &slot.last_eta != last_eta {
return error
}
@@ -167,13 +331,20 @@ pub fn validate_slot(
/// A blockchain is considered valid, when every block is valid,
/// based on validate_block checks.
/// Be careful as this will try to load everything in memory.
pub fn validate_blockchain(blockchain: &Blockchain) -> Result<()> {
pub fn validate_blockchain(blockchain: &Blockchain, pow_target: Option<usize>) -> Result<()> {
// Generate a PoW module
let mut module = PoWModule::new(blockchain.clone(), None, pow_target);
// We use block order store here so we have all blocks in order
let blocks = blockchain.order.get_all()?;
for (index, block) in blocks[1..].iter().enumerate() {
let full_blocks = blockchain.get_blocks_by_hash(&[blocks[index].1, block.1])?;
let expected_reward = expected_reward(full_blocks[1].header.height);
validate_block(&full_blocks[1], &full_blocks[0], expected_reward)?;
let full_block = &full_blocks[1];
validate_block(full_block, &full_blocks[0], expected_reward, &module)?;
// Update PoW module
if full_block.header.version == 1 {
module.append(full_block.header.timestamp.0, &module.next_difficulty());
}
}
Ok(())

View File

@@ -31,7 +31,7 @@ use crate::{
runtime::vm_runtime::Runtime,
tx::Transaction,
util::time::TimeKeeper,
validator::validation::validate_block,
validator::{pow::PoWModule, validation::validate_block},
zk::VerifyingKey,
Error, Result,
};
@@ -51,31 +51,33 @@ pub async fn verify_genesis_block(
return Err(Error::BlockAlreadyExists(block_hash))
}
// Block height must be 0
if block.header.height != 0 {
return Err(Error::BlockIsInvalid(block_hash))
}
// Block height must be the same as the time keeper verifying slot
if block.header.height != time_keeper.verifying_slot {
return Err(Error::VerifyingSlotMissmatch())
}
// Check extra stuff based on block version
if block.header.version > 0 {
// Check genesis slot exist
if block.slots.len() != 1 {
return Err(Error::BlockIsInvalid(block_hash))
}
// Check genesis slot exist
if block.slots.len() != 1 {
return Err(Error::BlockIsInvalid(block_hash))
}
// Retrieve genesis slot
let genesis_slot = block.slots.last().unwrap();
// Retrieve genesis slot
let genesis_slot = block.slots.last().unwrap();
// Genesis block slot total token must correspond to the total
// of all genesis transactions public inputs (genesis distribution).
if genesis_slot.total_tokens != genesis_txs_total {
return Err(Error::SlotIsInvalid(genesis_slot.id))
}
// Genesis block slot total token must correspond to the total
// of all genesis transactions public inputs (genesis distribution).
if genesis_slot.total_tokens != genesis_txs_total {
return Err(Error::SlotIsInvalid(genesis_slot.id))
}
// Verify there is no reward
if genesis_slot.reward != 0 {
return Err(Error::SlotIsInvalid(genesis_slot.id))
}
// Verify there is no reward
if genesis_slot.reward != 0 {
return Err(Error::SlotIsInvalid(genesis_slot.id))
}
// Verify transactions vector contains at least one(producers) transaction
@@ -110,6 +112,7 @@ pub async fn verify_genesis_block(
pub async fn verify_block(
overlay: &BlockchainOverlayPtr,
time_keeper: &TimeKeeper,
module: &PoWModule,
block: &BlockInfo,
previous: &BlockInfo,
expected_reward: u64,
@@ -128,8 +131,13 @@ pub async fn verify_block(
return Err(Error::VerifyingSlotMissmatch())
}
// Block epoch must be the correct one, calculated by the time keeper configuration
if block.header.epoch != time_keeper.slot_epoch(block.header.height) {
return Err(Error::VerifyingSlotMissmatch())
}
// Validate block, using its previous
validate_block(block, previous, expected_reward)?;
validate_block(block, previous, expected_reward, module)?;
// Verify transactions vector contains at least one(producers) transaction
if block.txs.is_empty() {
@@ -139,7 +147,8 @@ pub async fn verify_block(
// Validate proposal transaction if not in testing mode
if !testing_mode {
let tx = block.txs.last().unwrap();
let public_key = verify_producer_transaction(overlay, time_keeper, tx).await?;
let public_key =
verify_producer_transaction(overlay, time_keeper, tx, block.header.version).await?;
verify_producer_signature(block, &public_key)?;
}
@@ -176,22 +185,33 @@ pub async fn verify_producer_transaction(
overlay: &BlockchainOverlayPtr,
time_keeper: &TimeKeeper,
tx: &Transaction,
block_version: u8,
) -> Result<PublicKey> {
let tx_hash = tx.hash()?;
debug!(target: "validator::verification::verify_producer_transaction", "Validating proposal transaction {}", tx_hash);
// Transaction must contain a single call, either Money::PoWReward(0x08) or Consensus::Proposal(0x02)
if tx.calls.len() != 1 ||
(tx.calls[0].contract_id != *MONEY_CONTRACT_ID &&
tx.calls[0].contract_id != *CONSENSUS_CONTRACT_ID) ||
(tx.calls[0].contract_id == *MONEY_CONTRACT_ID && tx.calls[0].data[0] != 0x08) ||
(tx.calls[0].contract_id == *CONSENSUS_CONTRACT_ID && tx.calls[0].data[0] != 0x02)
{
error!(target: "validator::verification::verify_producer_transaction", "Proposal transaction is malformed");
// Transaction must contain a single call
if tx.calls.len() != 1 {
return Err(TxVerifyFailed::ErroneousTxs(vec![tx.clone()]).into())
}
// Verify call based on version
let call = &tx.calls[0];
match block_version {
1 => {
// Version 1 blocks must contain a Money::PoWReward(0x08) call
if call.contract_id != *MONEY_CONTRACT_ID || call.data[0] != 0x08 {
return Err(TxVerifyFailed::ErroneousTxs(vec![tx.clone()]).into())
}
}
2 => {
// Version 1 blocks must contain a Consensus::Proposal(0x02) call
if call.contract_id != *CONSENSUS_CONTRACT_ID || call.data[0] != 0x02 {
return Err(TxVerifyFailed::ErroneousTxs(vec![tx.clone()]).into())
}
}
_ => return Err(Error::BlockVersionIsInvalid(block_version)),
}
// Map of ZK proof verifying keys for the current transaction
let mut verifying_keys: HashMap<[u8; 32], HashMap<String, VerifyingKey>> = HashMap::new();

View File

@@ -20,47 +20,63 @@ use darkfi::{
blockchain::{BlockInfo, Blockchain, BlockchainOverlay, Header},
validator::{
pid::slot_pid_output,
pow::PoWModule,
validation::{validate_block, validate_blockchain},
},
Error, Result,
};
use darkfi_sdk::{
blockchain::{expected_reward, PidOutput, PreviousSlot, Slot},
blockchain::{expected_reward, PidOutput, PreviousSlot, Slot, POS_START},
pasta::{group::ff::Field, pallas},
};
const POW_TARGET: Option<usize> = Some(10);
struct Node {
blockchain: Blockchain,
module: PoWModule,
}
impl Node {
fn new() -> Result<Self> {
let blockchain = Blockchain::new(&sled::Config::new().temporary(true).open()?)?;
let module = PoWModule::new(blockchain.clone(), None, POW_TARGET);
Ok(Self { blockchain, module })
}
}
struct Harness {
pub alice: Blockchain,
pub bob: Blockchain,
pub alice: Node,
pub bob: Node,
}
impl Harness {
fn new() -> Result<Self> {
let alice = Blockchain::new(&sled::Config::new().temporary(true).open()?)?;
let bob = Blockchain::new(&sled::Config::new().temporary(true).open()?)?;
let alice = Node::new()?;
let bob = Node::new()?;
Ok(Self { alice, bob })
}
fn is_empty(&self) {
assert!(self.alice.is_empty());
assert!(self.bob.is_empty());
assert!(self.alice.blockchain.is_empty());
assert!(self.bob.blockchain.is_empty());
}
fn validate_chains(&self) -> Result<()> {
validate_blockchain(&self.alice)?;
validate_blockchain(&self.bob)?;
validate_blockchain(&self.alice.blockchain, POW_TARGET)?;
validate_blockchain(&self.bob.blockchain, POW_TARGET)?;
assert_eq!(self.alice.len(), self.bob.len());
assert_eq!(self.alice.blockchain.len(), self.bob.blockchain.len());
Ok(())
}
fn generate_next_block(&self, previous: &BlockInfo) -> Result<BlockInfo> {
fn generate_next_pos_block(&self, previous: &BlockInfo) -> Result<BlockInfo> {
let previous_hash = previous.hash()?;
// Generate slot
let previous_slot = previous.slots.last().unwrap();
let id = previous_slot.id + 1;
let id = if previous_slot.id < POS_START { POS_START } else { previous_slot.id + 1 };
let producers = 1;
let previous_slot_info = PreviousSlot::new(
producers,
@@ -94,17 +110,17 @@ impl Harness {
Ok(block)
}
fn add_blocks(&self, blocks: &[BlockInfo]) -> Result<()> {
self.add_blocks_to_chain(&self.alice, blocks)?;
self.add_blocks_to_chain(&self.bob, blocks)?;
fn add_pos_blocks(&mut self, blocks: &[BlockInfo]) -> Result<()> {
Self::add_pos_blocks_to_chain(&mut self.alice, blocks)?;
Self::add_pos_blocks_to_chain(&mut self.bob, blocks)?;
Ok(())
}
// This is what the validator will execute when it receives a block.
fn add_blocks_to_chain(&self, blockchain: &Blockchain, blocks: &[BlockInfo]) -> Result<()> {
fn add_pos_blocks_to_chain(node: &mut Node, blocks: &[BlockInfo]) -> Result<()> {
// Create overlay
let blockchain_overlay = BlockchainOverlay::new(blockchain)?;
let blockchain_overlay = BlockchainOverlay::new(&node.blockchain)?;
let lock = blockchain_overlay.lock().unwrap();
// When we insert genesis, chain is empty
@@ -123,7 +139,12 @@ impl Harness {
let expected_reward = expected_reward(block.header.height);
// Validate block
validate_block(block, &p, expected_reward)?;
validate_block(block, &p, expected_reward, &node.module)?;
// Update PoW module
if block.header.version == 1 {
node.module.append(block.header.timestamp.0, &node.module.next_difficulty());
}
}
// Insert block
@@ -141,30 +162,30 @@ impl Harness {
}
#[test]
fn blockchain_add_blocks() -> Result<()> {
fn blockchain_add_pos_blocks() -> Result<()> {
smol::block_on(async {
// Initialize harness
let th = Harness::new()?;
let mut th = Harness::new()?;
// Check that nothing exists
th.is_empty();
// We generate some blocks
// We generate some pos blocks
let mut blocks = vec![];
let genesis_block = BlockInfo::default();
blocks.push(genesis_block.clone());
let block = th.generate_next_block(&genesis_block)?;
let block = th.generate_next_pos_block(&genesis_block)?;
blocks.push(block.clone());
let block = th.generate_next_block(&block)?;
let block = th.generate_next_pos_block(&block)?;
blocks.push(block.clone());
let block = th.generate_next_block(&block)?;
let block = th.generate_next_pos_block(&block)?;
blocks.push(block.clone());
th.add_blocks(&blocks)?;
th.add_pos_blocks(&blocks)?;
// Validate chains
th.validate_chains()?;