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,78 @@
1
+ """Practical Byzantine Fault Tolerance (Castro and Liskov 1999): the normal-case round.
2
+
3
+ With n = 3f + 1 replicas, any two quorums of 2f + 1 overlap in at least f + 1
4
+ replicas, so at least one honest replica is in both. A replica *prepares* a
5
+ value after 2f matching prepare messages and *commits* it after 2f + 1
6
+ matching commits. Two honest replicas can then never commit different values
7
+ in one view, even if the leader equivocates, as long as at most f replicas
8
+ are faulty.
9
+ """
10
+
11
+ from collections.abc import Set
12
+
13
+ from blockchainkit._validation import integer
14
+ from blockchainkit.consensus.core.base import PBFTResult
15
+
16
+
17
+ def quorum_size(replicas: int) -> int:
18
+ """Return 2f + 1 for the largest f with n >= 3f + 1.
19
+
20
+ >>> from blockchainkit.consensus import quorum_size
21
+ >>> quorum_size(4), quorum_size(7)
22
+ (3, 5)
23
+ """
24
+ integer(replicas, "replicas", 4)
25
+ return 2 * ((replicas - 1) // 3) + 1
26
+
27
+
28
+ def pbft_round(
29
+ replicas: int, faulty: Set[int], value: str, *, equivocate: bool = False, other: str = "B"
30
+ ) -> PBFTResult:
31
+ """Run one PBFT pre-prepare / prepare / commit exchange with leader 0.
32
+
33
+ Faulty replicas send prepare and commit messages for *both* values to
34
+ everyone, the most confusing thing they can do. A faulty leader with
35
+ ``equivocate=True`` pre-prepares ``value`` to the first half of the honest
36
+ replicas and ``other`` to the rest.
37
+
38
+ Parameters
39
+ ----------
40
+ replicas : int
41
+ Number of replicas n, at least 4.
42
+ faulty : set of int
43
+ Faulty replica numbers. The protocol is sized for f = (n - 1) // 3;
44
+ pass more to see safety fail.
45
+ value, other : str
46
+ The leader's value, and the conflicting one an equivocating leader
47
+ also sends.
48
+
49
+ Returns
50
+ -------
51
+ PBFTResult
52
+ What each honest replica prepared and committed.
53
+ """
54
+ f = (quorum_size(replicas) - 1) // 2
55
+ if any(r not in range(replicas) for r in faulty):
56
+ raise ValueError("faulty replicas must be replica numbers")
57
+ honest = [r for r in range(replicas) if r not in faulty]
58
+ faulty_backups = len(faulty - {0})
59
+
60
+ # 1. Pre-prepare: the leader (replica 0) proposes a value to each replica.
61
+ equivocating = 0 in faulty and equivocate
62
+ split = (len(honest) + 1) // 2
63
+ proposal = {r: other if equivocating and i >= split else value for i, r in enumerate(honest)}
64
+
65
+ # 2. Prepare: honest backups echo their proposal; faulty backups echo both values.
66
+ def prepares(v: str) -> int:
67
+ return sum(1 for r in honest if r != 0 and proposal[r] == v) + faulty_backups
68
+
69
+ prepared = {r: proposal[r] if prepares(proposal[r]) >= 2 * f else None for r in honest}
70
+
71
+ # 3. Commit: honest replicas commit what they prepared; faulty ones commit both.
72
+ def commits(v: str) -> int:
73
+ return sum(1 for r in honest if prepared[r] == v) + len(faulty)
74
+
75
+ committed = {
76
+ r: v if v is not None and commits(v) >= 2 * f + 1 else None for r, v in prepared.items()
77
+ }
78
+ return PBFTResult(prepared, committed)
@@ -0,0 +1,53 @@
1
+ """Stake-weighted proposer sampling, not a complete PoS consensus protocol."""
2
+
3
+ from bisect import bisect_right
4
+ from collections.abc import Mapping
5
+ from itertools import accumulate
6
+ from random import Random
7
+
8
+ from blockchainkit._validation import integer
9
+ from blockchainkit._validation import seed as check_seed
10
+
11
+
12
+ class StakeSampler:
13
+ """Select proposers with exact integer weights and reproducible randomness.
14
+
15
+ Parameters
16
+ ----------
17
+ stakes : Mapping
18
+ Nonnegative weights with a positive total. Names are sorted so mapping
19
+ insertion order does not alter results.
20
+ seed : int
21
+ Simulation seed, not an unpredictable consensus randomness beacon.
22
+
23
+ Examples
24
+ --------
25
+ >>> from blockchainkit.consensus import StakeSampler
26
+ >>> StakeSampler({"alice": 1, "bob": 0}, seed=7).sample(3)
27
+ ('alice', 'alice', 'alice')
28
+ """
29
+
30
+ def __init__(self, stakes: Mapping[str, int], *, seed: int = 0) -> None:
31
+ check_seed(seed)
32
+ if not stakes or any(not isinstance(name, str) or not name for name in stakes):
33
+ raise ValueError("provide nonempty validator names")
34
+ for weight in stakes.values():
35
+ integer(weight, "stake")
36
+ self._stakes = tuple(sorted(stakes.items()))
37
+ self._total = sum(stakes.values())
38
+ if self._total == 0:
39
+ raise ValueError("total stake must be positive")
40
+ self._cumulative = tuple(accumulate(weight for _, weight in self._stakes))
41
+ self._random = Random(seed)
42
+
43
+ def choose(self) -> str:
44
+ """Select one validator with probability stake / total stake."""
45
+ ticket = self._random.randrange(self._total)
46
+ # ticket < total guarantees an index inside the cumulative weights.
47
+ # bisect_right skips repeated boundaries from zero-weight validators.
48
+ return self._stakes[bisect_right(self._cumulative, ticket)][0]
49
+
50
+ def sample(self, rounds: int) -> tuple[str, ...]:
51
+ """Return proposers for a bounded number of rounds."""
52
+ integer(rounds, "rounds")
53
+ return tuple(self.choose() for _ in range(rounds))
@@ -0,0 +1,58 @@
1
+ """Bounded proof-of-work experiments with explicit success probabilities."""
2
+
3
+ from dataclasses import replace
4
+ from typing import TYPE_CHECKING
5
+
6
+ from blockchainkit._validation import integer
7
+ from blockchainkit.consensus.core.base import MiningResult
8
+ from blockchainkit.constants import UINT64_LIMIT
9
+ from blockchainkit.crypto.systems.hashing import sha256
10
+
11
+ if TYPE_CHECKING:
12
+ from blockchainkit.structures.systems.block import Block
13
+
14
+
15
+ def target(difficulty: int) -> int:
16
+ """Return the inclusive target for a leading-zero-bit difficulty.
17
+
18
+ >>> from blockchainkit.consensus import target
19
+ >>> target(0) == 2**256 - 1
20
+ True
21
+ """
22
+ integer(difficulty, "difficulty")
23
+ if difficulty > 256:
24
+ raise ValueError("difficulty cannot exceed 256")
25
+ return (1 << (256 - difficulty)) - 1
26
+
27
+
28
+ def expected_trials(difficulty: int) -> int:
29
+ """Return 2**difficulty, assuming independent uniform 256-bit hashes."""
30
+ target(difficulty)
31
+ return 1 << difficulty
32
+
33
+
34
+ def valid_pow(block: "Block") -> bool:
35
+ """Check that the block hash, interpreted big-endian, is at most its target."""
36
+ return int.from_bytes(block.hash, "big") <= target(block.difficulty)
37
+
38
+
39
+ def mine(block: "Block", *, max_attempts: int = 100_000) -> MiningResult:
40
+ """Search sequential nonces starting at block.nonce, with a strict bound.
41
+
42
+ Raises
43
+ ------
44
+ TimeoutError
45
+ No solution was found within max_attempts (not evidence of no solution).
46
+ ValueError
47
+ The search would exceed the 64-bit nonce space.
48
+ """
49
+ integer(max_attempts, "max_attempts", 1)
50
+ if block.nonce + max_attempts > UINT64_LIMIT:
51
+ raise ValueError("search exceeds the 64-bit nonce space")
52
+ limit = target(block.difficulty)
53
+ # Only the header changes between attempts; the Merkle root is computed once.
54
+ for attempt in range(max_attempts):
55
+ nonce = block.nonce + attempt
56
+ if int.from_bytes(sha256(block.header(nonce=nonce)), "big") <= limit:
57
+ return MiningResult(replace(block, nonce=nonce), attempt + 1)
58
+ raise TimeoutError(f"no solution in {max_attempts} attempts")
@@ -0,0 +1,50 @@
1
+ """Pricing via processing (Dwork and Naor 1992): make each message cost computation.
2
+
3
+ To fight junk mail, Dwork and Naor proposed that a sender compute a
4
+ *pricing function*, moderately hard to evaluate but easy to check, for each
5
+ message. One of their examples is a square root modulo a prime: computing
6
+ it takes an exponentiation, checking it takes one multiplication.
7
+ Back's Hashcash later used hash preimages for the same purpose, and
8
+ Bitcoin's proof of work descends from that.
9
+ """
10
+
11
+ from blockchainkit._validation import integer
12
+ from blockchainkit.consensus.core.base import SquareRootResult
13
+ from blockchainkit.crypto.utils.primes import is_prime
14
+
15
+
16
+ def modular_square_root(value: int, prime: int) -> SquareRootResult:
17
+ """Compute a square root modulo a prime ``p = 3 (mod 4)``, counting multiplications.
18
+
19
+ For such primes, ``value**((p + 1) / 4)`` is a square root of any
20
+ quadratic residue. Square-and-multiply needs about ``1.5 log2 p``
21
+ multiplications; checking the answer needs one.
22
+
23
+ Raises
24
+ ------
25
+ ValueError
26
+ ``prime`` is not a prime congruent to 3 modulo 4, or ``value`` has no
27
+ square root (it is not a quadratic residue).
28
+
29
+ Examples
30
+ --------
31
+ >>> from blockchainkit.consensus import modular_square_root
32
+ >>> result = modular_square_root(4, 7)
33
+ >>> result.root ** 2 % 7
34
+ 4
35
+ """
36
+ integer(value, "value")
37
+ integer(prime, "prime", 3)
38
+ if not is_prime(prime) or prime % 4 != 3:
39
+ raise ValueError("prime must be a prime congruent to 3 modulo 4")
40
+ exponent, base, root, multiplications = (prime + 1) // 4, value % prime, 1, 0
41
+ while exponent:
42
+ if exponent & 1:
43
+ root = root * base % prime
44
+ multiplications += 1
45
+ base = base * base % prime
46
+ multiplications += 1
47
+ exponent >>= 1
48
+ if root * root % prime != value % prime:
49
+ raise ValueError("value is not a quadratic residue modulo prime")
50
+ return SquareRootResult(root, multiplications)
@@ -0,0 +1,114 @@
1
+ """Ben-Or's randomized consensus (1983), and the FLP impossibility (1985) it sidesteps.
2
+
3
+ Fischer, Lynch and Paterson proved that no *deterministic* protocol can
4
+ guarantee agreement in an asynchronous network if even one process may
5
+ crash: an adversary scheduling message delivery can keep it undecided
6
+ forever. Ben-Or's protocol escapes by tossing coins. In each round processes
7
+ exchange values, propose a strong majority if they see one, and adopt a
8
+ proposal or flip a coin. Whatever the schedule, the coins eventually line up.
9
+ """
10
+
11
+ from collections.abc import Callable, Sequence
12
+ from random import Random
13
+
14
+ from blockchainkit._validation import integer
15
+ from blockchainkit._validation import seed as check_seed
16
+ from blockchainkit.consensus.core.base import ConsensusRun
17
+
18
+ _UNDECIDED = -1
19
+
20
+
21
+ def _adversarial_pick(values: dict[int, int], quota: int, favor_mixed: bool) -> list[int]:
22
+ """Choose ``quota`` senders that keep the receiver as undecided as possible."""
23
+ ones = [p for p, v in values.items() if v == 1]
24
+ zeros = [p for p, v in values.items() if v == 0]
25
+ others = [p for p, v in values.items() if v not in (0, 1)]
26
+ if favor_mixed: # Phase 1: alternate values so no strong majority appears.
27
+ order: list[int] = []
28
+ while zeros or ones:
29
+ for group in (zeros, ones):
30
+ if group:
31
+ order.append(group.pop())
32
+ return order[:quota]
33
+ return (others + zeros + ones)[:quota] # Phase 2: as few concrete proposals as possible.
34
+
35
+
36
+ def ben_or(
37
+ initial: Sequence[int],
38
+ faults: int,
39
+ *,
40
+ coin: Callable[[int, int], int] | None = None,
41
+ adversarial: bool = True,
42
+ seed: int = 0,
43
+ max_rounds: int = 1000,
44
+ ) -> ConsensusRun:
45
+ """Run Ben-Or's binary consensus for crash faults (n > 2f).
46
+
47
+ Each process waits for ``n - f`` messages per phase, the most it can
48
+ wait for when f processes may have crashed; the scheduler chooses which.
49
+
50
+ Parameters
51
+ ----------
52
+ initial : sequence of int
53
+ Each process's input bit.
54
+ faults : int
55
+ The number of crashes f the protocol is sized for.
56
+ coin : callable, optional
57
+ ``coin(process, round) -> 0 or 1``. Defaults to a seeded fair coin.
58
+ Pass a deterministic function to see the FLP adversary win.
59
+ adversarial : bool
60
+ If True, the scheduler delivers the messages most likely to delay a
61
+ decision; otherwise a random ``n - f`` subset.
62
+ seed : int
63
+ Seed for the coin and the random scheduler.
64
+ max_rounds : int
65
+ Give up after this many rounds.
66
+
67
+ Returns
68
+ -------
69
+ ConsensusRun
70
+ Decisions so far, rounds run, and whether every process decided.
71
+
72
+ Examples
73
+ --------
74
+ >>> from blockchainkit.consensus import ben_or
75
+ >>> ben_or([1, 1, 1], faults=1).decisions
76
+ {0: 1, 1: 1, 2: 1}
77
+ """
78
+ n = len(initial)
79
+ integer(faults, "faults")
80
+ integer(max_rounds, "max_rounds", 1)
81
+ check_seed(seed)
82
+ if n <= 2 * faults:
83
+ raise ValueError("Ben-Or needs n > 2f processes")
84
+ if any(v not in (0, 1) for v in initial):
85
+ raise ValueError("inputs must be bits")
86
+ rng = Random(seed)
87
+ flip = coin or (lambda process, round_: rng.getrandbits(1))
88
+ quota = n - faults
89
+ values = dict(enumerate(initial))
90
+ decisions: dict[int, int] = {}
91
+ for round_ in range(1, max_rounds + 1):
92
+ proposals = {}
93
+ for p in range(n):
94
+ heard = (
95
+ _adversarial_pick(dict(values), quota, True)
96
+ if adversarial
97
+ else rng.sample(range(n), quota)
98
+ )
99
+ counts = [sum(values[q] == bit for q in heard) for bit in (0, 1)]
100
+ strong = [bit for bit in (0, 1) if counts[bit] > n / 2]
101
+ proposals[p] = strong[0] if strong else _UNDECIDED
102
+ for p in range(n):
103
+ heard = (
104
+ _adversarial_pick(dict(proposals), quota, False)
105
+ if adversarial
106
+ else rng.sample(range(n), quota)
107
+ )
108
+ seen = [proposals[q] for q in heard if proposals[q] != _UNDECIDED]
109
+ if seen and len(seen) >= faults + 1:
110
+ decisions.setdefault(p, seen[0])
111
+ values[p] = seen[0] if seen else flip(p, round_)
112
+ if len(decisions) == n:
113
+ return ConsensusRun(decisions, round_, True)
114
+ return ConsensusRun(decisions, max_rounds, False)
@@ -0,0 +1,81 @@
1
+ """Selfish mining (Eyal and Sirer 2014): majority is not enough.
2
+
3
+ A pool that withholds the blocks it finds, and publishes them only to
4
+ overtake or tie the honest chain, wastes the honest miners' work. Above a
5
+ threshold hashrate its share of the chain exceeds its share of the hashrate,
6
+ so honest miners gain by joining it. ``gamma`` is the fraction of honest
7
+ miners who build on the pool's block during a tie.
8
+ """
9
+
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 SelfishMiningResult
15
+
16
+
17
+ def _check(alpha: float, gamma: float) -> None:
18
+ if not 0 < alpha < 0.5:
19
+ raise ValueError("alpha must be in (0, 0.5)")
20
+ if not 0 <= gamma <= 1:
21
+ raise ValueError("gamma must be in [0, 1]")
22
+
23
+
24
+ def selfish_mining_revenue(alpha: float, gamma: float) -> float:
25
+ """Return the selfish pool's long-run share of blocks (Eyal and Sirer, eq. 8).
26
+
27
+ ``R = (alpha (1-alpha)**2 (4 alpha + gamma (1 - 2 alpha)) - alpha**3)
28
+ / (1 - alpha (1 + (2 - alpha) alpha))``
29
+
30
+ >>> from blockchainkit.consensus import selfish_mining_revenue
31
+ >>> round(selfish_mining_revenue(0.4, 0.0), 3)
32
+ 0.484
33
+ """
34
+ _check(alpha, gamma)
35
+ a = alpha
36
+ numerator = a * (1 - a) ** 2 * (4 * a + gamma * (1 - 2 * a)) - a**3
37
+ return float(numerator / (1 - a * (1 + (2 - a) * a)))
38
+
39
+
40
+ def selfish_mining_threshold(gamma: float) -> float:
41
+ """Return the hashrate above which selfish mining pays: ``(1 - gamma) / (3 - 2 gamma)``."""
42
+ if not 0 <= gamma <= 1:
43
+ raise ValueError("gamma must be in [0, 1]")
44
+ return (1 - gamma) / (3 - 2 * gamma)
45
+
46
+
47
+ def simulate_selfish_mining(
48
+ alpha: float, gamma: float, *, blocks: int, seed: int = 0
49
+ ) -> SelfishMiningResult:
50
+ """Simulate the selfish-mining state machine (Eyal and Sirer, Algorithm 1).
51
+
52
+ Each event, the pool finds a block with probability alpha, otherwise the
53
+ honest network does. The state is the pool's private lead, plus a tie
54
+ state after the pool publishes to match an honest block.
55
+ """
56
+ _check(alpha, gamma)
57
+ integer(blocks, "blocks", 1)
58
+ check_seed(seed)
59
+ rng = Random(seed)
60
+ lead, tie, selfish, honest = 0, False, 0, 0
61
+ for _ in range(blocks):
62
+ pool_found = rng.random() < alpha
63
+ if tie:
64
+ if pool_found:
65
+ selfish += 2 # The pool extends its own branch and wins both blocks.
66
+ elif rng.random() < gamma:
67
+ selfish, honest = selfish + 1, honest + 1 # Honest block on the pool's branch.
68
+ else:
69
+ honest += 2 # Honest block on the honest branch.
70
+ tie = False
71
+ elif pool_found:
72
+ lead += 1
73
+ elif lead == 0:
74
+ honest += 1
75
+ elif lead == 1:
76
+ lead, tie = 0, True # Publish and race.
77
+ elif lead == 2:
78
+ lead, selfish = 0, selfish + 2 # Publish both and orphan the honest block.
79
+ else:
80
+ lead, selfish = lead - 1, selfish + 1 # Reveal one block; stay ahead.
81
+ return SelfishMiningResult(selfish, honest, selfish / max(1, selfish + honest))
@@ -0,0 +1,48 @@
1
+ """Cryptographic sortition (Algorand 2017): secret, stake-weighted committee selection.
2
+
3
+ Algorand picks each round's committee by lottery. Every unit of stake is a
4
+ ticket; a user hashes a round seed with their private key (in Algorand a
5
+ verifiable random function, so others can check the result) and maps the
6
+ hash to a number of selected tickets that is Binomial(stake, tau / W).
7
+ Nobody, not even the user, knows who is on the committee before they speak,
8
+ and splitting stake across accounts does not change the expected seats.
9
+ """
10
+
11
+ from math import comb
12
+
13
+ from blockchainkit._validation import integer
14
+ from blockchainkit.constants import SORTITION_DOMAIN
15
+ from blockchainkit.crypto.systems.hashing import sha256
16
+
17
+
18
+ def sortition(
19
+ secret: bytes, stake: int, total_stake: int, expected: int, *, round_seed: bytes
20
+ ) -> int:
21
+ """Return how many of a user's stake units are selected this round.
22
+
23
+ The hash of ``secret || round_seed``, read as a uniform number in [0, 1),
24
+ is located in the cumulative Binomial(stake, expected / total_stake)
25
+ distribution. Teaching simplification: a hash of the secret replaces
26
+ the verifiable random function, so others cannot verify the draw.
27
+
28
+ Examples
29
+ --------
30
+ >>> from blockchainkit.consensus import sortition
31
+ >>> sortition(b"my key", 0, 1000, 20, round_seed=b"r1")
32
+ 0
33
+ """
34
+ if not isinstance(secret, bytes) or not isinstance(round_seed, bytes):
35
+ raise TypeError("secret and round_seed must be bytes")
36
+ integer(stake, "stake")
37
+ integer(total_stake, "total_stake", 1)
38
+ integer(expected, "expected", 1)
39
+ if stake > total_stake or expected > total_stake:
40
+ raise ValueError("stake and expected committee size cannot exceed total stake")
41
+ draw = int.from_bytes(sha256(SORTITION_DOMAIN + secret + round_seed), "big") / 2**256
42
+ p = expected / total_stake
43
+ cumulative = 0.0
44
+ for j in range(stake):
45
+ cumulative += comb(stake, j) * p**j * (1 - p) ** (stake - j)
46
+ if draw < cumulative:
47
+ return j
48
+ return stake # The last bucket, which also absorbs floating-point rounding.
@@ -0,0 +1,44 @@
1
+ """The nothing-at-stake problem (Buterin 2014): voting on every fork is free.
2
+
3
+ A proof-of-work miner must split its hashrate between forks. A proof-of-stake
4
+ validator can sign blocks on every fork at no cost, and collect the reward
5
+ whichever fork wins. If everyone does, forks never resolve. The remedy,
6
+ first proposed in Buterin's Slasher, is a penalty: evidence of signing two
7
+ conflicting blocks destroys the validator's deposit.
8
+ """
9
+
10
+
11
+ def fork_voting_payoffs(
12
+ fork_a_probability: float, reward: float, penalty: float
13
+ ) -> dict[str, float]:
14
+ """Expected payoff of voting for fork A, fork B, or both.
15
+
16
+ Parameters
17
+ ----------
18
+ fork_a_probability : float
19
+ Chance that fork A becomes canonical.
20
+ reward : float
21
+ Reward for having voted on the winning fork.
22
+ penalty : float
23
+ Deposit destroyed when a validator is caught voting on both (slashing).
24
+
25
+ Returns
26
+ -------
27
+ dict
28
+ Expected payoff of the strategies ``"A"``, ``"B"`` and ``"both"``.
29
+
30
+ Examples
31
+ --------
32
+ >>> from blockchainkit.consensus import fork_voting_payoffs
33
+ >>> fork_voting_payoffs(0.5, 1.0, 0.0)
34
+ {'A': 0.5, 'B': 0.5, 'both': 1.0}
35
+ """
36
+ if not 0 <= fork_a_probability <= 1:
37
+ raise ValueError("fork_a_probability must be in [0, 1]")
38
+ if reward < 0 or penalty < 0:
39
+ raise ValueError("reward and penalty must be non-negative")
40
+ return {
41
+ "A": fork_a_probability * reward,
42
+ "B": (1 - fork_a_probability) * reward,
43
+ "both": reward - penalty,
44
+ }
@@ -0,0 +1,49 @@
1
+ """Partial synchrony (Dwork, Lynch and Stockmeyer 1988): progress after an unknown GST.
2
+
3
+ Real networks are neither synchronous (known delay bound) nor fully
4
+ asynchronous (FLP applies). Partial synchrony assumes a bound Delta holds
5
+ after some unknown Global Stabilization Time (GST). Protocols stay safe
6
+ always, and make progress once a leader's view lasts long enough: doubling
7
+ the timeout each view guarantees that eventually it exceeds Delta.
8
+ """
9
+
10
+ from blockchainkit._validation import integer
11
+ from blockchainkit.consensus.core.base import ViewChangeRun
12
+
13
+
14
+ def view_changes(
15
+ gst: int, delta: int, base_timeout: int, *, growth: int = 2, max_views: int = 64
16
+ ) -> ViewChangeRun:
17
+ """Simulate leader views with growing timeouts until one makes progress.
18
+
19
+ Before GST the adversary delays the leader's message past every timeout;
20
+ from GST on it arrives after ``delta`` ticks. View v lasts
21
+ ``base_timeout * growth**v`` and succeeds when it starts at or after GST
22
+ and its timeout is at least ``delta``.
23
+
24
+ Raises
25
+ ------
26
+ TimeoutError
27
+ No view succeeded within ``max_views`` (for example, timeouts that
28
+ never grow past delta).
29
+
30
+ Examples
31
+ --------
32
+ >>> from blockchainkit.consensus import view_changes
33
+ >>> view_changes(gst=10, delta=5, base_timeout=1).decided_view
34
+ 4
35
+ """
36
+ integer(gst, "gst")
37
+ integer(delta, "delta", 1)
38
+ integer(base_timeout, "base_timeout", 1)
39
+ integer(growth, "growth", 1)
40
+ integer(max_views, "max_views", 1)
41
+ starts, timeouts, time = [], [], 0
42
+ for view in range(max_views):
43
+ timeout = base_timeout * growth**view
44
+ starts.append(time)
45
+ timeouts.append(timeout)
46
+ if time >= gst and delta <= timeout:
47
+ return ViewChangeRun(tuple(starts), tuple(timeouts), view, time + delta)
48
+ time += timeout
49
+ raise TimeoutError(f"no view made progress in {max_views} views")
@@ -0,0 +1,9 @@
1
+ """Plotting helpers for blockchainkit.consensus.
2
+
3
+ Imports matplotlib, so ``import blockchainkit`` does not load this module;
4
+ import it explicitly: ``from blockchainkit.consensus.visualizers import ...``.
5
+ """
6
+
7
+ from blockchainkit.consensus.visualizers.plots import plot_mining_trials, plot_stake_shares
8
+
9
+ __all__ = ["plot_mining_trials", "plot_stake_shares"]
@@ -0,0 +1,80 @@
1
+ """Plotting helpers for blockchainkit.consensus: mining effort and stake weighting."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections import Counter
6
+ from collections.abc import Mapping, Sequence
7
+
8
+ import matplotlib.pyplot as plt
9
+ import numpy as np
10
+ from matplotlib.axes import Axes
11
+
12
+ __all__ = ["plot_mining_trials", "plot_stake_shares"]
13
+
14
+
15
+ def plot_mining_trials(
16
+ difficulties: Sequence[int], attempts: Sequence[int], ax: Axes | None = None
17
+ ) -> Axes:
18
+ """Compare measured hash attempts with the expected 2**difficulty.
19
+
20
+ Parameters
21
+ ----------
22
+ difficulties : sequence of int
23
+ Difficulty of each mined block.
24
+ attempts : sequence of int
25
+ Hash attempts each search needed (``MiningResult.attempts``).
26
+ ax : matplotlib.axes.Axes, optional
27
+ Axes to draw on; a new figure is created if omitted.
28
+
29
+ Returns
30
+ -------
31
+ matplotlib.axes.Axes
32
+ """
33
+ if len(difficulties) != len(attempts):
34
+ raise ValueError("difficulties and attempts must have the same length")
35
+ if ax is None:
36
+ _, ax = plt.subplots()
37
+ ax.scatter(difficulties, attempts, color="#2563eb", alpha=0.6, label="measured")
38
+ grid = np.arange(min(difficulties), max(difficulties) + 1)
39
+ ax.plot(grid, 2.0**grid, color="black", label="expected 2^d")
40
+ ax.set_yscale("log", base=2)
41
+ ax.set_xlabel("difficulty d (leading zero bits)")
42
+ ax.set_ylabel("hash attempts")
43
+ ax.set_title("Each extra zero bit doubles the expected work")
44
+ ax.legend()
45
+ return ax
46
+
47
+
48
+ def plot_stake_shares(
49
+ stakes: Mapping[str, int], proposers: Sequence[str], ax: Axes | None = None
50
+ ) -> Axes:
51
+ """Compare each validator's stake fraction with its observed proposer fraction.
52
+
53
+ Parameters
54
+ ----------
55
+ stakes : Mapping
56
+ Validator weights, as given to :class:`~blockchainkit.consensus.systems.pos.StakeSampler`.
57
+ proposers : sequence of str
58
+ Sampled proposers, e.g. from ``StakeSampler.sample``.
59
+ ax : matplotlib.axes.Axes, optional
60
+ Axes to draw on; a new figure is created if omitted.
61
+
62
+ Returns
63
+ -------
64
+ matplotlib.axes.Axes
65
+ """
66
+ if not proposers:
67
+ raise ValueError("need at least one sampled proposer")
68
+ if ax is None:
69
+ _, ax = plt.subplots()
70
+ names = sorted(stakes)
71
+ total = sum(stakes.values())
72
+ counts = Counter(proposers)
73
+ xs = np.arange(len(names))
74
+ ax.bar(xs - 0.18, [stakes[n] / total for n in names], 0.36, label="stake fraction")
75
+ ax.bar(xs + 0.18, [counts[n] / len(proposers) for n in names], 0.36, label="proposer fraction")
76
+ ax.set_xticks(xs, names)
77
+ ax.set_ylabel("fraction")
78
+ ax.set_title(f"Stake-weighted selection over {len(proposers)} rounds")
79
+ ax.legend()
80
+ return ax