blockchainkit.channels#

Channels: layer-2 scaling and interoperability.

How a blockchain scales beyond what every node can verify, and how chains connect: Reed-Solomon codes, payment channels from Spilman’s to Lightning and eltoo, multi-hop routing and its privacy, atomic swaps, light-client relays and bridges, state channels and watchtowers, Plasma, sharding, rollups, and data-availability sampling.

How a blockchain scales beyond what every node can verify, and how chains connect: Reed-Solomon codes and data-availability sampling; payment channels from Spilman’s to Lightning and eltoo, state channels and watchtowers; multi-hop routing and balance probing; atomic swaps, a light-client relay and two bridge exploits; Plasma, sharding, and zk and optimistic rollups.

Every public name below is re-exported by the subpackage: import it as bk.channels.<name>. The plotting helpers are the exception: import them explicitly from blockchainkit.channels.visualizers, which loads Matplotlib.

Results#

Result containers for blockchainkit.channels.

class blockchainkit.channels.core.base.Settlement(kind, payouts, height, state)[source]#

Bases: object

How a channel was closed on chain, and who received what.

Variables:
  • kind (str) – "cooperative", "unilateral", "penalty" or "refund".

  • payouts (collections.abc.Mapping) – Coins paid to each party.

  • height (int) – The block at which the last output was claimed.

  • state (int) – The off-chain state that was settled.

Parameters:
kind: str#
payouts: Mapping[str, int]#
height: int#
state: int#
class blockchainkit.channels.core.base.SwapOutcome(alice, bob, secret_revealed)[source]#

Bases: object

Who ended up with which coins after an atomic swap between chains A and B.

Variables:
  • bob (alice,) – Coins each party holds on chain A and on chain B, out of the swap’s outputs.

  • secret_revealed (bool) – True if Alice’s preimage appeared on chain B.

Parameters:
alice: tuple[int, int]#
bob: tuple[int, int]#
secret_revealed: bool#
property completed: bool#

True if both parties received the other chain’s coins.

property refunded: bool#

True if both parties took their own coins back.

property atomic: bool#

True if the swap either completed or was refunded, so nobody lost.

class blockchainkit.channels.core.base.Commitment(holder, state, to_local, to_remote, revocation, txid)[source]#

Bases: object

One party’s commitment transaction in a Lightning channel.

Variables:
  • holder (str) – The party who can publish it.

  • state (int) – The channel state it records.

  • to_local (int) – The holder’s balance, locked behind the revocable delay.

  • to_remote (int) – The counterparty’s balance, paid at once.

  • revocation (tuple of int) – The public key that lets the counterparty take to_local once revoked.

  • txid (bytes) – Hash identifying the transaction.

Parameters:
holder: str#
state: int#
to_local: int#
to_remote: int#
revocation: tuple[int, int]#
txid: bytes#
class blockchainkit.channels.core.base.Route(nodes, amounts, expiries)[source]#

Bases: object

A payment route and what each hop forwards.

Variables:
  • nodes (tuple of int) – Sender first, recipient last.

  • amounts (tuple of int) – The HTLC amount offered on each hop, len(nodes) - 1 of them.

  • expiries (tuple of int) – Each hop’s HTLC expiry (an absolute block height).

Parameters:
nodes: tuple[int, ...]#
amounts: tuple[int, ...]#
expiries: tuple[int, ...]#
property fee: int#

What the sender pays the intermediate nodes.

class blockchainkit.channels.core.base.PaymentAttempt(success, route, failed_at, error)[source]#

Bases: object

The result of sending a payment along a route.

Variables:
  • success (bool) – True if the recipient received the payment.

  • route (blockchainkit.channels.core.base.Route) – The route tried.

  • failed_at (tuple of int or None) – The channel (from, to) where the HTLC was refused, if it was.

  • error (str or None) – "temporary_channel_failure" (not enough balance) or "unknown_payment_hash" (the recipient cannot claim it).

Parameters:
success: bool#
route: Route#
failed_at: tuple[int, int] | None#
error: str | None#
class blockchainkit.channels.core.base.ProbeResult(low, high, probes)[source]#

Bases: object

What an outsider learned about one side of a channel by sending probes.

Variables:
  • high (low,) – The balance lies in [low, high].

  • probes (int) – Payments sent.

Parameters:
low: int#
high: int#
probes: int#
class blockchainkit.channels.core.base.CoinTransfer(coin, owner, parent_block, signature=None)[source]#

Bases: object

A Plasma Cash transfer of one coin, signed by its previous owner.

Variables:
  • coin (int) – The coin’s identifier, its slot in the sparse Merkle tree.

  • owner (tuple of int) – The new owner’s public key.

  • parent_block (int) – The block holding the previous transfer of the coin (0 for a deposit).

  • signature (blockchainkit.crypto.core.base.SchnorrSignature or None) – The previous owner’s signature; None for a deposit.

Parameters:
coin: int#
owner: tuple[int, int]#
parent_block: int#
signature: SchnorrSignature | None = None#
class blockchainkit.channels.core.base.InclusionProof(transfer, block, proof)[source]#

Bases: object

A transfer, the block it is in, and the sparse Merkle proof that it is.

Variables:
Parameters:
transfer: CoinTransfer#
block: int#
proof: SparseMerkleProof#
class blockchainkit.channels.core.base.PlasmaExit(coin, owner, block, parent_block, deadline, status='pending')[source]#

Bases: object

An exit from the Plasma chain, waiting out its challenge period.

Variables:
  • coin (int)

  • owner (tuple of int) – Who receives the coin if the exit stands.

  • block (int) – The block of the exiting transfer.

  • parent_block (int) – The block of the transfer before it.

  • deadline (int) – The height from which the exit can be finalized.

  • status (str) – "pending", "challenged" or "finalized".

Parameters:
coin: int#
owner: tuple[int, int]#
block: int#
parent_block: int#
deadline: int#
status: str = 'pending'#
class blockchainkit.channels.core.base.AtomixResult(committed, balances, rejected)[source]#

Bases: object

A cross-shard transfer by Atomix’s lock, then commit or abort.

Variables:
  • committed (bool) – True if every input shard accepted and the output was credited.

  • balances (tuple of collections.abc.Mapping) – Each shard’s balances afterwards.

  • rejected (tuple of int) – The input shards that refused to lock.

Parameters:
committed: bool#
balances: tuple[Mapping[str, int], ...]#
rejected: tuple[int, ...]#
class blockchainkit.channels.core.base.Transfer(sender, recipient, amount)[source]#

Bases: object

A rollup transaction: move amount from sender to recipient.

Parameters:
sender: str#
recipient: str#
amount: int#
class blockchainkit.channels.core.base.ValidityProof(old_root, new_root, batch_digest)[source]#

Bases: object

The statement a zk-rollup’s proof attests: this batch takes old_root to new_root.

Variables:
  • new_root (old_root,) – State roots before and after the batch.

  • batch_digest (bytes) – Hash of the batch’s transactions.

Parameters:
old_root: bytes#
new_root: bytes#
batch_digest: bytes#
class blockchainkit.channels.core.base.FraudProof(step, rounds, claimed, correct)[source]#

Bases: object

The single step at which an optimistic rollup’s assertion went wrong.

Variables:
  • step (int) – The transaction whose execution the assertion got wrong (0-based).

  • rounds (int) – Bisection rounds the challenge game took.

  • correct (claimed,) – The asserted and the true state root after that step.

Parameters:
step: int#
rounds: int#
claimed: bytes#
correct: bytes#
class blockchainkit.channels.core.base.ErasureCodedData(k, shares, root)[source]#

Bases: object

A block’s data extended with Reed-Solomon parity and committed to by a Merkle root.

Variables:
  • k (int) – Data symbols; any k shares recover them.

  • shares (tuple of int) – The codeword.

  • root (bytes) – Merkle root over the shares.

Parameters:
k: int#
shares: tuple[int, ...]#
root: bytes#
class blockchainkit.channels.core.base.EncodingFraudProof(positions, shares, proofs)[source]#

Bases: object

Evidence that a block producer’s shares do not form a Reed-Solomon codeword.

