Reorganization in chain database (#93)

* tests first

* decanonize & test

* fix denote bug

* fork route & test

* some refactoring of the transaction processing

* reorganization on insert

* non-reorg tests

* fix warnings

* fix doc comment

* long fork reorg test

* todo for shorter reorgs
This commit is contained in:
Nikolay Volf 2016-11-07 13:57:23 +03:00 committed by Marek Kotewicz
parent 725120c126
commit 5d587e20f6
2 changed files with 518 additions and 23 deletions

View File

@ -28,6 +28,8 @@ const _COL_RESERVED6: u32 = 10;
const DB_VERSION: u32 = 1;
const MAX_FORK_ROUTE_PRESET: usize = 128;
/// Blockchain storage interface
pub trait Store : Send + Sync {
/// get best block
@ -89,6 +91,16 @@ pub enum Error {
Io(std::io::Error),
/// Invalid meta info
Meta(MetaError),
/// Unknown hash
Unknown(H256),
/// Not the block from the main chain
NotMain(H256),
/// Fork too long
ForkTooLong,
/// Main chain block transaction attempts to double-spend
DoubleSpend(H256),
/// Chain has no best block
NoBestBlock,
}
impl From<String> for Error {
@ -113,6 +125,30 @@ const KEY_VERSION: &'static[u8] = b"version";
const KEY_BEST_BLOCK_NUMBER: &'static[u8] = b"best_block_number";
const KEY_BEST_BLOCK_HASH: &'static[u8] = b"best_block_hash";
struct UpdateContext {
pub meta: HashMap<H256, TransactionMeta>,
pub db_transaction: DBTransaction,
}
impl UpdateContext {
pub fn new(db: &Database) -> Self {
UpdateContext {
meta: HashMap::new(),
db_transaction: db.transaction(),
}
}
pub fn apply(mut self, db: &Database) -> Result<(), Error> {
// actually saving meta
for (hash, meta) in self.meta.drain() {
self.db_transaction.put(Some(COL_TRANSACTIONS_META), &*hash, &meta.to_bytes());
}
try!(db.write(self.db_transaction));
Ok(())
}
}
impl Storage {
/// new storage at the selected path
@ -218,21 +254,34 @@ impl Storage {
.collect()
}
/// update transactions metadata in the specified database transaction
fn update_transactions_meta(&self, db_transaction: &mut DBTransaction, number: u32, accepted_txs: &[chain::Transaction]) {
let mut meta_buf = HashMap::<H256, TransactionMeta>::new();
fn block_header_by_hash(&self, h: &H256) -> Option<chain::BlockHeader> {
self.get(COL_BLOCK_HEADERS, &**h).and_then(|val|
serialization::deserialize(val.as_ref()).map_err(
|e| self.db_error(format!("Error deserializing block header, possible db corruption ({:?})", e))
).ok()
)
}
/// update transactions metadata in the specified database transaction
fn update_transactions_meta(&self, context: &mut UpdateContext, number: u32, accepted_txs: &[chain::Transaction])
-> Result<(), Error>
{
// inserting new meta for coinbase transaction
for accepted_tx in accepted_txs.iter() {
// adding unspent transaction meta
meta_buf.insert(accepted_tx.hash(), TransactionMeta::new(number, accepted_tx.outputs.len()));
context.meta.insert(accepted_tx.hash(), TransactionMeta::new(number, accepted_tx.outputs.len()));
}
// another iteration skipping coinbase transaction
for accepted_tx in accepted_txs.iter().skip(1) {
for input in accepted_tx.inputs.iter() {
if !match meta_buf.get_mut(&input.previous_output.hash) {
if !match context.meta.get_mut(&input.previous_output.hash) {
Some(ref mut meta) => {
if meta.is_spent(input.previous_output.index as usize) {
return Err(Error::DoubleSpend(input.previous_output.hash.clone()));
}
meta.note_used(input.previous_output.index as usize);
true
},
@ -245,19 +294,149 @@ impl Storage {
&input.previous_output.hash
));
if meta.is_spent(input.previous_output.index as usize) {
return Err(Error::DoubleSpend(input.previous_output.hash.clone()));
}
meta.note_used(input.previous_output.index as usize);
meta_buf.insert(
context.meta.insert(
input.previous_output.hash.clone(),
meta);
}
}
}
// actually saving meta
for (hash, meta) in meta_buf.drain() {
db_transaction.put(Some(COL_TRANSACTIONS_META), &*hash, &meta.to_bytes());
Ok(())
}
/// block decanonization
/// all transaction outputs used are marked as not used
/// all transaction meta is removed
/// DOES NOT update best block
fn decanonize_block(&self, context: &mut UpdateContext, hash: &H256) -> Result<(), Error> {
// ensure that block is of the main chain
try!(self.block_number(hash).ok_or(Error::NotMain(hash.clone())));
// transaction de-provisioning
let tx_hashes = self.block_transaction_hashes_by_hash(hash);
for (tx_hash_num, tx_hash) in tx_hashes.iter().enumerate() {
let tx = self.transaction(tx_hash)
.expect("Transaction in the saved block should exist as a separate entity indefinitely");
// remove meta
context.db_transaction.delete(Some(COL_TRANSACTIONS_META), &**tx_hash);
// denote outputs used
if tx_hash_num == 0 { continue; } // coinbase transaction does not have inputs
for input in tx.inputs.iter() {
if !match context.meta.get_mut(&input.previous_output.hash) {
Some(ref mut meta) => {
meta.denote_used(input.previous_output.index as usize);
true
},
None => false,
} {
let mut meta =
self.transaction_meta(&input.previous_output.hash)
.unwrap_or_else(|| panic!(
"No transaction metadata for {}! Corrupted DB? Reindex?",
&input.previous_output.hash
));
meta.denote_used(input.previous_output.index as usize);
context.meta.insert(
input.previous_output.hash.clone(),
meta);
}
}
}
Ok(())
}
/// Returns the height where the fork occurred and chain up to this place (not including last canonical hash)
fn fork_route(&self, max_route: usize, hash: &H256) -> Result<(u32, Vec<H256>), Error> {
let header = try!(self.block_header_by_hash(hash).ok_or(Error::Unknown(hash.clone())));
// only main chain blocks has block numbers
// so if it has, it is not a fork and we return empty route
if let Some(number) = self.block_number(hash) {
return Ok((number, Vec::new()));
}
let mut next_hash = header.previous_header_hash;
let mut result = Vec::new();
for _ in 0..max_route {
if let Some(number) = self.block_number(&next_hash) {
return Ok((number, result));
}
result.push(next_hash.clone());
next_hash = try!(self.block_header_by_hash(&next_hash).ok_or(Error::Unknown(hash.clone())))
.previous_header_hash;
}
Err(Error::ForkTooLong)
}
fn best_number(&self) -> Option<u32> {
self.read_meta_u32(KEY_BEST_BLOCK_NUMBER)
}
fn best_hash(&self) -> Option<H256> {
self.get(COL_META, KEY_BEST_BLOCK_HASH).map(|val| H256::from(&**val))
}
fn canonize_block(&self, context: &mut UpdateContext, at_height: u32, hash: &H256) -> Result<(), Error> {
let transactions = self.block_transactions_by_hash(hash);
try!(self.update_transactions_meta(context, at_height, &transactions));
// only canonical blocks are allowed to wield a number
context.db_transaction.put(Some(COL_BLOCK_HASHES), &u32_key(at_height), std::ops::Deref::deref(hash));
context.db_transaction.write_u32(Some(COL_BLOCK_NUMBERS), std::ops::Deref::deref(hash), at_height);
Ok(())
}
// maybe reorganize to the _known_ block
// it will actually reorganize only when side chain is at least the same length as main
fn maybe_reorganize(&self, context: &mut UpdateContext, hash: &H256) -> Result<Option<(u32, H256)>, Error> {
if self.block_number(hash).is_some() {
return Ok(None); // cannot reorganize to canonical block
}
// find the route of the block with hash `hash` to the main chain
let (at_height, route) = try!(self.fork_route(MAX_FORK_ROUTE_PRESET, hash));
// reorganization is performed only if length of side chain is at least the same as main chain
// todo: shorter chain may actualy become canonical during difficulty updates, though with rather low probability
if (route.len() as i32 + 1) < (self.best_number().unwrap_or(0) as i32 - at_height as i32) {
return Ok(None);
}
let mut now_best = try!(self.best_number().ok_or(Error::NoBestBlock));
// decanonizing main chain to the split point
loop {
let next_to_decanonize = try!(self.best_hash().ok_or(Error::NoBestBlock));
try!(self.decanonize_block(context, &next_to_decanonize));
now_best -= 1;
if now_best == at_height { break; }
}
// canonizing all route from the split point
for new_canonical_hash in route.iter() {
now_best += 1;
try!(self.canonize_block(context, now_best, &new_canonical_hash));
}
// finaly canonizing the top block we are reorganizing to
try!(self.canonize_block(context, now_best + 1, hash));
Ok(Some((now_best+1, hash.clone())))
}
}
@ -314,16 +493,20 @@ impl Store for Storage {
}
fn insert_block(&self, block: &chain::Block) -> Result<(), Error> {
// ! lock will be held during the entire insert routine
let mut best_block = self.best_block.write();
let mut context = UpdateContext::new(&self.database);
let block_hash = block.hash();
let new_best_hash = match best_block.as_ref().map(|bb| &bb.hash) {
let mut new_best_hash = match best_block.as_ref().map(|bb| &bb.hash) {
Some(best_hash) if &block.header().previous_header_hash != best_hash => best_hash.clone(),
_ => block_hash.clone(),
};
let new_best_number = match best_block.as_ref().map(|b| b.number) {
let mut new_best_number = match best_block.as_ref().map(|b| b.number) {
Some(best_number) => {
if block.hash() == new_best_hash { best_number + 1 }
else { best_number }
@ -331,40 +514,57 @@ impl Store for Storage {
None => 0,
};
let mut transaction = self.database.transaction();
let tx_space = block.transactions().len() * 32;
let mut tx_refs = Vec::with_capacity(tx_space);
for tx in block.transactions() {
let tx_hash = tx.hash();
tx_refs.extend(&*tx_hash);
transaction.put(
context.db_transaction.put(
Some(COL_TRANSACTIONS),
&*tx_hash,
&serialization::serialize(tx),
);
}
transaction.put(Some(COL_BLOCK_TRANSACTIONS), &*block_hash, &tx_refs);
context.db_transaction.put(Some(COL_BLOCK_TRANSACTIONS), &*block_hash, &tx_refs);
transaction.put(
context.db_transaction.put(
Some(COL_BLOCK_HEADERS),
&*block_hash,
&serialization::serialize(block.header())
);
// the block is continuing the main chain
if best_block.as_ref().map(|b| b.number) != Some(new_best_number) {
self.update_transactions_meta(&mut transaction, new_best_number, block.transactions());
transaction.write_u32(Some(COL_META), KEY_BEST_BLOCK_NUMBER, new_best_number);
try!(self.update_transactions_meta(&mut context, new_best_number, block.transactions()));
context.db_transaction.write_u32(Some(COL_META), KEY_BEST_BLOCK_NUMBER, new_best_number);
// updating main chain height reference
transaction.put(Some(COL_BLOCK_HASHES), &u32_key(new_best_number), std::ops::Deref::deref(&block_hash));
transaction.write_u32(Some(COL_BLOCK_NUMBERS), std::ops::Deref::deref(&block_hash), new_best_number);
context.db_transaction.put(Some(COL_BLOCK_HASHES), &u32_key(new_best_number), std::ops::Deref::deref(&block_hash));
context.db_transaction.write_u32(Some(COL_BLOCK_NUMBERS), std::ops::Deref::deref(&block_hash), new_best_number);
}
transaction.put(Some(COL_META), KEY_BEST_BLOCK_HASH, std::ops::Deref::deref(&new_best_hash));
// the block does not continue the main chain
// but can cause reorganization here
// this can canonize the block parent if block parent + this block is longer than the main chain
else if let Some((reorg_number, _)) = self.maybe_reorganize(&mut context, &block.header().previous_header_hash).unwrap_or(None) {
// if so, we have new best main chain block
new_best_number = reorg_number + 1;
new_best_hash = block_hash;
try!(self.database.write(transaction));
// and we canonize it also by provisioning transactions
try!(self.update_transactions_meta(&mut context, new_best_number, block.transactions()));
context.db_transaction.write_u32(Some(COL_META), KEY_BEST_BLOCK_NUMBER, new_best_number);
context.db_transaction.put(Some(COL_BLOCK_HASHES), &u32_key(new_best_number), std::ops::Deref::deref(&new_best_hash));
context.db_transaction.write_u32(Some(COL_BLOCK_NUMBERS), std::ops::Deref::deref(&new_best_hash), new_best_number);
}
// we always update best hash even if it is not changed
context.db_transaction.put(Some(COL_META), KEY_BEST_BLOCK_HASH, std::ops::Deref::deref(&new_best_hash));
// write accumulated transactions meta
try!(context.apply(&self.database));
// updating locked best block
*best_block = Some(BestBlock { hash: new_best_hash, number: new_best_number });
Ok(())
@ -388,7 +588,7 @@ impl Store for Storage {
#[cfg(test)]
mod tests {
use super::{Storage, Store};
use super::{Storage, Store, UpdateContext};
use devtools::RandomTempPath;
use chain::{Block, RepresentH256};
use super::super::BlockRef;
@ -591,4 +791,293 @@ mod tests {
assert!(!meta.is_spent(1), "Transaction #1 output #1 in the new block should be recorded as unspent");
assert!(!meta.is_spent(3), "Transaction #1 second #3 in the new block should be recorded as unspent");
}
#[test]
fn reorganize_simple() {
let path = RandomTempPath::create_dir();
let store = Storage::new(path.as_path()).unwrap();
let genesis = test_data::genesis();
store.insert_block(&genesis).unwrap();
let (main_hash1, main_block1) = test_data::block_hash_builder()
.block()
.header().parent(genesis.hash())
.nonce(1)
.build()
.build()
.build();
store.insert_block(&main_block1).expect("main block 1 should insert with no problems");
let (_, side_block1) = test_data::block_hash_builder()
.block()
.header().parent(genesis.hash())
.nonce(2)
.build()
.build()
.build();
store.insert_block(&side_block1).expect("side block 1 should insert with no problems");
// chain should not reorganize to side_block1
assert_eq!(store.best_block().unwrap().hash, main_hash1);
}
#[test]
fn fork_smoky() {
let path = RandomTempPath::create_dir();
let store = Storage::new(path.as_path()).unwrap();
let genesis = test_data::genesis();
store.insert_block(&genesis).unwrap();
let (_, main_block1) = test_data::block_hash_builder()
.block()
.header().parent(genesis.hash())
.nonce(1)
.build()
.build()
.build();
store.insert_block(&main_block1).expect("main block 1 should insert with no problems");
let (side_hash1, side_block1) = test_data::block_hash_builder()
.block()
.header().parent(genesis.hash())
.nonce(2)
.build()
.build()
.build();
store.insert_block(&side_block1).expect("side block 1 should insert with no problems");
let (side_hash2, side_block2) = test_data::block_hash_builder()
.block()
.header().parent(side_hash1)
.nonce(3)
.build()
.build()
.build();
store.insert_block(&side_block2).expect("side block 2 should insert with no problems");
// store should reorganize to side hash 2, because it represents the longer chain
assert_eq!(store.best_block().unwrap().hash, side_hash2);
}
#[test]
fn fork_long() {
let path = RandomTempPath::create_dir();
let store = Storage::new(path.as_path()).unwrap();
let genesis = test_data::genesis();
store.insert_block(&genesis).unwrap();
let mut last_main_block_hash = genesis.hash();
let mut last_side_block_hash = genesis.hash();
for n in 0..32 {
let (new_main_hash, main_block) = test_data::block_hash_builder()
.block()
.header().parent(last_main_block_hash)
.nonce(n*2)
.build()
.build()
.build();
store.insert_block(&main_block).expect(&format!("main block {} should insert with no problems", n));
last_main_block_hash = new_main_hash;
let (new_side_hash, side_block) = test_data::block_hash_builder()
.block()
.header().parent(last_side_block_hash)
.nonce(n*2 + 1)
.build()
.build()
.build();
store.insert_block(&side_block).expect(&format!("side block {} should insert with no problems", n));
last_side_block_hash = new_side_hash;
}
let (reorg_side_hash, reorg_side_block) = test_data::block_hash_builder()
.block()
.header().parent(last_side_block_hash)
.nonce(3)
.build()
.build()
.build();
store.insert_block(&reorg_side_block).expect("last side block should insert with no problems");
// store should reorganize to side hash 2, because it represents the longer chain
assert_eq!(store.best_block().unwrap().hash, reorg_side_hash);
}
// test simulates when main chain and side chain are competing all along, each adding
// block one by one
#[test]
fn fork_competing() {
let path = RandomTempPath::create_dir();
let store = Storage::new(path.as_path()).unwrap();
let genesis = test_data::genesis();
store.insert_block(&genesis).unwrap();
let (main_hash1, main_block1) = test_data::block_hash_builder()
.block()
.header().parent(genesis.hash())
.nonce(1)
.build()
.build()
.build();
store.insert_block(&main_block1).expect("main block 1 should insert with no problems");
let (side_hash1, side_block1) = test_data::block_hash_builder()
.block()
.header().parent(genesis.hash())
.nonce(2)
.build()
.build()
.build();
store.insert_block(&side_block1).expect("side block 1 should insert with no problems");
let (main_hash2, main_block2) = test_data::block_hash_builder()
.block()
.header().parent(main_hash1)
.nonce(3)
.build()
.build()
.build();
store.insert_block(&main_block2).expect("main block 2 should insert with no problems");
let (_side_hash2, side_block2) = test_data::block_hash_builder()
.block()
.header().parent(side_hash1)
.nonce(4)
.build()
.build()
.build();
store.insert_block(&side_block2).expect("side block 2 should insert with no problems");
// store should not reorganize to side hash 2, because it competing chains are of the equal length
assert_eq!(store.best_block().unwrap().hash, main_hash2);
}
#[test]
fn decanonize() {
let path = RandomTempPath::create_dir();
let store = Storage::new(path.as_path()).unwrap();
let genesis = test_data::genesis();
store.insert_block(&genesis).unwrap();
let genesis_coinbase = genesis.transactions()[0].hash();
let block = test_data::block_builder()
.header().parent(genesis.hash()).build()
.transaction().coinbase().build()
.transaction()
.input().hash(genesis_coinbase.clone()).build()
.build()
.build();
store.insert_block(&block).expect("inserting first block in the decanonize test should not fail");
let genesis_meta = store.transaction_meta(&genesis_coinbase)
.expect("Transaction meta for the genesis coinbase transaction should exist");
assert!(genesis_meta.is_spent(0), "Genesis coinbase should be recorded as spent because block#1 transaction spends it");
let mut update_context = UpdateContext::new(&store.database);
store.decanonize_block(&mut update_context, &block.hash())
.expect("Decanonizing block #1 which was just inserted should not fail");
update_context.apply(&store.database).unwrap();
let genesis_meta = store.transaction_meta(&genesis_coinbase)
.expect("Transaction meta for the genesis coinbase transaction should exist");
assert!(!genesis_meta.is_spent(0), "Genesis coinbase should be recorded as unspent because we retracted block #1");
}
#[test]
fn fork_route() {
let path = RandomTempPath::create_dir();
let store = Storage::new(path.as_path()).unwrap();
let genesis = test_data::genesis();
store.insert_block(&genesis).unwrap();
let (main_hash1, main_block1) = test_data::block_hash_builder()
.block()
.header().parent(genesis.hash())
.nonce(1)
.build()
.build()
.build();
store.insert_block(&main_block1).expect("main block 1 should insert with no problems");
let (main_hash2, main_block2) = test_data::block_hash_builder()
.block()
.header().parent(main_hash1)
.nonce(2)
.build()
.build()
.build();
store.insert_block(&main_block2).expect("main block 2 should insert with no problems");
let (main_hash3, main_block3) = test_data::block_hash_builder()
.block()
.header().parent(main_hash2)
.nonce(3)
.build()
.build()
.build();
store.insert_block(&main_block3).expect("main block 3 should insert with no problems");
let (_, main_block4) = test_data::block_hash_builder()
.block()
.header().parent(main_hash3)
.nonce(4)
.build()
.build()
.build();
store.insert_block(&main_block4).expect("main block 4 should insert with no problems");
let (side_hash1, side_block1) = test_data::block_hash_builder()
.block()
.header().parent(genesis.hash())
.nonce(5)
.build()
.build()
.build();
store.insert_block(&side_block1).expect("side block 1 should insert with no problems");
let (side_hash2, side_block2) = test_data::block_hash_builder()
.block()
.header().parent(side_hash1.clone())
.nonce(6)
.build()
.build()
.build();
store.insert_block(&side_block2).expect("side block 2 should insert with no problems");
let (side_hash3, side_block3) = test_data::block_hash_builder()
.block()
.header().parent(side_hash2.clone())
.nonce(7)
.build()
.build()
.build();
store.insert_block(&side_block3).expect("side block 3 should insert with no problems");
let (h, route) = store.fork_route(16, &side_hash3).expect("Fork route should have been built");
assert_eq!(h, 0);
assert_eq!(route, vec![side_hash2, side_hash1]);
}
}

View File

@ -29,6 +29,12 @@ impl TransactionMeta {
self.spent.set(index, true);
}
/// note that particular output has been used
pub fn denote_used(&mut self, index: usize) {
self.spent.set(index, false);
}
pub fn to_bytes(self) -> Vec<u8> {
let mut result = vec![0u8; 4];
LittleEndian::write_u32(&mut result[0..4], self.block_height);