Просмотр исходного кода

validator: full block validation added

aggstam 3 лет назад
Родитель
Сommit
bca71304f9

+ 20 - 29
src/blockchain/block_store.rs

@@ -21,7 +21,7 @@ use darkfi_serial::{deserialize, serialize, SerialDecodable, SerialEncodable};
 
 
 use crate::{tx::Transaction, Error, Result};
 use crate::{tx::Transaction, Error, Result};
 
 
-use super::{parse_record, Header, SledDbOverlayPtr};
+use super::{parse_record, validate_slot, Header, SledDbOverlayPtr};
 
 
 /// Block version number
 /// Block version number
 pub const BLOCK_VERSION: u8 = 1;
 pub const BLOCK_VERSION: u8 = 1;
@@ -109,60 +109,51 @@ impl BlockInfo {
         block.blockhash()
         block.blockhash()
     }
     }
 
 
-    /// A block is considered valid when its parent hash is equal to the hash of the
-    /// previous block and their slots are incremental.
+    /// 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.
     /// Additional validity rules can be applied.
     pub fn validate(&self, previous: &Self) -> Result<()> {
     pub fn validate(&self, previous: &Self) -> Result<()> {
         let error = Err(Error::BlockIsInvalid(self.blockhash().to_string()));
         let error = Err(Error::BlockIsInvalid(self.blockhash().to_string()));
         let previous_hash = previous.blockhash();
         let previous_hash = previous.blockhash();
 
 
-        // Check previous hash
+        // Check previous hash (1)
         if self.header.previous != previous_hash {
         if self.header.previous != previous_hash {
             return error
             return error
         }
         }
 
 
-        // Check timestamps are incremental
+        // Check timestamps are incremental (2)
         if self.header.timestamp <= previous.header.timestamp {
         if self.header.timestamp <= previous.header.timestamp {
             return error
             return error
         }
         }
 
 
-        // Check slots are incremental
+        // Check slots are incremental (3)
         if self.header.slot <= previous.header.slot {
         if self.header.slot <= previous.header.slot {
             return error
             return error
         }
         }
 
 
-        // Verify slots exist
-        let mut slots = self.slots.clone();
-        if slots.is_empty() {
+        // Verify slots (4)
+        if self.slots.is_empty() {
             return error
             return error
         }
         }
 
 
-        // Sort them just to be safe
-        slots.sort_by(|a, b| b.id.cmp(&a.id));
+        // Retrieve previous block last slot
+        let mut previous_slot = previous.slots.last().unwrap();
 
 
-        // Verify first slot increments from previous block
-        if slots[0].id <= previous.header.slot {
-            return error
-        }
-
-        // Check all slot cover same sequence
-        for slot in &slots {
-            if !slot.fork_hashes.contains(&previous_hash) {
-                return error
-            }
-            if !slot.fork_previous_hashes.contains(&previous.header.previous) {
-                return error
-            }
+        // Slots must already be in correct order (sorted by id)
+        for slot in &self.slots {
+            validate_slot(slot, previous_slot, &previous_hash, &previous.header.previous)?;
+            previous_slot = slot;
         }
         }
 
 
-        // Check block slot is the last slot in the slice
-        if slots.last().unwrap().id != self.header.slot {
+        // Check block slot is the last slot id (5)
+        if self.slots.last().unwrap().id != self.header.slot {
             return error
             return error
         }
         }
 
 
-        // TODO: also validate slots etas and sigmas if we can derive them
-        // from previous slots
-
         Ok(())
         Ok(())
     }
     }
 }
 }

+ 1 - 1
src/blockchain/mod.rs

@@ -38,7 +38,7 @@ pub use header_store::{Header, HeaderStore, HeaderStoreOverlay};
 
 
 /// Slots storage implementation
 /// Slots storage implementation
 pub mod slot_store;
 pub mod slot_store;
-pub use slot_store::{SlotStore, SlotStoreOverlay};
+pub use slot_store::{validate_slot, SlotStore, SlotStoreOverlay};
 
 
 /// Transactions related storage implementations
 /// Transactions related storage implementations
 pub mod tx_store;
 pub mod tx_store;

+ 38 - 1
src/blockchain/slot_store.rs

@@ -20,10 +20,47 @@
 use darkfi_sdk::blockchain::Slot;
 use darkfi_sdk::blockchain::Slot;
 use darkfi_serial::{deserialize, serialize};
 use darkfi_serial::{deserialize, serialize};
 
 