Variables:
Parameters:
positions: tuple[int, ...]#
shares: tuple[int, ...]#
proofs: tuple[MerkleProof, ...]#
class blockchainkit.channels.core.base.SamplingResult(detected, collected, recoverable)[source]#

Bases: object

Light clients sampling a block whose producer withholds some shares.

Variables:
  • detected (float) – Fraction of clients that asked for a withheld share.

  • collected (int) – Distinct shares that clients’ samples brought to the network.

  • recoverable (bool) – True if the collected shares are enough to rebuild the data.

Parameters:
detected: float#
collected: int#
recoverable: bool#
class blockchainkit.channels.core.base.AppointmentReceipt(tower, hint, expires, signature)[source]#

Bases: object

A watchtower’s signed promise to watch for one revoked commitment (PISA).

Variables:
Parameters:
tower: tuple[int, int]#
hint: bytes#
expires: int#
signature: SchnorrSignature#

Erasure codes and data availability#

Reed-Solomon codes (1960): data as a polynomial, redundancy as more of its values.

Reed and Solomon encoded k symbols of a finite field as a polynomial of degree below k and transmitted its values at n > k points. Two distinct such polynomials agree on at most k - 1 points, so any k values determine the rest: the code recovers from up to n - k lost symbols (erasures), and from up to (n - k) // 2 wrong ones (errors).

This is the systematic form used for data availability: the data are the polynomial’s values at 0, 1, ..., k - 1, and the extension adds its values at k, ..., n - 1. Errors are corrected by Berlekamp and Welch’s algorithm (1986), which finds an error-locator polynomial by linear algebra.

blockchainkit.channels.systems.reed_solomon.rs_encode(data, n, *, prime=65537)[source]#

Extend k = len(data) symbols to an n-symbol codeword.

Parameters:
  • data (collections.abc.Sequence of int) – Field elements, the polynomial’s values at 0, ..., k - 1.

  • n (int) – Codeword length, at least k and at most prime.

  • prime (int) – The field modulus.

Returns:

The values at 0, ..., n - 1; the first k are data.

Return type:

tuple of int

Examples

>>> from blockchainkit.channels import rs_encode
>>> rs_encode([3, 1, 4], 6)  # The parabola through (0, 3), (1, 1), (2, 4).
(3, 1, 4, 12, 25, 43)
blockchainkit.channels.systems.reed_solomon.is_codeword(shares, k, *, prime=65537)[source]#

True if the shares (position to value) lie on one polynomial of degree below k.

Parameters:
Return type:

bool

blockchainkit.channels.systems.reed_solomon.rs_recover(shares, k, n, *, prime=65537)[source]#

Rebuild the whole codeword from any k of its symbols (erasure decoding).

Parameters:
  • shares (collections.abc.Mapping) – Position to value, for at least k positions in range(n).

  • k (int) – Data and codeword lengths.

  • n (int) – Data and codeword lengths.

  • prime (int) – The field modulus.

Raises:

ValueError – Fewer than k shares, a position out of range, or shares that are not all on one polynomial of degree below k.

Return type:

tuple[int, …]

Examples

>>> from blockchainkit.channels import rs_recover
>>> rs_recover({3: 12, 5: 43, 1: 1}, 3, 6)
(3, 1, 4, 12, 25, 43)
blockchainkit.channels.systems.reed_solomon.rs_decode(received, k, *, prime=65537)[source]#

Correct errors and erasures, and return the k data symbols (Berlekamp-Welch).

With m symbols received (None marks an erasure), up to (m - k) // 2 of them may be wrong. The algorithm looks for an error locator E (monic, of degree e) and Q = P E of degree below k + e with Q(x_i) = y_i E(x_i) at every received point, a linear system, then divides.

Raises:

ValueError – Too many errors to correct.

Parameters:
Return type:

tuple[int, …]

Examples

>>> from blockchainkit.channels import rs_decode
>>> rs_decode([3, 1, 4, 99, 25, None], 3)  # One error, one erasure.
(3, 1, 4)

Fraud proofs and data-availability sampling (Al-Bassam, Sonnino and Buterin, 2018).

A light client checks headers, not blocks, and fraud proofs let it reject an invalid block if one honest full node complains. But a fraud proof needs the block’s data, and a producer can publish a header and withhold part of the data, so that nobody can prove anything. Al-Bassam, Sonnino and Buterin made withholding detectable. The producer extends the data with a Reed-Solomon code and commits to all the shares with a Merkle root. Any half of the shares rebuilds the rest, so to hide anything the producer must withhold more than half of them, and then a client that downloads s random shares, with their Merkle proofs, notices with probability

\[1 - \left(1 - \tfrac{1}{2}\right)^s,\]

already above 99% for s = 7. Many clients sampling also collect enough shares between them to rebuild the block.

A producer could instead publish shares that are not a codeword, so that different halves rebuild different data. That is caught by an encoding fraud proof: k shares that fix the polynomial, and one more that is not on it, each with its Merkle proof.

The paper arranges the shares in a two-dimensional square, so that a fraud proof needs one row, about the square root of the block; this module uses one dimension, so a fraud proof holds k + 1 shares.

blockchainkit.channels.systems.data_availability.commit_shares(k, shares)[source]#

Commit to shares as they are, whether or not they form a codeword.

Parameters:
Return type:

ErasureCodedData

blockchainkit.channels.systems.data_availability.extend(data, *, factor=2, prime=65537)[source]#

Extend data to factor * len(data) Reed-Solomon shares and commit to them.

Examples

>>> from blockchainkit.channels import extend
>>> extend([3, 1, 4]).shares
(3, 1, 4, 12, 25, 43)
Parameters:
Return type:

ErasureCodedData

blockchainkit.channels.systems.data_availability.prove_share(block, position)[source]#

The Merkle proof a client receives with the share at position.

Parameters:
Return type:

MerkleProof

blockchainkit.channels.systems.data_availability.verify_share(value, proof, root)[source]#

True if value is the share the root commits to at proof.index.

Parameters:
Return type:

bool

blockchainkit.channels.systems.data_availability.detection_probability(withheld, samples)[source]#

Chance that samples uniform draws (with replacement) hit a withheld share.

Examples

>>> from blockchainkit.channels import detection_probability
>>> round(detection_probability(0.5, 7), 4)
0.9922
Parameters:
Return type:

float

blockchainkit.channels.systems.data_availability.simulate_sampling(block, withheld, *, clients, samples, seed=0)[source]#

Clients each request samples distinct random shares from a producer withholding some.

Parameters:
Return type:

blockchainkit.channels.core.base.SamplingResult

blockchainkit.channels.systems.data_availability.encoding_fraud_proof(block, *, prime=65537)[source]#

Prove that the committed shares are not a codeword; None if they are one.

The first k shares fix the polynomial; the proof adds the first share that is not on it.

Parameters:
Return type:

EncodingFraudProof | None

blockchainkit.channels.systems.data_availability.verify_encoding_fraud_proof(proof, root, k, *, prime=65537)[source]#

Check an encoding fraud proof against a header’s data root, without the block.

Examples

>>> from blockchainkit.channels import (
...     commit_shares, encoding_fraud_proof, verify_encoding_fraud_proof)
>>> bad = commit_shares(3, (3, 1, 4, 12, 26, 43))  # The share at 4 is off the parabola.
>>> proof = encoding_fraud_proof(bad)
>>> proof.positions, verify_encoding_fraud_proof(proof, bad.root, 3)
((0, 1, 2, 4), True)
Parameters:
Return type:

bool

Payment and state channels#

One-way micropayment channels (Spilman and Hearn, 2013).

Paying a few satoshis per second of a service on chain would cost more in fees than the payments themselves. Jeremy Spilman’s channel moves them off chain. The payer locks the channel’s capacity in an output that needs both parties’ signatures, after the payee has signed a refund that returns everything to the payer once a lock time has passed. Each payment is a new transaction from that output, paying a little more to the payee, which the payer signs and hands over; the payee countersigns only the last one and publishes it before the refund becomes valid. Mike Hearn implemented the scheme in bitcoinj the same year.

The payee can take no more than the payer signed, and the payer can never take back what it paid, because the payee holds a signed transaction for it; the refund only protects the payer from a payee who disappears.

