blockchainkit.consensus#
Consensus: agreement protocols, proof of work and stake, and the attacks they resist.
Proof-of-work search and its expected cost, catch-up probabilities, and stake-weighted proposer selection.
Every public name below is re-exported by the subpackage: import it as
bk.consensus.<name>. The plotting helpers are the exception: import them
explicitly from blockchainkit.consensus.visualizers, which loads Matplotlib.
Types and results#
Result containers for blockchainkit.consensus.
- class blockchainkit.consensus.core.base.MiningResult(block, attempts)[source]#
Bases:
objectA successful mined block and the number of hashes attempted.
- class blockchainkit.consensus.core.base.GeneralsResult(decisions, agreement, validity)[source]#
Bases:
objectOutcome of an oral-messages run.
- Variables:
- Parameters:
- class blockchainkit.consensus.core.base.ConsensusRun(decisions, rounds, decided)[source]#
Bases:
objectOutcome of a randomized-consensus run: who decided what, after how many rounds.
- class blockchainkit.consensus.core.base.ViewChangeRun(view_starts, timeouts, decided_view, decision_time)[source]#
Bases:
objectViews tried under partial synchrony, until the first one that made progress.
- Parameters:
- class blockchainkit.consensus.core.base.SquareRootResult(root, multiplications)[source]#
Bases:
objectA modular square root and the multiplications spent computing it.
- class blockchainkit.consensus.core.base.PBFTResult(prepared, commits)[source]#
Bases:
objectValues each honest replica prepared and committed (None if it did not).
- class blockchainkit.consensus.core.base.DifficultyRun(block_times, targets)[source]#
Bases:
objectSimulated block intervals and the target in force for each block.
- class blockchainkit.consensus.core.base.SelfishMiningResult(selfish_blocks, honest_blocks, revenue)[source]#
Bases:
objectBlocks each side contributed to the final chain, and the pool’s share.
Constructions and protocols#
The Byzantine generals problem and the oral-messages algorithm (1982).
Lamport, Shostak and Pease asked how loyal generals can agree on a plan when some generals, possibly including the commander, are traitors who send different messages to different recipients. Their algorithm OM(m) has every lieutenant relay what it heard, recursively, and take a majority. It succeeds whenever there are more than three times as many generals as traitors: n > 3m. With three generals and one traitor no algorithm can.
- blockchainkit.consensus.systems.byzantine.oral_messages(generals, traitors, order, *, rounds, lie=<function _default_lie>)[source]#
Run OM(rounds) with general 0 as commander.
- Parameters:
generals (
int) – Total number of generals n, at least 3; general 0 commands.traitors (
setofint) – Traitorous generals; they sendlie(sender, receiver, value)instead of the true value.order (
str) – The commander’s order,"attack"or"retreat".rounds (
int) – The recursion depth m: OM(m) tolerates m traitors when n > 3m.lie (
collections.abc.Callable) – The traitors’ strategy.
- Returns:
Loyal lieutenants’ decisions and whether IC1 and IC2 hold.
- Return type:
Examples
>>> from blockchainkit.consensus import oral_messages >>> oral_messages(4, {3}, "attack", rounds=1).decisions {1: 'attack', 2: 'attack'}
Ben-Or’s randomized consensus (1983), and the FLP impossibility (1985) it sidesteps.
Fischer, Lynch and Paterson proved that no deterministic protocol can guarantee agreement in an asynchronous network if even one process may crash: an adversary scheduling message delivery can keep it undecided forever. Ben-Or’s protocol escapes by tossing coins. In each round processes exchange values, propose a strong majority if they see one, and adopt a proposal or flip a coin. Whatever the schedule, the coins eventually line up.
- blockchainkit.consensus.systems.randomized.ben_or(initial, faults, *, coin=None, adversarial=True, seed=0, max_rounds=1000)[source]#
Run Ben-Or’s binary consensus for crash faults (n > 2f).
Each process waits for
n - fmessages per phase, the most it can wait for when f processes may have crashed; the scheduler chooses which.- Parameters:
initial (
collections.abc.Sequenceofint) – Each process’s input bit.faults (
int) – The number of crashes f the protocol is sized for.coin (
collections.abc.Callable, optional) –coin(process, round) -> 0 or 1. Defaults to a seeded fair coin. Pass a deterministic function to see the FLP adversary win.adversarial (
bool) – If True, the scheduler delivers the messages most likely to delay a decision; otherwise a randomn - fsubset.seed (
int) – Seed for the coin and the random scheduler.max_rounds (
int) – Give up after this many rounds.
- Returns:
Decisions so far, rounds run, and whether every process decided.
- Return type:
Examples
>>> from blockchainkit.consensus import ben_or >>> ben_or([1, 1, 1], faults=1).decisions {0: 1, 1: 1, 2: 1}
Partial synchrony (Dwork, Lynch and Stockmeyer 1988): progress after an unknown GST.
Real networks are neither synchronous (known delay bound) nor fully asynchronous (FLP applies). Partial synchrony assumes a bound Delta holds after some unknown Global Stabilization Time (GST). Protocols stay safe always, and make progress once a leader’s view lasts long enough: doubling the timeout each view guarantees that eventually it exceeds Delta.
- blockchainkit.consensus.systems.synchrony.view_changes(gst, delta, base_timeout, *, growth=2, max_views=64)[source]#
Simulate leader views with growing timeouts until one makes progress.
Before GST the adversary delays the leader’s message past every timeout; from GST on it arrives after
deltaticks. View v lastsbase_timeout * growth**vand succeeds when it starts at or after GST and its timeout is at leastdelta.- Raises:
TimeoutError – No view succeeded within
max_views(for example, timeouts that never grow past delta).- Parameters:
- Return type:
Examples
>>> from blockchainkit.consensus import view_changes >>> view_changes(gst=10, delta=5, base_timeout=1).decided_view 4
Pricing via processing (Dwork and Naor 1992): make each message cost computation.
To fight junk mail, Dwork and Naor proposed that a sender compute a pricing function, moderately hard to evaluate but easy to check, for each message. One of their examples is a square root modulo a prime: computing it takes an exponentiation, checking it takes one multiplication. Back’s Hashcash later used hash preimages for the same purpose, and Bitcoin’s proof of work descends from that.
- blockchainkit.consensus.systems.pricing.modular_square_root(value, prime)[source]#
Compute a square root modulo a prime
p = 3 (mod 4), counting multiplications.For such primes,
value**((p + 1) / 4)is a square root of any quadratic residue. Square-and-multiply needs about1.5 log2 pmultiplications; checking the answer needs one.- Raises:
ValueError –
primeis not a prime congruent to 3 modulo 4, orvaluehas no square root (it is not a quadratic residue).- Parameters:
- Return type:
Examples
>>> from blockchainkit.consensus import modular_square_root >>> result = modular_square_root(4, 7) >>> result.root ** 2 % 7 4
Bounded proof-of-work experiments with explicit success probabilities.
- blockchainkit.consensus.systems.pow.target(difficulty)[source]#
Return the inclusive target for a leading-zero-bit difficulty.
>>> from blockchainkit.consensus import target >>> target(0) == 2**256 - 1 True
- blockchainkit.consensus.systems.pow.expected_trials(difficulty)[source]#
Return 2**difficulty, assuming independent uniform 256-bit hashes.
- blockchainkit.consensus.systems.pow.valid_pow(block)[source]#
Check that the block hash, interpreted big-endian, is at most its target.
- blockchainkit.consensus.systems.pow.mine(block, *, max_attempts=100000)[source]#
Search sequential nonces starting at block.nonce, with a strict bound.
- Raises:
TimeoutError – No solution was found within max_attempts (not evidence of no solution).
ValueError – The search would exceed the 64-bit nonce space.
- Parameters:
- Return type:
Practical Byzantine Fault Tolerance (Castro and Liskov 1999): the normal-case round.
With n = 3f + 1 replicas, any two quorums of 2f + 1 overlap in at least f + 1 replicas, so at least one honest replica is in both. A replica prepares a value after 2f matching prepare messages and commits it after 2f + 1 matching commits. Two honest replicas can then never commit different values in one view, even if the leader equivocates, as long as at most f replicas are faulty.
- blockchainkit.consensus.systems.pbft.quorum_size(replicas)[source]#
Return 2f + 1 for the largest f with n >= 3f + 1.
>>> from blockchainkit.consensus import quorum_size >>> quorum_size(4), quorum_size(7) (3, 5)
- blockchainkit.consensus.systems.pbft.pbft_round(replicas, faulty, value, *, equivocate=False, other='B')[source]#
Run one PBFT pre-prepare / prepare / commit exchange with leader 0.
Faulty replicas send prepare and commit messages for both values to everyone, the most confusing thing they can do. A faulty leader with
equivocate=Truepre-preparesvalueto the first half of the honest replicas andotherto the rest.- Parameters:
replicas (
int) – Number of replicas n, at least 4.faulty (
setofint) – Faulty replica numbers. The protocol is sized for f = (n - 1) // 3; pass more to see safety fail.value (
str) – The leader’s value, and the conflicting one an equivocating leader also sends.other (
str) – The leader’s value, and the conflicting one an equivocating leader also sends.equivocate (bool)
- Returns:
What each honest replica prepared and committed.
- Return type:
Idealized models of an attacker catching up with the honest chain.
- blockchainkit.consensus.systems.catch_up.eventual_catch_up(attacker_fraction, deficit)[source]#
Return eventual catch-up probability in an ideal infinite random walk.
For q<1/2 this is (q/(1-q))**deficit. This is NOT Nakamoto’s finite- confirmation Poisson model: there is no propagation delay or time bound.
>>> from blockchainkit.consensus import eventual_catch_up >>> eventual_catch_up(0.25, 2) 0.1111111111111111
- blockchainkit.consensus.systems.catch_up.attacker_success_probability(attacker_fraction, confirmations)[source]#
Return Nakamoto’s probability that an attacker ever overtakes
zconfirmations.While the honest chain gains z blocks, the attacker’s progress is approximately Poisson with mean
lambda = z q / p. From each possible lead k, the attacker still needs to make upz - kblocks, which byeventual_catch_up()happens with probability(q/p)**(z-k)(whitepaper, section 11):P = 1 - sum_{k=0}^{z} e^(-lambda) lambda^k / k! * (1 - (q/p)^(z-k)).Examples
>>> from blockchainkit.consensus import attacker_success_probability >>> round(attacker_success_probability(0.1, 5), 7) 0.0009137
Difficulty retargeting (Bitcoin 2009): hold the block interval steady as hashrate changes.
The whitepaper (section 4) says proof-of-work difficulty is set by a moving average targeting an average number of blocks per hour. Bitcoin’s first release recomputes the target every 2016 blocks, scaling it by the ratio of actual to expected time, clamped to a factor of four in either direction.
- blockchainkit.consensus.systems.difficulty.retarget(target, actual_time, expected_time)[source]#
Return
target * actual / expected, clamped to[target/4, 4*target].A larger target is easier. Blocks that came too fast shrink it.
>>> from blockchainkit.consensus import retarget >>> retarget(1000, actual_time=600, expected_time=1200) 500
- blockchainkit.consensus.systems.difficulty.simulate_difficulty(hashrates, *, interval=600, window=2016, seed=0)[source]#
Simulate block discovery with exponential waiting times and periodic retargets.
With target T, a block needs about
2**48 / Thashes on average, so at hashrate h its waiting time is exponential with mean2**48 / (T h).- Parameters:
hashrates (
collections.abc.Sequenceoffloat) – The network hashrate while each successive block is mined.interval (
int) – The target block time, in seconds.window (
int) – Blocks between retargets.seed (
int) – Seed for the waiting times.
- Returns:
Each block’s waiting time and the target it was mined under.
- Return type:
Stake-weighted proposer sampling, not a complete PoS consensus protocol.
- class blockchainkit.consensus.systems.pos.StakeSampler(stakes, *, seed=0)[source]#
Bases:
objectSelect proposers with exact integer weights and reproducible randomness.
- Parameters:
stakes (
collections.abc.Mapping) – Nonnegative weights with a positive total. Names are sorted so mapping insertion order does not alter results.seed (
int) – Simulation seed, not an unpredictable consensus randomness beacon.
Examples
>>> from blockchainkit.consensus import StakeSampler >>> StakeSampler({"alice": 1, "bob": 0}, seed=7).sample(3) ('alice', 'alice', 'alice')
GHOST fork choice (Sompolinsky and Zohar 2013): follow the heaviest subtree.
When blocks are frequent, many honest blocks end up off the longest chain, and that wasted work no longer protects it. GHOST (Greedy Heaviest Observed SubTree) walks from genesis and at each fork picks the child whose whole subtree carries the most work, counting the honest side branches too. Ethereum’s proof-of-work chain used a variant through uncle rewards.
- blockchainkit.consensus.systems.fork_choice.subtree_work(chain, block_hash)[source]#
Return the expected work of a block and all its descendants.
- Parameters:
chain (Blockchain)
block_hash (bytes)
- Return type:
- blockchainkit.consensus.systems.fork_choice.ghost_tip(chain)[source]#
Return the tip chosen by GHOST: descend into the heaviest subtree at every fork.
Ties go to the smaller hash, matching
Blockchain.- Parameters:
chain (Blockchain)
- Return type:
Selfish mining (Eyal and Sirer 2014): majority is not enough.
A pool that withholds the blocks it finds, and publishes them only to
overtake or tie the honest chain, wastes the honest miners’ work. Above a
threshold hashrate its share of the chain exceeds its share of the hashrate,
so honest miners gain by joining it. gamma is the fraction of honest
miners who build on the pool’s block during a tie.
- blockchainkit.consensus.systems.selfish.selfish_mining_revenue(alpha, gamma)[source]#
Return the selfish pool’s long-run share of blocks (Eyal and Sirer, eq. 8).
R = (alpha (1-alpha)**2 (4 alpha + gamma (1 - 2 alpha)) - alpha**3) / (1 - alpha (1 + (2 - alpha) alpha))>>> from blockchainkit.consensus import selfish_mining_revenue >>> round(selfish_mining_revenue(0.4, 0.0), 3) 0.484
- blockchainkit.consensus.systems.selfish.selfish_mining_threshold(gamma)[source]#
Return the hashrate above which selfish mining pays:
(1 - gamma) / (3 - 2 gamma).
- blockchainkit.consensus.systems.selfish.simulate_selfish_mining(alpha, gamma, *, blocks, seed=0)[source]#
Simulate the selfish-mining state machine (Eyal and Sirer, Algorithm 1).
Each event, the pool finds a block with probability alpha, otherwise the honest network does. The state is the pool’s private lead, plus a tie state after the pool publishes to match an honest block.
- Parameters:
- Return type:
The nothing-at-stake problem (Buterin 2014): voting on every fork is free.
A proof-of-work miner must split its hashrate between forks. A proof-of-stake validator can sign blocks on every fork at no cost, and collect the reward whichever fork wins. If everyone does, forks never resolve. The remedy, first proposed in Buterin’s Slasher, is a penalty: evidence of signing two conflicting blocks destroys the validator’s deposit.
- blockchainkit.consensus.systems.stake_games.fork_voting_payoffs(fork_a_probability, reward, penalty)[source]#
Expected payoff of voting for fork A, fork B, or both.
- Parameters:
- Returns:
Expected payoff of the strategies
"A","B"and"both".- Return type:
Examples
>>> from blockchainkit.consensus import fork_voting_payoffs >>> fork_voting_payoffs(0.5, 1.0, 0.0) {'A': 0.5, 'B': 0.5, 'both': 1.0}
Cryptographic sortition (Algorand 2017): secret, stake-weighted committee selection.
Algorand picks each round’s committee by lottery. Every unit of stake is a ticket; a user hashes a round seed with their private key (in Algorand a verifiable random function, so others can check the result) and maps the hash to a number of selected tickets that is Binomial(stake, tau / W). Nobody, not even the user, knows who is on the committee before they speak, and splitting stake across accounts does not change the expected seats.
- blockchainkit.consensus.systems.sortition.sortition(secret, stake, total_stake, expected, *, round_seed)[source]#
Return how many of a user’s stake units are selected this round.
The hash of
secret || round_seed, read as a uniform number in [0, 1), is located in the cumulative Binomial(stake, expected / total_stake) distribution. Teaching simplification: a hash of the secret replaces the verifiable random function, so others cannot verify the draw.Examples
>>> from blockchainkit.consensus import sortition >>> sortition(b"my key", 0, 1000, 20, round_seed=b"r1") 0
Casper the Friendly Finality Gadget (Buterin and Griffith 2017).
Validators with deposits vote for links between checkpoints, source ->
target. A checkpoint becomes justified when two thirds of the stake vote
for a link to it from a justified source, and its source becomes finalized
when the target is the very next checkpoint. Two slashing conditions make
conflicting finality cost at least a third of all stake: never cast two
different votes for the same target height, and never cast a vote that
surrounds another.
- class blockchainkit.consensus.systems.finality.FinalityGadget(stakes)[source]#
Bases:
objectTrack Casper FFG votes, justification, finality, and slashable offenses.
Checkpoint 0 (genesis) starts justified and finalized.
- Parameters:
stakes (
collections.abc.Mapping) – Each validator’s deposit.
Examples
>>> from blockchainkit.consensus import FinalityGadget >>> ffg = FinalityGadget({"a": 1, "b": 1, "c": 1}) >>> for v in "abc": ... ffg.vote(v, source=0, target=1) >>> sorted(ffg.justified) [0, 1]
Plotting#
Plotting helpers for blockchainkit.consensus: mining effort and stake weighting.
- blockchainkit.consensus.visualizers.plots.plot_mining_trials(difficulties, attempts, ax=None)[source]#
Compare measured hash attempts with the expected 2**difficulty.
- Parameters:
difficulties (
collections.abc.Sequenceofint) – Difficulty of each mined block.attempts (
collections.abc.Sequenceofint) – Hash attempts each search needed (MiningResult.attempts).ax (
matplotlib.axes.Axes, optional) – Axes to draw on; a new figure is created if omitted.
- Return type:
Compare each validator’s stake fraction with its observed proposer fraction.
- Parameters:
stakes (
collections.abc.Mapping) – Validator weights, as given toStakeSampler.proposers (
collections.abc.Sequenceofstr) – Sampled proposers, e.g. fromStakeSampler.sample.ax (
matplotlib.axes.Axes, optional) – Axes to draw on; a new figure is created if omitted.
- Return type: