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:
objectHow 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:
- class blockchainkit.channels.core.base.SwapOutcome(alice, bob, secret_revealed)[source]#
Bases:
objectWho ended up with which coins after an atomic swap between chains A and B.
- Variables:
- Parameters:
- class blockchainkit.channels.core.base.Commitment(holder, state, to_local, to_remote, revocation, txid)[source]#
Bases:
objectOne 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 (
tupleofint) – The public key that lets the counterparty taketo_localonce revoked.txid (
bytes) – Hash identifying the transaction.
- Parameters:
- class blockchainkit.channels.core.base.Route(nodes, amounts, expiries)[source]#
Bases:
objectA payment route and what each hop forwards.
- Variables:
- Parameters:
- class blockchainkit.channels.core.base.PaymentAttempt(success, route, failed_at, error)[source]#
Bases:
objectThe 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 (
tupleofintorNone) – The channel(from, to)where the HTLC was refused, if it was.error (
strorNone) –"temporary_channel_failure"(not enough balance) or"unknown_payment_hash"(the recipient cannot claim it).
- Parameters:
- class blockchainkit.channels.core.base.ProbeResult(low, high, probes)[source]#
Bases:
objectWhat an outsider learned about one side of a channel by sending probes.
- Variables:
- Parameters:
- class blockchainkit.channels.core.base.CoinTransfer(coin, owner, parent_block, signature=None)[source]#
Bases:
objectA 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.parent_block (
int) – The block holding the previous transfer of the coin (0 for a deposit).signature (
blockchainkit.crypto.core.base.SchnorrSignatureorNone) – The previous owner’s signature; None for a deposit.
- Parameters:
- signature: SchnorrSignature | None = None#
- class blockchainkit.channels.core.base.InclusionProof(transfer, block, proof)[source]#
Bases:
objectA transfer, the block it is in, and the sparse Merkle proof that it is.
- Variables:
block (
int)proof (
blockchainkit.structures.core.base.SparseMerkleProof)
- Parameters:
transfer (CoinTransfer)
block (int)
proof (SparseMerkleProof)
- transfer: CoinTransfer#
- proof: SparseMerkleProof#
- class blockchainkit.channels.core.base.PlasmaExit(coin, owner, block, parent_block, deadline, status='pending')[source]#
Bases:
objectAn exit from the Plasma chain, waiting out its challenge period.
- Variables:
coin (
int)owner (
tupleofint) – 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:
- class blockchainkit.channels.core.base.AtomixResult(committed, balances, rejected)[source]#
Bases:
objectA 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 (
tupleofcollections.abc.Mapping) – Each shard’s balances afterwards.rejected (
tupleofint) – The input shards that refused to lock.
- Parameters:
- class blockchainkit.channels.core.base.Transfer(sender, recipient, amount)[source]#
Bases:
objectA rollup transaction: move
amountfromsendertorecipient.
- class blockchainkit.channels.core.base.ValidityProof(old_root, new_root, batch_digest)[source]#
Bases:
objectThe statement a zk-rollup’s proof attests: this batch takes
old_roottonew_root.- Variables:
- Parameters:
- class blockchainkit.channels.core.base.FraudProof(step, rounds, claimed, correct)[source]#
Bases:
objectThe single step at which an optimistic rollup’s assertion went wrong.
- Variables:
- Parameters:
- class blockchainkit.channels.core.base.ErasureCodedData(k, shares, root)[source]#
Bases:
objectA block’s data extended with Reed-Solomon parity and committed to by a Merkle root.
- Variables:
- Parameters:
- class blockchainkit.channels.core.base.EncodingFraudProof(positions, shares, proofs)[source]#
Bases:
objectEvidence that a block producer’s shares do not form a Reed-Solomon codeword.
- Variables:
- Parameters:
- proofs: tuple[MerkleProof, ...]#
- class blockchainkit.channels.core.base.SamplingResult(detected, collected, recoverable)[source]#
Bases:
objectLight clients sampling a block whose producer withholds some shares.
- Variables:
- Parameters:
- class blockchainkit.channels.core.base.AppointmentReceipt(tower, hint, expires, signature)[source]#
Bases:
objectA watchtower’s signed promise to watch for one revoked commitment (PISA).
- Variables:
hint (
bytes) – Half the revoked commitment’s txid, which the tower watches for.expires (
int) – The last height the tower is responsible for.signature (
blockchainkit.crypto.core.base.SchnorrSignature)
- Parameters:
- 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 ann-symbol codeword.- Parameters:
data (
collections.abc.Sequenceofint) – Field elements, the polynomial’s values at0, ..., k - 1.n (
int) – Codeword length, at leastkand at mostprime.prime (
int) – The field modulus.
- Returns:
The values at
0, ..., n - 1; the firstkaredata.- Return type:
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 belowk.
- blockchainkit.channels.systems.reed_solomon.rs_recover(shares, k, n, *, prime=65537)[source]#
Rebuild the whole codeword from any
kof its symbols (erasure decoding).- Parameters:
shares (
collections.abc.Mapping) – Position to value, for at leastkpositions inrange(n).k (
int) – Data and codeword lengths.n (
int) – Data and codeword lengths.prime (
int) – The field modulus.
- Raises:
ValueError – Fewer than
kshares, a position out of range, or shares that are not all on one polynomial of degree belowk.- Return type:
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
kdata symbols (Berlekamp-Welch).With
msymbols received (Nonemarks an erasure), up to(m - k) // 2of them may be wrong. The algorithm looks for an error locatorE(monic, of degreee) andQ = P Eof degree belowk + ewithQ(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:
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
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.
Commit to
sharesas they are, whether or not they form a codeword.- Parameters:
- Return type:
- blockchainkit.channels.systems.data_availability.extend(data, *, factor=2, prime=65537)[source]#
Extend
datatofactor * 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)
The Merkle proof a client receives with the share at
position.- Parameters:
block (ErasureCodedData)
position (int)
- Return type:
True if
valueis the share the root commits to atproof.index.- Parameters:
value (int)
proof (MerkleProof)
root (bytes)
- Return type:
- blockchainkit.channels.systems.data_availability.detection_probability(withheld, samples)[source]#
Chance that
samplesuniform draws (with replacement) hit a withheld share.Examples
>>> from blockchainkit.channels import detection_probability >>> round(detection_probability(0.5, 7), 4) 0.9922
- blockchainkit.channels.systems.data_availability.simulate_sampling(block, withheld, *, clients, samples, seed=0)[source]#
Clients each request
samplesdistinct random shares from a producer withholding some.- Parameters:
block (
blockchainkit.channels.core.base.ErasureCodedData) – What the producer committed to.withheld (
collections.abc.Collectionofint) – Share positions the producer refuses to serve.clients (
int) – Number of light clients and shares each requests.samples (
int) – Number of light clients and shares each requests.seed (
int) – Seeds the clients’ choices.
- Return type:
- 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
kshares fix the polynomial; the proof adds the first share that is not on it.- Parameters:
block (ErasureCodedData)
prime (int)
- 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:
proof (EncodingFraudProof)
root (bytes)
k (int)
prime (int)
- Return type:
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:
objectA unidirectional payment channel from
payertopayee.- 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}
- close(*, height)[source]#
The payee countersigns the latest payment and publishes it.
- Parameters:
height (int)
- Return type:
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
- 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:
- Returns:
One lock time per level, root first.
- Return type:
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
- class blockchainkit.channels.systems.duplex.DuplexChannel(parties, deposits, *, depth, steps, delta)[source]#
Bases:
objectTwo one-way channels under an invalidation tree.
- Parameters:
deposits (
tupleofint) – Each party’s initial capacity in its outgoing channel.depth (
int) – The invalidation tree; seeinvalidation_locktimes().steps (
int) – The invalidation tree; seeinvalidation_locktimes().delta (
int) – The invalidation tree; seeinvalidation_locktimes().
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)
- 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
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:
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:
objectA two-party Poon-Dryja channel.
- Parameters:
parties (
tupleofstr) – The two parties; the first fundsdeposits[0], the seconddeposits[1].keys (
tupleofint) – Their private funding keys; revocation, delayed and per-commitment keys are derived from them.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}
- revocation_key(holder, state)[source]#
R = S + B: the holder’s per-commitment point plus the counterparty’s base point.
- 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.
- commitment(holder, state=None)[source]#
The holder’s commitment for
state(the current one by default).- Parameters:
- Return type:
- pay(sender, amount)[source]#
Move
amountfromsenderto 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.
- close_cooperatively(*, height)[source]#
Both parties sign a transaction paying the current balances, with no delay.
- Parameters:
height (int)
- Return type:
- 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:
- Return type:
- sweep(*, height)[source]#
The holder takes its delayed output, once
delayblocks have passed.- Raises:
ValueError – The delay has not passed, so Script rejects the spend.
- Parameters:
height (int)
- Return type:
- justice_message(commitment)[source]#
What the revocation key signs to take a revoked commitment’s
to_local.- Parameters:
commitment (Commitment)
- Return type:
- 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:
- penalize(*, height, justice=None)[source]#
The counterparty takes the holder’s output of a revoked commitment.
- Parameters:
height (
int) – When the penalty is published; it must come before the holder’s sweep.justice (
blockchainkit.crypto.core.base.SchnorrSignature, optional) – A presignedjustice_signature(), as a watchtower would publish.
- Raises:
ValueError – The published commitment is the current one, so it cannot be revoked.
- Return type:
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
versionof a channel’s state.
- blockchainkit.channels.systems.state_channels.sign_state(private, channel, version, balances, *, final=False)[source]#
One party’s signature on a channel state;
finalmarks a cooperative close.Examples
>>> from blockchainkit.channels import sign_state >>> sign_state(7, "0xchannel", 3, [60, 40]).response > 0 True
- class blockchainkit.channels.systems.state_channels.StateChannel[source]#
Bases:
ContractAn adjudicator holding two deposits and settling on the highest signed version.
Constructor arguments: the parties’ public
keys, theaccountspaid at the end, the initialbalances(sent as the deployment’s value), and the disputeperiodin 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')#
- submit(version, balances, signatures)[source]#
Submit a signed state; the first submission opens the dispute period.
- Parameters:
version (int)
signatures (Sequence[SchnorrSignature])
- Return type:
None
- close(version, balances, signatures)[source]#
Pay out at once a state both parties signed as final.
- Parameters:
version (int)
signatures (Sequence[SchnorrSignature])
- Return type:
None
- class blockchainkit.channels.systems.state_channels.PreimageManager[source]#
Bases:
ContractSprites’ global record of when each payment preimage was first published.
- 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))
- blockchainkit.channels.systems.state_channels.sprites_expiries(hops, *, delta, final=0)[source]#
Sprites’ expiries: every hop settles
deltaafter the shared preimage deadline.
- 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).
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:
objectA two-party eltoo channel.
- Parameters:
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.
- 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:
- Return type:
None
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 revokedstate.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)
- blockchainkit.channels.systems.watchtowers.receipt_message(hint, expires)[source]#
The bytes a tower signs to accept an appointment.
- class blockchainkit.channels.systems.watchtowers.Watchtower(key, *, collateral=0, online=True)[source]#
Bases:
objectA tower that stores encrypted penalties and publishes them when a hint matches.
- Parameters:
- appoint(hint, blob, *, expires)[source]#
Store an appointment until
expiresand return the signed receipt.- Parameters:
- Return type:
- watch(channel, *, height)[source]#
Look at the chain; if a watched commitment is there, publish its penalty.
- Parameters:
channel (LightningChannel)
height (int)
- 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:
- 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:
receipt (AppointmentReceipt)
channel (LightningChannel)
- Return type:
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:
objectPayment 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 whatucan send tov; 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:
- route(nodes, amount, *, height=0)[source]#
The amounts and expiries of a payment of
amountalongnodes.They are computed backwards from the recipient: each intermediate node is offered what it forwards plus its fee, and an expiry
cltv_deltablocks later than the one it offers.
- find_route(source, target, amount, *, height=0)[source]#
The fewest-hop route whose every channel has the capacity for
amount.
- send(route, *, known_hash=True)[source]#
Offer the HTLCs hop by hop; settle them all, or fail them all back.
- Parameters:
route (
blockchainkit.channels.core.base.Route) – Fromroute()orfind_route().known_hash (
bool) – False if the recipient does not know the preimage, as for a probe.
- Return type:
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
probes for a channel of capacity \(C\).
- blockchainkit.channels.systems.probing.probe_balance(network, prober, u, v)[source]#
Find
u’s balance towardvby sending unclaimable paymentsprober -> u -> v.- Parameters:
network (
blockchainkit.channels.systems.routing.ChannelNetwork) – The prober must have a channel withu, with enough balance on its side.prober (
int) – Nodes;uandvshare the target channel.u (
int) – Nodes;uandvshare the target channel.v (
int) – Nodes;uandvshare the target channel.
- Returns:
Narrowed to a single value unless the prober’s own channel runs out first.
- Return type:
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:
objectAlice’s
amount_aon chain A for Bob’samount_bon 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
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
payloadsthrough a Merkle root, with valid proof of work.- Parameters:
previous (
blockchainkit.structures.systems.block.BlockHeaderorNone) – The parent; None for a genesis header.payloads (
collections.abc.Sequenceofbytes) – The block’s transactions.difficulty (
int) – Required leading zero bits of the header hash.timestamp (
int) – The header’s timestamp.max_attempts (
int) – Nonces to try before giving up.
- Raises:
TimeoutError – No nonce worked within
max_attempts.- Return type:
- class blockchainkit.channels.systems.relay.BTCRelay[source]#
Bases:
ContractStores proof-of-work headers, follows the heaviest chain, and verifies inclusion.
Constructor arguments: the
genesisheader the relay trusts, and theminimum_difficultyevery 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
- constructor(genesis, minimum_difficulty)[source]#
- Parameters:
genesis (BlockHeader)
minimum_difficulty (int)
- 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
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
amounttorecipientfrombridge.
- 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
- class blockchainkit.channels.systems.bridges.ValidatorBridge[source]#
Bases:
ContractRonin’s bridge: locked ether, released on
thresholdvalidator 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
- class blockchainkit.channels.systems.bridges.GuardianVerifier[source]#
Bases:
ContractWormhole’s signature-verification step: records messages enough guardians signed.
Constructor arguments: the guardians’ public keys and the threshold.
- verify_signatures(message, approvals)[source]#
Check the guardians’ signatures on
messageand record its hash as verified.
- is_verified(message_hash)[source]#
True if the message with this hash passed
verify_signatures().
- class blockchainkit.channels.systems.bridges.ForgedVerifier[source]#
Bases:
ContractThe attacker’s stand-in for the verification account: it vouches for everything.
- class blockchainkit.channels.systems.bridges.WormholeBridge[source]#
Bases:
ContractWrapped 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).
- complete_transfer(recipient, amount, nonce, verifier)[source]#
Mint wrapped ether for a transfer that
verifiersays the guardians signed.With
check_verifieroff, as in the vulnerable program, any contract can be passed as the verifier.
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
cointoowner.
- blockchainkit.channels.systems.plasma.sign_transfer(private, coin, owner, parent_block)[source]#
Send
cointoowner, signed by its current owner, who received it inparent_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
- class blockchainkit.channels.systems.plasma.PlasmaChain(*, period=7, depth=16)[source]#
Bases:
objectA Plasma Cash root-chain contract and the operator’s child-chain blocks.
- Parameters:
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:
- Return type:
- submit_block(transfers)[source]#
The operator commits a block of transfers; the root chain checks none of them.
- Parameters:
transfers (Sequence[CoinTransfer])
- Return type:
- prove(coin, block)[source]#
The operator’s proof of
coin’s transfer inblock.- Raises:
ValueError – The block does not move the coin.
- Parameters:
- Return type:
- 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:
exiting (InclusionProof)
parent (InclusionProof | None)
height (int)
- Return type:
- 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:
exit_id (int)
evidence (InclusionProof)
height (int)
- Return type:
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
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_sizeis at leastthresholdmalicious.Examples
>>> from blockchainkit.channels import shard_failure_probability >>> f"{shard_failure_probability(1_000, 250, 100):.4f}" '0.0214'
- blockchainkit.channels.systems.sharding.assign_shards(validators, shards, *, seed=0)[source]#
Shuffle validators
0 .. validators - 1intoshardscommittees 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]
- 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 - 1are the malicious ones; every epoch reshuffles the committees.
- blockchainkit.channels.systems.sharding.atomix_transfer(shards, inputs, output)[source]#
Spend
inputson several shards and credit their total tooutput, atomically.- Parameters:
shards (
collections.abc.Sequenceofcollections.abc.Mapping) – Each shard’s balances.inputs (
collections.abc.Sequenceoftuple) –(shard, account, amount)to spend.output (
tuple) –(shard, account)to credit.
- Returns:
The shards’ balances after commit, or after abort and unlock.
- Return type:
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:
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.
- blockchainkit.channels.systems.rollups.batch_digest(batch)[source]#
Hash of a batch’s transactions, as posted on chain.
- blockchainkit.channels.systems.rollups.apply_transfer(balances, transfer)[source]#
The balances after
transfer; unchanged if it overdraws or moves nothing.
- 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) + 1roots: 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)
- 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
- class blockchainkit.channels.systems.rollups.ZKRollup(balances, *, verifier_gas=300000, bytes_per_transaction=12)[source]#
Bases:
objectA 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)
- prove(batch, *, claimed_root=None)[source]#
The operator proves the batch’s effect on the current state.
- Raises:
ValueError –
claimed_rootis not the batch’s true result: a sound proof system has no proof of a false statement.- Parameters:
- Return type:
- 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:
proof (ValidityProof)
- Return type:
- 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:
- 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:
objectA 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}
- 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.
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:
samples (
collections.abc.Sequenceofint) – Sample counts on the horizontal axis.withheld (
collections.abc.Sequenceoffloat) – Fractions of shares withheld, one curve each.ax (
matplotlib.axes.Axes, optional) – Axes to draw on; a new figure is created if omitted.
- Return type:
- blockchainkit.channels.visualizers.plots.plot_route(route, *, ax=None)[source]#
Draw the HTLC amount and expiry offered on each hop of a route.
- Parameters:
route (
blockchainkit.channels.core.base.Route) – Fromroute().ax (
matplotlib.axes.Axes, optional) – Axes to draw on; a new figure is created if omitted.
- Returns:
The axes holding the amount bars; the expiries are on a twin axis.
- Return type:
Draw a block’s shares as stems: data, parity, and any withheld by the producer.
- Parameters:
block (
blockchainkit.channels.core.base.ErasureCodedData) – Fromextend().withheld (
collections.abc.Collectionofint) – Positions drawn as missing.ax (
matplotlib.axes.Axes, optional) – Axes to draw on; a new figure is created if omitted.
- Return type: