Skip to content

Commit 83654b4

Browse files
committed
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)
1 parent 4176f05 commit 83654b4

6 files changed

Lines changed: 174 additions & 2 deletions

File tree

crates/floresta-node/src/json_rpc/blockchain.rs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ use bitcoin::block::Header;
22
use bitcoin::consensus::encode::serialize_hex;
33
use bitcoin::consensus::Encodable;
44
use bitcoin::constants::genesis_block;
5+
use bitcoin::hashes::hex::FromHex;
56
use bitcoin::hashes::Hash;
67
use bitcoin::Address;
78
use bitcoin::Block;
@@ -310,6 +311,26 @@ impl<Blockchain: RpcChain> RpcImpl<Blockchain> {
310311
// getchainstates
311312
// getchaintips
312313
// getchaintxstats
314+
315+
// invalidateblock
316+
pub(super) fn invalidate_block(&self, hash: BlockHash) -> Result<(), JsonRpcError> {
317+
self.chain
318+
.invalidate_block(hash)
319+
.map_err(|_| JsonRpcError::BlockNotFound)
320+
}
321+
322+
// submitheader
323+
pub(super) fn submit_header(&self, hex: String) -> Result<(), JsonRpcError> {
324+
let bytes = Vec::from_hex(&hex).map_err(|_| JsonRpcError::InvalidHex)?;
325+
326+
let header: Header = bitcoin::consensus::deserialize(&bytes)
327+
.map_err(|e| JsonRpcError::Decode(e.to_string()))?;
328+
329+
self.chain
330+
.accept_header(header)
331+
.map_err(|_| JsonRpcError::Chain)
332+
}
333+
313334
// getdeploymentinfo
314335
// getdifficulty
315336
// getmempoolancestors

crates/floresta-node/src/json_rpc/server.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -313,6 +313,18 @@ async fn handle_json_rpc_request(
313313

314314
"getroots" => state.get_roots().map(|v| serde_json::to_value(v).unwrap()),
315315

316+
"invalidateblock" => {
317+
let hash = get_hash(&params, 0, "blockhash")?;
318+
state.invalidate_block(hash)?;
319+
Ok(Value::Null)
320+
}
321+
322+
"submitheader" => {
323+
let hex = get_string(&params, 0, "hexdata")?;
324+
state.submit_header(hex)?;
325+
Ok(Value::Null)
326+
}
327+
316328
"findtxout" => {
317329
let txid = get_hash(&params, 0, "txid")?;
318330
let vout = get_numeric(&params, 1, "vout")?;
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
"""
2+
invalidateblock.py
3+
4+
Test the `invalidateblock` RPC on florestad.
5+
6+
We mine blocks via utreexod, sync them to florestad, then call `invalidateblock`
7+
on florestad to mark a block as invalid. We verify that florestad's tip rolls back
8+
to the parent of the invalidated block.
9+
"""
10+
11+
import time
12+
13+
from test_framework import FlorestaTestFramework
14+
from test_framework.node import NodeType
15+
16+
17+
class InvalidateBlockTest(FlorestaTestFramework):
18+
"""Test that invalidateblock marks a block invalid and rolls back the tip."""
19+
20+
def set_test_params(self):
21+
self.florestad = self.add_node_default_args(variant=NodeType.FLORESTAD)
22+
self.utreexod = self.add_node_extra_args(
23+
variant=NodeType.UTREEXOD,
24+
extra_args=[
25+
"--miningaddr=bcrt1q4gfcga7jfjmm02zpvrh4ttc5k7lmnq2re52z2y",
26+
"--utreexoproofindex",
27+
"--prune=0",
28+
],
29+
)
30+
31+
def run_test(self):
32+
self.run_node(self.florestad)
33+
self.run_node(self.utreexod)
34+
35+
# Mine 10 blocks on utreexod and sync to florestad
36+
self.log("=== Mining 10 blocks with utreexod")
37+
self.utreexod.rpc.generate(10)
38+
39+
self.log("=== Connecting florestad to utreexod")
40+
self.connect_nodes(self.florestad, self.utreexod)
41+
42+
self.log("=== Waiting for sync")
43+
time.sleep(20)
44+
45+
# Verify both nodes are synced
46+
floresta_info = self.florestad.rpc.get_blockchain_info()
47+
utreexo_info = self.utreexod.rpc.get_blockchain_info()
48+
self.assertEqual(floresta_info["height"], utreexo_info["blocks"])
49+
self.assertEqual(floresta_info["best_block"], utreexo_info["bestblockhash"])
50+
51+
# Get the hash of block 5 and its parent (block 4)
52+
hash_at_5 = self.florestad.rpc.get_blockhash(5)
53+
hash_at_4 = self.florestad.rpc.get_blockhash(4)
54+
55+
# Invalidate block 5 on florestad
56+
self.log(f"=== Invalidating block at height 5: {hash_at_5}")
57+
self.florestad.rpc.invalidate_block(hash_at_5)
58+
59+
# Verify florestad's tip rolled back to block 4
60+
new_info = self.florestad.rpc.get_blockchain_info()
61+
self.assertEqual(new_info["height"], 4)
62+
self.assertEqual(new_info["best_block"], hash_at_4)
63+
64+
self.log("=== invalidateblock test passed")
65+
66+
67+
if __name__ == "__main__":
68+
InvalidateBlockTest().main()

tests/floresta-cli/submitheader.py

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
"""
2+
submitheader.py
3+
4+
Test the `submitheader` RPC on florestad.
5+
6+
We mine a block on utreexod to obtain a valid header, then shut it down.
7+
We start florestad in isolation and submit the header via `submitheader`.
8+
Florestad should accept the header and advance its tip to height 1.
9+
"""
10+
11+
from test_framework import FlorestaTestFramework
12+
from test_framework.node import NodeType
13+
14+
15+
class SubmitHeaderTest(FlorestaTestFramework):
16+
"""Test that submitheader accepts a valid block header."""
17+
18+
def set_test_params(self):
19+
self.florestad = self.add_node_default_args(variant=NodeType.FLORESTAD)
20+
self.utreexod = self.add_node_extra_args(
21+
variant=NodeType.UTREEXOD,
22+
extra_args=[
23+
"--miningaddr=bcrt1q4gfcga7jfjmm02zpvrh4ttc5k7lmnq2re52z2y",
24+
"--prune=0",
25+
],
26+
)
27+
28+
def run_test(self):
29+
# Start utreexod, mine 1 block, grab its header
30+
self.run_node(self.utreexod)
31+
32+
self.log("=== Mining 1 block with utreexod")
33+
self.utreexod.rpc.generate(1)
34+
35+
block_hash = self.utreexod.rpc.get_blockhash(1)
36+
raw_block_hex = self.utreexod.rpc.get_block(block_hash, 0)
37+
38+
# First 160 hex chars = 80-byte block header
39+
header_hex = raw_block_hex[:160]
40+
41+
self.log("=== Stopping utreexod")
42+
self.utreexod.stop()
43+
44+
# Start florestad in isolation and submit the header
45+
self.run_node(self.florestad)
46+
47+
info_before = self.florestad.rpc.get_blockchain_info()
48+
self.assertEqual(info_before["height"], 0)
49+
50+
self.log(f"=== Submitting header for block {block_hash}")
51+
self.florestad.rpc.submit_header(header_hex)
52+
53+
# Verify florestad accepted the header and advanced to height 1
54+
info_after = self.florestad.rpc.get_blockchain_info()
55+
self.assertEqual(info_after["height"], 1)
56+
self.assertEqual(info_after["best_block"], block_hash)
57+
58+
self.log("=== submitheader test passed")
59+
60+
61+
if __name__ == "__main__":
62+
SubmitHeaderTest().main()

tests/test_framework/rpc/floresta.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,14 @@ def get_roots(self):
2424
"""
2525
return self.perform_request("getroots")
2626

27+
def invalidate_block(self, blockhash: str):
28+
"""Marks a block as invalid"""
29+
return self.perform_request("invalidateblock", params=[blockhash])
30+
31+
def submit_header(self, hexdata: str):
32+
"""Submits a raw block header as a candidate chain tip"""
33+
return self.perform_request("submitheader", params=[hexdata])
34+
2735
def get_memoryinfo(self, mode: str):
2836
"""
2937
Returns stats about our memory usage performing

tests/test_runner.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,8 @@
2222
import os
2323
import subprocess
2424
from collections import defaultdict
25-
from threading import Thread
2625
from queue import Queue
26+
from threading import Thread
2727
from time import time
2828

2929
from test_framework.util import Utility
@@ -71,6 +71,8 @@
7171
("example", "bitcoin"),
7272
("example", "utreexod"),
7373
("florestad", "node-info"),
74+
("floresta-cli", "invalidateblock"),
75+
("floresta-cli", "submitheader"),
7476
]
7577

7678
# Before running the tests, we check if the number of tests
@@ -206,7 +208,6 @@ def run_test_worker(task_queue: Queue, results_queue: Queue, args: argparse.Name
206208
with open(
207209
test_log_name, "wt", encoding="utf-8", buffering=args.log_buffer
208210
) as log_file:
209-
210211
# Avoid using 'with' for `subprocess.Popen` here, as we need the
211212
# process to start and stream output immediately for port detection
212213
# to work correctly. Using 'with' might delay output flushing,

0 commit comments

Comments
 (0)