blockchainkit 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- blockchainkit/__init__.py +12 -0
- blockchainkit/_validation.py +44 -0
- blockchainkit/consensus/__init__.py +66 -0
- blockchainkit/consensus/core/__init__.py +25 -0
- blockchainkit/consensus/core/base.py +97 -0
- blockchainkit/consensus/systems/__init__.py +46 -0
- blockchainkit/consensus/systems/byzantine.py +91 -0
- blockchainkit/consensus/systems/catch_up.py +60 -0
- blockchainkit/consensus/systems/difficulty.py +74 -0
- blockchainkit/consensus/systems/finality.py +92 -0
- blockchainkit/consensus/systems/fork_choice.py +44 -0
- blockchainkit/consensus/systems/pbft.py +78 -0
- blockchainkit/consensus/systems/pos.py +53 -0
- blockchainkit/consensus/systems/pow.py +58 -0
- blockchainkit/consensus/systems/pricing.py +50 -0
- blockchainkit/consensus/systems/randomized.py +114 -0
- blockchainkit/consensus/systems/selfish.py +81 -0
- blockchainkit/consensus/systems/sortition.py +48 -0
- blockchainkit/consensus/systems/stake_games.py +44 -0
- blockchainkit/consensus/systems/synchrony.py +49 -0
- blockchainkit/consensus/visualizers/__init__.py +9 -0
- blockchainkit/consensus/visualizers/plots.py +80 -0
- blockchainkit/constants.py +66 -0
- blockchainkit/crypto/__init__.py +157 -0
- blockchainkit/crypto/core/__init__.py +21 -0
- blockchainkit/crypto/core/base.py +109 -0
- blockchainkit/crypto/systems/__init__.py +137 -0
- blockchainkit/crypto/systems/asymmetric.py +136 -0
- blockchainkit/crypto/systems/commitments.py +93 -0
- blockchainkit/crypto/systems/curves.py +205 -0
- blockchainkit/crypto/systems/discrete_log.py +156 -0
- blockchainkit/crypto/systems/hashing.py +95 -0
- blockchainkit/crypto/systems/lamport.py +65 -0
- blockchainkit/crypto/systems/mac.py +34 -0
- blockchainkit/crypto/systems/merkle_damgard.py +139 -0
- blockchainkit/crypto/systems/multisig.py +98 -0
- blockchainkit/crypto/systems/one_time_pad.py +42 -0
- blockchainkit/crypto/systems/puzzles.py +96 -0
- blockchainkit/crypto/systems/sharing.py +159 -0
- blockchainkit/crypto/systems/signatures.py +232 -0
- blockchainkit/crypto/utils/__init__.py +5 -0
- blockchainkit/crypto/utils/primes.py +39 -0
- blockchainkit/crypto/visualizers/__init__.py +9 -0
- blockchainkit/crypto/visualizers/plots.py +88 -0
- blockchainkit/network/__init__.py +70 -0
- blockchainkit/network/core/__init__.py +21 -0
- blockchainkit/network/core/base.py +149 -0
- blockchainkit/network/systems/__init__.py +54 -0
- blockchainkit/network/systems/addresses.py +126 -0
- blockchainkit/network/systems/broadcast.py +124 -0
- blockchainkit/network/systems/clocks.py +149 -0
- blockchainkit/network/systems/epidemics.py +111 -0
- blockchainkit/network/systems/gossip.py +201 -0
- blockchainkit/network/systems/kademlia.py +147 -0
- blockchainkit/network/systems/privacy.py +98 -0
- blockchainkit/network/systems/propagation.py +60 -0
- blockchainkit/network/systems/relay.py +123 -0
- blockchainkit/network/systems/replication.py +122 -0
- blockchainkit/network/systems/topology.py +238 -0
- blockchainkit/network/visualizers/__init__.py +14 -0
- blockchainkit/network/visualizers/plots.py +177 -0
- blockchainkit/py.typed +0 -0
- blockchainkit/structures/__init__.py +61 -0
- blockchainkit/structures/core/__init__.py +21 -0
- blockchainkit/structures/core/base.py +100 -0
- blockchainkit/structures/systems/__init__.py +45 -0
- blockchainkit/structures/systems/block.py +144 -0
- blockchainkit/structures/systems/bloom.py +81 -0
- blockchainkit/structures/systems/chain.py +131 -0
- blockchainkit/structures/systems/hash_chain.py +35 -0
- blockchainkit/structures/systems/headers.py +54 -0
- blockchainkit/structures/systems/ledger.py +87 -0
- blockchainkit/structures/systems/merkle.py +273 -0
- blockchainkit/structures/systems/mmr.py +112 -0
- blockchainkit/structures/systems/sparse_merkle.py +139 -0
- blockchainkit/structures/systems/transaction.py +116 -0
- blockchainkit/structures/systems/utxo.py +156 -0
- blockchainkit/structures/utils/__init__.py +6 -0
- blockchainkit/structures/utils/accounts.py +13 -0
- blockchainkit/structures/utils/encoding.py +11 -0
- blockchainkit/structures/visualizers/__init__.py +13 -0
- blockchainkit/structures/visualizers/plots.py +154 -0
- blockchainkit/vm/__init__.py +60 -0
- blockchainkit/vm/core/__init__.py +25 -0
- blockchainkit/vm/core/base.py +171 -0
- blockchainkit/vm/systems/__init__.py +40 -0
- blockchainkit/vm/systems/assembler.py +80 -0
- blockchainkit/vm/systems/expressions.py +91 -0
- blockchainkit/vm/systems/programs.py +143 -0
- blockchainkit/vm/systems/reentrancy.py +70 -0
- blockchainkit/vm/systems/script.py +268 -0
- blockchainkit/vm/systems/stack_machine.py +239 -0
- blockchainkit/vm/systems/turing.py +111 -0
- blockchainkit/vm/systems/verifier.py +81 -0
- blockchainkit/vm/visualizers/__init__.py +9 -0
- blockchainkit/vm/visualizers/plots.py +97 -0
- blockchainkit-0.2.0.dist-info/METADATA +195 -0
- blockchainkit-0.2.0.dist-info/RECORD +101 -0
- blockchainkit-0.2.0.dist-info/WHEEL +5 -0
- blockchainkit-0.2.0.dist-info/licenses/LICENSE +21 -0
- blockchainkit-0.2.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"""Learn cryptography and blockchain systems through reproducible experiments.
|
|
2
|
+
|
|
3
|
+
>>> import blockchainkit as bk
|
|
4
|
+
>>> bk.crypto.sha256(b"abc").hex()[:8]
|
|
5
|
+
'ba7816bf'
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from blockchainkit import consensus, constants, crypto, network, structures, vm
|
|
9
|
+
|
|
10
|
+
__version__ = "0.2.0"
|
|
11
|
+
|
|
12
|
+
__all__ = ["__version__", "consensus", "constants", "crypto", "network", "structures", "vm"]
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"""Argument validation shared by every blockchainkit subpackage."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def integer(value: int, name: str, minimum: int = 0) -> None:
|
|
5
|
+
"""Validate an integer with an inclusive lower bound.
|
|
6
|
+
|
|
7
|
+
Raises
|
|
8
|
+
------
|
|
9
|
+
TypeError
|
|
10
|
+
``value`` is not an ``int``. Booleans are rejected even though
|
|
11
|
+
``bool`` subclasses ``int``: ``True`` as a key or an amount is a bug.
|
|
12
|
+
ValueError
|
|
13
|
+
``value`` is below ``minimum``.
|
|
14
|
+
"""
|
|
15
|
+
if type(value) is not int:
|
|
16
|
+
raise TypeError(f"{name} must be an integer, not {type(value).__name__}")
|
|
17
|
+
if value < minimum:
|
|
18
|
+
raise ValueError(f"{name} must be an integer >= {minimum}")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def seed(value: int) -> None:
|
|
22
|
+
"""Validate a simulation seed: any integer, but not a bool, float, or string.
|
|
23
|
+
|
|
24
|
+
``random.Random`` silently accepts strings and bytes, so a typo would
|
|
25
|
+
still produce a reproducible but unintended stream.
|
|
26
|
+
"""
|
|
27
|
+
if type(value) is not int:
|
|
28
|
+
raise TypeError(f"seed must be an integer, not {type(value).__name__}")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def probability(value: float, name: str) -> None:
|
|
32
|
+
"""Validate a real number in [0, 1]; booleans are rejected like in :func:`integer`."""
|
|
33
|
+
if isinstance(value, bool) or not isinstance(value, (int, float)):
|
|
34
|
+
raise TypeError(f"{name} must be a real number in [0, 1]")
|
|
35
|
+
if not 0 <= value <= 1:
|
|
36
|
+
raise ValueError(f"{name} must be in [0, 1]")
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def positive(value: float, name: str) -> None:
|
|
40
|
+
"""Validate a finite real number greater than zero, such as a delay or an interval."""
|
|
41
|
+
if isinstance(value, bool) or not isinstance(value, (int, float)):
|
|
42
|
+
raise TypeError(f"{name} must be a positive real number")
|
|
43
|
+
if not 0 < value < float("inf"):
|
|
44
|
+
raise ValueError(f"{name} must be positive and finite")
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"""Consensus: agreement protocols, proof of work and stake, and the attacks they resist."""
|
|
2
|
+
|
|
3
|
+
from blockchainkit.consensus.core.base import (
|
|
4
|
+
ConsensusRun,
|
|
5
|
+
DifficultyRun,
|
|
6
|
+
GeneralsResult,
|
|
7
|
+
MiningResult,
|
|
8
|
+
Offense,
|
|
9
|
+
PBFTResult,
|
|
10
|
+
SelfishMiningResult,
|
|
11
|
+
SquareRootResult,
|
|
12
|
+
ViewChangeRun,
|
|
13
|
+
)
|
|
14
|
+
from blockchainkit.consensus.systems.byzantine import oral_messages
|
|
15
|
+
from blockchainkit.consensus.systems.catch_up import attacker_success_probability, eventual_catch_up
|
|
16
|
+
from blockchainkit.consensus.systems.difficulty import retarget, simulate_difficulty
|
|
17
|
+
from blockchainkit.consensus.systems.finality import FinalityGadget
|
|
18
|
+
from blockchainkit.consensus.systems.fork_choice import ghost_tip, subtree_work
|
|
19
|
+
from blockchainkit.consensus.systems.pbft import pbft_round, quorum_size
|
|
20
|
+
from blockchainkit.consensus.systems.pos import StakeSampler
|
|
21
|
+
from blockchainkit.consensus.systems.pow import expected_trials, mine, target, valid_pow
|
|
22
|
+
from blockchainkit.consensus.systems.pricing import modular_square_root
|
|
23
|
+
from blockchainkit.consensus.systems.randomized import ben_or
|
|
24
|
+
from blockchainkit.consensus.systems.selfish import (
|
|
25
|
+
selfish_mining_revenue,
|
|
26
|
+
selfish_mining_threshold,
|
|
27
|
+
simulate_selfish_mining,
|
|
28
|
+
)
|
|
29
|
+
from blockchainkit.consensus.systems.sortition import sortition
|
|
30
|
+
from blockchainkit.consensus.systems.stake_games import fork_voting_payoffs
|
|
31
|
+
from blockchainkit.consensus.systems.synchrony import view_changes
|
|
32
|
+
|
|
33
|
+
__all__ = [
|
|
34
|
+
"ConsensusRun",
|
|
35
|
+
"DifficultyRun",
|
|
36
|
+
"GeneralsResult",
|
|
37
|
+
"MiningResult",
|
|
38
|
+
"Offense",
|
|
39
|
+
"PBFTResult",
|
|
40
|
+
"SelfishMiningResult",
|
|
41
|
+
"SquareRootResult",
|
|
42
|
+
"ViewChangeRun",
|
|
43
|
+
"oral_messages",
|
|
44
|
+
"attacker_success_probability",
|
|
45
|
+
"eventual_catch_up",
|
|
46
|
+
"retarget",
|
|
47
|
+
"simulate_difficulty",
|
|
48
|
+
"FinalityGadget",
|
|
49
|
+
"ghost_tip",
|
|
50
|
+
"subtree_work",
|
|
51
|
+
"pbft_round",
|
|
52
|
+
"quorum_size",
|
|
53
|
+
"StakeSampler",
|
|
54
|
+
"expected_trials",
|
|
55
|
+
"mine",
|
|
56
|
+
"target",
|
|
57
|
+
"valid_pow",
|
|
58
|
+
"modular_square_root",
|
|
59
|
+
"ben_or",
|
|
60
|
+
"selfish_mining_revenue",
|
|
61
|
+
"selfish_mining_threshold",
|
|
62
|
+
"simulate_selfish_mining",
|
|
63
|
+
"sortition",
|
|
64
|
+
"fork_voting_payoffs",
|
|
65
|
+
"view_changes",
|
|
66
|
+
]
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""Result containers for blockchainkit.consensus."""
|
|
2
|
+
|
|
3
|
+
from blockchainkit.consensus.core.base import (
|
|
4
|
+
ConsensusRun,
|
|
5
|
+
DifficultyRun,
|
|
6
|
+
GeneralsResult,
|
|
7
|
+
MiningResult,
|
|
8
|
+
Offense,
|
|
9
|
+
PBFTResult,
|
|
10
|
+
SelfishMiningResult,
|
|
11
|
+
SquareRootResult,
|
|
12
|
+
ViewChangeRun,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
__all__ = [
|
|
16
|
+
"ConsensusRun",
|
|
17
|
+
"DifficultyRun",
|
|
18
|
+
"GeneralsResult",
|
|
19
|
+
"MiningResult",
|
|
20
|
+
"Offense",
|
|
21
|
+
"PBFTResult",
|
|
22
|
+
"SelfishMiningResult",
|
|
23
|
+
"SquareRootResult",
|
|
24
|
+
"ViewChangeRun",
|
|
25
|
+
]
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"""Result containers for blockchainkit.consensus."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import TYPE_CHECKING
|
|
5
|
+
|
|
6
|
+
if TYPE_CHECKING:
|
|
7
|
+
from blockchainkit.structures.systems.block import Block
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass(frozen=True)
|
|
11
|
+
class MiningResult:
|
|
12
|
+
"""A successful mined block and the number of hashes attempted."""
|
|
13
|
+
|
|
14
|
+
block: "Block"
|
|
15
|
+
attempts: int
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass(frozen=True)
|
|
19
|
+
class GeneralsResult:
|
|
20
|
+
"""Outcome of an oral-messages run.
|
|
21
|
+
|
|
22
|
+
Attributes
|
|
23
|
+
----------
|
|
24
|
+
decisions : dict
|
|
25
|
+
Each loyal lieutenant's decision.
|
|
26
|
+
agreement : bool
|
|
27
|
+
All loyal lieutenants decided the same (condition IC1).
|
|
28
|
+
validity : bool
|
|
29
|
+
If the commander is loyal, every loyal lieutenant followed its order
|
|
30
|
+
(condition IC2); vacuously true for a traitorous commander.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
decisions: dict[int, str]
|
|
34
|
+
agreement: bool
|
|
35
|
+
validity: bool
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass(frozen=True)
|
|
39
|
+
class ConsensusRun:
|
|
40
|
+
"""Outcome of a randomized-consensus run: who decided what, after how many rounds."""
|
|
41
|
+
|
|
42
|
+
decisions: dict[int, int]
|
|
43
|
+
rounds: int
|
|
44
|
+
decided: bool
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@dataclass(frozen=True)
|
|
48
|
+
class ViewChangeRun:
|
|
49
|
+
"""Views tried under partial synchrony, until the first one that made progress."""
|
|
50
|
+
|
|
51
|
+
view_starts: tuple[int, ...]
|
|
52
|
+
timeouts: tuple[int, ...]
|
|
53
|
+
decided_view: int
|
|
54
|
+
decision_time: int
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@dataclass(frozen=True)
|
|
58
|
+
class SquareRootResult:
|
|
59
|
+
"""A modular square root and the multiplications spent computing it."""
|
|
60
|
+
|
|
61
|
+
root: int
|
|
62
|
+
multiplications: int
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@dataclass(frozen=True)
|
|
66
|
+
class PBFTResult:
|
|
67
|
+
"""Values each honest replica prepared and committed (None if it did not)."""
|
|
68
|
+
|
|
69
|
+
prepared: dict[int, str | None]
|
|
70
|
+
commits: dict[int, str | None]
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@dataclass(frozen=True)
|
|
74
|
+
class DifficultyRun:
|
|
75
|
+
"""Simulated block intervals and the target in force for each block."""
|
|
76
|
+
|
|
77
|
+
block_times: tuple[float, ...]
|
|
78
|
+
targets: tuple[int, ...]
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
@dataclass(frozen=True)
|
|
82
|
+
class SelfishMiningResult:
|
|
83
|
+
"""Blocks each side contributed to the final chain, and the pool's share."""
|
|
84
|
+
|
|
85
|
+
selfish_blocks: int
|
|
86
|
+
honest_blocks: int
|
|
87
|
+
revenue: float
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
@dataclass(frozen=True)
|
|
91
|
+
class Offense:
|
|
92
|
+
"""Two votes by one validator that violate a Casper slashing condition."""
|
|
93
|
+
|
|
94
|
+
validator: str
|
|
95
|
+
kind: str
|
|
96
|
+
first: tuple[int, int, bytes]
|
|
97
|
+
second: tuple[int, int, bytes]
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""Concrete consensus mechanisms, agreement protocols, and attack models."""
|
|
2
|
+
|
|
3
|
+
from blockchainkit.consensus.systems.byzantine import oral_messages
|
|
4
|
+
from blockchainkit.consensus.systems.catch_up import attacker_success_probability, eventual_catch_up
|
|
5
|
+
from blockchainkit.consensus.systems.difficulty import retarget, simulate_difficulty
|
|
6
|
+
from blockchainkit.consensus.systems.finality import FinalityGadget
|
|
7
|
+
from blockchainkit.consensus.systems.fork_choice import ghost_tip, subtree_work
|
|
8
|
+
from blockchainkit.consensus.systems.pbft import pbft_round, quorum_size
|
|
9
|
+
from blockchainkit.consensus.systems.pos import StakeSampler
|
|
10
|
+
from blockchainkit.consensus.systems.pow import expected_trials, mine, target, valid_pow
|
|
11
|
+
from blockchainkit.consensus.systems.pricing import modular_square_root
|
|
12
|
+
from blockchainkit.consensus.systems.randomized import ben_or
|
|
13
|
+
from blockchainkit.consensus.systems.selfish import (
|
|
14
|
+
selfish_mining_revenue,
|
|
15
|
+
selfish_mining_threshold,
|
|
16
|
+
simulate_selfish_mining,
|
|
17
|
+
)
|
|
18
|
+
from blockchainkit.consensus.systems.sortition import sortition
|
|
19
|
+
from blockchainkit.consensus.systems.stake_games import fork_voting_payoffs
|
|
20
|
+
from blockchainkit.consensus.systems.synchrony import view_changes
|
|
21
|
+
|
|
22
|
+
__all__ = [
|
|
23
|
+
"oral_messages",
|
|
24
|
+
"attacker_success_probability",
|
|
25
|
+
"eventual_catch_up",
|
|
26
|
+
"retarget",
|
|
27
|
+
"simulate_difficulty",
|
|
28
|
+
"FinalityGadget",
|
|
29
|
+
"ghost_tip",
|
|
30
|
+
"subtree_work",
|
|
31
|
+
"pbft_round",
|
|
32
|
+
"quorum_size",
|
|
33
|
+
"StakeSampler",
|
|
34
|
+
"expected_trials",
|
|
35
|
+
"mine",
|
|
36
|
+
"target",
|
|
37
|
+
"valid_pow",
|
|
38
|
+
"modular_square_root",
|
|
39
|
+
"ben_or",
|
|
40
|
+
"selfish_mining_revenue",
|
|
41
|
+
"selfish_mining_threshold",
|
|
42
|
+
"simulate_selfish_mining",
|
|
43
|
+
"sortition",
|
|
44
|
+
"fork_voting_payoffs",
|
|
45
|
+
"view_changes",
|
|
46
|
+
]
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"""The Byzantine generals problem and the oral-messages algorithm (1982).
|
|
2
|
+
|
|
3
|
+
Lamport, Shostak and Pease asked how loyal generals can agree on a plan when
|
|
4
|
+
some generals, possibly including the commander, are traitors who send
|
|
5
|
+
different messages to different recipients. Their algorithm OM(m) has every
|
|
6
|
+
lieutenant relay what it heard, recursively, and take a majority. It
|
|
7
|
+
succeeds whenever there are more than three times as many generals as
|
|
8
|
+
traitors: n > 3m. With three generals and one traitor no algorithm can.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from collections import Counter
|
|
12
|
+
from collections.abc import Callable, Set
|
|
13
|
+
|
|
14
|
+
from blockchainkit._validation import integer
|
|
15
|
+
from blockchainkit.consensus.core.base import GeneralsResult
|
|
16
|
+
|
|
17
|
+
ATTACK, RETREAT = "attack", "retreat"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _default_lie(sender: int, receiver: int, value: str) -> str:
|
|
21
|
+
"""A traitor's strategy: tell even-numbered receivers to attack, odd ones to retreat."""
|
|
22
|
+
return ATTACK if receiver % 2 == 0 else RETREAT
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _majority(values: list[str]) -> str:
|
|
26
|
+
counts = Counter(values)
|
|
27
|
+
return ATTACK if counts[ATTACK] > counts[RETREAT] else RETREAT # Ties retreat.
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def oral_messages(
|
|
31
|
+
generals: int,
|
|
32
|
+
traitors: Set[int],
|
|
33
|
+
order: str,
|
|
34
|
+
*,
|
|
35
|
+
rounds: int,
|
|
36
|
+
lie: Callable[[int, int, str], str] = _default_lie,
|
|
37
|
+
) -> GeneralsResult:
|
|
38
|
+
"""Run OM(rounds) with general 0 as commander.
|
|
39
|
+
|
|
40
|
+
Parameters
|
|
41
|
+
----------
|
|
42
|
+
generals : int
|
|
43
|
+
Total number of generals n, at least 3; general 0 commands.
|
|
44
|
+
traitors : set of int
|
|
45
|
+
Traitorous generals; they send ``lie(sender, receiver, value)``
|
|
46
|
+
instead of the true value.
|
|
47
|
+
order : str
|
|
48
|
+
The commander's order, ``"attack"`` or ``"retreat"``.
|
|
49
|
+
rounds : int
|
|
50
|
+
The recursion depth m: OM(m) tolerates m traitors when n > 3m.
|
|
51
|
+
lie : callable
|
|
52
|
+
The traitors' strategy.
|
|
53
|
+
|
|
54
|
+
Returns
|
|
55
|
+
-------
|
|
56
|
+
GeneralsResult
|
|
57
|
+
Loyal lieutenants' decisions and whether IC1 and IC2 hold.
|
|
58
|
+
|
|
59
|
+
Examples
|
|
60
|
+
--------
|
|
61
|
+
>>> from blockchainkit.consensus import oral_messages
|
|
62
|
+
>>> oral_messages(4, {3}, "attack", rounds=1).decisions
|
|
63
|
+
{1: 'attack', 2: 'attack'}
|
|
64
|
+
"""
|
|
65
|
+
integer(generals, "generals", 3)
|
|
66
|
+
integer(rounds, "rounds")
|
|
67
|
+
if order not in (ATTACK, RETREAT):
|
|
68
|
+
raise ValueError("order must be 'attack' or 'retreat'")
|
|
69
|
+
if any(t not in range(generals) for t in traitors):
|
|
70
|
+
raise ValueError("traitors must be general numbers")
|
|
71
|
+
|
|
72
|
+
def send(sender: int, receiver: int, value: str) -> str:
|
|
73
|
+
return lie(sender, receiver, value) if sender in traitors else value
|
|
74
|
+
|
|
75
|
+
def om(m: int, commander: int, value: str, lieutenants: list[int]) -> dict[int, str]:
|
|
76
|
+
received = {lt: send(commander, lt, value) for lt in lieutenants}
|
|
77
|
+
if m == 0:
|
|
78
|
+
return received
|
|
79
|
+
relayed = {
|
|
80
|
+
j: om(m - 1, j, received[j], [lt for lt in lieutenants if lt != j]) for j in lieutenants
|
|
81
|
+
}
|
|
82
|
+
return {
|
|
83
|
+
lt: _majority([received[lt]] + [relayed[j][lt] for j in lieutenants if j != lt])
|
|
84
|
+
for lt in lieutenants
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
result = om(rounds, 0, order, list(range(1, generals)))
|
|
88
|
+
loyal = {g: v for g, v in result.items() if g not in traitors}
|
|
89
|
+
agreement = len(set(loyal.values())) <= 1
|
|
90
|
+
validity = 0 in traitors or all(v == order for v in loyal.values())
|
|
91
|
+
return GeneralsResult(loyal, agreement, validity)
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"""Idealized models of an attacker catching up with the honest chain."""
|
|
2
|
+
|
|
3
|
+
from math import exp
|
|
4
|
+
|
|
5
|
+
from blockchainkit._validation import integer
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def eventual_catch_up(attacker_fraction: float, deficit: int) -> float:
|
|
9
|
+
"""Return eventual catch-up probability in an ideal infinite random walk.
|
|
10
|
+
|
|
11
|
+
For q<1/2 this is (q/(1-q))**deficit. This is NOT Nakamoto's finite-
|
|
12
|
+
confirmation Poisson model: there is no propagation delay or time bound.
|
|
13
|
+
|
|
14
|
+
>>> from blockchainkit.consensus import eventual_catch_up
|
|
15
|
+
>>> eventual_catch_up(0.25, 2)
|
|
16
|
+
0.1111111111111111
|
|
17
|
+
"""
|
|
18
|
+
if isinstance(attacker_fraction, bool) or not isinstance(attacker_fraction, (int, float)):
|
|
19
|
+
raise TypeError("attacker_fraction must be a real number in [0, 1]")
|
|
20
|
+
if not 0 <= attacker_fraction <= 1:
|
|
21
|
+
raise ValueError("attacker_fraction must be in [0, 1]")
|
|
22
|
+
integer(deficit, "deficit")
|
|
23
|
+
if deficit == 0 or attacker_fraction >= 0.5:
|
|
24
|
+
return 1.0
|
|
25
|
+
return (attacker_fraction / (1 - attacker_fraction)) ** deficit
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def attacker_success_probability(attacker_fraction: float, confirmations: int) -> float:
|
|
29
|
+
"""Return Nakamoto's probability that an attacker ever overtakes ``z`` confirmations.
|
|
30
|
+
|
|
31
|
+
While the honest chain gains z blocks, the attacker's progress is
|
|
32
|
+
approximately Poisson with mean ``lambda = z q / p``. From each possible
|
|
33
|
+
lead k, the attacker still needs to make up ``z - k`` blocks, which by
|
|
34
|
+
:func:`eventual_catch_up` happens with probability ``(q/p)**(z-k)``
|
|
35
|
+
(whitepaper, section 11):
|
|
36
|
+
|
|
37
|
+
``P = 1 - sum_{k=0}^{z} e^(-lambda) lambda^k / k! * (1 - (q/p)^(z-k))``.
|
|
38
|
+
|
|
39
|
+
Examples
|
|
40
|
+
--------
|
|
41
|
+
>>> from blockchainkit.consensus import attacker_success_probability
|
|
42
|
+
>>> round(attacker_success_probability(0.1, 5), 7)
|
|
43
|
+
0.0009137
|
|
44
|
+
"""
|
|
45
|
+
if isinstance(attacker_fraction, bool) or not isinstance(attacker_fraction, (int, float)):
|
|
46
|
+
raise TypeError("attacker_fraction must be a real number in [0, 1]")
|
|
47
|
+
if not 0 <= attacker_fraction <= 1:
|
|
48
|
+
raise ValueError("attacker_fraction must be in [0, 1]")
|
|
49
|
+
integer(confirmations, "confirmations")
|
|
50
|
+
q, z = attacker_fraction, confirmations
|
|
51
|
+
if q >= 0.5:
|
|
52
|
+
return 1.0
|
|
53
|
+
p = 1 - q
|
|
54
|
+
lam = z * q / p
|
|
55
|
+
total, poisson = 1.0, exp(-lam)
|
|
56
|
+
for k in range(z + 1):
|
|
57
|
+
if k:
|
|
58
|
+
poisson *= lam / k
|
|
59
|
+
total -= poisson * (1 - (q / p) ** (z - k))
|
|
60
|
+
return max(0.0, total)
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"""Difficulty retargeting (Bitcoin 2009): hold the block interval steady as hashrate changes.
|
|
2
|
+
|
|
3
|
+
The whitepaper (section 4) says proof-of-work difficulty is set by a moving
|
|
4
|
+
average targeting an average number of blocks per hour. Bitcoin's first
|
|
5
|
+
release recomputes the target every 2016 blocks, scaling it by the ratio of
|
|
6
|
+
actual to expected time, clamped to a factor of four in either direction.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from collections.abc import Sequence
|
|
10
|
+
from random import Random
|
|
11
|
+
|
|
12
|
+
from blockchainkit._validation import integer
|
|
13
|
+
from blockchainkit._validation import seed as check_seed
|
|
14
|
+
from blockchainkit.consensus.core.base import DifficultyRun
|
|
15
|
+
|
|
16
|
+
_SCALE = 2**48
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def retarget(target: int, actual_time: int, expected_time: int) -> int:
|
|
20
|
+
"""Return ``target * actual / expected``, clamped to ``[target/4, 4*target]``.
|
|
21
|
+
|
|
22
|
+
A larger target is easier. Blocks that came too fast shrink it.
|
|
23
|
+
|
|
24
|
+
>>> from blockchainkit.consensus import retarget
|
|
25
|
+
>>> retarget(1000, actual_time=600, expected_time=1200)
|
|
26
|
+
500
|
|
27
|
+
"""
|
|
28
|
+
integer(target, "target", 1)
|
|
29
|
+
integer(actual_time, "actual_time", 1)
|
|
30
|
+
integer(expected_time, "expected_time", 1)
|
|
31
|
+
clamped = min(max(actual_time, expected_time // 4), expected_time * 4)
|
|
32
|
+
return max(1, target * clamped // expected_time)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def simulate_difficulty(
|
|
36
|
+
hashrates: Sequence[float], *, interval: int = 600, window: int = 2016, seed: int = 0
|
|
37
|
+
) -> DifficultyRun:
|
|
38
|
+
"""Simulate block discovery with exponential waiting times and periodic retargets.
|
|
39
|
+
|
|
40
|
+
With target T, a block needs about ``2**48 / T`` hashes on average, so at
|
|
41
|
+
hashrate h its waiting time is exponential with mean ``2**48 / (T h)``.
|
|
42
|
+
|
|
43
|
+
Parameters
|
|
44
|
+
----------
|
|
45
|
+
hashrates : sequence of float
|
|
46
|
+
The network hashrate while each successive block is mined.
|
|
47
|
+
interval : int
|
|
48
|
+
The target block time, in seconds.
|
|
49
|
+
window : int
|
|
50
|
+
Blocks between retargets.
|
|
51
|
+
seed : int
|
|
52
|
+
Seed for the waiting times.
|
|
53
|
+
|
|
54
|
+
Returns
|
|
55
|
+
-------
|
|
56
|
+
DifficultyRun
|
|
57
|
+
Each block's waiting time and the target it was mined under.
|
|
58
|
+
"""
|
|
59
|
+
integer(interval, "interval", 1)
|
|
60
|
+
integer(window, "window", 1)
|
|
61
|
+
check_seed(seed)
|
|
62
|
+
if not hashrates or any(h <= 0 for h in hashrates):
|
|
63
|
+
raise ValueError("hashrates must be positive")
|
|
64
|
+
rng = Random(seed)
|
|
65
|
+
target = max(1, round(_SCALE / (interval * hashrates[0])))
|
|
66
|
+
times: list[float] = []
|
|
67
|
+
targets: list[int] = []
|
|
68
|
+
for height, rate in enumerate(hashrates):
|
|
69
|
+
targets.append(target)
|
|
70
|
+
times.append(rng.expovariate(target * rate / _SCALE))
|
|
71
|
+
if (height + 1) % window == 0:
|
|
72
|
+
actual = max(1, round(sum(times[-window:])))
|
|
73
|
+
target = retarget(target, actual, interval * window)
|
|
74
|
+
return DifficultyRun(tuple(times), tuple(targets))
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"""Casper the Friendly Finality Gadget (Buterin and Griffith 2017).
|
|
2
|
+
|
|
3
|
+
Validators with deposits vote for links between checkpoints, ``source ->
|
|
4
|
+
target``. A checkpoint becomes *justified* when two thirds of the stake vote
|
|
5
|
+
for a link to it from a justified source, and its source becomes *finalized*
|
|
6
|
+
when the target is the very next checkpoint. Two slashing conditions make
|
|
7
|
+
conflicting finality cost at least a third of all stake: never cast two
|
|
8
|
+
different votes for the same target height, and never cast a vote that
|
|
9
|
+
surrounds another.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from collections.abc import Mapping
|
|
13
|
+
|
|
14
|
+
from blockchainkit._validation import integer
|
|
15
|
+
from blockchainkit.consensus.core.base import Offense
|
|
16
|
+
|
|
17
|
+
Vote = tuple[int, int, bytes]
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class FinalityGadget:
|
|
21
|
+
"""Track Casper FFG votes, justification, finality, and slashable offenses.
|
|
22
|
+
|
|
23
|
+
Checkpoint 0 (genesis) starts justified and finalized.
|
|
24
|
+
|
|
25
|
+
Parameters
|
|
26
|
+
----------
|
|
27
|
+
stakes : Mapping
|
|
28
|
+
Each validator's deposit.
|
|
29
|
+
|
|
30
|
+
Examples
|
|
31
|
+
--------
|
|
32
|
+
>>> from blockchainkit.consensus import FinalityGadget
|
|
33
|
+
>>> ffg = FinalityGadget({"a": 1, "b": 1, "c": 1})
|
|
34
|
+
>>> for v in "abc":
|
|
35
|
+
... ffg.vote(v, source=0, target=1)
|
|
36
|
+
>>> sorted(ffg.justified)
|
|
37
|
+
[0, 1]
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
def __init__(self, stakes: Mapping[str, int]) -> None:
|
|
41
|
+
for weight in stakes.values():
|
|
42
|
+
integer(weight, "stake", 1)
|
|
43
|
+
self._stakes = dict(stakes)
|
|
44
|
+
self._total = sum(self._stakes.values())
|
|
45
|
+
self._votes: dict[str, list[Vote]] = {v: [] for v in self._stakes}
|
|
46
|
+
self._justified, self._finalized = {0}, {0}
|
|
47
|
+
self._offenses: list[Offense] = []
|
|
48
|
+
|
|
49
|
+
@property
|
|
50
|
+
def justified(self) -> frozenset[int]:
|
|
51
|
+
"""Heights of justified checkpoints."""
|
|
52
|
+
return frozenset(self._justified)
|
|
53
|
+
|
|
54
|
+
@property
|
|
55
|
+
def finalized(self) -> frozenset[int]:
|
|
56
|
+
"""Heights of finalized checkpoints."""
|
|
57
|
+
return frozenset(self._finalized)
|
|
58
|
+
|
|
59
|
+
@property
|
|
60
|
+
def slashable(self) -> tuple[Offense, ...]:
|
|
61
|
+
"""Every pair of votes that breaks a slashing condition, in detection order."""
|
|
62
|
+
return tuple(self._offenses)
|
|
63
|
+
|
|
64
|
+
def vote(self, validator: str, source: int, target: int, checkpoint: bytes = b"") -> None:
|
|
65
|
+
"""Record a vote for the link ``source -> target`` and update finality.
|
|
66
|
+
|
|
67
|
+
``checkpoint`` distinguishes conflicting blocks at the same height.
|
|
68
|
+
"""
|
|
69
|
+
if validator not in self._stakes:
|
|
70
|
+
raise ValueError(f"unknown validator {validator!r}")
|
|
71
|
+
integer(source, "source")
|
|
72
|
+
integer(target, "target", source + 1)
|
|
73
|
+
new = (source, target, checkpoint)
|
|
74
|
+
for old in self._votes[validator]:
|
|
75
|
+
if old == new:
|
|
76
|
+
return
|
|
77
|
+
if old[1] == target:
|
|
78
|
+
self._offenses.append(Offense(validator, "double vote", old, new))
|
|
79
|
+
elif old[0] < source and target < old[1] or source < old[0] and old[1] < target:
|
|
80
|
+
self._offenses.append(Offense(validator, "surround vote", old, new))
|
|
81
|
+
self._votes[validator].append(new)
|
|
82
|
+
if source not in self._justified:
|
|
83
|
+
return
|
|
84
|
+
support = sum(
|
|
85
|
+
self._stakes[v]
|
|
86
|
+
for v, votes in self._votes.items()
|
|
87
|
+
if any(vote == new for vote in votes)
|
|
88
|
+
)
|
|
89
|
+
if 3 * support >= 2 * self._total:
|
|
90
|
+
self._justified.add(target)
|
|
91
|
+
if target == source + 1:
|
|
92
|
+
self._finalized.add(source)
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"""GHOST fork choice (Sompolinsky and Zohar 2013): follow the heaviest subtree.
|
|
2
|
+
|
|
3
|
+
When blocks are frequent, many honest blocks end up off the longest chain,
|
|
4
|
+
and that wasted work no longer protects it. GHOST (Greedy Heaviest Observed
|
|
5
|
+
SubTree) walks from genesis and at each fork picks the child whose whole
|
|
6
|
+
subtree carries the most work, counting the honest side branches too.
|
|
7
|
+
Ethereum's proof-of-work chain used a variant through uncle rewards.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from blockchainkit.consensus.systems.pow import expected_trials
|
|
11
|
+
from blockchainkit.structures.systems.block import Block
|
|
12
|
+
from blockchainkit.structures.systems.chain import Blockchain
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _children(chain: Blockchain) -> dict[bytes, list[Block]]:
|
|
16
|
+
children: dict[bytes, list[Block]] = {}
|
|
17
|
+
for block in chain.blocks.values():
|
|
18
|
+
if block.height:
|
|
19
|
+
children.setdefault(block.previous_hash, []).append(block)
|
|
20
|
+
return children
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def subtree_work(chain: Blockchain, block_hash: bytes) -> int:
|
|
24
|
+
"""Return the expected work of a block and all its descendants."""
|
|
25
|
+
children = _children(chain)
|
|
26
|
+
stack, total = [chain.blocks[block_hash]], 0
|
|
27
|
+
while stack:
|
|
28
|
+
block = stack.pop()
|
|
29
|
+
total += expected_trials(block.difficulty)
|
|
30
|
+
stack.extend(children.get(block.hash, []))
|
|
31
|
+
return total
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def ghost_tip(chain: Blockchain) -> Block:
|
|
35
|
+
"""Return the tip chosen by GHOST: descend into the heaviest subtree at every fork.
|
|
36
|
+
|
|
37
|
+
Ties go to the smaller hash, matching
|
|
38
|
+
:class:`~blockchainkit.structures.systems.chain.Blockchain`.
|
|
39
|
+
"""
|
|
40
|
+
children = _children(chain)
|
|
41
|
+
block = chain.canonical_blocks()[0]
|
|
42
|
+
while children.get(block.hash):
|
|
43
|
+
block = min(children[block.hash], key=lambda c: (-subtree_work(chain, c.hash), c.hash))
|
|
44
|
+
return block
|