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.