class blockchainkit.channels.systems.micropayment.SpilmanChannel(payer, payee, *, payer_key, payee_key, capacity, expiry)[source]#

Bases: object

A unidirectional payment channel from payer to payee.

Parameters:
  • payer (str) – Names used in the settlement.

  • payee (str) – Names used in the settlement.

  • payer_key (int) – The parties’ private keys.

  • payee_key (int) – The parties’ private keys.

  • capacity (int) – Coins locked in the 2-of-2 funding output.

  • expiry (int) – The refund’s lock time: from this height the payer can take everything back.

Examples

>>> from blockchainkit.channels import SpilmanChannel
>>> channel = SpilmanChannel("alice", "bob", payer_key=7, payee_key=5, capacity=100, expiry=50)
>>> channel.pay(10), channel.pay(15)
(10, 25)
>>> dict(channel.close(height=20).payouts)
{'alice': 75, 'bob': 25}
pay(amount)[source]#

Sign a new transaction paying amount more to the payee; return the total paid.

Parameters:

amount (int)

Return type:

int

close(*, height)[source]#

The payee countersigns the latest payment and publishes it.

Parameters:

height (int)

Return type:

Settlement

refund(*, height)[source]#

The payer publishes the refund, which a block accepts only from expiry on.

Parameters:

height (int)

Return type:

Settlement

Duplex micropayment channels (Decker and Wattenhofer, 2015).

A Spilman channel pays in one direction only, and is used up once its capacity has been paid. Decker and Wattenhofer pair two of them, one each way, so payments can flow back and forth, and reset them when one runs dry: the parties sign a new pair whose capacities are the current balances. The old pair must then become unpublishable, and without a penalty mechanism they used time locks to rank the states.

The pairs hang below an invalidation tree: a chain of d transactions, each spending its parent, from the funding output down to the current pair. Each transaction can be confirmed only a set number of blocks after its parent (a relative lock time), and replacing a node lowers that number by delta. Of two branches that diverge at some level, the newer one has the lower lock time there, so it can be confirmed first and spends the shared parent before the older one is valid. With steps lock-time values per level, the tree supports steps ** d resets, and the state with digits c_0 ... c_{d-1} in base steps has lock times

\[t_l = \text{base} + (\text{steps} - 1 - c_l)\,\delta.\]
blockchainkit.channels.systems.duplex.invalidation_locktimes(state, *, depth, steps, delta, base=0)[source]#

Lock times along the invalidation tree’s branch for reset number state.

Parameters:
  • state (int) – The reset counter, below steps ** depth.

  • depth (int) – Levels of the tree.

  • steps (int) – Lock-time values per level.

  • delta (int) – Blocks between successive lock times of a node.

  • base (int) – The smallest lock time.

Returns:

One lock time per level, root first.

Return type:

tuple of int

Examples

>>> from blockchainkit.channels import invalidation_locktimes
>>> [invalidation_locktimes(s, depth=2, steps=3, delta=10) for s in (0, 1, 3, 8)]
[(20, 20), (20, 10), (10, 20), (0, 0)]
blockchainkit.channels.systems.duplex.first_confirmed(published, *, depth, steps, delta)[source]#

Which of several published branches the chain confirms.

At the first level where two branches differ, the one with the lower lock time becomes valid first and spends the shared parent output, which invalidates the other; Python’s tuple order compares exactly so.

Examples

>>> from blockchainkit.channels import first_confirmed
>>> first_confirmed([2, 7, 5], depth=2, steps=3, delta=10)
7
Parameters:
Return type:

int

class blockchainkit.channels.systems.duplex.DuplexChannel(parties, deposits, *, depth, steps, delta)[source]#

Bases: object

Two one-way channels under an invalidation tree.

Parameters:

Examples

>>> from blockchainkit.channels import DuplexChannel
>>> channel = DuplexChannel(("alice", "bob"), (10, 10), depth=2, steps=4, delta=6)
>>> for _ in range(3):
...     channel.pay("alice", 8)
...     channel.pay("bob", 8)
>>> channel.balances(), channel.resets
({'alice': 10, 'bob': 10}, 2)
balances()[source]#

What each party would receive if the channel closed now.

Return type:

dict[str, int]

pay(sender, amount)[source]#

Pay through the sender’s one-way channel, resetting both if it runs dry.

Raises:

ValueError – The sender’s balance is too small, or the tree has no resets left.

Parameters:
Return type:

None

locktimes()[source]#

The current branch’s lock times, root first.

Return type:

tuple[int, …]

close(*, height)[source]#

Publish the current branch at height; it settles after the sum of its lock times.

Parameters:

height (int)

Return type:

Settlement

The Lightning Network’s revocable commitments (Poon and Dryja, 2016).

A duplex channel ranks its states by time locks, which limits how many updates it can hold. Poon and Dryja rank them by punishment instead. Each party holds its own version of the current state, a commitment transaction spending the 2-of-2 funding output and signed by the other party. It pays the counterparty at once, but pays its holder only after a delay, and during that delay anyone holding the revocation key can take the holder’s output. To move to a new state, each party gives the other the secret behind its old commitment’s revocation key. Publishing a revoked commitment then hands the counterparty everything in the channel, if it notices within the delay.

The revocation key combines the holder’s per-commitment point with the counterparty’s revocation base point, so neither can sign with it alone until the holder reveals its per-commitment secret:

\[R_n = S_n + B, \qquad r_n = s_n + b \pmod q.\]

The per-commitment secrets are a hash chain used backwards, as in Lightning’s shachain: the secret of state n - 1 is the hash of the secret of state n, so the counterparty stores only the latest one and derives every older one.

Bitcoin enforces the delay with OP_CHECKSEQUENCEVERIFY, which this package’s Script lacks; the delayed branch here uses OP_CHECKLOCKTIMEVERIFY against the commitment’s age instead. The deployed protocol also hashes the base points into the revocation key to stop a party from cancelling the other’s point.

class blockchainkit.channels.systems.lightning.LightningChannel(parties, keys, deposits, *, delay=144, max_states=1000)[source]#

Bases: object

A two-party Poon-Dryja channel.

Parameters:
  • parties (tuple of str) – The two parties; the first funds deposits[0], the second deposits[1].

  • keys (tuple of int) – Their private funding keys; revocation, delayed and per-commitment keys are derived from them.

  • deposits (tuple of int) – Each party’s initial balance.

  • delay (int) – Blocks a commitment’s holder must wait before taking its own output.

  • max_states (int) – Length of each party’s per-commitment hash chain.

Examples

>>> from blockchainkit.channels import LightningChannel
>>> channel = LightningChannel(("alice", "bob"), (7, 5), (100, 0), delay=6)
>>> channel.pay("alice", 30), channel.pay("alice", 20)
(1, 2)
>>> _ = channel.publish("alice", state=0, height=100)  # Alice cheats with state 0.
>>> dict(channel.penalize(height=103).payouts)
{'alice': 0, 'bob': 100}
other(party)[source]#

The counterparty of party.

Parameters:

party (str)

Return type:

str

revocation_key(holder, state)[source]#

R = S + B: the holder’s per-commitment point plus the counterparty’s base point.

Parameters:
Return type:

tuple[int, int]

derive_secret(holder, state)[source]#

The per-commitment secret of the holder’s state, if the counterparty can derive it.

It can for every revoked state: it hashes the latest revealed secret once per state back.

Parameters:
Return type:

bytes | None

commitment(holder, state=None)[source]#

The holder’s commitment for state (the current one by default).

Parameters:
  • holder (str)

  • state (int | None)

Return type:

Commitment

pay(sender, amount)[source]#

Move amount from sender to the counterparty; return the new state number.

Both parties sign each other’s new commitment, then each revokes its previous one by revealing that commitment’s per-commitment secret.

Parameters:
Return type:

int

close_cooperatively(*, height)[source]#

Both parties sign a transaction paying the current balances, with no delay.

Parameters:

height (int)

Return type:

Settlement

publish(holder, *, state=None, height)[source]#

The holder broadcasts one of its commitments, current or revoked.

The holder adds its own signature to the counterparty’s; the pair spends the 2-of-2 funding output.

Parameters:
  • holder (str)

  • state (int | None)

  • height (int)

Return type:

Commitment

sweep(*, height)[source]#

The holder takes its delayed output, once delay blocks have passed.

Raises:

ValueError – The delay has not passed, so Script rejects the spend.

Parameters:

height (int)

Return type:

Settlement

justice_message(commitment)[source]#

What the revocation key signs to take a revoked commitment’s to_local.

Parameters:

commitment (Commitment)

Return type:

bytes

justice_signature(holder, state)[source]#

The counterparty’s signature with the revocation key of the holder’s revoked state.

It can be made in advance, and handed to a watchtower.

Raises:

ValueError – The state has not been revoked, so the counterparty lacks the secret.

Parameters:
Return type:

SchnorrSignature

penalize(*, height, justice=None)[source]#

The counterparty takes the holder’s output of a revoked commitment.

Parameters:
Raises:

ValueError – The published commitment is the current one, so it cannot be revoked.

Return type:

Settlement

Sprites and general state channels (Miller et al., 2017).

On a chain with contracts, a channel need not be built from transactions: a contract can hold the deposits and act as judge. The parties sign successive versions of any state, and close by submitting the last one. If one party submits an old version, the other has a dispute period to submit a newer one, and the contract settles with the highest version it saw. The state can be balances or the position of a game, so one adjudicator serves every application: a general state channel.

Sprites also cut the time a multi-hop payment locks up collateral. In Lightning, the expiry of each hop exceeds the next one’s by a margin delta, so a payment over n hops holds the first hop’s coins for about n * delta blocks. Sprites’ hops instead share one deadline: the recipient must publish the preimage in a global preimage manager contract by then, and every hop settles by asking it. Each hop’s expiry is the same constant, whatever the path’s length.

blockchainkit.channels.systems.state_channels.state_message(channel, version, balances, final)[source]#

The bytes both parties sign for version of a channel’s state.

Parameters:
Return type:

bytes

blockchainkit.channels.systems.state_channels.sign_state(private, channel, version, balances, *, final=False)[source]#

One party’s signature on a channel state; final marks a cooperative close.

Examples

>>> from blockchainkit.channels import sign_state
>>> sign_state(7, "0xchannel", 3, [60, 40]).response > 0
True
Parameters:
Return type:

SchnorrSignature

class blockchainkit.channels.systems.state_channels.StateChannel[source]#

Bases: Contract

An adjudicator holding two deposits and settling on the highest signed version.

Constructor arguments: the parties’ public keys, the accounts paid at the end, the initial balances (sent as the deployment’s value), and the dispute period in blocks.

Examples

>>> from blockchainkit.channels import StateChannel, sign_state
>>> from blockchainkit.contracts import World
>>> from blockchainkit.crypto import public_key
>>> world = World()
>>> world.fund("alice", 100)
>>> keys, accounts = [public_key(7), public_key(5)], ["alice", "bob"]
>>> channel = world.deploy("alice", StateChannel, keys, accounts, [100, 0], 10, value=100)
>>> signatures = [sign_state(k, channel, 4, [70, 30]) for k in (7, 5)]
>>> world.transact("bob", channel, "submit", 4, [70, 30], signatures).success
True
>>> world.advance(10)
>>> world.transact("bob", channel, "settle").success, world.balance("bob")
(True, 30)
layout: ClassVar[tuple[str, ...]] = ('keys', 'accounts', 'balances', 'version', 'deadline', 'period', 'closed')#
constructor(keys, accounts, balances, period)[source]#
Parameters:
Return type:

None

submit(version, balances, signatures)[source]#

Submit a signed state; the first submission opens the dispute period.

Parameters:
Return type:

None

close(version, balances, signatures)[source]#

Pay out at once a state both parties signed as final.

Parameters:
Return type:

None

settle()[source]#

Pay out the highest submitted version once the dispute period is over.

Return type:

None

class blockchainkit.channels.systems.state_channels.PreimageManager[source]#

Bases: Contract

Sprites’ global record of when each payment preimage was first published.

layout: ClassVar[tuple[str, ...]] = ('published',)#
publish(preimage)[source]#

Record preimage at the current block, unless it is already recorded.

Parameters:

preimage (bytes)

Return type:

None

published_by(payment_hash, deadline)[source]#

True if the preimage of payment_hash was published at or before deadline.

Parameters:
Return type:

bool

blockchainkit.channels.systems.state_channels.htlc_expiries(hops, *, delta, final=0)[source]#

Lightning’s expiries, sender’s hop first: each exceeds the next by delta.

Examples

>>> from blockchainkit.channels import htlc_expiries, sprites_expiries
>>> htlc_expiries(4, delta=10), sprites_expiries(4, delta=10)
((40, 30, 20, 10), (10, 10, 10, 10))
Parameters:
Return type:

tuple[int, …]

blockchainkit.channels.systems.state_channels.sprites_expiries(hops, *, delta, final=0)[source]#

Sprites’ expiries: every hop settles delta after the shared preimage deadline.

Parameters:
Return type:

tuple[int, …]

blockchainkit.channels.systems.state_channels.collateral_time(amounts, expiries, *, height=0)[source]#

Worst-case coins times blocks a payment locks: the sum of amount * (expiry - height).

Parameters:
Return type:

int

eltoo: channel updates without penalties (Decker, Russell and Osuntokun, 2018).

Lightning’s penalty is harsh, since a party that publishes an old state by mistake, after restoring a backup, loses everything, and it forces each party to keep a secret for every past state. eltoo replaces punishment with replacement. State n is a pair of transactions: an update, which spends the funding output or any earlier update, and a settlement, which pays out state n’s balances after a delay. If an old update is published, the other party simply publishes a newer one on top of it, and only the last update’s settlement ever becomes valid.

Two things make this work. Updates are signed with SIGHASH_NOINPUT (BIP 118), so the signature does not name the output it spends and update n can attach to any earlier update. And each update output can be spent by a later update only, by encoding the state number in the lock time:

OP_IF
    <delay> OP_CHECKSEQUENCEVERIFY OP_DROP <settlement 2-of-2>
OP_ELSE
    <n + 1> OP_CHECKLOCKTIMEVERIFY OP_DROP <update 2-of-2>
OP_ENDIF

Each party stores only the latest pair of transactions. Here a signature over a message that omits the spent output stands in for SIGHASH_NOINPUT, and the relative delay is checked with OP_CHECKLOCKTIMEVERIFY against the update’s age.

class blockchainkit.channels.systems.eltoo.EltooChannel(parties, keys, deposits, *, delay=144)[source]#

Bases: object

A two-party eltoo channel.

Parameters:
  • parties (tuple of str) – The two parties.

  • keys (tuple of int) – Their private update keys; settlement keys are derived from them.

  • deposits (tuple of int) – Each party’s initial balance.

  • delay (int) – Blocks between an update’s confirmation and its settlement.

Examples

>>> from blockchainkit.channels import EltooChannel
>>> channel = EltooChannel(("alice", "bob"), (7, 5), (100, 0), delay=6)
>>> channel.pay("alice", 30), channel.pay("alice", 20)
(1, 2)
>>> channel.publish(state=0, height=100)  # Alice publishes an old state...
>>> channel.publish(height=102)  # ...and Bob replaces it with the latest.
>>> dict(channel.settle(height=108).payouts)
{'alice': 50, 'bob': 50}
update_output(state)[source]#

Update state’s output: settled after the delay, or replaced by a later update.

Parameters:

state (int)

Return type:

tuple[bytes | str, …]

pay(sender, amount)[source]#

Move amount from sender to the counterparty and sign the new state.

Parameters:
Return type:

int

publish(*, state=None, height)[source]#

Publish update state (the latest by default) on the funding output or last update.

Raises:

ValueError – The update is not newer than the one on chain, so Script rejects it.

Parameters:
  • state (int | None)

  • height (int)

Return type:

None

settle(*, height)[source]#

Publish the settlement of the update on chain, once its delay has passed.

Parameters:

height (int)

Return type:

Settlement

