Exact conventions and model boundaries#
This page specifies exact rules for readers implementing or inspecting the model. It is optional on a first reading. Start with Start here: a blockchain without the jargon for the concepts and Your first payment, step by step for a complete worked payment.
Byte and number conventions#
Hash inputs are bytes; callers explicitly encode text. Digests are 32-byte
values, and proof-of-work interprets them as unsigned big-endian integers.
Amounts, sequence numbers, heights, and timestamps are integers. A value of
the wrong type (including a boolean, a float, or a string) raises TypeError;
an integer out of range raises ValueError. Simulation seeds must be integers. No binary floating-point
amounts enter consensus state. Transaction amounts and block fields fit in
unsigned 64-bit integers. Account balances can grow beyond that per-transfer
bound, using Python’s exact integers.
Signatures and accounts#
Accounts are lowercase SHA-256 hex digests of uncompressed secp256k1 public
points: 0x04 || x[32] || y[32]. The signature challenge is SHA-256 of:
"blockchainkit:schnorr:v1:{p}:{a}:{b}:{order}:"
|| encode(generator) || encode(commitment) || encode(public) || message
The digest is reduced modulo the subgroup order. A signature is the point
R and scalar s = k + challenge*x mod order. Verification checks
canonical coordinates, subgroup membership, scalar bounds, and the Schnorr
equation. Subgroup membership costs one extra scalar multiplication per point,
so it is skipped when Hasse’s bound proves the cofactor is 1
(2*order > p + 1 + 2*(isqrt(p) + 1)); then every curve point is in the
subgroup. This holds for secp256k1 and the toy curve. This format differs from BIP-340, ECDSA, and Bitcoin addresses.
Private keys and fresh signing nonces lie in [1, order).
Transactions and replay#
The signed payload is a JSON object with exactly these fields:
version=1, chain_id, sender (hex public point), recipient,
amount, and nonce. Encoding uses sorted keys, compact separators,
ASCII escapes, and UTF-8 bytes. There are no float-valued fields. This is
a package-specific encoding, not a general implementation of canonical JSON.
Signed transaction bytes encode another object with payload (that JSON
as a string) and signature ([[Rx, Ry], s] or null). A transaction’s
signature must be a SchnorrSignature or absent. The transaction ID
hashes those signed bytes. The ledger checks the signature, chain ID,
expected next sender nonce, and sufficient funds. It consumes a nonce even
for a self-transfer. Batch application is atomic.
The account nonce and signing nonce solve different problems. Account nonces are public sequence numbers preventing replay. Signing nonces are secret, fresh cryptographic scalars; their reuse can reveal a key.
Merkle commitments#
Leaf:
SHA256(0x00 || payload).Internal node:
SHA256(0x01 || left_digest || right_digest).Odd unpaired node: promote the digest unchanged.
Final root:
SHA256(0x02 || leaf_count[8, big-endian] || top_digest).Empty tree: the same final-root formula with count zero and no top digest.
Proofs carry a zero-based index, leaf count, and bottom-up siblings, using
None for an unpaired promotion. Verification rejects extra/missing levels
and inconsistent positions. The trusted root binds the count. These conventions
deliberately differ from Bitcoin’s odd-leaf duplication convention.
Block headers and chain selection#
Header JSON uses version=1, previous_hash, merkle_root (both hex),
height, timestamp, difficulty, and nonce. The block hash is
one SHA-256 of this encoding. All transaction bytes are committed by the root.
Mining varies only the header nonce: the Merkle root is computed once per
block, not once per attempt.
For difficulty d, the inclusive target is 2**(256-d)-1. Each valid
block contributes 2**d units of cumulative expected work. Genesis fixes
the difficulty; a child cannot choose a lower difficulty to gain acceptance.
The selected tip maximizes work, breaking ties by the smaller hash. Height
must increment by one; timestamps cannot decrease along a branch. There is
no claim that a supplied timestamp matches civil time.
Genesis must be mined, empty, at height zero, with a zero parent hash. The initial ledger allocation and chain ID are shared out-of-band simulation configuration; they are not committed in genesis. Peers must be configured identically. A node rejects unknown parents rather than silently buffering them; synchronization examples send parents before children.
There is no difficulty adjustment, issuance, reward, fee market, persistent database, production mempool, or live chain interoperability. Fork state is retained in memory for teaching, without pruning.
Network semantics#
Links are undirected. Latency is sampled uniformly from inclusive integer bounds, with a minimum of one tick. Duplicate payload hashes are suppressed per peer; origin receipt counts as a delivery. Equal-time events are ordered by a monotonically increasing sequence number. Peer enumeration is sorted to avoid dependence on input mapping order.
Disconnect/reconnect creates a new link generation: old in-flight events are discarded. Healing a partition does not trigger automatic anti-entropy; the experiment explicitly retransmits. A receive callback can reject a payload, which remains seen and is not forwarded by that peer. Exceptions from callbacks propagate to the caller; they are not silently interpreted as rejection, and the payload is not marked seen, so it can be delivered again.
VM instruction set#
Each instruction is (opcode, operand), where the opcode is a string.
Operand-free instructions use None. Every instruction costs one gas unit, including STOP. Whole
program syntax is validated before execution, even for unreachable code.
Opcode |
Semantics |
|---|---|
PUSH n |
Push a 256-bit unsigned integer. |
ADD, SUB, MUL |
Pop right then left; push the result modulo 2**256. |
DIV |
Unsigned integer division; division by zero raises VMError. |
EQ, LT |
Compare left and right; push 0 or 1. |
DUP, DROP, SWAP |
Duplicate, remove, or swap stack elements. |
LOAD k, STORE k |
Load a slot (default 0), or pop into a slot. |
JMP i |
Jump to an absolute instruction index. |
JZ i |
Pop a condition and jump if zero. |
STOP |
Finish successfully; falling off the end also succeeds. |
Storage is copied before execution. Underflow, invalid operands, out-of-gas,
and stack overflow raise VMError without mutating input state. Returned
storage is read-only. Gas bounds interpreter steps, not all host CPU/memory
costs: parsing/materializing a huge program is not metered. This teaching VM
is not a hostile-code sandbox and is not connected to the transfer ledger.
Contract world model#
World models the EVM’s call
semantics, not its bytecode. Contracts are Python classes; public methods
(names without a leading underscore) are dispatched by name, not by a 4-byte
selector, and values are Python objects rather than 256-bit words, with
unbounded integer arithmetic. Field i of a contract’s layout is
storage slot i, and entry k of a mapping field is slot (i, k)
rather than a Keccak hash; writing 0 clears a slot, and an unwritten slot
reads 0. delegatecall runs the callee’s code on the caller’s storage
through the callee’s layout.
Every call runs on a snapshot of balances, code, storage, nonces and
events: a revert (ContractError)
restores the snapshot of that call only, and the caller sees the error. A
Python exception that is not a revert restores the snapshot and propagates.
A reverted transaction still increments the sender’s nonce and reports the
gas it used, but no ether is charged for gas.
Operation |
Gas |
Ethereum, for comparison |
|---|---|---|
Transaction |
21,000 |
21,000 |
Storage read / write |
800 / 5,000 |
2,100 cold read; 20,000 to fill an empty slot, 2,900 to change one |
Call, create, log |
700, 32,000, 375 |
100-2,600 (warm/cold), 32,000, 375 plus data |
|
2,300 |
2,300 |
A call forwards at most 63/64 of the remaining gas (EIP-150), and the
default call-depth limit is 128 rather than 1024, to stay within Python’s
recursion limit. selfdestruct deletes code and storage at once rather
than at the end of the transaction. Contract addresses are
"0x" + SHA-256(tag || canonical_json([deployer, nonce]))[-20:] for CREATE
and "0x" + SHA-256(tag || canonical_json([deployer, salt]) ||
code_hash)[-20:] for CREATE2, where the code hash covers the class and its
constructor arguments. Block hashes are SHA-256 of a seed and the block
number, available for the 256 previous blocks. The domain tags are in
blockchainkit.constants.
Economic models#
blockchainkit.economics adds no consensus rule: the chain in
blockchainkit.structures still has no issuance, reward or fee. The
models are pure functions and seeded simulations, and the market contracts
run on the contract world model above.
Block subsidy.
block_subsidy()is Bitcoin’s schedule exactly:50 * 10**8 >> (height // 210000)satoshis, 0 from the 64th halving.issued_supplycounts the genesis coins, which Bitcoin cannot spend.Base fee.
next_base_fee()follows the EIP-1559 integer update (maximum change 1/8, elasticity 2, a minimum increase of 1 above target). The simulated mempool holds only 21,000-gas transfers, with values drawn uniformly and integer fees per gas; there are no nonces, replacements or blob fees.Swaps.
amount_out()is Uniswap V2’sgetAmountOutwith the fee in basis points.ConstantProductPoolkeeps its reserves in storage rather than reading token balances, records shares in a mapping rather than an ERC-20 token, locks no minimum liquidity, and exposes the price before the current block’s first trade in place of V2’s cumulative time-weighted price.Vaults.
VaultEngineuses Single-Collateral Dai’s 150% ratio and 13% penalty, with integer prices in Dai per ether. A liquidator buys the collateral at the oracle price, rounded up to whole ether; there is no auction, stability fee or global settlement.Mining games.
optimal_mining_revenue()caps both branches atmax_leadblocks and stops value iteration when the gain’s sign is certain, so the result is a lower bound accurate totolerance.Auctions and builders. Values are independent and uniform; ties in
first_price_auction()andsecond_price_auction()go to the alphabetically first bidder. Builder auctions insimulate_pbs()are sealed and second-price, without relays.
Channels, rollups and bridges#
blockchainkit.channels adds no consensus rule to the chain in
blockchainkit.structures. Channel outputs are
blockchainkit.vm.systems.script programs, and the relay, state
channel and bridges are contracts on the world model above. Off-chain
messages are signed as tag || canonical_json(fields) under the
CHANNEL_DOMAIN, PLASMA_DOMAIN, BRIDGE_DOMAIN and
WATCHTOWER_DOMAIN tags of blockchainkit.constants, with RFC 6979
signing nonces.
Scripts. There is no
OP_CHECKMULTISIG: a 2-of-2 output is<A> OP_CHECKSIG OP_VERIFY <B> OP_CHECKSIG. There is noOP_CHECKSEQUENCEVERIFY: a relative delay is written withOP_CHECKLOCKTIMEVERIFYand the spender passes the output’s age as the lock time. eltoo’sSIGHASH_NOINPUTis a signature over a message that omits the spent output.Lightning. The revocation key is the sum of the holder’s per-commitment point and the counterparty’s base point, without Lightning’s hashed tweaks. Per-commitment secrets are a SHA-256 hash chain used backwards; there are no in-channel HTLC outputs or fees.
Routing.
ChannelNetworkroutes by fewest hops among channels with enough capacity, with one fee policy (base fee plus millionths) and onecltv_deltafor every node. Errors are returned in the clear, without onion encryption.Relay.
BTCRelaystores teachingBlockHeaderobjects and requires a fixed minimum difficulty instead of retargeting.Plasma. Each block is a sparse Merkle tree of depth 16 keyed by the coin’s 8-byte identifier; leaves are the canonical JSON of the transfer. Only the spend and double-spend challenges exist, without bonds.
Rollups. The state root is a
MerkleTreeoverROLLUP_DOMAIN || canonical_json([account, balance]), sorted by account. Invalid transfers are skipped. A validity proof is verified by re-executing the batch; gas is the fixed cost plus 16 per calldata byte.Data availability. Reed-Solomon codes are one-dimensional and systematic over the integers modulo 65,537; shares are committed as 8-byte big-endian leaves of a Merkle tree.
Bridges. Validators sign
BRIDGE_DOMAIN || canonical_json([bridge, recipient, amount, nonce]). Wrapped ether is minted and redeemed by the same contract.
Proof systems#
blockchainkit.proofs adds no consensus rule: no block or transaction
carries a proof. Its parameters are chosen so that every number can be
inspected, not for security.
Field. Polynomials, circuits, FRI and STARKs work over BabyBear, \(p = 15 \cdot 2^{27} + 1\), whose multiplicative group (generator 31) has power-of-two subgroups up to \(2^{27}\). Coefficient tuples list the constant term first. Deployed systems use larger fields or extension fields for their challenges; Schwartz-Zippel errors here are about \(d / 2^{31}\).
Pairing.
PAIRING_CURVEis \(y^2 = x^3 + x\) over the 35-bit prime \(12p - 1\), with a generator of order \(p\). The pairing is the symmetric reduced Tate pairing with the distortion map \((x, y) \mapsto (-x, iy)\), computed by Miller’s algorithm with denominator elimination. Its discrete logarithms take about \(2^{16}\) steps, so every trusted setup can be broken by brute force.Groth16 and PLONK. Groth16 places \(B\) in the same group as \(A\). Every R1CS gets one row \(x_i \cdot 0 = 0\) for the constant and each public input, as Bellman and snarkjs do. PLONK opens every polynomial at \(\zeta\) (no linearization), adds no blinding, and has no public inputs; its copy-constraint cosets are shifted by 31 and 961.
Transcripts. Fiat-Shamir challenges are SHA-256 over
TRANSCRIPT_DOMAIN, a protocol label, and length-prefixed labelled messages, reduced modulo the field. Interactive protocols (sum-check, IP = PSPACE, the PCP verifier, Kilian) draw challenges from a seededrandom.Randominstead.Zerocash. Hashes are MiMC-7 with 12 rounds in Miyaguchi-Preneel mode, round constants from SHA-256 under
MIMC_DOMAIN. Notes are committed as \(H(H(\mathit{owner}, v), H(\rho, r))\), owners are \(H(\mathit{sk}, 0)\), nullifiers \(H(\mathit{sk}, \rho)\), and the commitment tree pads with zeros. A spend has one input and one output and range-checks the output to 16 bits.Bulletproofs and Halo. Both run in the multiplicative
TEACHING_GROUPofblockchainkit.crypto, with generators hashed into it underIPA_GENERATOR_DOMAIN. Halo’s commitments are not blinded, and its accumulation is verified in Python, not in a circuit.FRI and STARKs. Leaves are 8-byte big-endian field elements in a
MerkleTree. FRI folds by two down to a constant on the coset \(31 \langle \omega \rangle\); query positions are reduced modulo half the domain. The STARK proves only the Fibonacci recurrence from 1, 1, with a blowup of 8 by default.
Fraud models#
blockchainkit.fraud adds no consensus rule. Its scam contracts run on
the contract world model above, and its detectors are pure functions over
simulated transfers, trades and prices.
Merkle sum trees. A leaf is
SHA256(SUM_LEAF_DOMAIN || canonical_json([salt.hex(), label, balance]))with the balance as its sum; a node isSHA256(SUM_NODE_DOMAIN || canonical_json([left_sum, right_sum]) || left || right)with the two sums added. Odd nodes are promoted unchanged. Verification rejects a negative balance or sibling sum.Proofs of solvency. Pedersen commitments in the 62-bit
TEACHING_GROUP. OR proofs follow Cramer, Damgård and Schoenmakers with Fiat-Shamir challengesSHA256(SOLVENCY_DOMAIN || canonical_json([p, q, g, commitment, value, A0, A1])) mod q; range proofs commit to each of 16 bits. Unlike Provisions, nothing proves knowledge of the owned addresses’ keys or that two exchanges do not share an address.Malleability. A second signature by the exchange over the same payment stands in for a third party re-encoding a Bitcoin signature.
Verified source. Code is compared by Python class, not compiled bytecode;
effective_code()follows only EIP-1967 proxies.Honeypots. The Solidity-level techniques (inheritance disorder, uninitialised storage structs,
vartype deduction and the skipped empty string literal) are reproduced by their effect on storage and arguments, with no compiler or parser.explorer_view()lists every top-level call and only those internal calls that carry ether, as block explorers did around 2018.Markets. Prices follow a log-normal walk with linear impact (
simulate_bot_market()) or a fee-free constant-product market starting with a million coins (simulate_pump_and_dump()).Look-alike addresses. Candidates are the last 20 bytes of
SHA256(LOOKALIKE_DOMAIN || canonical_json([seed, index])), not addresses derived from key pairs.
Privacy models#
blockchainkit.privacy adds no consensus rule. Every construction works
in the 62-bit TEACHING_GROUP, written multiplicatively: a public key is
g**x mod p. Hashes into the group are framed as
canonical_json([p, q, g, ...]) after a domain tag; scalars are reduced
modulo q, and group elements are squared into the order-q subgroup.
Layers. A mix or onion layer is
R || plaintext XOR keystream, with an 8-byteR = g**kand the keystreamSHA256(MIX_DOMAIN || f"{R**x}:{i}"); the plaintext is a two-byte length, the next hop’s name and the inner layer. There is no padding and no authentication tag.Ring signatures. Abe-Ohkubo-Suzuki rings with challenges
H(RING_DOMAIN, message.hex(), ring, commitment) mod q. LSAG and MLSAG hash the ring, the key image and every column’s commitment, and the key image ishash_to_group(KEY_IMAGE_DOMAIN, P)**x, CryptoNote’s form, rather than Liu, Wei and Wong’s hash of the ring.Stealth addresses. The one-time key is
g**H(STEALTH_DOMAIN, A**r, index) * B.Hidden amounts. Pedersen commitments with
pedersen_generators(). Range proofs commit to each bit and sign a two-member ring over basehper bit, not Borromean signatures over base-4 digits. RingCT has one input and no pseudo-outputs. Mimblewimble kernels are Schnorr signatures overhwith challengeH(KERNEL_DOMAIN, E, R, fee); outputs carry no range proofs, and offsets add moduloq.Zerocoin. The accumulator’s modulus is a fixed 63-bit product of primes with base 4, and coins are hashed to 32-bit primes with
ACCUMULATOR_DOMAIN. Spends prove membership with a ring signature over every coin, not against the accumulator.TumbleBit. Puzzles use a 62-bit textbook RSA key with
e = 65537; tokens are encrypted withTUMBLER_DOMAINkeystreams. No escrow, fair exchange or cut-and-choose is modelled.Mixer contract. Withdrawals reuse the Zerocash spend circuit on the toy pairing. The recipient is bound by requiring the new-note commitment to be that of
Note(H(recipient) mod FIELD_PRIME, 0, 0, 0).Simulations. Clustering uses a toy economy that spends largest coins first; traceability creates one output per step, with geometric spending ages.
Wallet models#
blockchainkit.wallets adds no consensus rule. Where a standard
exists, it is followed byte for byte and checked against the standard’s
published test vectors: Base58Check, P2PKH addresses with HASH160 (a
pure-Python RIPEMD-160), Bech32 and Bech32m with segregated-witness
programs, PBKDF2, scrypt (RFC 7914), BIP32 extended keys and their
xprv/xpub serialization, BIP39’s seed derivation, and ECDSA on
secp256k1 with RFC 6979 nonces and low s. The departures:
Word list. BIP39 phrases use 2048 four-letter words generated as consonant, vowel, consonant, vowel, in sorted order, not BIP39’s English list. Entropy, checksum and seed derivation follow BIP39, so
mnemonic_to_seed()reproduces BIP39’s seeds for English phrases.P2SH. Spends run on
verify_script()in BIP16’s two stages, with Bitcoin’s opcode bytes and push prefixes (serialize_script()). The locking script commits withOP_HASH256(that Script has noOP_HASH160), signatures are this package’s Schnorr signatures over an explicit message, and m-of-n is oneOP_IFbranch per set of m signers (it has noOP_CHECKMULTISIG). Redeem scripts aboveMAX_SCRIPT_ELEMENT_SIZE(520) bytes are refused. The P2SH address is BIP16’s,HASH160of the script with version 5.Threshold ECDSA. Keys are generated jointly with Feldman commitments on secp256k1; signing follows Gennaro and Goldfeder’s shares of \(k\gamma\) and \(kx\), but the multiplicative-to-additive step is an ideal function rather than Paillier encryption with range proofs, and no step checks for a cheating party.
Brain wallets. Keys and addresses are real (uncompressed P2PKH); the population of owners and the thieves’ tables and delays are invented.
Social recovery. Guardians vote with transactions, not off-chain signatures; guardian changes wait
GUARDIAN_DELAY(100) blocks, and a recovery cancels a pending change.ERC-4337. A user operation is signed with a Schnorr signature over
SHA256(USER_OPERATION_DOMAIN || canonical_json([entry_point, chain_id, sender, nonce, function, args, fee])). The fee is a fixed amount, not gas used times a gas price; there are no paymasters, aggregators or factories, and validation may touch any storage.