Handle invalid blocks

This commit is contained in:
Youngjoon Lee 2025-03-18 15:37:50 +09:00
parent 0a6f131431
commit 5ec268b6d5
No known key found for this signature in database
GPG Key ID: 303963A54A81DD4D
4 changed files with 411 additions and 119 deletions

View File

@ -10,7 +10,6 @@ from collections import defaultdict
import numpy as np
logger = logging.getLogger(__name__)
@ -368,16 +367,15 @@ class EpochState:
class Follower:
def __init__(self, genesis_state: LedgerState, config: Config):
self.config = config
self.forks = []
self.forks: list[Id] = []
self.local_chain = genesis_state.block.id()
self.genesis_state = genesis_state
self.ledger_state = {genesis_state.block.id(): genesis_state.copy()}
self.epoch_state = {}
def validate_header(self, block: BlockHeader) -> bool:
def validate_header(self, block: BlockHeader):
# TODO: verify blocks are not in the 'future'
if block.parent not in self.ledger_state:
logger.warning("We have not seen block parent")
raise ParentNotFound
current_state = self.ledger_state[block.parent].copy()
@ -397,8 +395,7 @@ class Follower:
# We take a shortcut for (1.) by restricting orphans to proofs we've
# already processed in other branches.
if orphan.id() not in self.ledger_state:
logger.warning("missing orphan proof")
return False
raise MissingOrphanProof
# (2.) is satisfied by verifying the proof against current state ensuring:
# - it is a valid proof
@ -410,21 +407,21 @@ class Follower:
epoch_state,
current_state,
):
logger.warning("invalid orphan proof")
return False
raise InvalidOrphanProof
# if an adopted leadership proof is valid we need to apply its
# effects to the ledger state
current_state.apply_leader_proof(orphan.leader_proof)
# TODO: this is not the full block validation spec, only slot leader is verified
return self.verify_slot_leader(
if not self.verify_slot_leader(
block.slot,
block.parent,
block.leader_proof,
epoch_state,
current_state,
)
):
raise InvalidLeaderProof
def verify_slot_leader(
self,
@ -452,9 +449,7 @@ class Follower:
logger.warning("dropping already processed block")
return False
if not self.validate_header(block):
logger.warning("invalid header")
return False
self.validate_header(block)
new_state = self.ledger_state[block.parent].copy()
new_state.apply(block)
@ -803,7 +798,23 @@ def maxvalid_bg(
class ParentNotFound(Exception):
pass
def __str__(self):
return "Parent not found"
class MissingOrphanProof(Exception):
def __str__(self):
return "Missing orphan proof"
class InvalidOrphanProof(Exception):
def __str__(self):
return "Invalid orphan proof"
class InvalidLeaderProof(Exception):
def __str__(self):
return "Invalid leader proof"
if __name__ == "__main__":

View File

@ -18,110 +18,164 @@ def sync(local: Follower, peers: list[Follower]):
# Repeat the sync process until no peer has a tip ahead of the local tip,
# because peers' tips may advance during the sync process.
block_fetcher = BlockFetcher(peers)
rejected_blocks: set[Id] = set()
while True:
# Fetch blocks from the peers in the range of slots from the local tip to the latest tip.
# Gather orphaned blocks, which are blocks from forks that are absent in the local block tree.
start_slot = local.tip().slot
orphans: set[BlockHeader] = set()
# Filter peers that have a tip ahead of the local tip
# and group peers by their tip to minimize the number of fetches.
groups = filter_and_group_peers_by_tip(peers, start_slot)
# Finish the sync process if no peer has a tip ahead of the local tip.
if len(groups) == 0:
return
num_blocks = 0
for block in block_fetcher.fetch_blocks_from(start_slot):
num_blocks += 1
# Reject blocks that have been rejected in the past
# or whose parent has been rejected.
if {block.id(), block.parent} & rejected_blocks:
rejected_blocks.add(block.id())
continue
for group in groups.values():
for block in fetch_blocks_by_slot(group, start_slot):
try:
local.on_block(block)
orphans.discard(block)
except ParentNotFound:
orphans.add(block)
try:
local.on_block(block)
orphans.discard(block)
except ParentNotFound:
orphans.add(block)
except Exception:
rejected_blocks.add(block.id())
# Finish the sync process if no block has been fetched,
# which means that no peer has a tip ahead of the local tip.
if num_blocks == 0:
return
# Backfill the orphan forks starting from the orphan blocks with applying fork choice rule.
#
# Sort the orphan blocks by slot in descending order to minimize the number of backfillings.
for orphan in sorted(orphans, key=lambda b: b.slot, reverse=True):
# Skip the orphan block if it has been processed during the previous backfillings.
# (i.e. if it has been already added to the local block tree)
if orphan not in local.ledger_state:
backfill_fork(local, peers, orphan)
# Skip the orphan block if it has been processed during the previous backfillings
# (i.e. if it has been already added to the local block tree).
# Or, skip if it has been rejected during the previous backfillings.
if (
orphan.id() not in local.ledger_state
and orphan.id() not in rejected_blocks
):
try:
backfill_fork(local, orphan, block_fetcher)
except InvalidBlockFromBackfillFork as e:
rejected_blocks.update(block.id() for block in e.invalid_suffix)
def filter_and_group_peers_by_tip(
peers: list[Follower], start_slot: Slot
) -> dict[BlockHeader, list[Follower]]:
# Group peers by their tip.
# Filter only the peers whose tip is ahead of the start_slot.
groups: dict[BlockHeader, list[Follower]] = defaultdict(list)
for peer in peers:
if peer.tip().slot.absolute_slot > start_slot.absolute_slot:
groups[peer.tip()].append(peer)
return groups
def fetch_blocks_by_slot(
peers: list[Follower], start_slot: Slot
) -> Generator[BlockHeader, None, None]:
# Fetch blocks in the given range of slots from one of the peers.
# Blocks should be returned in order of slot.
# If a peer fails, try the next peer.
for peer in peers:
try:
for block in peer.blocks_by_slot(start_slot):
yield block
# Update start_slot for the potential try with the next peer.
start_slot = block.slot
# The peer successfully returned all blocks. No need to try the next peer.
break
except Exception:
continue
def backfill_fork(local: Follower, peers: list[Follower], fork_tip: BlockHeader):
def backfill_fork(
local: Follower,
fork_tip: BlockHeader,
block_fetcher: "BlockFetcher",
):
# Backfills a fork, which is absent in the local block tree, by fetching blocks from the peers.
# During backfilling, the fork choice rule is continuously applied.
#
# If necessary, the local honest chain is also backfilled for the fork choice rule.
# This can happen if the honest chain has been built not from the genesis
# (e.g. checkpoint sync, or a partially backfilled chain).
# This can happen if the honest chain has been built not from the genesis (i.e. checkpoint sync).
_, tip_suffix, _, fork_suffix = common_prefix_depth_from_chains(
fetch_chain_blocks(local.tip_id(), local, peers),
fetch_chain_blocks(fork_tip.id(), local, peers),
block_fetcher.fetch_chain_backward(local.tip_id(), local),
block_fetcher.fetch_chain_backward(fork_tip.id(), local),
)
# First, backfill the local honest chain if some blocks are missing.
# Just applying the blocks to the ledger state is enough,
# instead of calling `on_block` which updates the tip (by fork choice).
# because we're just backfilling the old part of the current tip.
# In other words, backfill the local block tree, which contains the honest chain.
for block in tip_suffix:
local.apply_block_to_ledger_state(block)
try:
# Just apply the block to the ledger state is enough
# instead of calling `on_block` which runs the fork choice rule.
local.apply_block_to_ledger_state(block)
except Exception as e:
raise InvalidBlockTree(e)
# Then, process blocks in the fork suffix by applying fork choice rule.
for block in fork_suffix:
local.on_block(block)
# Then, add blocks in the fork suffix with applying fork choice rule.
# After all, add the tip of the fork suffix to apply the fork choice rule.
for i, block in enumerate(fork_suffix):
try:
local.on_block(block)
except Exception as e:
raise InvalidBlockFromBackfillFork(e, fork_suffix[i:])
def fetch_chain_blocks(
tip: Id, local: Follower, peers: list[Follower]
) -> Generator[BlockHeader, None, None]:
# Fetches a chain of blocks from the peers, starting from the given tip to the genesis.
# Attempts to extend the chain as much as possible by querying multiple peers,
# considering that not all peers may have the full chain (from the genesis).
class BlockFetcher:
# NOTE: This class is a mock, which uses a naive approach to fetch blocks from multiple peers.
# In real implementation, any optimized way can be used, such as parallel fetching.
id = tip
# First, try to iterate the chain from the local block tree.
for block in iter_chain_blocks(id, local.ledger_state):
yield block
if block.id() == local.genesis_state.block.id():
return
id = block.parent
def __init__(self, peers: list[Follower]):
self.peers = peers
# Try to continue by fetching the remaining blocks from the peers
for peer in peers:
for block in iter_chain_blocks(id, peer.ledger_state):
def fetch_blocks_from(self, start_slot: Slot) -> Generator[BlockHeader, None, None]:
# Filter peers that have a tip ahead of the local tip
# and group peers by their tip to minimize the number of fetches.
groups = self.filter_and_group_peers_by_tip(start_slot)
for group in groups.values():
for block in BlockFetcher.fetch_blocks_by_slot(group, start_slot):
yield block
def filter_and_group_peers_by_tip(
self, start_slot: Slot
) -> dict[BlockHeader, list[Follower]]:
# Group peers by their tip.
# Filter only the peers whose tip is ahead of the start_slot.
groups: dict[BlockHeader, list[Follower]] = defaultdict(list)
for peer in self.peers:
if peer.tip().slot.absolute_slot > start_slot.absolute_slot:
groups[peer.tip()].append(peer)
return groups
@staticmethod
def fetch_blocks_by_slot(
peers: list[Follower], start_slot: Slot
) -> Generator[BlockHeader, None, None]:
# Fetch blocks in the given range of slots from one of the peers.
# Blocks should be returned in order of slot.
# If a peer fails, try the next peer.
for peer in peers:
try:
for block in peer.blocks_by_slot(start_slot):
yield block
# Update start_slot for the potential try with the next peer.
start_slot = block.slot
# The peer successfully returned all blocks. No need to try the next peer.
break
except Exception:
continue
def fetch_chain_backward(
self, tip: Id, local: Follower
) -> Generator[BlockHeader, None, None]:
# Fetches a chain of blocks from the peers, starting from the given tip to the genesis.
# Attempts to extend the chain as much as possible by querying multiple peers,
# considering that not all peers may have the full chain (from the genesis).
id = tip
# First, try to iterate the chain from the local block tree.
for block in iter_chain_blocks(id, local.ledger_state):
yield block
if block.id() == local.genesis_state.block.id():
return
id = block.parent
# Try to continue by fetching the remaining blocks from the peers
for peer in self.peers:
for block in iter_chain_blocks(id, peer.ledger_state):
yield block
if block.id() == local.genesis_state.block.id():
return
id = block.parent
class InvalidBlockTree(Exception):
def __init__(self, cause: Exception):
super().__init__()
self.cause = cause
class InvalidBlockFromBackfillFork(Exception):
def __init__(self, cause: Exception, invalid_suffix: list[BlockHeader]):
super().__init__()
self.cause = cause
self.invalid_suffix = invalid_suffix

View File

@ -1,10 +1,14 @@
from unittest import TestCase
import numpy as np
from .cryptarchia import Follower, Coin, ParentNotFound, iter_chain
from .test_common import mk_config, mk_block, mk_genesis_state
from .cryptarchia import (
Coin,
Follower,
InvalidLeaderProof,
MissingOrphanProof,
ParentNotFound,
iter_chain,
)
from .test_common import mk_block, mk_config, mk_genesis_state
class TestLedgerStateUpdate(TestCase):
@ -46,7 +50,8 @@ class TestLedgerStateUpdate(TestCase):
assert follower.tip_state().verify_unspent(leader_coin.nullifier()) == False
reuse_coin_block = mk_block(slot=1, parent=block, coin=leader_coin)
follower.on_block(reuse_coin_block)
with self.assertRaises(InvalidLeaderProof):
follower.on_block(reuse_coin_block)
# Follower should *not* have accepted the block
assert len(list(iter_chain(follower.tip_id(), follower.ledger_state))) == 2
@ -170,7 +175,8 @@ class TestLedgerStateUpdate(TestCase):
# so that the new block can be accepted only if that is the snapshot used
# first, verify that if we don't change the state, the block is not accepted
block_4 = mk_block(slot=40, parent=block_3, coin=Coin(sk=4, value=100))
follower.on_block(block_4)
with self.assertRaises(InvalidLeaderProof):
follower.on_block(block_4)
assert follower.tip() == block_3
# then we add the coin to "spendable commitments" associated with slot 9
follower.ledger_state[block_2.id()].commitments_spend.add(
@ -194,7 +200,8 @@ class TestLedgerStateUpdate(TestCase):
# coin can't be reused to win following slots:
block_2_reuse = mk_block(slot=1, parent=block_1, coin=coin)
follower.on_block(block_2_reuse)
with self.assertRaises(InvalidLeaderProof):
follower.on_block(block_2_reuse)
assert follower.tip() == block_1
# but the evolved coin is eligible
@ -225,7 +232,8 @@ class TestLedgerStateUpdate(TestCase):
# the new coin is not yet eligible for elections
block_0_1_attempt = mk_block(slot=1, parent=block_0_0, coin=coin_new)
follower.on_block(block_0_1_attempt)
with self.assertRaises(InvalidLeaderProof):
follower.on_block(block_0_1_attempt)
assert follower.tip() == block_0_0
# whereas the evolved coin from genesis can be spent immediately
@ -239,7 +247,8 @@ class TestLedgerStateUpdate(TestCase):
# stake distribution snapshot is taken at the beginning of the previous epoch
block_1_0 = mk_block(slot=20, parent=block_0_1, coin=coin_new)
follower.on_block(block_1_0)
with self.assertRaises(InvalidLeaderProof):
follower.on_block(block_1_0)
assert follower.tip() == block_0_1
# ---- EPOCH 2 ----
@ -268,7 +277,8 @@ class TestLedgerStateUpdate(TestCase):
coin_new = coin.evolve()
coin_new_new = coin_new.evolve()
block_0_1 = mk_block(slot=1, parent=block_0_0, coin=coin_new_new)
follower.on_block(block_0_1)
with self.assertRaises(InvalidLeaderProof):
follower.on_block(block_0_1)
# the coin evolved twice should not be accepted as it is not in the lead commitments
assert follower.tip() == block_0_0
@ -284,7 +294,8 @@ class TestLedgerStateUpdate(TestCase):
coin=coin_orphan.evolve(),
orphaned_proofs=[orphan],
)
follower.on_block(block_0_1)
with self.assertRaises(MissingOrphanProof):
follower.on_block(block_0_1)
# since follower had not seen this orphan prior to being included as
# an orphan proof, it will be rejected

View File

@ -1,12 +1,13 @@
from unittest import TestCase
from cryptarchia.cryptarchia import Coin, Follower
from cryptarchia.sync import sync
from cryptarchia.cryptarchia import BlockHeader, Coin, Follower
from cryptarchia.sync import InvalidBlockTree, sync
from cryptarchia.test_common import mk_block, mk_config, mk_genesis_state
class TestSync(TestCase):
def test_sync_single_chain_from_genesis(self):
# Prepare a peer with a single chain:
# b0 - b1 - b2 - b3
coin = Coin(sk=0, value=10)
config = mk_config([coin])
@ -21,12 +22,15 @@ class TestSync(TestCase):
self.assertEqual(peer.tip(), b3)
self.assertEqual(peer.forks, [])
# Start a sync from genesis.
# Result: The same block tree as the peer's.
local = Follower(genesis, config)
sync(local, [peer])
self.assertEqual(local.tip(), peer.tip())
self.assertEqual(local.forks, peer.forks)
def test_sync_single_chain_from_middle(self):
# Prepare a peer with a single chain:
# b0 - b1 - b2 - b3
coin = Coin(sk=0, value=10)
config = mk_config([coin])
@ -41,16 +45,19 @@ class TestSync(TestCase):
self.assertEqual(peer.tip(), b3)
self.assertEqual(peer.forks, [])
# Start a sync from a tree:
# b0 - b1
#
# Result: The same block tree as the peer's.
local = Follower(genesis, config)
# add until b1
for b in [b0, b1]:
peer.on_block(b)
# start syncing from b1
sync(local, [peer])
self.assertEqual(local.tip(), peer.tip())
self.assertEqual(local.forks, peer.forks)
def test_sync_forks_from_genesis(self):
# Prepare a peer with forks:
# b0 - b1 - b2 - b5 == tip
# \
# b3 - b4
@ -69,12 +76,15 @@ class TestSync(TestCase):
self.assertEqual(peer.tip(), b5)
self.assertEqual(peer.forks, [b4.id()])
# Start a sync from genesis.
# Result: The same block tree as the peer's.
local = Follower(genesis, config)
sync(local, [peer])
self.assertEqual(local.tip(), peer.tip())
self.assertEqual(local.forks, peer.forks)
def test_sync_forks_from_middle(self):
# Prepare a peer with forks:
# b0 - b1 - b2 - b5 == tip
# \
# b3 - b4
@ -93,9 +103,12 @@ class TestSync(TestCase):
self.assertEqual(peer.tip(), b5)
self.assertEqual(peer.forks, [b4.id()])
# Start a sync from a tree:
# b0 - b1
# \
# b3
#
# Result: The same block tree as the peer's.
local = Follower(genesis, config)
for b in [b0, b1, b3]:
peer.on_block(b)
@ -104,6 +117,7 @@ class TestSync(TestCase):
self.assertEqual(local.forks, peer.forks)
def test_sync_forks_by_backfilling(self):
# Prepare a peer with forks:
# b0 - b1 - b2 - b5 == tip
# \
# b3 - b4
@ -123,7 +137,10 @@ class TestSync(TestCase):
self.assertEqual(peer.forks, [b4.id()])
self.assertEqual(len(peer.ledger_state), 7)
# Start a sync from a tree without the fork:
# b0 - b1
#
# Result: The same block tree as the peer's.
local = Follower(genesis, config)
for b in [b0, b1]:
peer.on_block(b)
@ -133,6 +150,7 @@ class TestSync(TestCase):
self.assertEqual(len(local.ledger_state), len(peer.ledger_state))
def test_sync_multiple_peers_from_genesis(self):
# Prepare multiple peers:
# Peer-0: b5
# /
# Peer-1: b0 - b1 - b2
@ -163,15 +181,109 @@ class TestSync(TestCase):
self.assertEqual(peer2.tip(), b4)
self.assertEqual(peer2.forks, [])
# Start a sync from genesis.
#
# Result: A merged block tree
# b5
# /
# b0 - b1 - b2
# \
# b3 - b4
local = Follower(genesis, config)
sync(local, [peer0, peer1, peer2])
self.assertEqual(local.tip(), b5)
self.assertEqual(local.forks, [b4.id()])
self.assertEqual(len(local.ledger_state), 7)
def test_reject_invalid_blocks(self):
# Prepare a peer with invalid blocks:
# b0 - b1 - b2 - b3 - (invalid_b4) - (invalid_b5)
#
# First, build a valid chain (b0 ~ b3):
coin = Coin(sk=0, value=10)
config = mk_config([coin])
genesis = mk_genesis_state([coin])
peer = Follower(genesis, config)
b0, coin = mk_block(genesis.block, 1, coin), coin.evolve()
b1, coin = mk_block(b0, 2, coin), coin.evolve()
b2, coin = mk_block(b1, 3, coin), coin.evolve()
b3, coin = mk_block(b2, 4, coin), coin.evolve()
for b in [b0, b1, b2, b3]:
peer.on_block(b)
self.assertEqual(peer.tip(), b3)
self.assertEqual(peer.forks, [])
# And deliberately, add invalid blocks (b4 ~ b5):
fake_coin = Coin(sk=1, value=10)
b4, fake_coin = mk_block(b3, 5, fake_coin), fake_coin.evolve()
b5, fake_coin = mk_block(b4, 6, fake_coin), fake_coin.evolve()
apply_invalid_block_to_ledger_state(peer, b4)
apply_invalid_block_to_ledger_state(peer, b5)
# the tip shouldn't be changed.
self.assertEqual(peer.tip(), b3)
self.assertEqual(peer.forks, [])
# Start a sync from genesis.
#
# Result: The same honest chain, but without invalid blocks.
# b0 - b1 - b2 - b3 == tip
local = Follower(genesis, config)
sync(local, [peer])
self.assertEqual(local.tip(), peer.tip())
self.assertEqual(local.forks, peer.forks)
def test_reject_invalid_blocks_from_backfilling(self):
# Prepare a peer with invalid blocks in a fork:
# b0 - b1 - b3 - b4 - b5 == tip
# \
# b2 - (invalid_b6) - (invalid_b7)
#
# First, build a valid chain (b0 ~ b5):
c_a, c_b = Coin(sk=0, value=10), Coin(sk=1, value=10)
config = mk_config([c_a, c_b])
genesis = mk_genesis_state([c_a, c_b])
peer = Follower(genesis, config)
b0, c_a = mk_block(genesis.block, 1, c_a), c_a.evolve()
b1, c_a = mk_block(b0, 2, c_a), c_a.evolve()
b2, c_b = mk_block(b0, 2, c_b), c_b.evolve()
b3, c_a = mk_block(b1, 3, c_a), c_a.evolve()
b4, c_a = mk_block(b3, 4, c_a), c_a.evolve()
b5, c_a = mk_block(b4, 5, c_a), c_a.evolve()
for b in [b0, b1, b2, b3, b4, b5]:
peer.on_block(b)
self.assertEqual(peer.tip(), b5)
self.assertEqual(peer.forks, [b2.id()])
# And deliberately, add invalid blocks (b6 ~ b7):
fake_coin = Coin(sk=2, value=10)
b6, fake_coin = mk_block(b2, 3, fake_coin), fake_coin.evolve()
b7, fake_coin = mk_block(b6, 4, fake_coin), fake_coin.evolve()
apply_invalid_block_to_ledger_state(peer, b6)
apply_invalid_block_to_ledger_state(peer, b7)
# the tip shouldn't be changed.
self.assertEqual(peer.tip(), b5)
self.assertEqual(peer.forks, [b2.id()])
# Start a sync from a tree:
# b0 - b1 - b3 - b4
#
# Result: The same forks, but without invalid blocks
# b0 - b1 - b3 - b4 - b5 == tip
# \
# b2
local = Follower(genesis, config)
for b in [b0, b1, b3, b4]:
peer.on_block(b)
sync(local, [peer])
self.assertEqual(local.tip(), peer.tip())
self.assertEqual(local.forks, peer.forks)
self.assertNotIn(b6.id(), local.ledger_state)
self.assertNotIn(b7.id(), local.ledger_state)
class TestSyncFromCheckpoint(TestCase):
def test_sync_single_chain(self):
# Prepare a peer with a single chain:
# b0 - b1 - b2 - b3
# ||
# checkpoint
@ -188,18 +300,17 @@ class TestSyncFromCheckpoint(TestCase):
self.assertEqual(peer.tip(), b3)
self.assertEqual(peer.forks, [])
# Start from the checkpoint:
# Start a sync from the checkpoint:
# () - () - b2
# ||
# checkpoint
#
# Result: A honest chain without historical blocks
# () - () - b2 - b3
checkpoint = peer.ledger_state[b2.id()]
local = Follower(genesis, config)
local.apply_checkpoint(checkpoint)
sync(local, [peer])
# Result:
# () - () - b2 - b3
# ||
# checkpoint
self.assertEqual(local.tip(), peer.tip())
self.assertEqual(local.forks, peer.forks)
self.assertEqual(
@ -207,6 +318,7 @@ class TestSyncFromCheckpoint(TestCase):
)
def test_sync_forks(self):
# Prepare a peer with forks:
# checkpoint
# ||
# b0 - b1 - b2 - b5 == tip
@ -227,23 +339,25 @@ class TestSyncFromCheckpoint(TestCase):
self.assertEqual(peer.tip(), b5)
self.assertEqual(peer.forks, [b4.id()])
# Start from the checkpoint:
# Start a sync from the checkpoint:
# checkpoint
# ||
# () - () - b2
#
# Result: Backfilled forks.
# b0 - b1 - b2 - b5 == tip
# \
# b3 - b4
checkpoint = peer.ledger_state[b2.id()]
local = Follower(genesis, config)
local.apply_checkpoint(checkpoint)
sync(local, [peer])
# Result:
# b0 - b1 - b2 - b5 == tip
# \
# b3 - b4
self.assertEqual(local.tip(), peer.tip())
self.assertEqual(local.forks, peer.forks)
self.assertEqual(set(local.ledger_state.keys()), set(peer.ledger_state.keys()))
def test_sync_from_dishonest_checkpoint(self):
# Prepare multiple peers and a dishonest checkpoint:
# Peer0: b0 - b1 - b2 - b5 == tip
# \
# Peer1: b3 - b4
@ -269,17 +383,119 @@ class TestSyncFromCheckpoint(TestCase):
self.assertEqual(peer1.tip(), b4)
self.assertEqual(peer1.forks, [])
# Start from the checkpoint:
# Start a sync from the dishonest checkpoint:
# checkpoint
# ||
# () - () - b4
#
# Result: The honest chain is found evetually by backfilling.
# b0 - b1 - b2 - b5 == tip
# \
# b3 - b4
checkpoint = peer1.ledger_state[b4.id()]
local = Follower(genesis, config)
local.apply_checkpoint(checkpoint)
sync(local, [peer0, peer1])
# b0 - b1 - b2 - b5 == tip
# \
# b3 - b4
self.assertEqual(local.tip(), b5)
self.assertEqual(local.forks, [b4.id()])
self.assertEqual(len(local.ledger_state.keys()), 7)
def test_reject_invalid_blocks_from_backfilling_fork(self):
# Prepare a peer with invalid blocks in a fork:
# b0 - b1 - b3 - b4 - b5 == tip
# \
# b2 - (invalid_b6) - (invalid_b7)
#
# First, build a valid chain (b0 ~ b5):
c_a, c_b = Coin(sk=0, value=10), Coin(sk=1, value=10)
config = mk_config([c_a, c_b])
genesis = mk_genesis_state([c_a, c_b])
peer = Follower(genesis, config)
b0, c_a = mk_block(genesis.block, 1, c_a), c_a.evolve()
b1, c_a = mk_block(b0, 2, c_a), c_a.evolve()
b2, c_b = mk_block(b0, 2, c_b), c_b.evolve()
b3, c_a = mk_block(b1, 3, c_a), c_a.evolve()
b4, c_a = mk_block(b3, 4, c_a), c_a.evolve()
b5, c_a = mk_block(b4, 5, c_a), c_a.evolve()
for b in [b0, b1, b2, b3, b4, b5]:
peer.on_block(b)
self.assertEqual(peer.tip(), b5)
self.assertEqual(peer.forks, [b2.id()])
# And deliberately, add invalid blocks (b6 ~ b7):
fake_coin = Coin(sk=2, value=10)
b6, fake_coin = mk_block(b2, 3, fake_coin), fake_coin.evolve()
b7, fake_coin = mk_block(b6, 4, fake_coin), fake_coin.evolve()
apply_invalid_block_to_ledger_state(peer, b6)
apply_invalid_block_to_ledger_state(peer, b7)
# the tip shouldn't be changed.
self.assertEqual(peer.tip(), b5)
self.assertEqual(peer.forks, [b2.id()])
# Start a sync from a checkpoint where all anscestors are valid:
# checkpoint
# ||
# () - () - () - b4
#
# Result: A fork is backfilled, but without invalid blocks.
# b0 - b1 - b3 - b4 - b5 == tip
# \
# b2
checkpoint = peer.ledger_state[b4.id()]
local = Follower(genesis, config)
local.apply_checkpoint(checkpoint)
sync(local, [peer])
self.assertEqual(local.tip(), peer.tip())
self.assertEqual(local.forks, peer.forks)
self.assertNotIn(b6.id(), local.ledger_state)
self.assertNotIn(b7.id(), local.ledger_state)
def test_reject_invalid_blocks_from_backfilling_block_tree(self):
# Prepare a peer with invalid blocks in a fork:
# b0 - b1 - b3 - b4 - b5 == tip
# \
# b2 - (invalid_b6) - (invalid_b7)
#
# First, build a valid chain (b0 ~ b5):
c_a, c_b = Coin(sk=0, value=10), Coin(sk=1, value=10)
config = mk_config([c_a, c_b])
genesis = mk_genesis_state([c_a, c_b])
peer = Follower(genesis, config)
b0, c_a = mk_block(genesis.block, 1, c_a), c_a.evolve()
b1, c_a = mk_block(b0, 2, c_a), c_a.evolve()
b2, c_b = mk_block(b0, 2, c_b), c_b.evolve()
b3, c_a = mk_block(b1, 3, c_a), c_a.evolve()
b4, c_a = mk_block(b3, 4, c_a), c_a.evolve()
b5, c_a = mk_block(b4, 5, c_a), c_a.evolve()
for b in [b0, b1, b2, b3, b4, b5]:
peer.on_block(b)
self.assertEqual(peer.tip(), b5)
self.assertEqual(peer.forks, [b2.id()])
# And deliberately, add invalid blocks (b6 ~ b7):
fake_coin = Coin(sk=2, value=10)
b6, fake_coin = mk_block(b2, 3, fake_coin), fake_coin.evolve()
b7, fake_coin = mk_block(b6, 4, fake_coin), fake_coin.evolve()
apply_invalid_block_to_ledger_state(peer, b6)
apply_invalid_block_to_ledger_state(peer, b7)
# the tip shouldn't be changed.
self.assertEqual(peer.tip(), b5)
self.assertEqual(peer.forks, [b2.id()])
# Start a sync from a checkpoint where some anscestors are invalid:
# () checkpoint
# \ ||
# () - () - (invalid_b7)
#
# Result: `InvalidBlockTree` exception
checkpoint = peer.ledger_state[b7.id()]
local = Follower(genesis, config)
local.apply_checkpoint(checkpoint)
with self.assertRaises(InvalidBlockTree):
sync(local, [peer])
def apply_invalid_block_to_ledger_state(follower: Follower, block: BlockHeader):
state = follower.ledger_state[block.parent].copy()
state.apply(block)
follower.ledger_state[block.id()] = state