Watchtowers, and PISA’s accountable ones (McCorry, Bakshi, Bentov, Meiklejohn and Miller, 2019).

A Lightning party who goes offline for longer than the channel’s delay can be robbed: the counterparty publishes a revoked commitment and sweeps it before anyone penalizes it. A watchtower watches the chain on the party’s behalf. After every update the party hands it an appointment: the first half of the revoked commitment’s txid as a hint, and a presigned penalty transaction encrypted with a key derived from the whole txid. The tower learns nothing about the channel until the revoked commitment actually appears on chain, and then it can decrypt and publish only a penalty that pays the party.

Nothing obliges such a tower to act. PISA makes it accountable: the tower signs a receipt for each appointment and locks a deposit in a contract; if a revoked state is swept while the receipt was valid, the party shows the receipt and claims the deposit. PISA’s arbitration contract judges state channels in general; here the recourse is a function judging one Lightning channel’s outcome.

blockchainkit.channels.systems.watchtowers.make_appointment(channel, customer, state)[source]#

The (hint, blob) a customer gives a tower for the counterparty’s revoked state.

Examples

>>> from blockchainkit.channels import LightningChannel, make_appointment
>>> channel = LightningChannel(("alice", "bob"), (7, 5), (100, 0), delay=6)
>>> _ = channel.pay("alice", 40)
>>> hint, blob = make_appointment(channel, "bob", 0)  # Bob guards against Alice's state 0.
>>> len(hint), len(blob)
(16, 97)
Parameters:
Return type:

tuple[bytes, bytes]

blockchainkit.channels.systems.watchtowers.receipt_message(hint, expires)[source]#

The bytes a tower signs to accept an appointment.

Parameters:
Return type:

bytes

class blockchainkit.channels.systems.watchtowers.Watchtower(key, *, collateral=0, online=True)[source]#

Bases: object

A tower that stores encrypted penalties and publishes them when a hint matches.

Parameters:
  • key (int) – The tower’s private key, which signs receipts.

  • collateral (int) – The deposit a customer can claim if the tower fails it (PISA).

  • online (bool) – False models a tower that is down, or that does not keep its promise.

appoint(hint, blob, *, expires)[source]#

Store an appointment until expires and return the signed receipt.

Parameters:
Return type:

AppointmentReceipt

watch(channel, *, height)[source]#

Look at the chain; if a watched commitment is there, publish its penalty.

Parameters:
Return type:

Settlement | None

blockchainkit.channels.systems.watchtowers.verify_receipt(receipt)[source]#

True if the tower named in the receipt signed it.

Parameters:

receipt (AppointmentReceipt)

Return type:

bool

blockchainkit.channels.systems.watchtowers.tower_liable(receipt, channel)[source]#

PISA’s recourse: True if the receipt’s commitment was published in time and swept.

The customer then claims the tower’s collateral.

Parameters:
Return type:

bool

Channel networks#

Multi-hop payments over a network of channels, with hash time-locked contracts.

Poon and Dryja’s paper (2016) also made channels a network. To pay Carol through Bob, Alice offers Bob an HTLC for the amount plus Bob’s fee, and Bob offers Carol an HTLC for the amount, both locked to the same payment hash. Carol claims with the preimage, which lets Bob claim from Alice: the payment completes on every hop or on none. Each hop’s expiry must exceed the next one’s by a margin, the cltv_delta, so that a node that learns the preimage downstream has time to claim upstream.

A sender knows every channel’s capacity, which is announced, but not how it is split between the two sides. It picks a route by capacity and learns only from failures whether a hop could forward the amount.

class blockchainkit.channels.systems.routing.ChannelNetwork(graph, balances, *, base_fee=1, fee_rate=1000, cltv_delta=40, final_cltv=9)[source]#

Bases: object

Payment channels on the edges of a Graph.

Parameters:
  • graph (blockchainkit.network.systems.topology.Graph) – One channel per edge.

  • balances (collections.abc.Mapping) – balances[u, v] is what u can send to v; both directions of every edge are required.

  • base_fee (int) – Fee every intermediate node charges per forwarded payment.

  • fee_rate (int) – Proportional fee, in millionths of the amount forwarded.

  • cltv_delta (int) – Blocks each intermediate node adds to the expiry it is offered.

  • final_cltv (int)

Examples

>>> from blockchainkit.channels import ChannelNetwork
>>> from blockchainkit.network import Graph
>>> line = Graph(3, [(0, 1), (1, 2)])
>>> network = ChannelNetwork(line, {(0, 1): 500, (1, 0): 500, (1, 2): 500, (2, 1): 500})
>>> attempt = network.pay(0, 2, 100)
>>> attempt.success, attempt.route.amounts, attempt.route.expiries
(True, (101, 100), (49, 9))
>>> network.balance(1, 2), network.balance(0, 1)
(400, 399)
classmethod random(graph, *, capacity, seed=0, **policy)[source]#

Channels of equal capacity, each split uniformly at random between its two sides.

Parameters:
Return type:

ChannelNetwork

balance(u, v)[source]#

What u can currently send to v.

Parameters:
Return type:

int

capacity(u, v)[source]#

The channel’s announced capacity: both sides’ balances together.

Parameters:
Return type:

int

fee(amount)[source]#

What an intermediate node charges to forward amount.

Parameters:

amount (int)

Return type:

int

route(nodes, amount, *, height=0)[source]#

The amounts and expiries of a payment of amount along nodes.

They are computed backwards from the recipient: each intermediate node is offered what it forwards plus its fee, and an expiry cltv_delta blocks later than the one it offers.

Parameters:
Return type:

Route

find_route(source, target, amount, *, height=0)[source]#

The fewest-hop route whose every channel has the capacity for amount.

Raises:

ValueError – No such route exists.

Parameters:
Return type:

Route

send(route, *, known_hash=True)[source]#

Offer the HTLCs hop by hop; settle them all, or fail them all back.

Parameters:
Return type:

PaymentAttempt

pay(source, target, amount, *, height=0)[source]#

Find a route by capacity and send the payment along it.

Parameters:
Return type:

PaymentAttempt

Balance probing in Lightning (Herrera-Joancomartí et al., 2019).

A channel announces its capacity but keeps secret how it is split, which hides how much each party has paid the other. Herrera-Joancomartí and co-authors showed the split leaks to anyone with a channel. The prober sends a payment through the target channel to its far end with a random payment hash that nobody can claim. If the hop cannot forward the amount, the error comes back from it, temporary_channel_failure; if it can, the payment reaches the recipient and fails there, unknown_payment_hash. Either way no money moves and the prober pays nothing, and a binary search pins the balance down in

\[\lceil \log_2 (C + 1) \rceil\]

probes for a channel of capacity \(C\).

blockchainkit.channels.systems.probing.probe_balance(network, prober, u, v)[source]#

Find u’s balance toward v by sending unclaimable payments prober -> u -> v.

Parameters:
Returns:

Narrowed to a single value unless the prober’s own channel runs out first.

Return type:

blockchainkit.channels.core.base.ProbeResult

Examples

>>> from blockchainkit.channels import ChannelNetwork, probe_balance
>>> from blockchainkit.network import Graph
>>> graph = Graph(3, [(0, 1), (1, 2)])
>>> network = ChannelNetwork(graph, {(0, 1): 5_000, (1, 0): 0, (1, 2): 618, (2, 1): 382})
>>> probe_balance(network, 0, 1, 2)
ProbeResult(low=618, high=618, probes=10)

Interoperability#

Atomic cross-chain swaps (Tier Nolan, 2013).

Alice has coins on chain A and wants Bob’s coins on chain B, and neither trusts the other to pay second. Tier Nolan’s protocol makes the exchange all-or-nothing with one secret and two hash time-locked contracts (HTLCs). Alice picks a secret s and locks her coins on A to Bob, payable against the preimage of h = SHA256(s), refundable to her after T_A. Bob, seeing that lock, locks his coins on B to Alice against the same h, refundable after T_B. Alice claims on B by revealing s, which Bob then reads from chain B and uses to claim on A.

The timeouts must satisfy T_A > T_B with a margin: Alice must reveal s before T_B to be paid, and Bob then has until T_A to use it. With the order reversed, Alice can take her refund on A and still claim on B before Bob’s refund unlocks, and Bob loses.