-use crate::{Error, Result};
+use crate::{validator::consensus::pid::slot_sigmas, Error, Result};
 
 
 use super::{parse_record, SledDbOverlayPtr};
 use super::{parse_record, SledDbOverlayPtr};
 
 
+/// A 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
+///     4. Sigmas are the expected ones, based on consensus PID
+/// Additional validity rules can be applied.
+pub fn validate_slot(
+    slot: &Slot,
+    previous: &Slot,
+    previous_block_hash: &blake3::Hash,
+    previous_block_sequence: &blake3::Hash,
+) -> Result<()> {
+    let error = Err(Error::SlotIsInvalid(slot.id));
+
+    // Check slots are incremental (1)
+    if slot.id <= previous.id {
+        return error
+    }
+
+    // Check previous block hash (2)
+    if !slot.fork_hashes.contains(previous_block_hash) {
+        return error
+    }
+
+    // Check previous block sequence (3)
+    if !slot.fork_previous_hashes.contains(previous_block_sequence) {
+        return error
+    }
+
+    // Check sigmas (4)
+    if (slot.sigma1, slot.sigma2) != slot_sigmas() {
+        return error
+    }
+
+    Ok(())
+}
+
 const SLED_SLOT_TREE: &[u8] = b"_slots";
 const SLED_SLOT_TREE: &[u8] = b"_slots";
 
 
 /// The `SlotStore` is a `sled` tree storing the blockhains' slots,
 /// The `SlotStore` is a `sled` tree storing the blockhains' slots,

+ 9 - 0
src/error.rs

@@ -319,12 +319,21 @@ pub enum Error {
     #[error("Block {0} already in database")]
     #[error("Block {0} already in database")]
     BlockAlreadyExists(String),
     BlockAlreadyExists(String),
 
 
+    #[error("Didn't provide blocks' previous")]
+    BlockPreviousMissing(),
+
     #[error("Block {0} not found in database")]
     #[error("Block {0} not found in database")]
     BlockNotFound(String),
     BlockNotFound(String),
 
 
     #[error("Block with order number {0} not found in database")]
     #[error("Block with order number {0} not found in database")]
     BlockNumberNotFound(u64),
     BlockNumberNotFound(u64),
 
 
+    #[error("Verifying slot missmatch")]
+    VerifyingSlotMissmatch(),
+
+    #[error("Slot {0} is invalid")]
+    SlotIsInvalid(u64),
+
     #[error("Slot {0} not found in database")]
     #[error("Slot {0} not found in database")]
     SlotNotFound(u64),
     SlotNotFound(u64),
 
 

+ 3 - 0
src/validator/consensus/mod.rs

@@ -18,6 +18,9 @@
 
 
 use crate::{blockchain::Blockchain, util::time::TimeKeeper};
 use crate::{blockchain::Blockchain, util::time::TimeKeeper};
 
 
+/// DarkFi consensus PID controller
+pub mod pid;
+
 /// This struct represents the information required by the consensus algorithm
 /// This struct represents the information required by the consensus algorithm
 pub struct Consensus {
 pub struct Consensus {
     /// Canonical (finalized) blockchain
     /// Canonical (finalized) blockchain

+ 36 - 0
src/validator/consensus/pid.rs

@@ -0,0 +1,36 @@
+/* This file is part of DarkFi (https://dark.fi)
+ *
+ * Copyright (C) 2020-2023 Dyne.org foundation
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+//! TODO: this is just the foundation layout, so we can complete
+//! the basic validator. We will use pallas::Base::zero() everywhere,
+//! since we just want to simulate its functionality. After layout is
+//! complete, the proper pid functionality will be implemented.
+
+use darkfi_sdk::pasta::pallas;
+
+/// Return 2-term target approximation sigma coefficients,
+/// corresponding to current slot consensus state.
+pub fn current_sigmas() -> (pallas::Base, pallas::Base) {
+    (pallas::Base::zero(), pallas::Base::zero())
+}
+
+/// Return 2-term target approximation sigma coefficients,
+/// corresponding to provided slot consensus state.
+pub fn slot_sigmas() -> (pallas::Base, pallas::Base) {
+    (pallas::Base::zero(), pallas::Base::zero())
+}

+ 12 - 7
src/validator/mod.rs

@@ -84,7 +84,7 @@ impl Validator {
         // Add genesis block if blockchain is empty
         // Add genesis block if blockchain is empty
         if blockchain.genesis().is_err() {
         if blockchain.genesis().is_err() {
             info!(target: "validator", "Appending genesis block");
             info!(target: "validator", "Appending genesis block");
-            verify_block(&overlay, &config.genesis_block, None)?;
+            verify_block(&overlay, &config.time_keeper, &config.genesis_block, None).await?;
         };
         };
 
 
         // Deploy native wasm contracts
         // Deploy native wasm contracts
@@ -120,19 +120,24 @@ impl Validator {
         debug!(target: "validator", "Instantiating BlockchainOverlay");
         debug!(target: "validator", "Instantiating BlockchainOverlay");
         let overlay = BlockchainOverlay::new(&self.blockchain)?;
         let overlay = BlockchainOverlay::new(&self.blockchain)?;
 
 
-        // Retrieve last block
-        let lock = overlay.lock().unwrap();
-        // If blockchain is empty it will error out here
-        let last_block = match lock.last_block() {
+        // Retrieve last block. If blockchain is empty it will error out here
+        let last_block = match overlay.lock().unwrap().last_block() {
             Ok(l) => l,
             Ok(l) => l,
             Err(_) => BlockInfo::default(),
             Err(_) => BlockInfo::default(),
         };
         };
         // We only need the reference, thats why we do it like this
         // We only need the reference, thats why we do it like this
-        let mut previous = if !lock.is_empty()? { Some(&last_block) } else { None };
+        let mut previous =
+            if !overlay.lock().unwrap().is_empty()? { Some(&last_block) } else { None };
+
+        // Create a time keeper to validate each block
+        let mut time_keeper = self.consensus.time_keeper.clone();
 
 
         // Validate and insert each block
         // Validate and insert each block
         for block in blocks {
         for block in blocks {
-            if verify_block(&overlay, block, previous).is_err() {
+            // Use block slot in time keeper
+            time_keeper.verifying_slot = block.header.slot;
+
+            if verify_block(&overlay, &time_keeper, block, previous).await.is_err() {
                 warn!(target: "validator", "Erroneous block found in set");
                 warn!(target: "validator", "Erroneous block found in set");
                 overlay.lock().unwrap().overlay.lock().unwrap().purge_new_trees()?;
                 overlay.lock().unwrap().overlay.lock().unwrap().purge_new_trees()?;
                 return Err(Error::BlockIsInvalid(block.blockhash().to_string()))
                 return Err(Error::BlockIsInvalid(block.blockhash().to_string()))

+ 70 - 13
src/validator/verification.rs

@@ -18,7 +18,10 @@
 
 
 use std::{collections::HashMap, io::Cursor};
 use std::{collections::HashMap, io::Cursor};
 
 
-use darkfi_sdk::{crypto::PublicKey, pasta::pallas};
+use darkfi_sdk::{
+    crypto::{PublicKey, CONSENSUS_CONTRACT_ID},
+    pasta::pallas,
+};
 use darkfi_serial::{Decodable, Encodable, WriteExt};
 use darkfi_serial::{Decodable, Encodable, WriteExt};
 use log::{debug, error, warn};
 use log::{debug, error, warn};
 
 
@@ -32,38 +35,92 @@ use crate::{
     Error, Result,
     Error, Result,
 };
 };
 
 
-/// Validate given [`Transaction`], and apply it to the provided overlay
-pub fn verify_block(
+/// Validate given [`BlockInfo`], and apply it to the provided overlay
+pub async fn verify_block(
     overlay: &BlockchainOverlayPtr,
     overlay: &BlockchainOverlayPtr,
+    time_keeper: &TimeKeeper,
     block: &BlockInfo,
     block: &BlockInfo,
     previous: Option<&BlockInfo>,
     previous: Option<&BlockInfo>,
 ) -> Result<()> {
 ) -> Result<()> {
     let block_hash = block.blockhash();
     let block_hash = block.blockhash();
     debug!(target: "validator", "Validating block {}", block_hash);
     debug!(target: "validator", "Validating block {}", block_hash);
 
 
-    let lock = overlay.lock().unwrap();
-
     // Check if block already exists
     // Check if block already exists
-    if lock.has_block(block)? {
+    if overlay.lock().unwrap().has_block(block)? {
         return Err(Error::BlockAlreadyExists(block.blockhash().to_string()))
         return Err(Error::BlockAlreadyExists(block.blockhash().to_string()))
     }
     }
 
 
-    // This will be true for every block, apart from genesis
-    if let Some(p) = previous {
-        block.validate(p)?;
+    // Block slot must be the same as the time keeper verifying slot
+    if block.header.slot != time_keeper.verifying_slot {
+        return Err(Error::VerifyingSlotMissmatch())
+    }
+
+    // Validate block using its previous, excluding genesis
+    if block.header.slot != 0 {
+        if previous.is_none() {
+            return Err(Error::BlockPreviousMissing())
+        }
+        block.validate(previous.unwrap())?;
     }
     }
 
 
-    // TODO: Add rest block verifications here
+    // Validate proposal transaction
+    verify_proposal_transaction(overlay, time_keeper, &block.producer.proposal).await?;
+
+    // Verify transactions
+    verify_transactions(overlay, time_keeper, &block.txs).await?;
 
 
     // Insert block
     // Insert block
-    lock.add_block(block)?;
+    overlay.lock().unwrap().add_block(block)?;
 
 
     debug!(target: "validator", "Block {} verified successfully", block_hash);
     debug!(target: "validator", "Block {} verified successfully", block_hash);
     Ok(())
     Ok(())
 }
 }
 
 
+/// Validate WASM execution, signatures, and ZK proofs for a given proposal [`Transaction`],
+/// and apply it to the provided overlay.
+pub async fn verify_proposal_transaction(
+    overlay: &BlockchainOverlayPtr,
+    time_keeper: &TimeKeeper,
+    tx: &Transaction,
+) -> Result<()> {
+    let tx_hash = tx.hash();
+    debug!(target: "validator", "Validating proposal transaction {}", tx_hash);
+
+    // Genesis transaction must be the Transaction::default() one (empty)
+    if time_keeper.verifying_slot == 0 {
+        if *tx != Transaction::default() {
+            error!(target: "validator", "Genesis proposal transaction is not default one");
+            return Err(TxVerifyFailed::ErroneousTxs(vec![tx.clone()]).into())
+        }
+
+        return Ok(())
+    }
+
+    // Transaction must contain a single Consensus::Proposal (0x02) call
+    if tx.calls.len() != 1 ||
+        (tx.calls[0].contract_id != *CONSENSUS_CONTRACT_ID && tx.calls[0].data[0] != 0x02)
+    {
+        error!(target: "validator", "Proposal transaction is malformed");
+        return Err(TxVerifyFailed::ErroneousTxs(vec![tx.clone()]).into())
+    }
+
+    // Map of ZK proof verifying keys for the current transaction batch
+    let mut vks: HashMap<[u8; 32], HashMap<String, VerifyingKey>> = HashMap::new();
+
+    // Initialize the map
+    vks.insert(tx.calls[0].contract_id.to_bytes(), HashMap::new());
+
+    // TODO: when fee is implemented, differentiate here since this transaction
+    // won't have fee
+    verify_transaction(overlay, time_keeper, tx, &mut vks).await?;
+
+    debug!(target: "validator", "Proposal transaction {} verified successfully", tx_hash);
+
+    Ok(())
+}
+
 /// Validate WASM execution, signatures, and ZK proofs for a given [`Transaction`],
 /// Validate WASM execution, signatures, and ZK proofs for a given [`Transaction`],
-/// and apply them it to the provided overlay.
+/// and apply it to the provided overlay.
 pub async fn verify_transaction(
 pub async fn verify_transaction(
     overlay: &BlockchainOverlayPtr,
     overlay: &BlockchainOverlayPtr,
     time_keeper: &TimeKeeper,
     time_keeper: &TimeKeeper,
@@ -159,7 +216,7 @@ pub async fn verify_transaction(
 
 
     debug!(target: "validator", "Verifying ZK proofs for transaction {}", tx_hash);
     debug!(target: "validator", "Verifying ZK proofs for transaction {}", tx_hash);
     if let Err(e) = tx.verify_zkps(verifying_keys, zkp_table).await {
     if let Err(e) = tx.verify_zkps(verifying_keys, zkp_table).await {
-        error!(target: "consensus::validator", "ZK proof verification for tx {} failed: {}", tx_hash, e);
+        error!(target: "validator", "ZK proof verification for tx {} failed: {}", tx_hash, e);
         return Err(TxVerifyFailed::InvalidZkProof.into())
         return Err(TxVerifyFailed::InvalidZkProof.into())
     }
     }