From 7ef0727f2a36f8be43872814b4f35b5f29896e33 Mon Sep 17 00:00:00 2001 From: jaoleal Date: Mon, 9 Mar 2026 23:27:50 -0300 Subject: [PATCH 1/5] test(chain): add test invalidate_block that raises validation_index bug Add test_invalidate_block_updates_validation_index that connects FullyValid blocks, then invalidates one and asserts that get_validation_index() still succeeds and points to a FullyValid block. This test fails without the fix in the next commit: invalidate_block leaves validation_index pointing at a now-InvalidChain block, causing get_validation_index() to return BadValidationIndex. --- .../src/pruned_utreexo/chain_state.rs | 88 +++++++++++++++++++ 1 file changed, 88 insertions(+) diff --git a/crates/floresta-chain/src/pruned_utreexo/chain_state.rs b/crates/floresta-chain/src/pruned_utreexo/chain_state.rs index 637258755..58630a425 100644 --- a/crates/floresta-chain/src/pruned_utreexo/chain_state.rs +++ b/crates/floresta-chain/src/pruned_utreexo/chain_state.rs @@ -1472,6 +1472,7 @@ mod test { use crate::pruned_utreexo::utxo_data::UtxoData; use crate::AssumeValidArg; use crate::BlockchainError; + use crate::ChainStore; use crate::FlatChainStore; fn setup_test_chain( @@ -1783,4 +1784,91 @@ mod test { headers[1].prev_blockhash ); } + + /// After connecting FullyValid blocks, invalidating one must roll back the + /// validation_index so that `get_validation_index()` still returns a valid + /// height. Before the fix, `invalidate_block` left `validation_index` + /// pointing at a now-InvalidChain block, causing `get_validation_index()` + /// to fail with `BadValidationIndex`. + #[test] + fn test_invalidateblock_updatesvalidationindex() { + let chain = setup_test_chain(Network::Regtest, AssumeValidArg::Hardcoded); + let json_blocks = include_str!("../../testdata/test_reorg.json"); + let blocks: Vec> = serde_json::from_str(json_blocks).unwrap(); + + // Connect the first 10 blocks (they become FullyValid) + let short_chain: Vec = blocks[0] + .iter() + .map(|s| deserialize_hex(s).unwrap()) + .collect(); + + for block in &short_chain { + chain.accept_header(block.header).unwrap(); + chain + .connect_block(block, Proof::default(), HashMap::new(), Vec::new()) + .unwrap(); + } + + let expected_height = 10; + + assert_eq!(chain.get_height().unwrap(), expected_height); + + // validation_index should be the same as height. + let val_before = chain.get_validation_index().unwrap(); + assert!(val_before == expected_height); + + // Invalidate block at height 5 + let hash_at_5 = chain.get_block_hash(5).unwrap(); + + chain.invalidate_block(hash_at_5).unwrap(); + + assert_eq!(chain.get_height().unwrap(), 4); + + // This is the critical check. Before the commit fixing + // the update of validation_index, this will break. Otherwise will pass fine. + let validation_index_after = chain.get_validation_index().unwrap(); + assert!(validation_index_after == 4); + + // The blocks before `hash_at_5`, 4..0 should be FullyValid. + + // check the tip: + let val_hash = read_lock!(chain).best_block.validation_index; + let val_header = chain.get_disk_block_header(&val_hash).unwrap(); + assert!( + matches!(val_header, DiskBlockHeader::FullyValid(..)), + "expected FullyValid, got: {val_header:?}" + ); + + // check the rest of the blocks. + for i in 0..=4 { + let val_hash = read_lock!(chain) + .chainstore + .get_block_hash(i) + .unwrap() + .unwrap(); + + let val_header = chain.get_disk_block_header(&val_hash).unwrap(); + + assert!( + matches!(val_header, DiskBlockHeader::FullyValid(..)), + "expected FullyValid, got: {val_header:?}" + ); + } + + // The blocks after `hash_at_5`, including `hash_at_5`, 5..10 should be InvalidChain. + for i in 5..10 { + let invalid_hash = read_lock!(chain) + .chainstore + .get_block_hash(i) + .unwrap() + .unwrap(); + + let invalid_header = chain.get_disk_block_header(&invalid_hash).unwrap(); + + assert!( + matches!(invalid_header, DiskBlockHeader::InvalidChain(..)), + "expected InvalidChain, got: {invalid_header:?}" + ); + } + } } From 7db383edfb5d29353324be6ac890c6f013c278ca Mon Sep 17 00:00:00 2001 From: jaoleal Date: Mon, 9 Mar 2026 23:30:18 -0300 Subject: [PATCH 2/5] fix(chain): update validation_index on invalidate_block invalidate_block marks blocks as InvalidChain but the validation_index was not being updated on update_tip(). Since InvalidChain headers lose their stored height, get_validation_index() would fail with BadValidationIndex when the p2p layer called it during its maintenance tick. --- .../src/pruned_utreexo/chain_state.rs | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/crates/floresta-chain/src/pruned_utreexo/chain_state.rs b/crates/floresta-chain/src/pruned_utreexo/chain_state.rs index 58630a425..74380ddd7 100644 --- a/crates/floresta-chain/src/pruned_utreexo/chain_state.rs +++ b/crates/floresta-chain/src/pruned_utreexo/chain_state.rs @@ -850,6 +850,7 @@ impl ChainState { fn update_tip(&self, best_block: BlockHash, height: u32) { let mut inner = write_lock!(self); + inner.best_block.validation_index = best_block; inner.best_block.best_block = best_block; inner.best_block.depth = height; } @@ -1201,23 +1202,23 @@ impl UpdatableChainstate for ChainState Result<(), BlockchainError> { - let height = self.get_disk_block_header(&block)?.try_height()?; + let height_to_invalidate = self.get_disk_block_header(&block)?.try_height()?; let current_height = self.get_height()?; // Mark all blocks after this one as invalid - for h in height..=current_height { + for h in height_to_invalidate..=current_height { let hash = self.get_block_hash(h)?; let header = self.get_block_header(&hash)?; let new_header = DiskBlockHeader::InvalidChain(header); self.update_header(&new_header)?; } - // Row back to our previous state. Note that acc doesn't actually change in this case + // Roll back to our previous state. Note that acc doesn't actually change in this case // only the currently best known block. - self.update_tip( - self.get_ancestor(&self.get_block_header(&block)?)? - .block_hash(), - height - 1, - ); + let new_tip = self + .get_ancestor(&self.get_block_header(&block)?)? + .block_hash(); + + self.update_tip(new_tip, height_to_invalidate - 1); Ok(()) } From fab18bb9df2accd5c71620d4863ed370c7a81bb9 Mon Sep 17 00:00:00 2001 From: jaoleal Date: Mon, 9 Mar 2026 23:36:42 -0300 Subject: [PATCH 3/5] feat(rpc): add invalidateblock and submitheader RPCs Add two new JSON-RPC methods: - invalidateblock: marks a block and all descendants as invalid, rolling back the chain tip to the parent block. - submitheader: decodes a hex-encoded 80-byte block header and submits it as a candidate chain tip via accept_header. Also: * Added RPC helpers in the Python test framework (floresta.py) * Functional tests for both RPCs (invalidateblock.py, submitheader.py) --- bin/floresta-cli/src/main.rs | 24 +++++ .../floresta-node/src/json_rpc/blockchain.rs | 21 +++++ crates/floresta-node/src/json_rpc/server.rs | 12 +++ crates/floresta-rpc/src/rpc.rs | 14 +++ doc/rpc/invalidateblock.md | 39 ++++++++ doc/rpc/submitheader.md | 40 +++++++++ tests/floresta-cli/invalidateblock.py | 68 ++++++++++++++ tests/floresta-cli/submitheader.py | 88 +++++++++++++++++++ tests/test_framework/rpc/floresta.py | 8 ++ tests/test_runner.py | 5 +- 10 files changed, 317 insertions(+), 2 deletions(-) create mode 100644 doc/rpc/invalidateblock.md create mode 100644 doc/rpc/submitheader.md create mode 100644 tests/floresta-cli/invalidateblock.py create mode 100644 tests/floresta-cli/submitheader.py diff --git a/bin/floresta-cli/src/main.rs b/bin/floresta-cli/src/main.rs index 2a77e6bdd..fb0f5c155 100644 --- a/bin/floresta-cli/src/main.rs +++ b/bin/floresta-cli/src/main.rs @@ -128,6 +128,12 @@ fn do_request(cmd: &Cli, client: Client) -> anyhow::Result { Methods::Uptime => serde_json::to_string_pretty(&client.uptime()?)?, Methods::ListDescriptors => serde_json::to_string_pretty(&client.list_descriptors()?)?, Methods::Ping => serde_json::to_string_pretty(&client.ping()?)?, + Methods::InvalidateBlock { blockhash } => { + serde_json::to_string_pretty(&client.invalidate_block(blockhash)?)? + } + Methods::SubmitHeader { hexdata } => { + serde_json::to_string_pretty(&client.submit_header(hexdata)?)? + } }) } @@ -373,4 +379,22 @@ pub enum Methods { /// Result: json null #[command(name = "ping")] Ping, + + #[doc = include_str!("../../../doc/rpc/invalidateblock.md")] + #[command( + name = "invalidateblock", + about = "Permanently marks a block as invalid, as if it violated a consensus rule.", + long_about = Some(include_str!("../../../doc/rpc/invalidateblock.md")), + disable_help_subcommand = true + )] + InvalidateBlock { blockhash: BlockHash }, + + #[doc = include_str!("../../../doc/rpc/submitheader.md")] + #[command( + name = "submitheader", + about = "Decodes the given hex data as a block header and submits it as a candidate chain tip if valid.", + long_about = Some(include_str!("../../../doc/rpc/submitheader.md")), + disable_help_subcommand = true + )] + SubmitHeader { hexdata: String }, } diff --git a/crates/floresta-node/src/json_rpc/blockchain.rs b/crates/floresta-node/src/json_rpc/blockchain.rs index a33e75484..f358912bf 100644 --- a/crates/floresta-node/src/json_rpc/blockchain.rs +++ b/crates/floresta-node/src/json_rpc/blockchain.rs @@ -4,6 +4,7 @@ use bitcoin::block::Header; use bitcoin::consensus::encode::serialize_hex; use bitcoin::consensus::Encodable; use bitcoin::constants::genesis_block; +use bitcoin::hashes::hex::FromHex; use bitcoin::hashes::Hash; use bitcoin::Address; use bitcoin::Block; @@ -312,6 +313,26 @@ impl RpcImpl { // getchainstates // getchaintips // getchaintxstats + + // invalidateblock + pub(super) fn invalidate_block(&self, hash: BlockHash) -> Result<(), JsonRpcError> { + self.chain + .invalidate_block(hash) + .map_err(|_| JsonRpcError::BlockNotFound) + } + + // submitheader + pub(super) fn submit_header(&self, hex: String) -> Result<(), JsonRpcError> { + let bytes = Vec::from_hex(&hex).map_err(|_| JsonRpcError::InvalidHex)?; + + let header: Header = bitcoin::consensus::deserialize(&bytes) + .map_err(|e| JsonRpcError::Decode(e.to_string()))?; + + self.chain + .accept_header(header) + .map_err(|_| JsonRpcError::Chain) + } + // getdeploymentinfo // getdifficulty // getmempoolancestors diff --git a/crates/floresta-node/src/json_rpc/server.rs b/crates/floresta-node/src/json_rpc/server.rs index 925a5af98..68d5bad81 100644 --- a/crates/floresta-node/src/json_rpc/server.rs +++ b/crates/floresta-node/src/json_rpc/server.rs @@ -315,6 +315,18 @@ async fn handle_json_rpc_request( "getroots" => state.get_roots().map(|v| serde_json::to_value(v).unwrap()), + "invalidateblock" => { + let hash = get_hash(¶ms, 0, "blockhash")?; + state.invalidate_block(hash)?; + Ok(Value::Null) + } + + "submitheader" => { + let hex = get_string(¶ms, 0, "hexdata")?; + state.submit_header(hex)?; + Ok(Value::Null) + } + "findtxout" => { let txid = get_hash(¶ms, 0, "txid")?; let vout = get_numeric(¶ms, 1, "vout")?; diff --git a/crates/floresta-rpc/src/rpc.rs b/crates/floresta-rpc/src/rpc.rs index 66df3d723..d03260373 100644 --- a/crates/floresta-rpc/src/rpc.rs +++ b/crates/floresta-rpc/src/rpc.rs @@ -144,6 +144,12 @@ pub trait FlorestaRPC { fn list_descriptors(&self) -> Result>; /// Sends a ping to all peers, checking if they are still alive fn ping(&self) -> Result<()>; + + #[doc = include_str!("../../../doc/rpc/invalidateblock.md")] + fn invalidate_block(&self, blockhash: BlockHash) -> Result<()>; + + #[doc = include_str!("../../../doc/rpc/submitheader.md")] + fn submit_header(&self, hexdata: String) -> Result<()>; } /// Since the workflow for jsonrpc is the same for all methods, we can implement a trait @@ -336,4 +342,12 @@ impl FlorestaRPC for T { fn ping(&self) -> Result<()> { self.call("ping", &[]) } + + fn invalidate_block(&self, blockhash: BlockHash) -> Result<()> { + self.call("invalidateblock", &[Value::String(blockhash.to_string())]) + } + + fn submit_header(&self, hexdata: String) -> Result<()> { + self.call("submitheader", &[Value::String(hexdata)]) + } } diff --git a/doc/rpc/invalidateblock.md b/doc/rpc/invalidateblock.md new file mode 100644 index 000000000..47768801e --- /dev/null +++ b/doc/rpc/invalidateblock.md @@ -0,0 +1,39 @@ +# `invalidateblock` + +Permanently marks a block as invalid, as if it violated a consensus rule. All descendants of the block are also marked invalid and the tip rolls back to the parent of the invalidated block. + +## Usage + +### Synopsis + +```bash +floresta-cli invalidateblock +``` + +### Examples + +```bash +# Invalidate a specific block +floresta-cli invalidateblock "00000000000000000002a7c4c1e48d76c5a37902165a270156b7a8d72f9a68c5" +``` + +## Arguments + +`blockhash` - (string, required) The hash of the block to mark as invalid, in hexadecimal format (64 characters). + +## Returns + +### Ok Response + +Returns `null` on success. + +### Error Enum `CommandError` + +* `JsonRpcError::BlockNotFound` - If the specified block hash is not found in the blockchain or the invalidation fails. + +## Notes + +- This is a hidden RPC in Bitcoin Core, intended for testing and debugging. +- The accumulator state is not modified; only the best known block and validation index are rolled back. +- All blocks from the invalidated height up to the current tip are marked as `InvalidChain`. +- There is currently no `reconsiderblock` equivalent to undo this operation. diff --git a/doc/rpc/submitheader.md b/doc/rpc/submitheader.md new file mode 100644 index 000000000..ae841deed --- /dev/null +++ b/doc/rpc/submitheader.md @@ -0,0 +1,40 @@ +# `submitheader` + +Decodes the given hex data as a block header and submits it as a candidate chain tip if valid. The header is accepted but the full block data is not required. + +## Usage + +### Synopsis + +```bash +floresta-cli submitheader +``` + +### Examples + +```bash +# Submit a raw 80-byte block header in hex (160 hex characters) +floresta-cli submitheader "0000002006226e46111a0b59caaf126043eb5bbf28c34f3a5e332a1fc7b2b73cf188910f..." +``` + +## Arguments + +`hexdata` - (string, required) The hex-encoded block header data (160 hex characters representing 80 bytes). + +## Returns + +### Ok Response + +Returns `null` on success. + +### Error Enum `CommandError` + +- `JsonRpcError::InvalidHex` - If the provided hex string is not valid hexadecimal. +- `JsonRpcError::Decode` - If the hex data cannot be deserialized as a block header. +- `JsonRpcError::Chain` - If the header is invalid (e.g., unknown previous block, invalid proof of work). + +## Notes + +- The header's `prev_blockhash` must reference a block already known to the node. +- On success the node's tip advances but the block is stored as `HeadersOnly`, itll become `FullyValid` after validating its transactions. +- This is useful for testing header-first sync scenarios without providing full block data. diff --git a/tests/floresta-cli/invalidateblock.py b/tests/floresta-cli/invalidateblock.py new file mode 100644 index 000000000..868bd67f4 --- /dev/null +++ b/tests/floresta-cli/invalidateblock.py @@ -0,0 +1,68 @@ +""" +invalidateblock.py + +Test the `invalidateblock` RPC on florestad. + +We mine blocks via utreexod, sync them to florestad, then call `invalidateblock` +on florestad to mark a block as invalid. We verify that florestad's tip rolls back +to the parent of the invalidated block. +""" + +import time + +from test_framework import FlorestaTestFramework +from test_framework.node import NodeType + + +class InvalidateBlockTest(FlorestaTestFramework): + """Test that invalidateblock marks a block invalid and rolls back the tip.""" + + def set_test_params(self): + self.florestad = self.add_node_default_args(variant=NodeType.FLORESTAD) + self.utreexod = self.add_node_extra_args( + variant=NodeType.UTREEXOD, + extra_args=[ + "--miningaddr=bcrt1q4gfcga7jfjmm02zpvrh4ttc5k7lmnq2re52z2y", + "--utreexoproofindex", + "--prune=0", + ], + ) + + def run_test(self): + self.run_node(self.florestad) + self.run_node(self.utreexod) + + # Mine 10 blocks on utreexod and sync to florestad + self.log("=== Mining 10 blocks with utreexod") + self.utreexod.rpc.generate(10) + + self.log("=== Connecting florestad to utreexod") + self.connect_nodes(self.florestad, self.utreexod) + + self.log("=== Waiting for sync") + time.sleep(20) + + # Verify both nodes are synced + floresta_info = self.florestad.rpc.get_blockchain_info() + utreexo_info = self.utreexod.rpc.get_blockchain_info() + self.assertEqual(floresta_info["height"], utreexo_info["blocks"]) + self.assertEqual(floresta_info["best_block"], utreexo_info["bestblockhash"]) + + # Get the hash of block 5 and its parent (block 4) + hash_at_5 = self.florestad.rpc.get_blockhash(5) + hash_at_4 = self.florestad.rpc.get_blockhash(4) + + # Invalidate block 5 on florestad + self.log(f"=== Invalidating block at height 5: {hash_at_5}") + self.florestad.rpc.invalidate_block(hash_at_5) + + # Verify florestad's tip rolled back to block 4 + new_info = self.florestad.rpc.get_blockchain_info() + self.assertEqual(new_info["height"], 4) + self.assertEqual(new_info["best_block"], hash_at_4) + + self.log("=== invalidateblock test passed") + + +if __name__ == "__main__": + InvalidateBlockTest().main() diff --git a/tests/floresta-cli/submitheader.py b/tests/floresta-cli/submitheader.py new file mode 100644 index 000000000..19701857c --- /dev/null +++ b/tests/floresta-cli/submitheader.py @@ -0,0 +1,88 @@ +""" +submitheader.py + +Test the `submitheader` RPC on florestad. + +We mine a block on bitcoind to obtain a valid header, then shut it down. +We start florestad in isolation and exercise submitheader: + - reject invalid hex + - reject truncated (non-80-byte) data + - reject a header whose prev_blockhash is unknown + - accept a valid header and advance the tip to height 1 + - accept a duplicate submission idempotently +""" + +from requests import HTTPError +from test_framework import FlorestaTestFramework +from test_framework.bitcoin import BlockHeader +from test_framework.node import NodeType + +REGTEST_GENESIS = "0f9188f13cb7b2c71f2a335e3a4fc328bf5beb436012afca590b1a11466e2206" + + +class SubmitHeaderTest(FlorestaTestFramework): + """Test that submitheader accepts valid headers and rejects invalid ones.""" + + def set_test_params(self): + self.florestad = self.add_node_default_args(variant=NodeType.FLORESTAD) + self.bitcoind = self.add_node_default_args(variant=NodeType.BITCOIND) + + def run_test(self): + # Start bitcoind, mine 1 block, grab its header + self.run_node(self.bitcoind) + + self.log("=== Mining 1 block with bitcoind") + self.bitcoind.rpc.generate_block(1) + + block_hash = self.bitcoind.rpc.get_blockhash(1) + raw_block_hex = self.bitcoind.rpc.get_block(block_hash, 0) + + # First 160 hex chars = 80-byte block header + header_hex = raw_block_hex[:160] + + self.log("=== Stopping bitcoind") + self.bitcoind.stop() + + # Start florestad in isolation + self.run_node(self.florestad) + + info_before = self.florestad.rpc.get_blockchain_info() + self.assertEqual(info_before["height"], 0) + + # --- Error case 1: invalid hex --- + self.log("=== Submitting invalid hex string") + with self.assertRaises(HTTPError): + self.florestad.rpc.submit_header("zzzz_not_hex") + + # --- Error case 2: valid hex but wrong length (not 80 bytes) --- + self.log("=== Submitting truncated header (too short)") + with self.assertRaises(HTTPError): + self.florestad.rpc.submit_header(header_hex[:80]) + + # --- Error case 3: 80-byte header with unknown prev_blockhash --- + self.log("=== Submitting header with unknown prev_blockhash") + header = BlockHeader.deserialize(bytes.fromhex(header_hex)) + header.prev_blockhash = "aa" * 32 # unknown parent + bad_parent_hex = header.serialize().hex() + with self.assertRaises(HTTPError): + self.florestad.rpc.submit_header(bad_parent_hex) + + # --- Happy path: valid header --- + self.log(f"=== Submitting valid header for block {block_hash}") + self.florestad.rpc.submit_header(header_hex) + + info_after = self.florestad.rpc.get_blockchain_info() + self.assertEqual(info_after["height"], 1) + self.assertEqual(info_after["best_block"], block_hash) + + # --- Submitting the same header again, should succeed. --- + self.log("=== Submitting the same header again (duplicate)") + self.florestad.rpc.submit_header(header_hex) + + info_dup = self.florestad.rpc.get_blockchain_info() + self.assertEqual(info_dup["height"], 1) + self.assertEqual(info_dup["best_block"], block_hash) + + +if __name__ == "__main__": + SubmitHeaderTest().main() diff --git a/tests/test_framework/rpc/floresta.py b/tests/test_framework/rpc/floresta.py index 5664ea91d..f64ebfe7f 100644 --- a/tests/test_framework/rpc/floresta.py +++ b/tests/test_framework/rpc/floresta.py @@ -26,6 +26,14 @@ def get_roots(self): """ return self.perform_request("getroots") + def invalidate_block(self, blockhash: str): + """Marks a block as invalid""" + return self.perform_request("invalidateblock", params=[blockhash]) + + def submit_header(self, hexdata: str): + """Submits a raw block header as a candidate chain tip""" + return self.perform_request("submitheader", params=[hexdata]) + def get_memoryinfo(self, mode: str): """ Returns stats about our memory usage performing diff --git a/tests/test_runner.py b/tests/test_runner.py index 6050e5445..14cb714bd 100644 --- a/tests/test_runner.py +++ b/tests/test_runner.py @@ -24,8 +24,8 @@ import os import subprocess from collections import defaultdict -from threading import Thread from queue import Queue +from threading import Thread from time import time from test_framework.util import Utility @@ -73,6 +73,8 @@ ("example", "bitcoin"), ("example", "utreexod"), ("florestad", "node-info"), + ("floresta-cli", "invalidateblock"), + ("floresta-cli", "submitheader"), ] # Before running the tests, we check if the number of tests @@ -208,7 +210,6 @@ def run_test_worker(task_queue: Queue, results_queue: Queue, args: argparse.Name with open( test_log_name, "wt", encoding="utf-8", buffering=args.log_buffer ) as log_file: - # Avoid using 'with' for `subprocess.Popen` here, as we need the # process to start and stream output immediately for port detection # to work correctly. Using 'with' might delay output flushing, From 010ba72ea226d2d6fb6e8b6ea4474d15b50e48ee Mon Sep 17 00:00:00 2001 From: jaoleal Date: Thu, 12 Mar 2026 17:38:27 -0300 Subject: [PATCH 4/5] test(invalidateblock): Extends invalidateblock.py with bug trigger case. This commit extend invalidateblock.py to exercise the scenario where florestad invalidates a block and, because `invalidateblock` does not trigger any acc update, florestad will reject blocks that, even if they extend the new tip, they do not extend the loaded acc. Florestad would eventualy catch correctly the new tip only if the node was restarted or if the chain acquires more pow, both cases triggers a reload of the acc. --- .../src/pruned_utreexo/chain_state.rs | 1 + tests/floresta-cli/invalidateblock.py | 31 ++++++++++++++++++- 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/crates/floresta-chain/src/pruned_utreexo/chain_state.rs b/crates/floresta-chain/src/pruned_utreexo/chain_state.rs index 74380ddd7..c10b9e53a 100644 --- a/crates/floresta-chain/src/pruned_utreexo/chain_state.rs +++ b/crates/floresta-chain/src/pruned_utreexo/chain_state.rs @@ -1214,6 +1214,7 @@ impl UpdatableChainstate for ChainState Date: Thu, 12 Mar 2026 19:03:27 -0300 Subject: [PATCH 5/5] fix!(invalidateblock): Updates acc while updating tip. The last commit states a case where floresta can be in a broken state, because invalidateblock was not updating the acc. This commit fix that by loading the accordingly acc for the new tip, when such is known. --- .../src/pruned_utreexo/chain_state.rs | 22 ++++++++++++++----- 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/crates/floresta-chain/src/pruned_utreexo/chain_state.rs b/crates/floresta-chain/src/pruned_utreexo/chain_state.rs index c10b9e53a..c3216c488 100644 --- a/crates/floresta-chain/src/pruned_utreexo/chain_state.rs +++ b/crates/floresta-chain/src/pruned_utreexo/chain_state.rs @@ -848,11 +848,20 @@ impl ChainState { Ok(()) } - fn update_tip(&self, best_block: BlockHash, height: u32) { + /// Updates the chain tip state to point at `best_block` at the given `height`. + /// + /// This sets both `best_block` and `validation_index` to the same hash, + /// so it should only be used when the caller knows the target block is + /// fully validated. If `acc` is provided, the in-memory accumulator is + /// also replaced. + fn update_tip(&self, best_block: BlockHash, height: u32, acc: Option) { let mut inner = write_lock!(self); inner.best_block.validation_index = best_block; inner.best_block.best_block = best_block; inner.best_block.depth = height; + if let Some(unwrapped_acc) = acc { + inner.acc = unwrapped_acc + } } fn verify_script(&self, height: u32) -> Result { @@ -1212,14 +1221,15 @@ impl UpdatableChainstate for ChainState