class blockchainkit.channels.systems.swaps.AtomicSwap(alice_key, bob_key, secret, *, amount_a, amount_b, timeout_a, timeout_b)[source]#

Bases: object

Alice’s amount_a on chain A for Bob’s amount_b on chain B.

Both chains share one block height in this model. Each claim and refund is a spend of an HTLC output judged by verify_script().

Parameters:
  • alice_key (int) – The parties’ private keys.

  • bob_key (int) – The parties’ private keys.

  • secret (bytes) – Alice’s preimage.

  • amount_a (int) – The coins each party locks.

  • amount_b (int) – The coins each party locks.

  • timeout_a (int) – Refund heights of Alice’s lock on A and Bob’s lock on B.

  • timeout_b (int) – Refund heights of Alice’s lock on A and Bob’s lock on B.

Examples

>>> from blockchainkit.channels import AtomicSwap
>>> swap = AtomicSwap(7, 5, b"s3cret", amount_a=10, amount_b=3, timeout_a=48, timeout_b=24)
>>> swap.lock_bob()
>>> swap.claim_b(height=5), swap.claim_a(height=6)
(True, True)
>>> swap.outcome().completed
True
lock_bob()[source]#

Bob, having seen Alice’s lock, locks his coins on B to Alice against the same hash.

Return type:

None

claim_b(*, height)[source]#

Alice claims Bob’s coins on B, revealing the secret on chain B.

Parameters:

height (int)

Return type:

bool

claim_a(*, height)[source]#

Bob claims Alice’s coins on A, if the secret has appeared on chain B.

Parameters:

height (int)

Return type:

bool

refund_a(*, height)[source]#

Alice takes her coins on A back; Script accepts it from timeout_a.

Parameters:

height (int)

Return type:

bool

refund_b(*, height)[source]#

Bob takes his coins on B back; Script accepts it from timeout_b.

Parameters:

height (int)

Return type:

bool

outcome()[source]#

Coins each party has received on A and B so far.

Return type:

SwapOutcome

BTC Relay (2016): a Bitcoin light client inside an Ethereum contract.

A contract on one chain cannot see another chain, so it cannot tell whether a payment there happened. BTC Relay, launched on Ethereum in 2016, made it see Bitcoin the way a light client does (Nakamoto’s simplified payment verification). Anyone could submit Bitcoin block headers; the contract checked that each linked to a stored one and met its proof-of-work target, and tracked the chain with the most cumulative work. A contract could then ask whether a transaction was in a block of that chain, with enough blocks on top, by checking a Merkle proof against the header’s root. That made trustless ether-for-bitcoin swaps possible, and is how later light-client bridges work.

The relay is only as safe as the work it demands. Here the headers are BlockHeader objects at teaching difficulty, without Bitcoin’s retargeting, so the contract demands a fixed minimum_difficulty instead; without one, anyone could feed it a heavier chain of worthless headers. Relayers were paid fees in the deployed contract; here they are not.

blockchainkit.channels.systems.relay.mine_header(previous, payloads, *, difficulty=8, timestamp=0, max_attempts=1000000)[source]#

A header committing to payloads through a Merkle root, with valid proof of work.

Parameters:
Raises:

TimeoutError – No nonce worked within max_attempts.

Return type:

BlockHeader

class blockchainkit.channels.systems.relay.BTCRelay[source]#

Bases: Contract

Stores proof-of-work headers, follows the heaviest chain, and verifies inclusion.

Constructor arguments: the genesis header the relay trusts, and the minimum_difficulty every later header must meet.

Examples

>>> from blockchainkit.channels import BTCRelay, mine_header
>>> from blockchainkit.contracts import World
>>> genesis = mine_header(None, [b"coinbase"])
>>> block = mine_header(genesis, [b"coinbase 1", b"alice pays bob"])
>>> world = World()
>>> relay = world.deploy("deployer", BTCRelay, genesis, 8)
>>> world.transact("relayer", relay, "store_header", block).success
True
>>> world.view(relay, "best_height")
1
layout: ClassVar[tuple[str, ...]] = ('headers', 'work', 'tip', 'minimum_difficulty')#
constructor(genesis, minimum_difficulty)[source]#
Parameters:
Return type:

None

store_header(header)[source]#

Accept a header that extends a stored one and carries enough proof of work.

Parameters:

header (BlockHeader)

Return type:

None

best_height()[source]#

Height of the heaviest stored chain’s tip.

Return type:

int

confirmations(block_hash)[source]#

Blocks from block_hash to the heaviest tip, both included; 0 off that chain.

Parameters:

block_hash (bytes)

Return type:

int

verify_transaction(payload, block_hash, proof, confirmations)[source]#

True if payload is in that block, on the heaviest chain, under enough blocks.

Parameters:
Return type:

bool

Bridge failures of 2022: the Ronin and Wormhole exploits.

A bridge locks coins in a contract on one chain and issues wrapped coins on another; to release the locked coins, the contract needs evidence that the wrapped ones were burned. A light-client relay checks that evidence itself; most bridges instead trust a committee of validators to sign it. The contract is then exactly as safe as the committee’s keys and the code that checks their signatures, and in 2022 bridges lost more than two billion dollars to failures of both.

Ronin (March 2022). The bridge of the Axie Infinity sidechain released funds on 5 of 9 validator signatures. Sky Mavis ran four validators, and had been allowed to sign for a fifth, the Axie DAO’s, during a load spike months before; the permission was never revoked. An attacker who compromised Sky Mavis’s systems held five keys, and withdrew 173,600 ether and 25.5 million USDC.

Wormhole (February 2022). Wormhole’s Solana program checked guardian signatures in one instruction and then, in the next, trusted an account saying that the check had passed. It took that account from the caller and never verified that it was the genuine system account. The attacker passed a forged one, minted 120,000 wrapped ether without any signature, and redeemed most of it for ether held by the bridge.

Here both are contracts on World. Solana’s accounts become a caller-supplied verifier contract, and minting and redeeming wrapped ether happen in one contract rather than on two chains.

blockchainkit.channels.systems.bridges.withdrawal_message(bridge, recipient, amount, nonce)[source]#

The bytes validators sign to release amount to recipient from bridge.

Parameters:
Return type:

bytes

blockchainkit.channels.systems.bridges.sign_withdrawal(private, bridge, recipient, amount, nonce)[source]#

One validator’s approval of a withdrawal: (public_key, signature).

Examples

>>> from blockchainkit.channels import sign_withdrawal
>>> from blockchainkit.crypto import public_key
>>> sign_withdrawal(7, "0xbridge", "alice", 10, 0)[0] == public_key(7)
True
Parameters:
Return type:

tuple[tuple[int, int], SchnorrSignature]

class blockchainkit.channels.systems.bridges.ValidatorBridge[source]#

Bases: Contract

Ronin’s bridge: locked ether, released on threshold validator signatures.

Constructor arguments: the validators’ public keys and the threshold.

Examples

>>> from blockchainkit.channels import ValidatorBridge, sign_withdrawal
>>> from blockchainkit.contracts import World
>>> from blockchainkit.crypto import public_key
>>> world = World()
>>> world.fund("alice", 50)
>>> keys = range(1, 10)
>>> bridge = world.deploy("ronin", ValidatorBridge, [public_key(k) for k in keys], 5)
>>> world.transact("alice", bridge, "deposit", value=50).success
True
>>> approvals = [sign_withdrawal(k, bridge, "alice", 50, 0) for k in (1, 2, 3, 4, 5)]
>>> world.transact("relayer", bridge, "withdraw", "alice", 50, 0, approvals).success
True
layout: ClassVar[tuple[str, ...]] = ('validators', 'threshold', 'processed')#
constructor(validators, threshold)[source]#
Parameters:
Return type:

None

deposit()[source]#

Lock ether; the validators then mint as much on the other chain.

Return type:

None

withdraw(recipient, amount, nonce, approvals)[source]#

Release amount to recipient, approved by enough distinct validators.

Parameters:
Return type:

None

