-
Notifications
You must be signed in to change notification settings - Fork 2
Treat reorged scripts-valid bodies as BIP22 duplicate #596
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -723,22 +723,19 @@ impl MiningCoordinator { | |
|
|
||
| /// Core `LookupBlockIndex` / BIP22 proposal vocabulary. | ||
| /// | ||
| /// A node on the applied chain has had its body connected (Core | ||
| /// `BLOCK_VALID_SCRIPTS`). `Invalid` is `BLOCK_FAILED_VALID`. Any other | ||
| /// tree entry, including a header-only `Active` tip, is still | ||
| /// inconclusive — `NodeStatus::Active` is the header chain, not scripts. | ||
| /// `Invalid` is `BLOCK_FAILED_VALID`. A non-zero `chain_tx_count` is set | ||
| /// only after a successful apply (`record_applied_tx_count`) and survives | ||
| /// disconnect, matching Core `IsValid(BLOCK_VALID_SCRIPTS)` including | ||
| /// reorged bodies. Header-only entries stay 0 and are inconclusive — | ||
| /// `NodeStatus::Active` and `Stale` are the header chain, not scripts. | ||
| fn known_block_result(&self, block_hash: Hash256) -> Option<BlockValidationResult> { | ||
| let tree = self.block_tree.read(); | ||
| let node_id = tree.lookup(block_hash)?; | ||
| let node = tree.node(node_id).ok()?; | ||
| if node.status == NodeStatus::Invalid { | ||
| return Some(BlockValidationResult::DuplicateInvalid); | ||
| } | ||
| let on_applied = self | ||
| .applied_tip | ||
| .load_full() | ||
| .is_some_and(|tip| tree.node_at_height_from(tip.tip_id, node.height) == Some(node_id)); | ||
| if on_applied { | ||
| if node.chain_tx_count != 0 { | ||
| return Some(BlockValidationResult::Duplicate); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 3. Restart loses duplicate state Checkpoint restore assigns chain_tx_count only to the applied tip, leaving already-applied ancestors at zero, so known_block_result returns DuplicateInconclusive for them after restart. submitblock then bypasses the duplicate fast path and attempts to process an already-applied ancestor instead of returning duplicate. Agent Prompt
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 4. Pruned bodies report duplicate A nonzero chain_tx_count records historical validation but does not prove that the body remains stored, because pruning deletes bodies without clearing the count. A stale previously-applied block whose body was pruned is therefore reported as duplicate and its submitted body is discarded instead of being treated as new storage. Agent Prompt
|
||
| } | ||
| Some(BlockValidationResult::DuplicateInconclusive) | ||
|
|
@@ -781,9 +778,10 @@ impl MiningCoordinator { | |
|
|
||
| fn submit(&self, block: &Block) -> Result<BlockValidationResult, MiningControlError> { | ||
| let block_hash: Hash256 = block.block_hash().into(); | ||
| // Core v31 `submitblock` dropped the index pre-check. `ProcessNewBlock` | ||
| // returns `duplicate` only when the block was already accepted | ||
| // (`!new_block && accepted`). A header-only tree entry must still | ||
| // Core v31 `submitblock` dropped the hash pre-check. `ProcessNewBlock` | ||
| // still returns `duplicate` when the body is already stored | ||
| // (`!new_block`). Scripts-valid (`chain_tx_count != 0`), including a | ||
| // later reorg, is already stored. A header-only tree entry must still | ||
| // receive the body so `submitheader` then `submitblock` works. | ||
| if matches!( | ||
| self.known_block_result(block_hash), | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -1259,6 +1259,77 @@ fn proposal_of_a_header_only_block_is_duplicate_inconclusive() -> anyhow::Result | |
| Ok(()) | ||
| } | ||
|
|
||
| fn disconnect_applied(state: &NodeState, block: &Block) -> anyhow::Result<()> { | ||
| state | ||
| .chain_followers() | ||
| .apply_disconnect(&state.apply_handles(), block) | ||
| .map(|_| ()) | ||
| .map_err(|error| anyhow::anyhow!("{error}")) | ||
| } | ||
|
|
||
| #[test] | ||
| fn proposal_of_a_disconnected_scripts_valid_block_is_duplicate() -> anyhow::Result<()> { | ||
|
Comment on lines
+1270
to
+1272
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 2. Tests omit api-21 annotation The two new permanent compatibility tests do not identify API-21, BIP22, or another named contract in their names or adjacent annotations. Although the contract catalog maps them externally, the tests themselves do not exercise an explicitly documented contract as required. Agent Prompt
|
||
| let state = open_regtest()?; | ||
| apply_genesis(&state)?; | ||
| let mining = coordinator(&state); | ||
| mining.publish_generation(); | ||
| let genesis = Network::Regtest.genesis_block(); | ||
| let child = mined_child(genesis.block_hash())?; | ||
| let child_hash = Hash256::from(child.block_hash()); | ||
| assert_eq!( | ||
| mining.submit_block(child.clone())?, | ||
| BlockValidationResult::Accepted | ||
| ); | ||
| disconnect_applied(&state, &child)?; | ||
| let chain_tx_count = { | ||
| let tree = state.block_tree(); | ||
| tree.read() | ||
| .node_by_hash(child_hash) | ||
| .ok_or_else(|| anyhow::anyhow!("disconnected child missing from tree"))? | ||
| .chain_tx_count | ||
| }; | ||
| assert_ne!( | ||
| chain_tx_count, 0, | ||
| "disconnect must keep the scripts-valid chain_tx_count" | ||
| ); | ||
| let tip = state | ||
| .applied_tip() | ||
| .load_full() | ||
| .unwrap_or_else(|| panic!("applied tip missing after disconnect")); | ||
| assert_eq!(tip.hash, Hash256::from(genesis.block_hash())); | ||
| assert_eq!( | ||
| propose_block(&mining, child)?, | ||
| BlockValidationResult::Duplicate | ||
| ); | ||
| Ok(()) | ||
| } | ||
|
|
||
| #[test] | ||
| fn submit_of_a_disconnected_scripts_valid_block_is_duplicate() -> anyhow::Result<()> { | ||
| let state = open_regtest()?; | ||
| apply_genesis(&state)?; | ||
| let mining = coordinator(&state); | ||
| mining.publish_generation(); | ||
| let genesis = Network::Regtest.genesis_block(); | ||
| let genesis_hash = Hash256::from(genesis.block_hash()); | ||
| let child = mined_child(genesis.block_hash())?; | ||
| assert_eq!( | ||
| mining.submit_block(child.clone())?, | ||
| BlockValidationResult::Accepted | ||
| ); | ||
| disconnect_applied(&state, &child)?; | ||
| assert_eq!( | ||
| mining.submit_block(child)?, | ||
| BlockValidationResult::Duplicate | ||
| ); | ||
| let tip = state | ||
| .applied_tip() | ||
| .load_full() | ||
| .unwrap_or_else(|| panic!("applied tip missing after duplicate submit")); | ||
| assert_eq!(tip.hash, genesis_hash); | ||
| Ok(()) | ||
| } | ||
|
|
||
| #[test] | ||
| fn submit_block_applies_a_header_already_in_the_tree() -> anyhow::Result<()> { | ||
| let state = open_regtest()?; | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
1. Comment duplicates api-21 rule
📘 Rule violation⚙ MaintainabilityAgent Prompt
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools