Core protocol
State, mempool & storage
Canonical account state, Redis nonce reservations, deterministic mining selection, RocksDB indexes, append-only block files, locking, idempotence, and crash boundaries.
This page documents the implemented alpha. Executable source, interoperability vectors, and tests remain authoritative if prose and code ever diverge.
Canonical and derived data
The reference full node spreads responsibility across several stores:
| Store | Role | Authority |
|---|---|---|
blocks/blkNNNNNN.dat |
serialized canonical blocks | durable block bodies |
blocks.db |
height and transaction locations | canonical lookup index |
accounts.db |
address balance and confirmed nonce | current canonical account state |
chainstate.db |
tip and chain metadata | current canonical position |
| Redis | unconfirmed transactions and nonce reservations | ephemeral mempool policy state |
explorer.sqlite3 |
denormalized query projection | rebuildable, never consensus authority |
The block files plus canonical RocksDB state are the full-node source of truth. Redis may be cleared without invalidating confirmed history. Explorer SQLite may be moved aside and rebuilt.
This alpha has no general replay/reindex command. “Rebuildable in principle” does not mean operators can delete RocksDB casually and recover with one supported command today.
Account state model
Luracoin is account-based. A confirmed account is represented by:
address -> {
balance: uint64 lurashis,
nonce: uint32 last confirmed outgoing nonce
}
An address absent from accounts.db has no confirmed account. Receiving value creates or updates its record. Sending requires an existing account, enough balance, and exactly confirmed_nonce + 1 after considering ordered earlier transactions.
There are no UTXOs, script programs, token balances, smart-contract storage, staking records, or multi-asset namespaces in v0.1.0.
State-transition workspace
Block validation does not mutate live accounts one transaction at a time. It first constructs a temporary map for every touched sender, receiver, and miner, then applies normal transactions sequentially.
Only after the complete candidate—including coinbase—passes does the node publish the new state. This prevents a late invalid transaction from leaving a partially updated balance set.
The temporary map also provides deterministic intra-block semantics:
- a receiver sees prior credits in the same block;
- a sender’s nonce advances after each outgoing transaction;
- a miner does not see the new coinbase credit until normal processing ends;
- every addition and subtraction is checked against
uint64oruint32bounds.
Redis mempool schema
Every accepted normal transaction creates two logical entries:
<txid> -> canonical 213-byte transaction
nonce:<chain_id>:<sender>:<nonce> -> <txid>
The payload key supports ID lookup and relay. The nonce key reserves the one valid outgoing slot for that account state.
Coinbase is never a mempool entry. Payloads must deserialize canonically, produce the key’s transaction ID, belong to the active chain, and pass contextual checks before they are usable.
Atomic nonce claims
Admission uses a Redis Lua script so checking and claiming a nonce slot cannot race:
if reservation is absent:
set reservation = candidate_txid
set candidate_txid = candidate_bytes
accept
else if reservation already equals candidate_txid:
ensure payload is present/canonical
report idempotent state
else:
reject conflicting slot
The practical policy is first-writer reservation. A later higher-fee transaction with the same sender and nonce does not replace the existing candidate. There is no stable replace-by-fee protocol.
Scripts that remove entries also compare expected IDs and, where needed, expected bytes. This prevents cleanup for an old observation from deleting a different concurrent write.
Contiguous pending nonces
The mempool permits multiple pending transfers from one sender, but only as a readable contiguous sequence.
For a candidate nonce N, admission starts at confirmed_nonce + 1 and walks each prior pending slot. Every reservation must exist, its referenced payload must exist, and that transaction must validate against a simulated sender state. The candidate then validates against the result.
Examples for confirmed nonce 7:
| Existing pending | Candidate | Result |
|---|---|---|
| none | 8 | may be admitted |
| 8 | 9 | may be admitted after simulating 8 |
| none | 9 | rejected: gap at 8 |
| 8 with missing payload | 9 | rejected: inconsistent Redis state |
| valid 8 | another 8 | rejected unless exact idempotent transaction |
The walk is bounded by the transaction-count domain. The node does not interpret a Redis error as an empty mempool; unavailable state fails closed for sending.
Spendable balance and next nonce
RPC distinguishes confirmed and pending-aware state:
balance = confirmed account balance
nonce = confirmed account nonce
spendable_balance = confirmed balance minus valid contiguous pending debits
next_nonce = confirmed nonce plus contiguous pending count plus one
These values depend on Redis being available and internally consistent. When it is not, the RPC reports mempool_available: false; wallets must disable transaction construction rather than guessing.
Incoming pending transactions do not become spendable. Only confirmed account credits affect spendability.
Miner selection policy
The reference miner scans Redis payload keys and discards or cleans candidates that are malformed, noncanonical, duplicate, coinbase-like, or inconsistent with their IDs. It orders usable candidates by:
fee descending
transaction ID ascending
It then repeatedly traverses the remaining candidates, applying any transaction valid against a simulated multi-account state. Repeated passes allow nonce 9 to become eligible after nonce 8, even when fee ordering originally placed them apart.
Selection ends when:
- no remaining candidate can make progress;
- the next transaction would exceed the height-specific block size;
- 65,534 normal transaction slots are filled.
The ordering is template policy, not consensus. Blocks remain valid with a different transaction order when their complete ordered state transition succeeds.
Confirmation cleanup
After a block is accepted, included transaction payloads and their reservations are removed. The node also handles a competing local candidate for the same (chain, sender, nonce) when a different transaction is confirmed.
Before state application it snapshots the competing transaction ID and exact payload. After success, conditional Lua deletion removes:
- the nonce reservation only if it still points to that captured ID;
- the payload only if it still matches the captured bytes.
This closes a race where cleanup could otherwise erase a newer candidate installed between observation and deletion.
Block-file format
The blocks directory contains numbered append-only files:
blk000000.dat
blk000001.dat
...
Each record is:
serialized_block_length_le_u32 || serialized_block
The length excludes its own four-byte prefix. Files rotate before an append would cross 134,217,728 bytes (128 MiB). A block is never intentionally split across two files.
The chain’s consensus block-size maximum is independent of rotation. Rotation governs local file organization, not validity.
Versioned block locations
blocks.db maps a height to an exact 17-byte location:
| Offset | Size | Meaning |
|---|---|---|
| 0 | 1 | location format version; currently 1 |
| 1 | 4 | block file number, little-endian |
| 5 | 8 | byte offset of the length prefix, little-endian |
| 13 | 4 | expected serialized block length, little-endian |
A lookup seeks directly to the offset, reads the prefix, requires it to equal the indexed length, then reads exactly that many bytes. Truncation, mismatched lengths, invalid locations, and malformed blocks fail closed.
Transaction and address-history indexes in RocksDB locate confirmed activity without scanning the entire chain. They are canonical-node indexes used by private RPC and core explorer queries; they are distinct from optional SQLite.
Locks and concurrent reads
The implementation separates two concerns:
- a state-transition lock serializes next-block validation and publication;
- a block-file lock coordinates appends and exact reads.
A reader therefore cannot observe an append halfway through and mistake it for a truncated committed record. At the same time, a block read need not remain blocked while unrelated Redis cleanup continues.
These are process-local synchronization guarantees. The data directory must not be opened for mutation by two separate node processes.
Adjacent orphan recovery
A crash can occur after a complete block record is appended but before its RocksDB location is published. On the next attempt, the node checks the only two expected positions after the known tip:
- the end of the current block file;
- the beginning of the next file if rotation would have occurred.
If an exact candidate record matches the block being committed, the node can publish its missing index instead of appending a duplicate. A truncated or conflicting record fails closed.
This recovery is intentionally narrow and constant in scope. It is not a general scan, repair, rollback, or arbitrary-corruption recovery system.
Narrow legacy location upgrade
Early clean v2-alpha directories stored a four-byte file number as the height lookup. When encountered, the current code scans that specific file, derives exact locations, and upgrades represented entries in a RocksDB batch.
Only this location-index shape is supported. It does not make prototype/v1 block files, state layouts, Redis entries, or network messages compatible with v2.
Unknown location sizes and versions fail closed.
SQLite explorer projection
When enabled, explorer.sqlite3 stores denormalized blocks, transactions, addresses, relationships, statistics, and progress metadata. It uses WAL and commits one block projection at a time.
SQLite deliberately omits raw block bodies, unlocking signatures, and secret material. Monetary values are stored as decimal text to preserve the full unsigned 64-bit domain across SQLite and JavaScript clients.
The index validates schema, network, chain ID, height, and block identity before resuming. A divergence causes projection rebuild; it never rewrites the canonical node state.
Backup boundaries
For a consistent operator backup:
- stop node and miner processes cleanly;
- snapshot the entire network data directory as a unit;
- preserve file permissions, especially
rpc.token; - include block files and all RocksDB directories together;
- include SQLite with its
-waland-shmcompanions, or omit all three and rebuild later; - treat Redis as disposable unless preserving pending policy state is explicitly required;
- keep wallet mnemonic/private recovery material in a separate secret backup.
Copying live RocksDB directories file-by-file is not a documented consistent-backup mechanism.
Failure semantics
| Failure | Expected behavior |
|---|---|
| Redis unavailable | confirmed reads continue where possible; pending-aware sending fails closed |
| SQLite unavailable/error | full node continues; indexed explorer routes return stable 503 errors |
| missing required chain history | validation/difficulty derivation fails closed |
| malformed block location | exact block read fails; no broad scan fallback |
| conflicting adjacent record | acceptance fails; operator investigation required |
| partial SQLite block transaction | SQLite rolls back and resumes from last complete indexed block |
Errors must not be converted into plausible empty answers. “No pending transaction,” “zero balance,” and “block not found” are meaningful states and must remain distinguishable from storage unavailability.
Operational invariants
Treat these as hard rules:
- only one writer process may own a network data directory;
- never point two networks at the same RocksDB, block, Redis namespace, or SQLite data;
- never edit block files or RocksDB values manually while the node runs;
- do not expose Redis as a trusted public interface;
- do not treat explorer SQLite as recoverable secret or consensus state;
- do not delete a nonce reservation without understanding its paired payload;
- do not reuse prototype/v1 directories with protocol v2;
- retain a stopped, read-only copy before any manual recovery experiment.
The current alpha favors explicit failure over silent reconstruction. If canonical files and indexes disagree outside the supported adjacent-recovery case, preserve evidence and start a clean testnet directory rather than improvising an in-place mutation.
Source anchors
Primary implementation files used for this chapter:
