Mining¶
The mining loop¶
Every mining node runs the same loop in mining/miner.py:
flowchart TD
A[Idle] --> B{Synced?}
B -- no --> S[Wait for sync to complete]
S --> A
B -- yes --> C[Fetch 64 bytes from QRNG source]
C --> D[Generate QRNG attestation]
D --> E[Pull mempool: get_transactions_for_block]
E --> F[Build block template + coinbase]
F --> G[PoW search: nonce → hash with N leading zeros]
G -- found --> H[Validate block locally]
H -- valid --> I[Add to chain + persist]
I --> J[Announce INV to peers]
J --> A
G -- interrupted --> A
H -- invalid --> A Steps in detail:
- Sync gate — if
sync_state == "syncing", sleep 5s and loop. Don't mine on a stale tip. - Entropy fetch —
qrng_provider.generate_qrng(num_bits=512)returns ≥ 64 bytes. If it fails or returns insufficient bytes, raise — no fallback. - Attestation —
QRNGAttestation.generate_attestation(...)builds the commitment envelope (see Entropy). - Tx selection —
transaction_pool.get_transactions_for_block()returns up toMAX_TRANSACTIONS_PER_BLOCK(100) txs, sorted by fee desc + timestamp asc. - Coinbase — synthesize a
Transaction(sender="mining_reward", recipient=miner_address, amount=subsidy+fees, signature="reward_signature")and insert at index 0. - PoW — iterate
nonce = 0, 1, 2, ..., hash the block header, stop when the hash hasdifficultyleading hex zeros. Runs in an executor so the async loop stays responsive. - Local validation —
validate_received_block(block). Must pass. - Add to chain —
add_validated_block(block). Updates balances, persists to SQLite, removes confirmed txs from mempool. - Announce —
propagation.announce_block(block)— INV message to all peers; they respond with GETDATA if interested.
Interruption: if a peer's block arrives at the same height during the PoW search, the local miner is interrupted via miner.interrupt() and restarts on the new tip.
Picking a miner address¶
Every mining node has a wallet that receives the coinbase. Two options:
-
Auto-create (default if
miner_addressis unset). The node generates a fresh ML-DSA-87 keypair on first boot and stores it in the local wallet store. The chain'swalletsdict gets the new wallet attached. -
Bring your own. Set
--miner-address <hex>or[mining].miner_addressinconfig.toml. The node will refuse to start mining if it doesn't have the private key for that address.
For long-running miners, prefer option 2 with a key you control offline.
Difficulty adjustment¶
See Blocks and consensus → Difficulty adjustment. Key points:
- Recalculated only at
DIFFICULTY_ADJUSTMENT_INTERVAL-block boundaries (every 10 blocks). - Bound to
[2, 8]mainnet,[2, 4]testnet. - Each adjustment is
±1max — no large jumps.
The testnet cap exists because performance-1x Fly VMs can't reasonably mine past difficulty 4 (block times explode). On bare metal with GPU or ASIC, mainnet's [2, 8] range is appropriate.
Hardware vs hosted mining¶
Three credible profiles:
| Setup | Cost | Block time at network difficulty 4 |
|---|---|---|
performance-1x Fly VM (CPU-only Python) | ~$25/mo | ~1-5 sec |
| Bare-metal x86 with rust impl (planned) | $50-200/mo | ms range |
| ASIC (future) | — | ms range |
The reference implementation is deliberately Python, for auditability over speed. A miner that wants competitive economics on mainnet will use a rewritten PoW search loop.
What the miner doesn't do¶
- Doesn't choose contract execution order. Tx execution is in selection order (fee desc, timestamp asc). No flashbots-style reordering markets in v1.
- Doesn't censor. The chain has no opinion on which txs miners include. Censorship-resistance comes from miner diversity.
- Doesn't run the QRNG itself. The miner is a consumer of the entropy source, not a producer. If you want to also run the source, run the entropy aggregator as a second service.