feat: add LMDB unit tests for BlockchainBackend read methods#7741
feat: add LMDB unit tests for BlockchainBackend read methods#77410xPepeSilvia wants to merge 1 commit intotari-project:developmentfrom
Conversation
|
If you're new to commit signing, there are different ways to set it up: Sign commits with
|
There was a problem hiding this comment.
Code Review
This pull request introduces a new suite of LMDB unit tests to verify BlockchainDatabase read methods under conditions involving forks and chain reorganizations. The reviewer identified several areas for improvement: the apply_mmr_to_block helper is missing the block_output_mr field, the test suite efficiency could be improved by reducing redundant calls to build_test_chain, the assertion in the commitment lookup test is too weak, and the spend state test should explicitly verify that outputs are marked as unspent.
| block.header.input_mr = mmr_roots.input_mr; | ||
| block.header.output_mr = mmr_roots.output_mr; | ||
| block.header.output_smt_size = mmr_roots.output_smt_size; | ||
| block.header.kernel_mr = mmr_roots.kernel_mr; | ||
| block.header.kernel_mmr_size = mmr_roots.kernel_mmr_size; | ||
| block.header.validator_node_mr = mmr_roots.validator_node_mr; | ||
| block.header.validator_node_size = mmr_roots.validator_node_size; |
There was a problem hiding this comment.
The apply_mmr_to_block helper function is missing the assignment of block_output_mr from the calculated MMR roots. While the current tests use a mock validator that might not check this field, it should be included to ensure the block header is correctly populated and consistent with the chain state.
| block.header.input_mr = mmr_roots.input_mr; | |
| block.header.output_mr = mmr_roots.output_mr; | |
| block.header.output_smt_size = mmr_roots.output_smt_size; | |
| block.header.kernel_mr = mmr_roots.kernel_mr; | |
| block.header.kernel_mmr_size = mmr_roots.kernel_mmr_size; | |
| block.header.validator_node_mr = mmr_roots.validator_node_mr; | |
| block.header.validator_node_size = mmr_roots.validator_node_size; | |
| block.header.input_mr = mmr_roots.input_mr; | |
| block.header.output_mr = mmr_roots.output_mr; | |
| block.header.block_output_mr = mmr_roots.block_output_mr; | |
| block.header.output_smt_size = mmr_roots.output_smt_size; | |
| block.header.kernel_mr = mmr_roots.kernel_mr; | |
| block.header.kernel_mmr_size = mmr_roots.kernel_mmr_size; | |
| block.header.validator_node_mr = mmr_roots.validator_node_mr; | |
| block.header.validator_node_size = mmr_roots.validator_node_size; |
| // Coinbase outputs that haven't been spent should be findable. | ||
| // Some outputs may have been spent if they were inputs to later blocks, | ||
| // so we only assert that unspent ones return the correct hash. | ||
| if let Some(found_hash) = result { | ||
| assert_eq!( | ||
| found_hash, | ||
| output.hash(), | ||
| "Commitment lookup returned wrong output hash for block {}", | ||
| block.header.height | ||
| ); | ||
| } |
There was a problem hiding this comment.
The assertion in fetch_unspent_output_hash_by_commitment_finds_canonical_outputs is too weak. Since the test chain construction does not include any transactions that spend these outputs, they should all be present in the UTXO set. Using if let Some(...) allows the test to pass silently if an output is missing. It should explicitly assert that the output is found. Note: In internal functions, using expect is acceptable for conditions that should be guaranteed by the calling code's logic.
| // Coinbase outputs that haven't been spent should be findable. | |
| // Some outputs may have been spent if they were inputs to later blocks, | |
| // so we only assert that unspent ones return the correct hash. | |
| if let Some(found_hash) = result { | |
| assert_eq!( | |
| found_hash, | |
| output.hash(), | |
| "Commitment lookup returned wrong output hash for block {}", | |
| block.header.height | |
| ); | |
| } | |
| // Coinbase outputs that haven't been spent should be findable. | |
| let found_hash = result.expect(&format!( | |
| "Commitment for output in block {} should be found as unspent", | |
| block.header.height | |
| )); | |
| assert_eq!( | |
| found_hash, | |
| output.hash(), | |
| "Commitment lookup returned wrong output hash for block {}", | |
| block.header.height | |
| ); |
References
- In internal functions, using
expectis acceptable for conditions that should be guaranteed by the calling code's logic, as a panic indicates a programming error (broken invariant) rather than a recoverable error.
| outputs_with_state.len(), | ||
| block.body.outputs().len(), | ||
| "Should return all outputs from block at height {}", | ||
| block.header.height | ||
| ); | ||
| } |
There was a problem hiding this comment.
This test verifies the number of outputs returned when no spend header is provided, but it doesn't check the actual spend state. When spend_status_at_header is None, all outputs should be marked as unspent (false). Adding this check would make the test more robust.
| outputs_with_state.len(), | |
| block.body.outputs().len(), | |
| "Should return all outputs from block at height {}", | |
| block.header.height | |
| ); | |
| } | |
| assert_eq!( | |
| outputs_with_state.len(), | |
| block.body.outputs().len(), | |
| "Should return all outputs from block at height {}", | |
| block.header.height | |
| ); | |
| for (output, is_spent) in &outputs_with_state { | |
| assert!( | |
| !is_spent, | |
| "Output {} in block at height {} should be marked as unspent when no spend header is provided", | |
| output.hash(), | |
| block.header.height | |
| ); | |
| } |
7d14630 to
337e113
Compare
Create comprehensive unit tests that exercise LMDB read operations against
a realistic blockchain containing a fork and reorg.
Test chain topology:
Genesis -> B1..B5 -> B6..B10 (original)
\-> F6'..F15' (fork, triggers reorg)
After reorg: canonical = Genesis + B1..B5 + F6'..F15'
Reorged blocks: B6..B10
Tests cover:
- Chain construction and reorg verification
- JSON serialization for reproducibility
- fetch_outputs_in_block / fetch_output / fetch_unspent_output_hash_by_commitment
- fetch_inputs_in_block
- fetch_kernels_in_block / fetch_kernel_by_excess_sig
- fetch_header_containing_kernel_mmr
- fetch_outputs_in_block_with_spend_state
- fetch_mined_info_by_payref
22 test functions organized in write_tests and read_tests modules.
Fixes tari-project#7715
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
337e113 to
c4148b9
Compare
SWvheerden
left a comment
There was a problem hiding this comment.
these tests dont actually test anything, we need to test against a static generated json data and lmdb files. And we need to test against the actual lmdb file, not what we think is in them.
| } | ||
|
|
||
| #[derive(Debug, Serialize, Deserialize)] | ||
| struct SerializableBlock { |
There was a problem hiding this comment.
we dont need this, blocks already impl Serialize, Deserialize
| block.header.input_mr = mmr_roots.input_mr; | ||
| block.header.output_mr = mmr_roots.output_mr; | ||
| block.header.output_smt_size = mmr_roots.output_smt_size; | ||
| block.header.kernel_mr = mmr_roots.kernel_mr; | ||
| block.header.kernel_mmr_size = mmr_roots.kernel_mmr_size; | ||
| block.header.validator_node_mr = mmr_roots.validator_node_mr; | ||
| block.header.validator_node_size = mmr_roots.validator_node_size; |
| assert_eq!(chain.canonical_blocks.len(), 16); | ||
|
|
||
| // The reorged blocks should be the 5 original blocks that were replaced. | ||
| assert_eq!(chain.reorged_blocks.len(), 5); | ||
|
|
||
| // Verify heights of canonical blocks. | ||
| for (i, block) in chain.canonical_blocks.iter().enumerate() { | ||
| assert_eq!( | ||
| block.header.height, i as u64, | ||
| "Canonical block at index {} should have height {}", | ||
| i, i | ||
| ); | ||
| } | ||
|
|
||
| // Verify reorged blocks had heights 6..10. | ||
| for (i, block) in chain.reorged_blocks.iter().enumerate() { | ||
| assert_eq!( | ||
| block.header.height, | ||
| (i + 6) as u64, | ||
| "Reorged block at index {} should have height {}", | ||
| i, | ||
| i + 6 | ||
| ); | ||
| } | ||
|
|
||
| // Verify the tip header matches the last canonical block. | ||
| let tip = chain.db.fetch_tip_header().unwrap(); |
There was a problem hiding this comment.
all of these should test agains the alctual lmdb, not the memory of what we think is in the lmdb, this defeats the purpose of having to add lmdb tests
| let json = serialize_chain_to_json(&chain); | ||
|
|
||
| // Basic sanity: the JSON should be parseable and contain the expected number of blocks. | ||
| let snapshot: TestChainSnapshot = serde_json::from_str(&json).expect("Failed to parse chain JSON"); |
There was a problem hiding this comment.
this just tests if we can make a json file of the test data structs, this does not test anything.
we need to test the actual lmdb files
Summary
BlockchainBackendread methods against a realistic chain with a fork and reorgfetch_outputs_in_block,fetch_output,fetch_unspent_output_hash_by_commitment,fetch_inputs_in_block,fetch_kernels_in_block,fetch_kernel_by_excess_sig,fetch_header_containing_kernel_mmr,fetch_outputs_in_block_with_spend_state,fetch_mined_info_by_payrefChain topology
Test plan
cargo test --package tari_core lmdb_unit_testswrite_tests)Fixes #7715
🤖 Generated with Claude Code