From 04292853ba2cbc2eae3c979d1e518da313612cd0 Mon Sep 17 00:00:00 2001 From: arvidn Date: Wed, 13 Nov 2024 07:51:29 +0100 Subject: [PATCH 1/2] start a tool to validate aspects of the blockchain database --- Cargo.lock | 1 + .../chia-consensus/src/consensus_constants.rs | 2 +- crates/chia-tools/Cargo.toml | 6 + crates/chia-tools/src/bin/analyze-chain.rs | 7 +- crates/chia-tools/src/bin/gen-corpus.rs | 7 +- .../src/bin/test-block-generators.rs | 7 +- .../src/bin/validate-blockchain-db.rs | 294 ++++++++++++++++++ crates/chia-tools/src/visit_spends.rs | 6 +- 8 files changed, 319 insertions(+), 11 deletions(-) create mode 100644 crates/chia-tools/src/bin/validate-blockchain-db.rs diff --git a/Cargo.lock b/Cargo.lock index 786d572f9..533c88631 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -478,6 +478,7 @@ dependencies = [ "clvm-utils", "clvmr", "hex", + "hex-literal", "rusqlite", "zstd", ] diff --git a/crates/chia-consensus/src/consensus_constants.rs b/crates/chia-consensus/src/consensus_constants.rs index 94de2277d..64b8f7332 100644 --- a/crates/chia-consensus/src/consensus_constants.rs +++ b/crates/chia-consensus/src/consensus_constants.rs @@ -157,7 +157,7 @@ pub const TEST_CONSTANTS: ConsensusConstants = ConsensusConstants { max_future_time2: 2 * 60, number_of_timestamps: 11, genesis_challenge: Bytes32::new(hex!( - "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + "ccd5bb71183532bff220ba46c268991a3ff07eb358e8255a65c30a2dce0e5fbb" )), agg_sig_me_additional_data: Bytes32::new(hex!( "ccd5bb71183532bff220ba46c268991a3ff07eb358e8255a65c30a2dce0e5fbb" diff --git a/crates/chia-tools/Cargo.toml b/crates/chia-tools/Cargo.toml index 776633791..33c91b34c 100644 --- a/crates/chia-tools/Cargo.toml +++ b/crates/chia-tools/Cargo.toml @@ -25,6 +25,7 @@ clap = { workspace = true, features = ["derive"] } zstd = { workspace = true } blocking-threadpool = { workspace = true } hex = { workspace = true } +hex-literal = { workspace = true } [lib] name = "chia_tools" @@ -69,3 +70,8 @@ bench = false name = "get-generator" test = false bench = false + +[[bin]] +name = "validate-blockchain-db" +test = false +bench = false diff --git a/crates/chia-tools/src/bin/analyze-chain.rs b/crates/chia-tools/src/bin/analyze-chain.rs index 9d9c58638..5da4b0c49 100644 --- a/crates/chia-tools/src/bin/analyze-chain.rs +++ b/crates/chia-tools/src/bin/analyze-chain.rs @@ -6,7 +6,7 @@ use std::time::SystemTime; use chia_consensus::consensus_constants::TEST_CONSTANTS; use chia_consensus::gen::flags::{ALLOW_BACKREFS, MEMPOOL_MODE}; use chia_consensus::gen::run_block_generator::{run_block_generator, run_block_generator2}; -use chia_tools::iterate_tx_blocks; +use chia_tools::iterate_blocks; use clvmr::Allocator; /// Analyze the blocks in a chia blockchain database @@ -42,11 +42,14 @@ fn main() { let mut a = Allocator::new_limited(500_000_000); let allocator_checkpoint = a.checkpoint(); let mut prev_timestamp = 0; - iterate_tx_blocks( + iterate_blocks( &args.file, args.start, Some(args.end), |height, block, block_refs| { + if block.transactions_generator.is_none() { + return; + } // after the hard fork, we run blocks without paying for the // CLVM generator ROM let block_runner = if height >= 5_496_000 { diff --git a/crates/chia-tools/src/bin/gen-corpus.rs b/crates/chia-tools/src/bin/gen-corpus.rs index 801d9a906..ff3ad6850 100644 --- a/crates/chia-tools/src/bin/gen-corpus.rs +++ b/crates/chia-tools/src/bin/gen-corpus.rs @@ -5,7 +5,7 @@ use clap::Parser; -use chia_tools::{iterate_tx_blocks, visit_spends}; +use chia_tools::{iterate_blocks, visit_spends}; use chia_traits::streamable::Streamable; use chia_bls::G2Element; @@ -78,11 +78,14 @@ fn main() { let mut last_height = 0; let mut last_time = Instant::now(); let corpus_counter = Arc::new(AtomicUsize::new(0)); - iterate_tx_blocks( + iterate_blocks( &args.file, args.start_height, args.max_height, |height, block, block_refs| { + if block.transactions_generator.is_none() { + return; + } // this is called for each transaction block let max_cost = block.transactions_info.unwrap().cost; let prg = block.transactions_generator.unwrap(); diff --git a/crates/chia-tools/src/bin/test-block-generators.rs b/crates/chia-tools/src/bin/test-block-generators.rs index 5f2c1f791..148feb02e 100644 --- a/crates/chia-tools/src/bin/test-block-generators.rs +++ b/crates/chia-tools/src/bin/test-block-generators.rs @@ -5,7 +5,7 @@ use chia_consensus::consensus_constants::TEST_CONSTANTS; use chia_consensus::gen::conditions::{NewCoin, SpendBundleConditions, SpendConditions}; use chia_consensus::gen::flags::{ALLOW_BACKREFS, DONT_VALIDATE_SIGNATURE, MEMPOOL_MODE}; use chia_consensus::gen::run_block_generator::{run_block_generator, run_block_generator2}; -use chia_tools::iterate_tx_blocks; +use chia_tools::iterate_blocks; use clvmr::allocator::NodePtr; use clvmr::Allocator; use std::collections::HashSet; @@ -165,11 +165,14 @@ fn main() { let mut last_height = args.start_height; let mut last_time = Instant::now(); println!("opening blockchain database file: {}", args.file); - iterate_tx_blocks( + iterate_blocks( &args.file, args.start_height, max_height, |height, block, block_refs| { + if block.transactions_generator.is_none() { + return; + } pool.execute(move || { let mut a = Allocator::new_limited(500_000_000); diff --git a/crates/chia-tools/src/bin/validate-blockchain-db.rs b/crates/chia-tools/src/bin/validate-blockchain-db.rs new file mode 100644 index 000000000..a53eebf6d --- /dev/null +++ b/crates/chia-tools/src/bin/validate-blockchain-db.rs @@ -0,0 +1,294 @@ +use clap::Parser; + +use chia_consensus::consensus_constants::ConsensusConstants; +use chia_consensus::consensus_constants::TEST_CONSTANTS; +use chia_consensus::gen::flags::{ALLOW_BACKREFS, DONT_VALIDATE_SIGNATURE}; +use chia_consensus::gen::run_block_generator::{run_block_generator, run_block_generator2}; +use chia_protocol::{Bytes32, Coin}; +use chia_tools::iterate_blocks; +use clvmr::Allocator; +use rusqlite::Connection; +use std::collections::HashMap; +use std::collections::HashSet; +use std::io::Write; +use std::thread::available_parallelism; +use std::time::{Duration, Instant}; + +use hex_literal::hex; + +/// Validates a blockchain database (must use v2 schema) +#[derive(Parser, Debug)] +#[command(author, version, about, long_about = None)] +#[allow(clippy::struct_excessive_bools)] +struct Args { + /// Path to blockchain database file to validate + file: String, + + /// The number of paralell thread to run block generators in + #[arg(short = 'j', long)] + num_jobs: Option, + + /// Start at this block height. Assume all blocks up to this height are + /// valid. This is meant for resuming validation or re-producing a failure + #[arg(short, long, default_value_t = 0)] + start: u32, + + /// Don't validate block signatures (saves time) + #[arg(long, default_value_t = false)] + skip_signature_validation: bool, + + /// use testnet 11 constants instead of mainnet. This is required when + /// validating testnet blockchain database.s + #[arg(long, default_value_t = false)] + testnet: bool, +} + +const MAINNET_CONSTANTS: ConsensusConstants = TEST_CONSTANTS; +const TESTNET11_CONSTANTS: ConsensusConstants = ConsensusConstants { + agg_sig_me_additional_data: Bytes32::new(hex!( + "37a90eb5185a9c4439a91ddc98bbadce7b4feba060d50116a067de66bf236615" + )), + agg_sig_parent_additional_data: Bytes32::new(hex!( + "c0754ae8602c47489b5394af8972c58238c4389d715f0585ca512d9428395e62" + )), + agg_sig_puzzle_additional_data: Bytes32::new(hex!( + "2e63e4ca0796d9ef8e8a748d740f4b8632c4d994ad6cce51bd61a6612d602697" + )), + agg_sig_amount_additional_data: Bytes32::new(hex!( + "cf15f86103bee6260b0e020a1ba02bcf61230fe209592543399dcf9267f8dfcc" + )), + agg_sig_puzzle_amount_additional_data: Bytes32::new(hex!( + "02c0ecb453e75bd77823dd0affd3f224d968012a8c6c6c423801cc30dd5eb347" + )), + agg_sig_parent_amount_additional_data: Bytes32::new(hex!( + "fc5eaa82087943fbee8683d42ae7a2a7aac0d4eecd4c98d71c228b9c62bf9497" + )), + agg_sig_parent_puzzle_additional_data: Bytes32::new(hex!( + "54c3ed8017f77354acca4000b40424396a369740e5a504467784f392b961ab37" + )), + difficulty_constant_factor: 10052721566054, + difficulty_starting: 30, + epoch_blocks: 768, + genesis_challenge: Bytes32::new(hex!( + "37a90eb5185a9c4439a91ddc98bbadce7b4feba060d50116a067de66bf236615" + )), + genesis_pre_farm_farmer_puzzle_hash: Bytes32::new(hex!( + "08296fc227decd043aee855741444538e4cc9a31772c4d1a9e6242d1e777e42a" + )), + genesis_pre_farm_pool_puzzle_hash: Bytes32::new(hex!( + "3ef7c233fc0785f3c0cae5992c1d35e7c955ca37a423571c1607ba392a9d12f7" + )), + mempool_block_buffer: 10, + min_plot_size: 18, + sub_slot_iters_starting: 67108864, + // forks activated from the beginning on testnet11 + hard_fork_height: 0, + plot_filter_128_height: 6029568, + plot_filter_64_height: 11075328, + plot_filter_32_height: 16121088, + ..MAINNET_CONSTANTS +}; + +fn main() { + let args = Args::parse(); + + let constants = if args.testnet { + &TESTNET11_CONSTANTS + } else { + &MAINNET_CONSTANTS + }; + + let num_cores = args + .num_jobs + .unwrap_or_else(|| available_parallelism().unwrap().into()); + + let pool = blocking_threadpool::Builder::new() + .num_threads(num_cores) + .queue_len(num_cores + 5) + .build(); + + let mut last_height = 0; + let mut last_time = Instant::now(); + println!( + r"THIS TOOL DOES NOT VALIDATE ALL ASPECTS OF A BLOCKCHAIN DATABASE +features that are validated: + * block hashes and heights + * some conditions + * block signatures (unless disabled by command line option) + * the coin_record table +" + ); + println!("opening blockchain database file: {}", args.file); + + let connection = Connection::open(&args.file).expect("failed to open database file"); + let mut select_spends = connection + .prepare("SELECT coin_name FROM coin_record WHERE spent_index == ?;") + .expect("failed to prepare SQL statement finding spent coins"); + let mut select_created = connection + .prepare( + "SELECT coin_name, coinbase, puzzle_hash, coin_parent, amount FROM coin_record WHERE confirmed_index == ?;", + ) + .expect("failed to prepare SQL statement finding created coins"); + + let mut prev_hash = constants.genesis_challenge; + let mut prev_height: i64 = args.start as i64 - 1; + + println!("iterating over blocks starting at height {}", args.start); + iterate_blocks(&args.file, args.start, None, |height, block, block_refs| { + // If we don't start validation from height 0, we need to initialize the + // expected prev-hash based on the first block we pull from the DB + if args.start != 0 && prev_hash == constants.genesis_challenge { + prev_hash = block.prev_header_hash(); + } + + // this is not a transaction block + assert_eq!( + block.prev_header_hash(), + prev_hash, + "at height {height} the previous header hash mismatches. {} expected {} from height {}", + block.prev_header_hash(), + prev_hash, + prev_height, + ); + assert_eq!( + block.height(), + height, + "at height {height} the height recorded in the block mismatches, {}", + block.height(), + ); + assert_eq!(height, (prev_height + 1) as u32, + "at height {height} the the block height did not increment by 1, from previous block (at height {prev_height})"); + prev_hash = block.header_hash(); + prev_height = height as i64; + if block.transactions_generator.is_none() { + return; + } + let mut removals_rows = select_spends + .query([height]) + .expect("failed to query spent coins"); + let mut removals = HashSet::<[u8; 32]>::new(); + while let Ok(Some(row)) = removals_rows.next() { + removals.insert(row.get::<_, [u8; 32]>(0).expect("missing coin_name")); + } + let mut additions_rows = select_created + .query([height]) + .expect("failed to query created coins"); + // coin-id -> (puzzle-hash, parent-coin, amount, reward) + let mut additions = HashMap::<[u8; 32], ([u8; 32], [u8; 32], u64, bool)>::new(); + while let Ok(Some(row)) = additions_rows.next() { + let coin_name = row.get::<_, [u8; 32]>(0).expect("missing coin_name"); + let reward = row.get::<_, bool>(1).expect("missing coinbase"); + let ph = row.get::<_, [u8; 32]>(2).expect("missing puzzle_hash"); + let parent = row.get::<_, [u8; 32]>(3).expect("missing parent"); + let amount = u64::from_be_bytes(row.get::<_, [u8; 8]>(4).expect("missing amount")); + additions.insert(coin_name, (ph, parent, amount, reward)); + } + pool.execute(move || { + let mut a = Allocator::new_limited(500_000_000); + + let ti = block.transactions_info.as_ref().expect("transactions_info"); + let generator = block + .transactions_generator + .as_ref() + .expect("transactions_generator"); + + // after the hard fork, we run blocks without paying for the + // CLVM generator ROM + let block_runner = if height >= constants.hard_fork_height { + run_block_generator2 + } else { + run_block_generator + }; + let flags = ALLOW_BACKREFS + | if args.skip_signature_validation { + DONT_VALIDATE_SIGNATURE + } else { + 0 + }; + let conditions = block_runner( + &mut a, + generator, + &block_refs, + ti.cost, + flags, + &ti.aggregated_signature, + None, + constants, + ) + .expect("failed to run block generator"); + + assert_eq!(conditions.cost, ti.cost); + + for spend in &conditions.spends { + let coin_name = *spend.coin_id; + assert!(removals.remove(coin_name.as_slice()), + "could not find coin {coin_name} in coin_record table, which is being spent at height {height}"); + for add in &spend.create_coin { + let new_coin_id = Coin::new(coin_name, add.puzzle_hash, add.amount).coin_id(); + let Some(new_db_coin) = additions.get(new_coin_id.as_slice()) else { + panic!("at height {height} the block created a coin {new_coin_id} that's not in the coin_record table"); + }; + assert_eq!(new_db_coin.0, add.puzzle_hash.as_slice()); + assert_eq!(new_db_coin.1, coin_name.as_slice()); + assert_eq!(new_db_coin.2, add.amount); + // this is not a reward coin + assert!(!new_db_coin.3, "at height {height}, the created coin {new_coin_id} is incorrectly marked as coin-base in the database"); + additions.remove(new_coin_id.as_slice()); + } + } + if !removals.is_empty() { + println!("at height {height} the coin_table has {} extra spends", removals.len()); + for coin_id in removals { + println!(" id: {}", hex::encode(coin_id)); + } + panic!(); + } + + // at this point, the only additions left from the DB should be + // rewards. Ensure that's the case + let rewards = block.get_included_reward_coins(); + for add in &rewards { + let new_coin_id = add.coin_id(); + let Some(new_db_coin) = additions.get(new_coin_id.as_slice()) else { + panic!("at height {height} the block created a coin {new_coin_id} that's not in the coin_record table"); + }; + assert_eq!(new_db_coin.0, add.puzzle_hash.as_slice(), + "at height {height} the coin {new_coin_id} has an incorrect puzzle hash in the coin_record table {} expected {}", + hex::encode(new_db_coin.0), + add.puzzle_hash + ); + // ensure the parent hash has the expected look + assert_eq!(new_db_coin.2, add.amount, "at height {height} reward coin {new_coin_id} has amount {} in coin_record table, but the block has amount {}", new_db_coin.2, add.amount); + // this is a reward coin + assert!(new_db_coin.3, "at height {height} the reward coin {new_coin_id} is not marked as coin-base in the database"); + additions.remove(new_coin_id.as_slice()); + } + if !additions.is_empty() { + println!("at height {height} the coin_table has {} extra coin additions", additions.len()); + for (coin_id, (ph, parent, amount, reward)) in additions { + println!(" id: {} - {} {} {amount} {}", + hex::encode(coin_id), + hex::encode(ph), + hex::encode(parent), + if reward { "(coinbase)" } else {""} + ); + } + panic!(); + } + }); + + assert_eq!(pool.panic_count(), 0); + if last_time.elapsed() > Duration::new(2, 0) { + let rate = f64::from(height - last_height) / last_time.elapsed().as_secs_f64(); + print!("\rheight: {height} ({rate:0.1} blocks/s) "); + let _ = std::io::stdout().flush(); + last_height = height; + last_time = Instant::now(); + } + }); + + pool.join(); + assert_eq!(pool.panic_count(), 0); + + println!("\nALL DONE, success!"); +} diff --git a/crates/chia-tools/src/visit_spends.rs b/crates/chia-tools/src/visit_spends.rs index 76cff174f..a2c4b6219 100644 --- a/crates/chia-tools/src/visit_spends.rs +++ b/crates/chia-tools/src/visit_spends.rs @@ -12,7 +12,7 @@ use clvmr::serde::{node_from_bytes, node_from_bytes_backrefs}; use clvmr::Allocator; use rusqlite::Connection; -pub fn iterate_tx_blocks( +pub fn iterate_blocks( db: &str, start_height: u32, max_height: Option, @@ -52,10 +52,8 @@ pub fn iterate_tx_blocks( let block = FullBlock::from_bytes_unchecked(&block_buffer).expect("failed to parse FullBlock"); - if block.transactions_info.is_none() { - continue; - } if block.transactions_generator.is_none() { + callback(height, block, vec![]); continue; } From a705f71bb525b77b7933eb1e6d34c82ea951a34a Mon Sep 17 00:00:00 2001 From: Arvid Norberg Date: Thu, 14 Nov 2024 15:01:40 +0100 Subject: [PATCH 2/2] address review comments --- .../src/bin/validate-blockchain-db.rs | 95 ++++++++++++------- 1 file changed, 62 insertions(+), 33 deletions(-) diff --git a/crates/chia-tools/src/bin/validate-blockchain-db.rs b/crates/chia-tools/src/bin/validate-blockchain-db.rs index a53eebf6d..10a4abfaf 100644 --- a/crates/chia-tools/src/bin/validate-blockchain-db.rs +++ b/crates/chia-tools/src/bin/validate-blockchain-db.rs @@ -141,7 +141,6 @@ features that are validated: prev_hash = block.prev_header_hash(); } - // this is not a transaction block assert_eq!( block.prev_header_hash(), prev_hash, @@ -160,15 +159,16 @@ features that are validated: "at height {height} the the block height did not increment by 1, from previous block (at height {prev_height})"); prev_hash = block.header_hash(); prev_height = height as i64; - if block.transactions_generator.is_none() { - return; - } - let mut removals_rows = select_spends - .query([height]) - .expect("failed to query spent coins"); let mut removals = HashSet::<[u8; 32]>::new(); - while let Ok(Some(row)) = removals_rows.next() { - removals.insert(row.get::<_, [u8; 32]>(0).expect("missing coin_name")); + // height 0 is not a transaction block so unspent coins have a + // spent_index of 0 to indicate that they have not been spent. + if height != 0 { + let mut removals_rows = select_spends + .query([height]) + .expect("failed to query spent coins"); + while let Ok(Some(row)) = removals_rows.next() { + removals.insert(row.get::<_, [u8; 32]>(0).expect("missing coin_name")); + } } let mut additions_rows = select_created .query([height]) @@ -183,6 +183,54 @@ features that are validated: let amount = u64::from_be_bytes(row.get::<_, [u8; 8]>(4).expect("missing amount")); additions.insert(coin_name, (ph, parent, amount, reward)); } + + // first ensure that the reward coins for this block are all included in + // the coin record table. + let rewards = block.get_included_reward_coins(); + for add in &rewards { + let new_coin_id = add.coin_id(); + let Some((ph, _parent, amount, coin_base)) = additions.get(new_coin_id.as_slice()) + else { + panic!("at height {height} the block created a reward coin {new_coin_id} that's not in the coin_record table"); + }; + // TODO: ensure the parent coin ID is set correctly + assert_eq!(ph, add.puzzle_hash.as_slice(), + "at height {height} the reward coin {new_coin_id} has an incorrect puzzle hash in the coin_record table {} expected {}", + hex::encode(ph), + add.puzzle_hash + ); + // ensure the parent hash has the expected look + assert_eq!(*amount, add.amount, "at height {height} reward coin {new_coin_id} has amount {} in coin_record table, but the block has amount {}", amount, add.amount); + // this is a reward coin + assert!(coin_base, "at height {height} the reward coin {new_coin_id} is not marked as coin-base in the database"); + additions.remove(new_coin_id.as_slice()); + } + if block.transactions_generator.is_none() { + // this is not a transaction block + // there should be no coins in the coin table spent at this height. + if !removals.is_empty() { + println!("block at height {height} is not a transaction block, but the coin_record table has coins spent at this block height"); + for coin_id in removals { + println!(" id: {}", hex::encode(coin_id)); + } + panic!(); + } + // there should not be any non-reward coins created in this block + if !additions.is_empty() { + println!("block at height {height} is not a transaction block, but the coin_record table has coins created at this block height"); + for (coin_id, (ph, parent, amount, reward)) in additions { + println!( + " id: {} - {} {} {amount} {}", + hex::encode(coin_id), + hex::encode(ph), + hex::encode(parent), + if reward { "(coinbase)" } else { "" } + ); + } + panic!(); + } + return; + } pool.execute(move || { let mut a = Allocator::new_limited(500_000_000); @@ -225,14 +273,14 @@ features that are validated: "could not find coin {coin_name} in coin_record table, which is being spent at height {height}"); for add in &spend.create_coin { let new_coin_id = Coin::new(coin_name, add.puzzle_hash, add.amount).coin_id(); - let Some(new_db_coin) = additions.get(new_coin_id.as_slice()) else { + let Some((ph, parent, amount, coin_base)) = additions.get(new_coin_id.as_slice()) else { panic!("at height {height} the block created a coin {new_coin_id} that's not in the coin_record table"); }; - assert_eq!(new_db_coin.0, add.puzzle_hash.as_slice()); - assert_eq!(new_db_coin.1, coin_name.as_slice()); - assert_eq!(new_db_coin.2, add.amount); + assert_eq!(ph, add.puzzle_hash.as_slice(), "at height {height} the spent coin with id {new_coin_id} has a mismatching puzzle hash"); + assert_eq!(parent, coin_name.as_slice(), "at height {height} the spent coin with id {new_coin_id} has a mismatching parent"); + assert_eq!(*amount, add.amount, "at height {height} the spent coin with id {new_coin_id} has a mismatching amount"); // this is not a reward coin - assert!(!new_db_coin.3, "at height {height}, the created coin {new_coin_id} is incorrectly marked as coin-base in the database"); + assert!(!coin_base, "at height {height}, the created coin {new_coin_id} is incorrectly marked as coin-base in the database"); additions.remove(new_coin_id.as_slice()); } } @@ -244,25 +292,6 @@ features that are validated: panic!(); } - // at this point, the only additions left from the DB should be - // rewards. Ensure that's the case - let rewards = block.get_included_reward_coins(); - for add in &rewards { - let new_coin_id = add.coin_id(); - let Some(new_db_coin) = additions.get(new_coin_id.as_slice()) else { - panic!("at height {height} the block created a coin {new_coin_id} that's not in the coin_record table"); - }; - assert_eq!(new_db_coin.0, add.puzzle_hash.as_slice(), - "at height {height} the coin {new_coin_id} has an incorrect puzzle hash in the coin_record table {} expected {}", - hex::encode(new_db_coin.0), - add.puzzle_hash - ); - // ensure the parent hash has the expected look - assert_eq!(new_db_coin.2, add.amount, "at height {height} reward coin {new_coin_id} has amount {} in coin_record table, but the block has amount {}", new_db_coin.2, add.amount); - // this is a reward coin - assert!(new_db_coin.3, "at height {height} the reward coin {new_coin_id} is not marked as coin-base in the database"); - additions.remove(new_coin_id.as_slice()); - } if !additions.is_empty() { println!("at height {height} the coin_table has {} extra coin additions", additions.len()); for (coin_id, (ph, parent, amount, reward)) in additions {