Changeset View
Standalone View
test/functional/abc-finalize-block.py
#!/usr/bin/env python3 | #!/usr/bin/env python3 | ||||
# Copyright (c) 2018 The Bitcoin developers | # Copyright (c) 2018 The Bitcoin developers | ||||
# Distributed under the MIT software license, see the accompanying | # Distributed under the MIT software license, see the accompanying | ||||
# file COPYING or http://www.opensource.org/licenses/mit-license.php. | # file COPYING or http://www.opensource.org/licenses/mit-license.php. | ||||
"""Test the finalizeblock RPC calls.""" | """Test the finalizeblock RPC calls.""" | ||||
import os | import os | ||||
from test_framework.test_framework import BitcoinTestFramework | from test_framework.test_framework import BitcoinTestFramework | ||||
from test_framework.util import assert_equal, assert_raises_rpc_error, connect_nodes_bi, sync_blocks, wait_until | from test_framework.util import assert_equal, assert_raises_rpc_error, connect_nodes_bi, sync_blocks, wait_until | ||||
RPC_FINALIZE_INVALID_BLOCK_ERROR = 'finalize-invalid-block' | RPC_FINALIZE_INVALID_BLOCK_ERROR = 'finalize-invalid-block' | ||||
RPC_BLOCK_NOT_FOUND_ERROR = 'Block not found' | |||||
AUTO_FINALIZATION_DEPTH = 10 | AUTO_FINALIZATION_DEPTH = 10 | ||||
class FinalizeBlockTest(BitcoinTestFramework): | class FinalizeBlockTest(BitcoinTestFramework): | ||||
def set_test_params(self): | def set_test_params(self): | ||||
self.num_nodes = 2 | self.num_nodes = 2 | ||||
self.extra_flags = [["-maxreorgdepth={}".format(AUTO_FINALIZATION_DEPTH)], [ | self.extra_flags = [["-maxreorgdepth={}".format(AUTO_FINALIZATION_DEPTH)], [ | ||||
"-maxreorgdepth={}".format(AUTO_FINALIZATION_DEPTH)]] | "-maxreorgdepth={}".format(AUTO_FINALIZATION_DEPTH)]] | ||||
Show All 11 Lines | class FinalizeBlockTest(BitcoinTestFramework): | ||||
def run_test(self): | def run_test(self): | ||||
node = self.nodes[0] | node = self.nodes[0] | ||||
self.log.info("Test block finalization...") | self.log.info("Test block finalization...") | ||||
node.generate(10) | node.generate(10) | ||||
tip = node.getbestblockhash() | tip = node.getbestblockhash() | ||||
node.finalizeblock(tip) | node.finalizeblock(tip) | ||||
assert_equal(node.getbestblockhash(), tip) | assert_equal(node.getbestblockhash(), tip) | ||||
assert_equal(node.getfinalizedblockhash(), tip) | |||||
alt_node = self.nodes[1] | alt_node = self.nodes[1] | ||||
connect_nodes_bi(self.nodes, 0, 1) | connect_nodes_bi(self.nodes, 0, 1) | ||||
sync_blocks(self.nodes[0:2]) | sync_blocks(self.nodes[0:2]) | ||||
alt_node.invalidateblock(tip) | alt_node.invalidateblock(tip) | ||||
# We will use this later to check auto-finalization during a reorg | # We will use this later to check auto-finalization during a reorg | ||||
auto_finalized_tip = alt_node.getbestblockhash() | auto_finalized_tip = alt_node.getbestblockhash() | ||||
alt_node.generate(10) | |||||
# Wait for node 0 to invalidate the chain. | # Node 0 should not accept the whole alt_node's chain due to tip being finalized, | ||||
def wait_for_invalid_block(node, block): | # even though it is longer. | ||||
# Headers would not be accepted if previousblock is invalid: | |||||
# - First block from alt node has same height than node tip, it will be accepted but inactive | |||||
# - Second block from alt node has height > node tip height, will be marked as invalid because | |||||
# node tip is finalized | |||||
# - Later blocks from alt node will be rejected because their previous block are invalid | |||||
# | |||||
# Expected state: | |||||
# | |||||
# On alt_node: | |||||
# >(210)->(211)-> // ->(218 tip) | |||||
# / | |||||
# (200)->(201)-> // ->(209)->(210 invalid) | |||||
# | |||||
# On node: | |||||
# >(210 valid-headers)->(211 invalid)->(212 to 218 dropped) | |||||
jasonbcox: It seems strange to me that the ordering of finalizing a particular block changes whether block… | |||||
deadalnixUnsubmitted Not Done Inline ActionsThere is really no point tracking down every single orphan that ever existed since the beginning of time. It only matter that we do so for chain that actually have more work. deadalnix: There is really no point tracking down every single orphan that ever existed since the… | |||||
# / | |||||
# (200)->(201)-> // ->(209)->(210 finalized, tip) | |||||
def wait_for_block(node, block, status="invalid"): | |||||
def check_block(): | def check_block(): | ||||
for tip in node.getchaintips(): | for tip in node.getchaintips(): | ||||
if tip["hash"] == block: | if tip["hash"] == block: | ||||
assert(tip["status"] != "active") | assert(tip["status"] != "active") | ||||
return tip["status"] == "invalid" | return tip["status"] == status | ||||
return False | return False | ||||
wait_until(check_block) | wait_until(check_block) | ||||
# node do not accept alt_node's chain due to tip being finalized, | # First block header is accepted as valid-header | ||||
# even though it is longer. | alt_node.generate(1) | ||||
wait_for_invalid_block(node, alt_node.getbestblockhash()) | alt_210 = alt_node.getbestblockhash() | ||||
assert_equal(node.getbestblockhash(), tip) | wait_for_block(node, alt_node.getbestblockhash(), "valid-headers") | ||||
# Second block header is accepted but set invalid | |||||
alt_node.generate(1) | |||||
invalid_block = alt_node.getbestblockhash() | |||||
wait_for_block(node, invalid_block) | |||||
# Later block headers are rejected | |||||
for i in range(2, 9): | |||||
alt_node.generate(1) | |||||
assert_raises_rpc_error(-5, RPC_BLOCK_NOT_FOUND_ERROR, | |||||
node.getblockheader, alt_node.getbestblockhash()) | |||||
# alt_node has mined 10 block, so tip must be invalid due to finalization. | assert_equal(node.getbestblockhash(), tip) | ||||
wait_for_invalid_block(alt_node, tip) | assert_equal(node.getfinalizedblockhash(), tip) | ||||
self.log.info("Test that an invalid block cannot be finalized...") | self.log.info("Test that an invalid block cannot be finalized...") | ||||
assert_raises_rpc_error(-20, RPC_FINALIZE_INVALID_BLOCK_ERROR, | assert_raises_rpc_error(-20, RPC_FINALIZE_INVALID_BLOCK_ERROR, | ||||
node.finalizeblock, alt_node.getbestblockhash()) | node.finalizeblock, invalid_block) | ||||
self.log.info( | self.log.info( | ||||
"Test that invalidating a finalized block moves the finalization backward...") | "Test that invalidating a finalized block moves the finalization backward...") | ||||
# Node's finalized block will be invalidated, which causes the finalized block to | |||||
# move to the previous block. | |||||
# | |||||
# Expected state: | |||||
# | |||||
# On alt_node: | |||||
# >(210)->(211)-> // ->(218 tip) | |||||
# / | |||||
# (200)->(201)-> // ->(208 auto-finalized)->(209)->(210 invalid) | |||||
# | |||||
# On node: | |||||
# >(210 valid-headers)->(211 invalid)->(212 to 218 dropped) | |||||
# / | |||||
# (200)->(201)-> // ->(209 finalized)->(210 tip) | |||||
node.invalidateblock(tip) | node.invalidateblock(tip) | ||||
node.reconsiderblock(tip) | node.reconsiderblock(tip) | ||||
finalized_block = node.getblockhash( | |||||
int(node.getblockheader(tip)['height']) - AUTO_FINALIZATION_DEPTH) | |||||
assert_equal(node.getbestblockhash(), tip) | assert_equal(node.getbestblockhash(), tip) | ||||
assert_equal( | |||||
node.getfinalizedblockhash(), | assert_equal(node.getblockheader( | ||||
finalized_block) | node.getbestblockhash())['height'], 210) | ||||
deadalnixUnsubmitted Not Done Inline Actionsassert_equal(node.getbestblockhash(), tip) Checking that the top block is at a certain height do not test anything useful and certainly do not test what is described in the comment above. deadalnix: assert_equal(node.getbestblockhash(), tip)
Checking that the top block is at a certain height… | |||||
assert_equal(node.getblockheader( | |||||
node.getfinalizedblockhash())['height'], 209) | |||||
assert_equal(alt_node.getblockheader( | |||||
alt_node.getbestblockhash())['height'], 218) | |||||
assert_equal(alt_node.getblockheader( | |||||
alt_node.getfinalizedblockhash())['height'], 208) | |||||
# The node will now accept that chain as the finalized block moved back. | # The node will now accept that chain as the finalized block moved back. | ||||
node.reconsiderblock(alt_node.getbestblockhash()) | # Generate a new block on alt_node to trigger getheader from node | ||||
# Previous 212-218 height blocks have been droped because their previous was invalid | |||||
# | |||||
# Expected state: | |||||
# | |||||
# On alt_node: | |||||
# >(210)->(211)-> // ->(218)->(219 tip) | |||||
# / | |||||
# (200)->(201)-> // ->(209 auto-finalized)->(210 invalid) | |||||
# | |||||
# On node: | |||||
# >(210)->(211)->(212)-> // ->(218)->(219 tip) | |||||
# / | |||||
# (200)->(201)-> // ->(209 finalized)->(210) | |||||
node.reconsiderblock(invalid_block) | |||||
alt_node.generate(1) | |||||
sync_blocks(self.nodes[0:2]) | |||||
assert_equal(node.getbestblockhash(), alt_node.getbestblockhash()) | assert_equal(node.getbestblockhash(), alt_node.getbestblockhash()) | ||||
assert_equal(node.getfinalizedblockhash(), auto_finalized_tip) | assert_equal(node.getfinalizedblockhash(), auto_finalized_tip) | ||||
assert_equal(node.getblockheader( | |||||
node.getbestblockhash())['height'], 219) | |||||
assert_equal(node.getblockheader( | |||||
node.getfinalizedblockhash())['height'], 209) | |||||
assert_equal(alt_node.getblockheader( | |||||
alt_node.getbestblockhash())['height'], 219) | |||||
assert_equal(alt_node.getblockheader( | |||||
alt_node.getfinalizedblockhash())['height'], 209) | |||||
self.log.info("Trigger reorg via block finalization...") | self.log.info("Trigger reorg via block finalization...") | ||||
# Generate some more blockes on alt_node. Auto-finalization will occur on | |||||
# both chains. | |||||
# | |||||
# Expected state: | |||||
# | |||||
# On alt_node: | |||||
# >(210)->(211)-> // ->(219 auto-finalized)-> // ->(229 tip) | |||||
# / | |||||
# (200)->(201)-> // ->(209)->(210 invalid) | |||||
# | |||||
# On node: | |||||
# >(210)->(211)-> // ->(219 auto-finalized)-> // ->(229 tip) | |||||
# / | |||||
# (200)->(201)-> // ->(209)->(210 invalid) | |||||
for i in range(10): | |||||
alt_node.generate(1) | |||||
sync_blocks(self.nodes[0:2]) | |||||
assert_equal(node.getbestblockhash(), alt_node.getbestblockhash()) | |||||
assert_equal(node.getblockheader( | |||||
node.getbestblockhash())['height'], 229) | |||||
assert_equal(node.getblockheader( | |||||
node.getfinalizedblockhash())['height'], 219) | |||||
assert_equal(alt_node.getblockheader( | |||||
alt_node.getbestblockhash())['height'], 229) | |||||
assert_equal(alt_node.getblockheader( | |||||
alt_node.getfinalizedblockhash())['height'], 219) | |||||
# Finalizing the tip will move the finalized block and invalidate the chain | |||||
# from alt_node. | |||||
# | |||||
# Expected state: | |||||
# | |||||
# On alt_node: | |||||
# >(210)->(211)-> // ->(219 auto-finalized)-> // ->(229 tip) | |||||
# / | |||||
# (200)->(201)-> // ->(209)->(210 invalid) | |||||
# | |||||
# On node: | |||||
# >(210 invalid)->(211 invalid)->(212 invalid)-> // ->(229 invalid) | |||||
jasonbcoxUnsubmitted Not Done Inline Actionsmarking this line (see related comment) jasonbcox: marking this line (see related comment) | |||||
# / | |||||
# (200)->(201)-> // ->(209)->(210 finalized, tip) | |||||
node.finalizeblock(tip) | node.finalizeblock(tip) | ||||
assert_equal(node.getfinalizedblockhash(), | |||||
finalized_block) | |||||
self.log.info("Try to finalized a block on a competiting fork...") | assert_equal(node.getbestblockhash(), tip) | ||||
assert_equal(node.getfinalizedblockhash(), tip) | |||||
assert_equal(node.getblockheader( | |||||
node.getbestblockhash())['height'], 210) | |||||
assert_equal(node.getblockheader( | |||||
node.getfinalizedblockhash())['height'], 210) | |||||
self.log.info("Try to finalize a block on a competiting fork...") | |||||
assert_raises_rpc_error(-20, RPC_FINALIZE_INVALID_BLOCK_ERROR, | assert_raises_rpc_error(-20, RPC_FINALIZE_INVALID_BLOCK_ERROR, | ||||
node.finalizeblock, alt_node.getbestblockhash()) | node.finalizeblock, alt_node.getbestblockhash()) | ||||
assert node.getfinalizedblockhash() != alt_node.getbestblockhash(), \ | assert node.getfinalizedblockhash() != alt_node.getbestblockhash(), \ | ||||
"Finalize block is alt_node's tip!" | "Finalize block is alt_node's tip!" | ||||
jasonbcoxUnsubmitted Not Done Inline Actionsis -> should be jasonbcox: `is -> should be`
The error message should always read in a way that reveals what the problem… | |||||
FabienAuthorUnsubmitted Done Inline ActionsI don't get your point, this message will only appear if the assertion is false, so the message looks correct. Anyway this whole test case makes no sense, I will update. Fabien: I don't get your point, this message will only appear if the assertion is false, so the message… | |||||
jasonbcoxUnsubmitted Not Done Inline ActionsOh yes. You're correct. Sorry for the confusion. "Finalize block should not be alt_node's tip" is more correct that the current message because the message is making an assumption of what the value could be. These kinds of messages age poorly as modifications to the test and underlying code may change the behavior. jasonbcox: Oh yes. You're correct. Sorry for the confusion. `"Finalize block should not be alt_node's… | |||||
assert_equal(node.getfinalizedblockhash(), finalized_block) | |||||
self.log.info( | self.log.info( | ||||
"Make sure reconsidering block move the finalization point...") | "Make sure reconsidering block move the finalization point...") | ||||
# Reconsidering alt_node tip will reorg and move finalization point | |||||
# | |||||
# Expected state: | |||||
# | |||||
# On alt_node: | |||||
# >(210)->(211)-> // ->(219 auto-finalized)-> // ->(229 tip) | |||||
# / | |||||
# (200)->(201)-> // ->(209)->(210 invalid) | |||||
# | |||||
# On node: | |||||
# >(210)->(211)-> // ->(219 auto-finalized)-> // ->(229 tip) | |||||
# / | |||||
# (200)->(201)-> // ->(209)->(210) | |||||
node.reconsiderblock(alt_node.getbestblockhash()) | node.reconsiderblock(alt_node.getbestblockhash()) | ||||
assert_equal(node.getbestblockhash(), alt_node.getbestblockhash()) | assert_equal(node.getbestblockhash(), alt_node.getbestblockhash()) | ||||
assert_equal(node.getblockheader( | |||||
node.getbestblockhash())['height'], 229) | |||||
assert_equal(node.getblockheader( | |||||
node.getfinalizedblockhash())['height'], 219) | |||||
if __name__ == '__main__': | if __name__ == '__main__': | ||||
FinalizeBlockTest().main() | FinalizeBlockTest().main() |
It seems strange to me that the ordering of finalizing a particular block changes whether block 210 is invalid or valid-headers. I would expect behavior similar to the marked comment (see below) where finding a block forked from before a finalized block would be marked as invalid, same as marking a block as finalized invalidates prior forks.
Since this diff is building upon existing behavior, this should be fixed in a future diff.