mirror of
https://github.com/logos-blockchain/logos-blockchain-specs.git
synced 2026-01-10 00:53:08 +00:00
* Add Boostrapping/Online modes Add Boostrapping and Online modes to cryptarchia, including relevant tests. The Boostrap mode uses the Genesis fc rule, while Online uses Praos. Swtitching between the two rules is left to the implementation and is specified in the public Notion as linked in the comment * Add explicit LIB * prune forks
376 lines
11 KiB
Python
376 lines
11 KiB
Python
from unittest import TestCase
|
|
|
|
from copy import deepcopy
|
|
from cryptarchia.cryptarchia import (
|
|
maxvalid_bg,
|
|
maxvalid_mc,
|
|
Slot,
|
|
Note,
|
|
State,
|
|
Follower,
|
|
common_prefix_depth,
|
|
LedgerState,
|
|
ImmutableFork,
|
|
)
|
|
|
|
from .test_common import mk_chain, mk_config, mk_genesis_state, mk_block
|
|
|
|
|
|
class TestForkChoice(TestCase):
|
|
def test_common_prefix_depth(self):
|
|
# 6 - 7
|
|
# /
|
|
# 0 - 1 - 2 - 3
|
|
# \
|
|
# 4 - 5
|
|
|
|
note = Note(sk=1, value=100)
|
|
|
|
b0 = mk_genesis_state([]).block
|
|
b1, b2, b3 = mk_chain(b0, Note(sk=1, value=1), slots=[1, 2, 3])
|
|
b4, b5 = mk_chain(b0, Note(sk=2, value=1), slots=[1, 2])
|
|
b6, b7 = mk_chain(b2, Note(sk=3, value=1), slots=[3, 4])
|
|
|
|
states = {
|
|
b.id(): LedgerState(block=b) for b in [b0, b1, b2, b3, b4, b5, b6, b7]
|
|
}
|
|
|
|
assert (d := common_prefix_depth(b0.id(), b0.id(), states)) == (
|
|
0,
|
|
[b0],
|
|
0,
|
|
[b0],
|
|
), d
|
|
assert (d := common_prefix_depth(b1.id(), b0.id(), states)) == (
|
|
1,
|
|
[b0, b1],
|
|
0,
|
|
[b0],
|
|
), d
|
|
assert (d := common_prefix_depth(b0.id(), b1.id(), states)) == (
|
|
0,
|
|
[b0],
|
|
1,
|
|
[b0, b1],
|
|
), d
|
|
assert (d := common_prefix_depth(b1.id(), b1.id(), states)) == (
|
|
0,
|
|
[b1],
|
|
0,
|
|
[b1],
|
|
), d
|
|
assert (d := common_prefix_depth(b2.id(), b0.id(), states)) == (
|
|
2,
|
|
[b0, b1, b2],
|
|
0,
|
|
[b0],
|
|
), d
|
|
assert (d := common_prefix_depth(b0.id(), b2.id(), states)) == (
|
|
0,
|
|
[b0],
|
|
2,
|
|
[b0, b1, b2],
|
|
), d
|
|
assert (d := common_prefix_depth(b3.id(), b0.id(), states)) == (
|
|
3,
|
|
[b0, b1, b2, b3],
|
|
0,
|
|
[b0],
|
|
), d
|
|
assert (d := common_prefix_depth(b0.id(), b3.id(), states)) == (
|
|
0,
|
|
[b0],
|
|
3,
|
|
[b0, b1, b2, b3],
|
|
), d
|
|
assert (d := common_prefix_depth(b1.id(), b4.id(), states)) == (
|
|
1,
|
|
[b0, b1],
|
|
1,
|
|
[b0, b4],
|
|
), d
|
|
assert (d := common_prefix_depth(b4.id(), b1.id(), states)) == (
|
|
1,
|
|
[b0, b4],
|
|
1,
|
|
[b0, b1],
|
|
), d
|
|
assert (d := common_prefix_depth(b1.id(), b5.id(), states)) == (
|
|
1,
|
|
[b0, b1],
|
|
2,
|
|
[b0, b4, b5],
|
|
), d
|
|
assert (d := common_prefix_depth(b5.id(), b1.id(), states)) == (
|
|
2,
|
|
[b0, b4, b5],
|
|
1,
|
|
[b0, b1],
|
|
), d
|
|
assert (d := common_prefix_depth(b2.id(), b5.id(), states)) == (
|
|
2,
|
|
[b0, b1, b2],
|
|
2,
|
|
[b0, b4, b5],
|
|
), d
|
|
assert (d := common_prefix_depth(b5.id(), b2.id(), states)) == (
|
|
2,
|
|
[b0, b4, b5],
|
|
2,
|
|
[b0, b1, b2],
|
|
), d
|
|
assert (d := common_prefix_depth(b3.id(), b5.id(), states)) == (
|
|
3,
|
|
[b0, b1, b2, b3],
|
|
2,
|
|
[b0, b4, b5],
|
|
), d
|
|
assert (d := common_prefix_depth(b5.id(), b3.id(), states)) == (
|
|
2,
|
|
[b0, b4, b5],
|
|
3,
|
|
[b0, b1, b2, b3],
|
|
), d
|
|
assert (d := common_prefix_depth(b3.id(), b6.id(), states)) == (
|
|
1,
|
|
[b2, b3],
|
|
1,
|
|
[b2, b6],
|
|
), d
|
|
assert (d := common_prefix_depth(b6.id(), b3.id(), states)) == (
|
|
1,
|
|
[b2, b6],
|
|
1,
|
|
[b2, b3],
|
|
), d
|
|
assert (d := common_prefix_depth(b3.id(), b7.id(), states)) == (
|
|
1,
|
|
[b2, b3],
|
|
2,
|
|
[b2, b6, b7],
|
|
), d
|
|
assert (d := common_prefix_depth(b7.id(), b3.id(), states)) == (
|
|
2,
|
|
[b2, b6, b7],
|
|
1,
|
|
[b2, b3],
|
|
), d
|
|
assert (d := common_prefix_depth(b5.id(), b7.id(), states)) == (
|
|
2,
|
|
[b0, b4, b5],
|
|
4,
|
|
[b0, b1, b2, b6, b7],
|
|
), d
|
|
assert (d := common_prefix_depth(b7.id(), b5.id(), states)) == (
|
|
4,
|
|
[b0, b1, b2, b6, b7],
|
|
2,
|
|
[b0, b4, b5],
|
|
), d
|
|
|
|
def test_fork_choice_long_sparse_chain(self):
|
|
# The longest chain is not dense after the fork
|
|
genesis = mk_genesis_state([]).block
|
|
|
|
short_note, long_note = Note(sk=0, value=100), Note(sk=1, value=100)
|
|
common = mk_chain(parent=genesis, note=long_note, slots=range(50))
|
|
|
|
long_chain_sparse_ext = mk_chain(
|
|
parent=common[-1], note=long_note, slots=range(50, 100, 2)
|
|
)
|
|
|
|
short_chain_dense_ext = mk_chain(
|
|
parent=common[-1], note=short_note, slots=range(50, 100)
|
|
)
|
|
|
|
# add more blocks to the long chain to ensure the long chain is indeed longer
|
|
long_chain_further_ext = mk_chain(
|
|
parent=long_chain_sparse_ext[-1], note=long_note, slots=range(100, 126)
|
|
)
|
|
|
|
long_chain = deepcopy(common) + long_chain_sparse_ext + long_chain_further_ext
|
|
short_chain = deepcopy(common) + short_chain_dense_ext
|
|
assert len(long_chain) > len(short_chain)
|
|
|
|
# by setting a low k we trigger the density choice rule
|
|
k = 1
|
|
s = 50
|
|
|
|
states = {b.id(): LedgerState(block=b) for b in short_chain + long_chain}
|
|
|
|
assert (
|
|
maxvalid_bg(short_chain[-1].id(), [long_chain[-1].id()], k, s, states)
|
|
== short_chain[-1].id()
|
|
)
|
|
|
|
assert (
|
|
maxvalid_mc(short_chain[-1].id(), [long_chain[-1].id()], k,states)
|
|
== short_chain[-1].id()
|
|
)
|
|
|
|
# However, if we set k to the fork length, it will be accepted
|
|
k = len(long_chain)
|
|
assert (
|
|
maxvalid_bg(short_chain[-1].id(), [long_chain[-1].id()], k, s, states)
|
|
== long_chain[-1].id()
|
|
)
|
|
|
|
assert (
|
|
maxvalid_mc(short_chain[-1].id(), [long_chain[-1].id()], k, states)
|
|
== long_chain[-1].id()
|
|
)
|
|
|
|
def test_fork_choice_long_dense_chain(self):
|
|
# The longest chain is also the densest after the fork
|
|
short_note, long_note = Note(sk=0, value=100), Note(sk=1, value=100)
|
|
common = mk_chain(
|
|
parent=mk_genesis_state([]).block,
|
|
note=long_note,
|
|
slots=range(1, 50),
|
|
)
|
|
|
|
long_chain_dense_ext = mk_chain(
|
|
parent=common[-1], note=long_note, slots=range(50, 100)
|
|
)
|
|
short_chain_sparse_ext = mk_chain(
|
|
parent=common[-1], note=short_note, slots=range(50, 100, 2)
|
|
)
|
|
|
|
long_chain = deepcopy(common) + long_chain_dense_ext
|
|
short_chain = deepcopy(common) + short_chain_sparse_ext
|
|
|
|
k = 1
|
|
s = 50
|
|
states = {b.id(): LedgerState(block=b) for b in short_chain + long_chain}
|
|
|
|
assert (
|
|
maxvalid_bg(short_chain[-1].id(), [long_chain[-1].id()], k, s, states)
|
|
== long_chain[-1].id()
|
|
)
|
|
|
|
# praos fc rule should not accept a chain that diverged more than k blocks,
|
|
# even if it is longer
|
|
assert (
|
|
maxvalid_mc(short_chain[-1].id(), [long_chain[-1].id()], k, states)
|
|
== short_chain[-1].id()
|
|
)
|
|
|
|
def test_fork_choice_integration(self):
|
|
n_a, n_b = Note(sk=0, value=10), Note(sk=1, value=10)
|
|
notes = [n_a, n_b]
|
|
config = mk_config(notes)
|
|
genesis = mk_genesis_state(notes)
|
|
follower = Follower(genesis, config)
|
|
|
|
b1 = mk_block(genesis.block, 1, n_a)
|
|
|
|
follower.on_block(b1)
|
|
|
|
assert follower.tip_id() == b1.id()
|
|
assert follower.forks == [], follower.forks
|
|
|
|
# -- then we fork --
|
|
#
|
|
# b2 == tip
|
|
# /
|
|
# b1
|
|
# \
|
|
# b3
|
|
#
|
|
|
|
b2 = mk_block(b1, 2, n_a)
|
|
b3 = mk_block(b1, 2, n_b)
|
|
|
|
follower.on_block(b2)
|
|
follower.on_block(b3)
|
|
|
|
assert follower.tip_id() == b2.id()
|
|
assert len(follower.forks) == 1 and follower.forks[0] == b3.id()
|
|
|
|
# -- extend the fork causing a re-org --
|
|
#
|
|
# b2
|
|
# /
|
|
# b1
|
|
# \
|
|
# b3 - b4 == tip
|
|
#
|
|
|
|
b4 = mk_block(b3, 3, n_b)
|
|
follower.on_block(b4)
|
|
|
|
assert follower.tip_id() == b4.id()
|
|
assert len(follower.forks) == 1 and follower.forks[0] == b2.id(), follower.forks
|
|
|
|
# -- switch to online mode --
|
|
#
|
|
# b2 (does not descend from the LIB and is thus pruned)
|
|
# /
|
|
# b1
|
|
# \
|
|
# b3 (LIB) - b4 == tip
|
|
#
|
|
follower.to_online()
|
|
assert follower.lib == b3.id(), follower.lib
|
|
assert len(follower.forks) == 0, follower.forks
|
|
assert b2.id() not in follower.forks
|
|
|
|
# -- extend a fork deeper than the LIB --
|
|
#
|
|
# - - - - - - b5
|
|
# /
|
|
# b1
|
|
# \
|
|
# b3 (LIB) - b4 == tip
|
|
#
|
|
b5 = mk_block(b1, 4, n_a)
|
|
with self.assertRaises(ImmutableFork):
|
|
follower.on_block(b5)
|
|
|
|
# -- extend the main chain shallower than k --
|
|
#
|
|
# b1
|
|
# \
|
|
# b3 - b4 (pruned)
|
|
# \
|
|
# - - b7 (LIB) - b8 == tip
|
|
b7 = mk_block(b3, 4, n_b)
|
|
b8 = mk_block(b7, 5, n_b)
|
|
|
|
follower.on_block(b7)
|
|
assert len(follower.forks) == 1 and b7.id() in follower.forks
|
|
|
|
follower.on_block(b8)
|
|
assert follower.tip_id() == b8.id()
|
|
# b4 was pruned as it forks deeper than the LIB
|
|
assert len(follower.forks) == 0, follower.forks
|
|
|
|
# Even in bootstrap mode, the follower should not accept blocks that fork deeper than k
|
|
follower.state = State.BOOTSTRAPPING
|
|
with self.assertRaises(ImmutableFork):
|
|
follower.on_block(b5)
|
|
|
|
# But it should switch a chain diverging more than k as long as it
|
|
# descends from the LIB
|
|
#
|
|
# b1
|
|
# \
|
|
# b3 - - - - - - - b10 - b11 - b12
|
|
# \ |
|
|
# - - b7 (LIB) - b8 - b9 == tip
|
|
b8 = mk_block(b7, 5, n_b)
|
|
b9 = mk_block(b8, 6, n_b)
|
|
b10 = mk_block(b7, 7, n_a)
|
|
b11 = mk_block(b10, 8, n_a)
|
|
b12 = mk_block(b11, 9, n_a)
|
|
follower.on_block(b8)
|
|
follower.on_block(b9)
|
|
|
|
assert follower.tip_id() == b9.id()
|
|
|
|
follower.on_block(b10)
|
|
follower.on_block(b11)
|
|
follower.on_block(b12)
|
|
|
|
assert follower.tip_id() == b12.id()
|
|
assert follower.lib == b7.id(), follower.lib |