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.
Files changed (101) hide show
  1. blockchainkit/__init__.py +12 -0
  2. blockchainkit/_validation.py +44 -0
  3. blockchainkit/consensus/__init__.py +66 -0
  4. blockchainkit/consensus/core/__init__.py +25 -0
  5. blockchainkit/consensus/core/base.py +97 -0
  6. blockchainkit/consensus/systems/__init__.py +46 -0
  7. blockchainkit/consensus/systems/byzantine.py +91 -0
  8. blockchainkit/consensus/systems/catch_up.py +60 -0
  9. blockchainkit/consensus/systems/difficulty.py +74 -0
  10. blockchainkit/consensus/systems/finality.py +92 -0
  11. blockchainkit/consensus/systems/fork_choice.py +44 -0
  12. blockchainkit/consensus/systems/pbft.py +78 -0
  13. blockchainkit/consensus/systems/pos.py +53 -0
  14. blockchainkit/consensus/systems/pow.py +58 -0
  15. blockchainkit/consensus/systems/pricing.py +50 -0
  16. blockchainkit/consensus/systems/randomized.py +114 -0
  17. blockchainkit/consensus/systems/selfish.py +81 -0
  18. blockchainkit/consensus/systems/sortition.py +48 -0
  19. blockchainkit/consensus/systems/stake_games.py +44 -0
  20. blockchainkit/consensus/systems/synchrony.py +49 -0
  21. blockchainkit/consensus/visualizers/__init__.py +9 -0
  22. blockchainkit/consensus/visualizers/plots.py +80 -0
  23. blockchainkit/constants.py +66 -0
  24. blockchainkit/crypto/__init__.py +157 -0
  25. blockchainkit/crypto/core/__init__.py +21 -0
  26. blockchainkit/crypto/core/base.py +109 -0
  27. blockchainkit/crypto/systems/__init__.py +137 -0
  28. blockchainkit/crypto/systems/asymmetric.py +136 -0
  29. blockchainkit/crypto/systems/commitments.py +93 -0
  30. blockchainkit/crypto/systems/curves.py +205 -0
  31. blockchainkit/crypto/systems/discrete_log.py +156 -0
  32. blockchainkit/crypto/systems/hashing.py +95 -0
  33. blockchainkit/crypto/systems/lamport.py +65 -0
  34. blockchainkit/crypto/systems/mac.py +34 -0
  35. blockchainkit/crypto/systems/merkle_damgard.py +139 -0
  36. blockchainkit/crypto/systems/multisig.py +98 -0
  37. blockchainkit/crypto/systems/one_time_pad.py +42 -0
  38. blockchainkit/crypto/systems/puzzles.py +96 -0
  39. blockchainkit/crypto/systems/sharing.py +159 -0
  40. blockchainkit/crypto/systems/signatures.py +232 -0
  41. blockchainkit/crypto/utils/__init__.py +5 -0
  42. blockchainkit/crypto/utils/primes.py +39 -0
  43. blockchainkit/crypto/visualizers/__init__.py +9 -0
  44. blockchainkit/crypto/visualizers/plots.py +88 -0
  45. blockchainkit/network/__init__.py +70 -0
  46. blockchainkit/network/core/__init__.py +21 -0
  47. blockchainkit/network/core/base.py +149 -0
  48. blockchainkit/network/systems/__init__.py +54 -0
  49. blockchainkit/network/systems/addresses.py +126 -0
  50. blockchainkit/network/systems/broadcast.py +124 -0
  51. blockchainkit/network/systems/clocks.py +149 -0
  52. blockchainkit/network/systems/epidemics.py +111 -0
  53. blockchainkit/network/systems/gossip.py +201 -0
  54. blockchainkit/network/systems/kademlia.py +147 -0
  55. blockchainkit/network/systems/privacy.py +98 -0
  56. blockchainkit/network/systems/propagation.py +60 -0
  57. blockchainkit/network/systems/relay.py +123 -0
  58. blockchainkit/network/systems/replication.py +122 -0
  59. blockchainkit/network/systems/topology.py +238 -0
  60. blockchainkit/network/visualizers/__init__.py +14 -0
  61. blockchainkit/network/visualizers/plots.py +177 -0
  62. blockchainkit/py.typed +0 -0
  63. blockchainkit/structures/__init__.py +61 -0
  64. blockchainkit/structures/core/__init__.py +21 -0
  65. blockchainkit/structures/core/base.py +100 -0
  66. blockchainkit/structures/systems/__init__.py +45 -0
  67. blockchainkit/structures/systems/block.py +144 -0
  68. blockchainkit/structures/systems/bloom.py +81 -0
  69. blockchainkit/structures/systems/chain.py +131 -0
  70. blockchainkit/structures/systems/hash_chain.py +35 -0
  71. blockchainkit/structures/systems/headers.py +54 -0
  72. blockchainkit/structures/systems/ledger.py +87 -0
  73. blockchainkit/structures/systems/merkle.py +273 -0
  74. blockchainkit/structures/systems/mmr.py +112 -0
  75. blockchainkit/structures/systems/sparse_merkle.py +139 -0
  76. blockchainkit/structures/systems/transaction.py +116 -0
  77. blockchainkit/structures/systems/utxo.py +156 -0
  78. blockchainkit/structures/utils/__init__.py +6 -0
  79. blockchainkit/structures/utils/accounts.py +13 -0
  80. blockchainkit/structures/utils/encoding.py +11 -0
  81. blockchainkit/structures/visualizers/__init__.py +13 -0
  82. blockchainkit/structures/visualizers/plots.py +154 -0
  83. blockchainkit/vm/__init__.py +60 -0
  84. blockchainkit/vm/core/__init__.py +25 -0
  85. blockchainkit/vm/core/base.py +171 -0
  86. blockchainkit/vm/systems/__init__.py +40 -0
  87. blockchainkit/vm/systems/assembler.py +80 -0
  88. blockchainkit/vm/systems/expressions.py +91 -0
  89. blockchainkit/vm/systems/programs.py +143 -0
  90. blockchainkit/vm/systems/reentrancy.py +70 -0
  91. blockchainkit/vm/systems/script.py +268 -0
  92. blockchainkit/vm/systems/stack_machine.py +239 -0
  93. blockchainkit/vm/systems/turing.py +111 -0
  94. blockchainkit/vm/systems/verifier.py +81 -0
  95. blockchainkit/vm/visualizers/__init__.py +9 -0
  96. blockchainkit/vm/visualizers/plots.py +97 -0
  97. blockchainkit-0.2.0.dist-info/METADATA +195 -0
  98. blockchainkit-0.2.0.dist-info/RECORD +101 -0
  99. blockchainkit-0.2.0.dist-info/WHEEL +5 -0
  100. blockchainkit-0.2.0.dist-info/licenses/LICENSE +21 -0
  101. 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