Skip to content
Snippets Groups Projects
Commit 5f001e39 authored by Éloïs's avatar Éloïs
Browse files

[feat] blockchain: create fork resolution algo & its tests

parent db87ca43
Branches
Tags
1 merge request!109Resolve "Fork resolution algorithm"
......@@ -376,10 +376,12 @@ version = "0.1.0-a0.1"
dependencies = [
"dirs 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)",
"dubp-documents 0.12.0",
"dubp-documents-tests-tools 0.1.0",
"duniter-conf 0.1.0-a0.1",
"duniter-module 0.1.0-a0.1",
"duniter-network 0.1.0-a0.1",
"dup-crypto 0.6.0",
"dup-crypto-tests-tools 0.1.0",
"durs-blockchain-dal 0.1.0-a0.1",
"durs-common-tools 0.1.0",
"durs-message 0.1.0-a0.1",
......
......@@ -23,7 +23,7 @@ pub fn insert_new_head_block(
blockstamp: Blockstamp,
) -> Result<Vec<Blockstamp>, DALError> {
fork_tree_db.write(|fork_tree| {
let parent_id_opt = if blockstamp.id.0 > 0 {
let parent_id_opt = if blockstamp.id.0 > 0 && fork_tree.size() > 0 {
Some(fork_tree.get_main_branch_node_id(BlockId(blockstamp.id.0 - 1))
.expect("Fatal error: fail to insert new head block : previous block not exist in main branch"))
} else {
......
......@@ -31,3 +31,7 @@ serde = "1.0.*"
serde_json = "1.0.*"
sqlite = "0.23.*"
threadpool = "1.7.*"
[dev-dependencies]
dup-crypto-tests-tools = { path = "../../../tests-tools/crypto-tests-tools" }
dubp-documents-tests-tools = { path = "../../../tests-tools/documents-tests-tools" }
\ No newline at end of file
// Copyright (C) 2018 The Durs Project Developers.
//
// 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/>.
use dubp_documents::Blockstamp;
use durs_blockchain_dal::{DALError, ForksDBs};
use std::collections::HashSet;
/// Number of advance blocks required
pub static ADVANCE_BLOCKS: &'static u32 = &3;
/// Advance blockchain time required (in seconds)
pub static ADVANCE_TIME: &'static u64 = &900;
pub fn fork_resolution_algo(
forks_dbs: &ForksDBs,
current_blockstamp: Blockstamp,
invalid_blocks: &HashSet<Blockstamp>,
) -> Result<Option<Vec<Blockstamp>>, DALError> {
let current_bc_time = forks_dbs.fork_blocks_db.read(|db| {
db.get(&current_blockstamp)
.expect("safe unwrap")
.block
.median_time
})?;
let mut sheets = forks_dbs
.fork_tree_db
.read(|fork_tree| fork_tree.get_sheets())?;
sheets.sort_unstable_by(|s1, s2| s2.1.id.cmp(&s1.1.id));
for sheet in sheets {
if sheet.1 != current_blockstamp {
let branch = forks_dbs
.fork_tree_db
.read(|fork_tree| fork_tree.get_fork_branch(sheet.0))?;
if branch.is_empty() {
continue;
}
let branch_head_blockstamp = branch.last().expect("safe unwrap");
let branch_head_median_time = forks_dbs.fork_blocks_db.read(|db| {
db.get(&branch_head_blockstamp)
.expect("safe unwrap")
.block
.median_time
})?;
if branch_head_blockstamp.id.0 >= current_blockstamp.id.0 + *ADVANCE_BLOCKS
&& branch_head_median_time >= current_bc_time + *ADVANCE_TIME
&& branch[0].id.0 + *durs_blockchain_dal::constants::FORK_WINDOW_SIZE as u32
> current_blockstamp.id.0
{
let mut valid_branch = true;
for blockstamp in &branch {
if invalid_blocks.contains(blockstamp) {
valid_branch = false;
break;
}
}
if valid_branch {
return Ok(Some(branch));
}
}
}
}
Ok(None)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::*;
use dubp_documents::documents::block::BlockDocument;
use dubp_documents::BlockHash;
use durs_blockchain_dal::entities::block::DALBlock;
#[test]
fn test_fork_resolution_algo() -> Result<(), DALError> {
// Get FORK_WINDOW_SIZE value
let fork_window_size = *durs_blockchain_dal::constants::FORK_WINDOW_SIZE;
// Open empty databases in memory mode
let bc_dbs = BlocksV10DBs::open(None);
let forks_dbs = ForksDBs::open(None);
// Begin with no invalid blocks
let invalid_blocks: HashSet<Blockstamp> = HashSet::new();
// Generate `FORK_WINDOW_SIZE + 2` mock blocks
let main_branch: Vec<BlockDocument> =
dubp_documents_tests_tools::mocks::gen_empty_timed_blocks(fork_window_size + 2, 0u64);
// Insert mock blocks in forks_dbs
for block in &main_branch {
durs_blockchain_dal::writers::block::insert_new_head_block(
&bc_dbs.blockchain_db,
&forks_dbs,
DALBlock {
block: block.clone(),
expire_certs: None,
},
)?;
}
assert_eq!(
fork_window_size,
forks_dbs.fork_tree_db.read(|fork_tree| fork_tree.size())?
);
assert_eq!(
fork_window_size,
forks_dbs.fork_blocks_db.read(|db| db.len())?
);
// Get current blockstamp
let mut current_blockstamp = forks_dbs
.fork_tree_db
.read(|fork_tree| fork_tree.get_sheets())?
.get(0)
.expect("must be one sheet")
.1;
// Generate 3 fork block
let fork_point = &main_branch[main_branch.len() - 2];
let fork_blocks: Vec<BlockDocument> = (0..3)
.map(|i| {
dubp_documents_tests_tools::mocks::gen_empty_timed_block(
Blockstamp {
id: BlockId(fork_point.number.0 + i + 1),
hash: BlockHash(dup_crypto_tests_tools::mocks::hash('A')),
},
ADVANCE_TIME - 1,
if i == 0 {
fork_point.hash.expect("safe unwrap").0
} else {
dup_crypto_tests_tools::mocks::hash('A')
},
)
})
.collect();
// Add forks blocks into fork tree
insert_fork_blocks(&forks_dbs, &fork_blocks)?;
assert_eq!(
2,
forks_dbs
.fork_tree_db
.read(|tree| tree.get_sheets().len())?
);
// Must not fork
assert_eq!(
None,
fork_resolution_algo(&forks_dbs, current_blockstamp, &invalid_blocks)?
);
// Add the determining fork block
let determining_blockstamp = Blockstamp {
id: BlockId(fork_point.number.0 + 4),
hash: BlockHash(dup_crypto_tests_tools::mocks::hash('A')),
};
assert_eq!(
true,
durs_blockchain_dal::writers::block::insert_new_fork_block(
&forks_dbs,
DALBlock {
block: dubp_documents_tests_tools::mocks::gen_empty_timed_block(
determining_blockstamp,
*ADVANCE_TIME,
dup_crypto_tests_tools::mocks::hash('A'),
),
expire_certs: None,
},
)?,
);
// Must fork
assert_eq!(
Some(vec![
fork_blocks[0].blockstamp(),
fork_blocks[1].blockstamp(),
fork_blocks[2].blockstamp(),
determining_blockstamp,
]),
fork_resolution_algo(&forks_dbs, current_blockstamp, &invalid_blocks)?
);
current_blockstamp = determining_blockstamp;
// The old main branch catches up and overlaps with the fork
let new_main_blocks: Vec<BlockDocument> = (0..7)
.map(|i| {
dubp_documents_tests_tools::mocks::gen_empty_timed_block(
Blockstamp {
id: BlockId(fork_point.number.0 + i + 1),
hash: BlockHash(dup_crypto_tests_tools::mocks::hash('B')),
},
ADVANCE_TIME * 2,
if i == 0 {
fork_point.hash.expect("safe unwrap").0
} else {
dup_crypto_tests_tools::mocks::hash('B')
},
)
})
.collect();
insert_fork_blocks(&forks_dbs, &new_main_blocks)?;
// Must refork
assert_eq!(
Some(new_main_blocks.iter().map(|b| b.blockstamp()).collect()),
fork_resolution_algo(&forks_dbs, current_blockstamp, &invalid_blocks)?
);
//current_blockstamp = new_main_blocks.last().expect("safe unwrap").blockstamp();
Ok(())
}
fn insert_fork_blocks(forks_dbs: &ForksDBs, blocks: &[BlockDocument]) -> Result<(), DALError> {
for block in blocks {
assert_eq!(
true,
durs_blockchain_dal::writers::block::insert_new_fork_block(
forks_dbs,
DALBlock {
block: block.clone(),
expire_certs: None,
},
)?,
);
}
Ok(())
}
}
......@@ -38,6 +38,7 @@ mod apply_valid_block;
mod check_and_apply_block;
mod constants;
mod dbex;
mod fork_algo;
mod revert_block;
mod sync;
mod verify_block;
......@@ -419,8 +420,14 @@ impl BlockchainModule {
}
}
CheckAndApplyBlockReturn::ForkBlock => {
// TODO fork resolution algo
info!("new fork block({})", blockstamp);
if let Ok(Some(_new_bc_branch)) = fork_algo::fork_resolution_algo(
&self.forks_dbs,
current_blockstamp,
&self.invalid_forks,
) {
// TODO apply roolback here
}
}
CheckAndApplyBlockReturn::OrphanBlock => {
debug!("new orphan block({})", blockstamp);
......
......@@ -15,8 +15,9 @@
//! Crypto mocks for projects use dubp-documents
use dubp_documents::BlockHash;
use dubp_documents::{BlockId, Blockstamp};
use dubp_documents::documents::block::BlockDocument;
use dubp_documents::*;
use dup_crypto::hashs::Hash;
/// Generate n mock blockstamps
pub fn generate_blockstamps(n: usize) -> Vec<Blockstamp> {
......@@ -29,3 +30,66 @@ pub fn generate_blockstamps(n: usize) -> Vec<Blockstamp> {
})
.collect()
}
/// Generate n empty timed block document
pub fn gen_empty_timed_blocks(n: usize, time_step: u64) -> Vec<BlockDocument> {
(0..n)
.map(|i| {
gen_empty_timed_block(
Blockstamp {
id: BlockId(i as u32),
hash: BlockHash(dup_crypto_tests_tools::mocks::hash_from_byte(
(i % 255) as u8,
)),
},
time_step * n as u64,
if i == 0 {
Hash::default()
} else {
dup_crypto_tests_tools::mocks::hash_from_byte(((i - 1) % 255) as u8)
},
)
})
.collect()
}
/// Generate empty timed block document
/// (usefull for tests that only need blockstamp and median_time fields)
pub fn gen_empty_timed_block(
blockstamp: Blockstamp,
time: u64,
previous_hash: Hash,
) -> BlockDocument {
BlockDocument {
version: 10,
nonce: 0,
number: blockstamp.id,
pow_min: 0,
time: 0,
median_time: time,
members_count: 0,
monetary_mass: 0,
unit_base: 0,
issuers_count: 0,
issuers_frame: 0,
issuers_frame_var: 0,
currency: CurrencyName::default(),
issuers: vec![],
signatures: vec![],
hash: Some(blockstamp.hash),
parameters: None,
previous_hash,
previous_issuer: None,
dividend: None,
identities: vec![],
joiners: vec![],
actives: vec![],
leavers: vec![],
revoked: vec![],
excluded: vec![],
certifications: vec![],
transactions: vec![],
inner_hash: None,
inner_hash_and_nonce_str: "".to_owned(),
}
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please to comment