Life of a payment#
Follow one payment, Alice paying Bob 25 units, from a private key to a block buried under others, through a fork that undoes it and the reinclusion that restores it. Each stage uses a different subpackage, and each stage answers a different question: who authorized it, who knows about it, which history holds it, and how sure we are.
All snippets use import blockchainkit as bk. The keys and nonces are
teaching fixtures; never use fixed values like these for real money.
1. Keys and addresses#
Alice’s private key is a number; her public key is a point on secp256k1; her address is the hash of the public key (Hashes everywhere).
>>> alice_key, bob_key = 7, 11
>>> alice_public = bk.crypto.public_key(alice_key)
>>> alice = bk.structures.address(alice_public)
>>> bob = bk.structures.address(bk.crypto.public_key(bob_key))
>>> len(alice), alice == bk.crypto.sha256(bk.crypto.encode_point(alice_public)).hex()
(64, True)
Who can spend? Whoever knows alice_key. Nothing else identifies Alice.
3. Validation against a state#
Each node keeps a ledger of balances and account nonces. Applying a payment returns a new ledger and leaves the old one untouched; a payment that overspends or reuses a nonce is rejected.
>>> genesis_state = bk.structures.Ledger({alice: 100})
>>> after = genesis_state.apply([payment])
>>> after.balances[bob], after.balances[alice], genesis_state.balances.get(bob, 0)
(25, 75, 0)
>>> greedy = bk.structures.Transaction(alice_public, bob, 101, 0).signed(alice_key, signing_nonce=19)
>>> try:
... genesis_state.apply([greedy])
... except ValueError:
... print("rejected: insufficient funds")
rejected: insufficient funds
4. Propagation: gossip#
Alice’s node broadcasts the payment, and peers forward it to their neighbors. For a while, peers disagree about what is pending.
>>> net = bk.network.SimulatedNetwork(["alice", "relay", "miner"], seed=1)
>>> net.connect("alice", "relay", latency=(1, 3))
>>> net.connect("relay", "miner", latency=(1, 3))
>>> net.broadcast("alice", payment.txid)
>>> _ = net.run(until=1)
>>> sorted(d.recipient for d in net.deliveries)
['alice', 'relay']
>>> _ = net.run()
>>> sorted(d.recipient for d in net.deliveries)
['alice', 'miner', 'relay']
See Discrete-event simulation: a network on an event queue (GPSS 1961, Simula 1965).
5. Inclusion: a mined block#
A miner puts the payment in a block, whose header commits to the transactions through a Merkle root, and searches for a nonce that makes the header’s hash small enough.
>>> genesis = bk.consensus.mine(bk.structures.Block(difficulty=4)).block
>>> chain = bk.structures.Blockchain(genesis, genesis_state)
>>> block = bk.consensus.mine(bk.structures.Block(genesis.hash, (payment,), 1, 1, 4)).block
>>> bk.consensus.valid_pow(block), chain.add(block)
(True, True)
>>> chain.state.balances[bob]
25
A light client that holds only the header can check inclusion with a Merkle proof:
>>> proof = bk.structures.MerkleTree([payment.to_bytes()]).proof(0)
>>> bk.structures.verify_proof(payment.to_bytes(), proof, block.merkle_root)
True
6. Confirmation: how sure?#
Each block mined on top makes reversal less likely. With an attacker holding 10% of the hash power, Nakamoto’s calculation gives:
>>> [round(bk.consensus.attacker_success_probability(0.1, z), 5) for z in (1, 3, 6)]
[0.20459, 0.01317, 0.00024]
This is why merchants wait for confirmations. There is no point at which a proof-of-work payment becomes impossible to reverse, only ever less likely (compare Casper’s finality in Who do you trust?).
7. A fork undoes it#
Meanwhile, another miner who never saw the payment built a longer branch. Every node switches to the branch with the most work, and the payment’s effect disappears.
>>> rival = bk.consensus.mine(bk.structures.Block(genesis.hash, (), 1, 2, 4)).block
>>> rival_2 = bk.consensus.mine(bk.structures.Block(rival.hash, (), 2, 3, 4)).block
>>> chain.add(rival), chain.add(rival_2)
(True, True)
>>> chain.tip == rival_2, chain.state.balances.get(bob, 0)
(True, 0)
>>> len(chain.tips()) # Both branches are kept; only one is selected.
2
The signature is still valid, and on the new branch Alice has not used account nonce 0, so the payment is eligible again: back to the pending queue.
8. Reinclusion, and no replay#
>>> chain.state.apply([payment]).balances[bob]
25
>>> again = bk.consensus.mine(bk.structures.Block(rival_2.hash, (payment,), 3, 4, 4)).block
>>> chain.add(again), chain.state.balances[bob]
(True, 25)
>>> try:
... chain.state.apply([payment])
... except ValueError:
... print("replay rejected: account nonce already used")
replay rejected: account nonce already used
Had the winning branch contained a conflicting payment from Alice with the same account nonce, the original would not have been eligible again, despite its valid signature (see Exercises: ledgers and data structures, problem 7).
Where each question was answered#
Question |
Answered by |
Subpackage |
|---|---|---|
Did Alice authorize it? |
Schnorr signature |
|
Can she afford it? Is it a replay? |
ledger state and account nonce |
|
Who knows about it? |
gossip |
|
Which history holds it? |
proof of work and fork choice |
|
How sure are we? |
confirmations against an attacker’s share |
The full simulation, with two peers, a partition, and a balance and queue table, is One payment from signature to reorganization and reinclusion.