class blockchainkit.channels.systems.bridges.GuardianVerifier[source]#

Bases: Contract

Wormhole’s signature-verification step: records messages enough guardians signed.

Constructor arguments: the guardians’ public keys and the threshold.

layout: ClassVar[tuple[str, ...]] = ('guardians', 'threshold', 'verified')#
constructor(guardians, threshold)[source]#
Parameters:
Return type:

None

verify_signatures(message, approvals)[source]#

Check the guardians’ signatures on message and record its hash as verified.

Parameters:
Return type:

None

is_verified(message_hash)[source]#

True if the message with this hash passed verify_signatures().

Parameters:

message_hash (bytes)

Return type:

bool

class blockchainkit.channels.systems.bridges.ForgedVerifier[source]#

Bases: Contract

The attacker’s stand-in for the verification account: it vouches for everything.

is_verified(message_hash)[source]#
Parameters:

message_hash (bytes)

Return type:

bool

class blockchainkit.channels.systems.bridges.WormholeBridge[source]#

Bases: Contract

Wrapped ether minted on verified guardian messages, and redeemed for locked ether.

Constructor arguments: the genuine verifier’s address, and whether to check that the caller-supplied verifier is that one (the fix).

layout: ClassVar[tuple[str, ...]] = ('verifier', 'check_verifier', 'wrapped', 'processed')#
constructor(verifier, check_verifier)[source]#
Parameters:
  • verifier (str)

  • check_verifier (bool)

Return type:

None

deposit()[source]#

Lock ether, to be minted as wrapped ether on another chain.

Return type:

None

complete_transfer(recipient, amount, nonce, verifier)[source]#

Mint wrapped ether for a transfer that verifier says the guardians signed.

With check_verifier off, as in the vulnerable program, any contract can be passed as the verifier.

Parameters:
Return type:

None

redeem(amount)[source]#

Burn wrapped ether and receive as much locked ether.

Parameters:

amount (int)

Return type:

None

wrapped_balance(account)[source]#

Wrapped ether held by account.

Parameters:

account (str)

Return type:

int

Scaling the chain#

Plasma (Poon and Buterin, 2017): child chains secured by exit games.

Plasma moves whole blocks off chain. An operator runs a child chain and posts only each block’s Merkle root to a root-chain contract. The contract cannot check the child chain’s transactions, and the operator may withhold blocks or include invalid ones; what keeps coins safe is that their owner can always exit: prove on the root chain that it owns a coin, then wait out a challenge period during which anyone can prove the exit wrong.

This module follows Plasma Cash (Buterin, 2018), the simplest exit game. Every coin is indivisible and has its own slot in a sparse Merkle tree, so each block proves either the coin’s transfer or that the coin did not move. An exit presents the coin’s last transfer and the one before it, and a challenger answers with one of:

  • a spend: a later transfer signed by the exiting owner, so it no longer owns the coin;

  • a double spend: a different transfer signed by the previous owner, included between the two blocks, so the exiting transfer was never valid.

Plasma Cash’s third challenge, an invalid history answered by its owner, and the bonds that pay challengers are omitted here.

blockchainkit.channels.systems.plasma.transfer_message(coin, owner, parent_block)[source]#

The bytes the previous owner signs to send coin to owner.

Parameters:
Return type:

bytes

blockchainkit.channels.systems.plasma.sign_transfer(private, coin, owner, parent_block)[source]#

Send coin to owner, signed by its current owner, who received it in parent_block.

Examples

>>> from blockchainkit.channels import sign_transfer
>>> from blockchainkit.crypto import public_key
>>> sign_transfer(7, 0, public_key(5), 1).parent_block
1
Parameters:
Return type:

CoinTransfer

class blockchainkit.channels.systems.plasma.PlasmaChain(*, period=7, depth=16)[source]#

Bases: object

A Plasma Cash root-chain contract and the operator’s child-chain blocks.

Parameters:
  • period (int) – Blocks an exit waits for challenges.

  • depth (int) – Depth of each block’s sparse Merkle tree; coins are numbered below 2 ** depth.

Examples

>>> from blockchainkit.channels import PlasmaChain, sign_transfer
>>> from blockchainkit.crypto import public_key
>>> chain = PlasmaChain(period=7)
>>> deposit = chain.deposit(public_key(7))  # Alice deposits coin 0 in block 1.
>>> block = chain.submit_block([sign_transfer(7, 0, public_key(5), 1)])  # To Bob.
>>> exit_id = chain.start_exit(chain.prove(0, block), deposit, height=10)
>>> chain.finalize(exit_id, height=17).owner == public_key(5)
True
deposit(owner)[source]#

Lock a new coin on the root chain, which makes its deposit block itself.

Parameters:

owner (tuple[int, int])

Return type:

InclusionProof

submit_block(transfers)[source]#

The operator commits a block of transfers; the root chain checks none of them.

Parameters:

transfers (Sequence[CoinTransfer])

Return type:

int

prove(coin, block)[source]#

The operator’s proof of coin’s transfer in block.

Raises:

ValueError – The block does not move the coin.

Parameters:
Return type:

InclusionProof

start_exit(exiting, parent, *, height)[source]#

Claim a coin with its last transfer and the one before it; return the exit’s id.

A coin that has not moved since its deposit exits with parent=None.

Raises:

ValueError – A proof fails, or the exiting transfer does not spend parent.

Parameters:
Return type:

int

challenge(exit_id, evidence, *, height)[source]#

Cancel a pending exit with a spend or a double spend of its coin; True if it worked.

Parameters:
Return type:

bool

finalize(exit_id, *, height)[source]#

Release the coin to an unchallenged exit once its period is over.

Parameters:
Return type:

PlasmaExit

Sharding: OmniLedger (Kokoris-Kogias, Jovanovic, Gasser, Gailly, Syta and Ford, 2018).

Splitting the validators into shards, each keeping its own part of the ledger, multiplies throughput by the number of shards, but each shard is only as safe as its own committee. OmniLedger assigns validators to shards at random every epoch, from unbiasable randomness (RandHound), so an adversary controlling a fraction of all validators cannot choose where they land. A Byzantine-fault-tolerant committee of m fails if a third or more of it is malicious, which for M malicious validators out of N has the hypergeometric probability

\[P(X \ge m/3) = \sum_{x \ge m/3} \frac{\binom{M}{x}\binom{N-M}{m-x}}{\binom{N}{m}}.\]

It falls exponentially with the committee size, which is why shards must stay large. Transactions that spend coins on several shards use Atomix, a client-driven atomic commit: every input shard first locks the input and returns a proof of acceptance or rejection; if all accept, the client presents the proofs to the output shard to commit, and otherwise to the input shards to unlock. RandHound is replaced here by a seeded shuffle, and proofs of acceptance by the shards’ own word.

blockchainkit.channels.systems.sharding.shard_failure_probability(validators, malicious, shard_size, *, threshold=0.3333333333333333)[source]#

Chance that a random committee of shard_size is at least threshold malicious.

Examples

>>> from blockchainkit.channels import shard_failure_probability
>>> f"{shard_failure_probability(1_000, 250, 100):.4f}"
'0.0214'
Parameters:
Return type:

float

blockchainkit.channels.systems.sharding.assign_shards(validators, shards, *, seed=0)[source]#

Shuffle validators 0 .. validators - 1 into shards committees of near-equal size.

Examples

>>> from blockchainkit.channels import assign_shards
>>> [len(c) for c in assign_shards(10, 3, seed=1)]
[4, 3, 3]
Parameters:
Return type:

tuple[tuple[int, …], …]

blockchainkit.channels.systems.sharding.compromised_epochs(validators, malicious, shards, *, epochs, seed=0)[source]#

Fraction of epochs in which some committee is a third or more malicious.

Validators 0 .. malicious - 1 are the malicious ones; every epoch reshuffles the committees.

Parameters:
Return type:

float

blockchainkit.channels.systems.sharding.atomix_transfer(shards, inputs, output)[source]#

Spend inputs on several shards and credit their total to output, atomically.

Parameters:
Returns:

The shards’ balances after commit, or after abort and unlock.

Return type:

blockchainkit.channels.core.base.AtomixResult

Examples

>>> from blockchainkit.channels import atomix_transfer
>>> shards = [{"alice": 5}, {"alice": 3}, {}]
>>> atomix_transfer(shards, [(0, "alice", 5), (1, "alice", 3)], (2, "bob")).committed
True
>>> atomix_transfer(shards, [(0, "alice", 5), (1, "alice", 4)], (2, "bob")).rejected
(1,)

Rollups: zk-rollups (2018) and optimistic rollups (2019).

A rollup executes transactions off chain but posts them, compressed, to the chain, with the state root they lead to. Because the data is on chain, anyone can rebuild the state; what remains is to convince the chain that the posted root is right. There are two ways.

zk-rollups (Barry Whitehat’s roll_up, and Buterin’s proposal, 2018) post a succinct validity proof with each batch. The contract accepts the new root only if the proof verifies, which costs a constant amount of gas however many transactions the batch holds, so the cost per transaction falls toward that of its calldata alone:

\[g(n) = \frac{G_{\text{verify}}}{n} + 16\,b,\]

for n transactions of b bytes each, against 21,000 gas for a transfer on chain.

Optimistic rollups (Adler and Quintyne-Collins, 2019) post no proof. A root becomes final after a challenge window, during which anyone who recomputes the batch and disagrees can prove fraud. The proof is found by bisection (Kalodner et al., Arbitrum, 2018): asserter and challenger compare intermediate state roots, halving the disputed range each round, until they disagree about a single transaction, which the chain re-executes. That takes \(\lceil \log_2 n \rceil\) rounds, and a withdrawal must wait out the window.

Here invalid transactions, such as overdrafts, are skipped rather than rejecting the batch, as rollups do. There is no proof system yet: a ValidityProof states what it proves, and verification re-executes the batch on the state rebuilt from the posted data, standing in for a SNARK verifier.

blockchainkit.channels.systems.rollups.CALLDATA_GAS_PER_BYTE = 16#

Gas per nonzero byte of calldata (EIP-2028).

blockchainkit.channels.systems.rollups.TRANSFER_GAS = 21000#

Gas of a plain transfer on chain.

blockchainkit.channels.systems.rollups.state_root(balances)[source]#

Merkle root over the accounts’ (name, balance) leaves, sorted by name.

Parameters:

balances (Mapping[str, int])

Return type:

bytes

blockchainkit.channels.systems.rollups.batch_digest(batch)[source]#

Hash of a batch’s transactions, as posted on chain.

Parameters:

batch (Sequence[Transfer])

Return type:

bytes

blockchainkit.channels.systems.rollups.apply_transfer(balances, transfer)[source]#

The balances after transfer; unchanged if it overdraws or moves nothing.

Parameters:
Return type:

dict[str, int]

blockchainkit.channels.systems.rollups.execute_batch(balances, batch)[source]#

Run a batch; return the final balances and the trace of state roots.

The trace has len(batch) + 1 roots: before the batch, then after each transaction.

Examples

>>> from blockchainkit.channels import Transfer, execute_batch
>>> after, trace = execute_batch({"alice": 5}, [Transfer("alice", "bob", 3)])
>>> after, len(trace)
({'alice': 2, 'bob': 3}, 2)
Parameters:
Return type:

tuple[dict[str, int], tuple[bytes, …]]

blockchainkit.channels.systems.rollups.batch_gas(transactions, *, bytes_per_transaction, fixed_gas)[source]#

Gas to post a batch: a fixed cost (proof verification, root update) plus its calldata.

Examples

>>> from blockchainkit.channels import batch_gas
>>> batch_gas(1_000, bytes_per_transaction=12, fixed_gas=300_000) / 1_000
492.0
Parameters:
  • transactions (int)

  • bytes_per_transaction (int)

  • fixed_gas (int)

Return type:

int

class blockchainkit.channels.systems.rollups.ZKRollup(balances, *, verifier_gas=300000, bytes_per_transaction=12)[source]#

Bases: object

A rollup contract that accepts a new state root only with a validity proof.

Parameters:
  • balances (collections.abc.Mapping) – The genesis state.

  • verifier_gas (int) – Gas to verify one proof (a Groth16 verification costs about 200,000 to 300,000).

  • bytes_per_transaction (int) – Calldata per compressed transaction.

Examples

>>> from blockchainkit.channels import Transfer, ZKRollup
>>> rollup = ZKRollup({"alice": 10})
>>> batch = [Transfer("alice", "bob", 4)]
>>> gas = rollup.submit(batch, rollup.prove(batch))
>>> dict(rollup.balances), gas
({'alice': 6, 'bob': 4}, 300192)
balances: Mapping[str, int]#
prove(batch, *, claimed_root=None)[source]#

The operator proves the batch’s effect on the current state.

Raises:

ValueError – claimed_root is not the batch’s true result: a sound proof system has no proof of a false statement.

Parameters:
Return type:

ValidityProof

submit(batch, proof)[source]#

Post a batch and its proof; return the gas it cost.

Raises:

ValueError – The proof is for another state, batch, or result.

Parameters:
Return type:

int

blockchainkit.channels.systems.rollups.bisect(claimed, honest)[source]#

Find the first transaction whose result two traces disagree on, by halving.

Both traces start from the same root and end in different ones.

Returns:

The disputed transaction’s index and the rounds it took.

Return type:

tuple of int

Parameters:

Examples

>>> from blockchainkit.channels import bisect
>>> bisect(list(b"abcdefgh"), list(b"abcdeXYZ"))
(4, 3)
class blockchainkit.channels.systems.rollups.OptimisticRollup(balances, *, window, bond=10)[source]#

Bases: object

A rollup contract that accepts state roots unless someone proves fraud in time.

Parameters:
  • balances (collections.abc.Mapping) – The genesis state.

  • window (int) – Blocks an assertion can be challenged.

  • bond (int) – What a proposer stakes on each assertion, paid to a successful challenger.

Examples

>>> from blockchainkit.channels import OptimisticRollup, Transfer, execute_batch, state_root
>>> rollup = OptimisticRollup({"alice": 10}, window=100)
>>> batch = [Transfer("alice", "bob", 4), Transfer("bob", "carol", 1)]
>>> _, trace = execute_batch({"alice": 10}, batch)
>>> forged = (*trace[:2], state_root({"alice": 6, "bob": 3, "mallory": 1}))
>>> rollup.propose("mallory", batch, forged, height=1)
0
>>> rollup.challenge(0, "carol", height=50).step
1
>>> dict(rollup.payouts)
{'carol': 10}
balances: Mapping[str, int]#
pending: list[tuple[str, tuple[Transfer, ...], tuple[bytes, ...], int]]#
payouts: dict[str, int]#
propose(proposer, batch, trace, *, height)[source]#

Post a batch with its claimed trace of state roots, and a bond; return its index.

The contract checks only that the trace starts at the previous claimed root.

Parameters:
Return type:

int

challenge(index, challenger, *, height)[source]#

Re-execute pending assertion index; on fraud, discard it and every later one.

Returns:

The single wrong step, or None if the assertion is correct.

Return type:

blockchainkit.channels.core.base.FraudProof or None

Parameters:
finalize(*, height)[source]#

Make final every assertion whose window has closed; return how many.

Parameters:

height (int)

Return type:

int

Plotting#

Plotting helpers for blockchainkit.channels: erasure-coded shares, routes, and sampling.

blockchainkit.channels.visualizers.plots.plot_detection(samples, withheld=(0.1, 0.25, 0.5), *, ax=None)[source]#

Draw the chance a light client notices withholding, against how many shares it samples.

Parameters:
Return type:

matplotlib.axes.Axes

blockchainkit.channels.visualizers.plots.plot_route(route, *, ax=None)[source]#

Draw the HTLC amount and expiry offered on each hop of a route.

Parameters:
Returns:

The axes holding the amount bars; the expiries are on a twin axis.

Return type:

matplotlib.axes.Axes

blockchainkit.channels.visualizers.plots.plot_shares(block, *, withheld=(), ax=None)[source]#

Draw a block’s shares as stems: data, parity, and any withheld by the producer.

Parameters:
Return type:

matplotlib.axes.Axes