Skip to content

Ledger implementation

Adam Waldenberg edited this page Oct 16, 2026 · 3 revisions

Ledger implementation

Upcoming. This page describes the ledger-node branch. It is not part of a release, and details may change.

These pages are for a reader who wants to open the code of the ledger or to build on it. The concepts, what the ledger records, how a block is agreed and why it needs more than two thirds of the validators, are on the Ledger page. This page gives the module, its classes and its API. The detail is on these pages:

Page What it covers
Ledger encoding The bytes of every record and message, what each signature covers, hashes, roots and references
Ledger state and rules The state, the order of every check, fees, rounds, the waiting transactions and heartbeats
Ledger consensus engine The voting engine, the sign state and the node that ties them to a ledger
Ledger storage The block log, the sign state file and the replay at start
Ledger networking The driver: intake, limits, gossip, sync, promotion and halting, and the transport over QUIC
Ledger in the daemon The service, the genesis and key files, the snapshot, the authority over the sporks

The module

The ledger is the Maven module ledger, artifact hedgehog-ledger, with one package, org.unigrid.hedgehog.ledger, exported by its module-info.java. It has no socket code, no files of its own choosing, no options and no threads. The application module depends on it (requires org.unigrid.hedgehog.ledger) and supplies all of those, in the package org.unigrid.hedgehog.service.ledger.

Dependencies BouncyCastle bcprov-jdk18on 1.85, SLF4J 2.0.20, Lombok 1.18.48
Test dependencies jqwik 1.10.2, Hamcrest 2.2, Jimfs 1.3.2
Compiler release 25, set in the parent pom.xml
Language features Records, sealed Transaction and Address, pattern-matching switch, switch expressions
Run its tests mvn -pl ledger test from the repository root, -Dtest=BlockLogTest for one class

The classes

Rules and state, see Ledger state and rules

Class Job
Ledger Implements ConsensusApp: ties the state, the waiting transactions and the block log together
LedgerState Balances, references, votes, validators, supply and the tip. Holds every validation rule and computes the state root
LedgerAuthority What the ledger asks the node: which mints are due, and which mints, claims and transfers it accepts
LedgerGenesis, Snapshot The start of the chain with the hash and total of the legacy snapshot, and the balances behind it
MintSchedule Turns a height of the legacy chain into a time
Mempool Waiting transactions and the newest heartbeat of each validator
ValidatorSet The ordered keys with the proposer and quorum rules
VerifiedSignatures Remembers the claims and transfers whose signatures were checked

Data, see Ledger encoding

Class Job
Transaction, Mint, Vote, Claim, Transfer The four kinds of transaction. Transaction is sealed, so there are no others
Address, AccountKey, LegacyAddress An Ed25519 public key or a 20-byte legacy address, and the sealed type over both
LegacySignature Verifies a signature of the legacy wallet
Reference A 32-byte single-use mint reference, and the two ways to derive one
Block, Certificate, Endorsement, Precommit A header with its certificates and transactions, a view with its signatures, one signature, and what it covers
Heartbeat, Status A validator's signed statement that it sees a tip, and an unsigned claim of where a chain stands
BlockCodec, TransactionCodec, ConsensusCodec, WireCodec Byte encoding of blocks, transactions, consensus messages and the rest
MerkleRoot, Digests, Ed25519, Hashing Hashing and signing, and a salted ranking of peer ids
BlockLog The append-only file of blocks and its crash recovery, see Ledger storage
BlockRequest, BlockBatch A request for blocks, and up to 16 blocks in answer

Consensus and node, see Ledger consensus engine

Class Job
Consensus, ConsensusApp The voting engine for one validator, and the interface it uses to ask the ledger
Message, Proposal, Ballot, Decision, BallotBox The three consensus messages and the box that counts the ballots of a height
Action, Timeout, Timeouts What the engine asks its caller to do, a timer id, and the waiting times
SignState, SignStore, SignStateFile What a validator has signed, and the file that keeps it
LedgerNode One ledger, one engine and one sign state tied together
ChainGuard Refuses blocks and statuses that contradict the chain held or a trusted checkpoint

Driver and network, see Ledger networking

Class Job
LedgerDriver The only caller of a node: intake, gossip, sync, promotion and halting
NodeSupervisor, NodeCall, Promotion, Recency Replaces a node that failed, decides when an observer may vote, and remembers recent trouble
Syncer, SyncState, PeerView Fetches blocks from a peer that is ahead, keeps the claims and doubts about peers, lists the peers
Gossip Passes on what is new and charges the sender for what is forged
Door, DoorKeeper, InboundLanes, QueueBudget, PeerLimiter Sorting, queueing and rate limiting of what arrives
LedgerNetwork, LedgerPeer, Serial, Scheduler What the application provides: peers, a serial executor and timers
NodeFactory, DriverOptions, Checkpoint Inside LedgerDriver: how a node is built, the settings, and the block a synced ledger must have

Errors are InvalidBlockException, CorruptLogException, CorruptSignStateException, FileLockedException and BlockLogBehindException. Bytes holds package-private helpers.

In the application, package org.unigrid.hedgehog.service.ledger, see Ledger in the daemon and Ledger networking

Class Job
LedgerService The ledger as a part of the daemon: starts, reads and stops it
LedgerNodes, LedgerRuntime The NodeFactory over the files of the node, and the Serial and Scheduler
LedgerSetup, LedgerKeyFile, GenesisFile The checks of the options at start, the validator's seed, and the genesis as JSON
LedgerSnapshot, SnapshotCheck The bootstrap snapshot as the ledger's Snapshot, and whether it is the one of the genesis
SporkLedgerAuthority The LedgerAuthority over the sporks
LedgerGateway, LedgerProtocol, NettyLedgerNetwork, NettyLedgerPeer The transport: the gate, the protocol, the peers and one stream

Beside them are MintEntry in model.bootstrap, the sporks LedgerBlacklist and LedgerFee and the tracker SporkLag in model.spork, and LedgerOptions in command.option.

flowchart TD
    Service[LedgerService] --> Driver[LedgerDriver]
    Service --> Nodes[LedgerNodes]
    Gateway[LedgerGateway] --> Driver
    Driver --> Supervisor[NodeSupervisor]
    Driver --> Network[LedgerNetwork]
    Supervisor -.builds through.-> Nodes
    Supervisor --> Node[LedgerNode]
    Node --> Consensus
    Node --> Ledger
    Node --> SignState[SignStateFile]
    Consensus --> BallotBox
    Consensus -.asks.-> Ledger
    Ledger --> LedgerState
    Ledger --> Mempool
    Ledger --> BlockLog
    LedgerState --> Authority[LedgerAuthority]
    LedgerState --> ValidatorSet
    LedgerState --> MerkleRoot
    Authority -.implemented by.-> Spork[SporkLedgerAuthority]
    BlockLog --> BlockCodec
    BlockCodec --> TransactionCodec
Loading

Driving the ledger

A ledger is put together by the caller, in three steps:

  1. new LedgerState(genesis, authority), or with a Snapshot as a third argument;
  2. BlockLog.open(path), which creates the file and its directory if they are missing;
  3. new Ledger(state, log), with an optional clock as a third argument, which replays every block of the log into the state with all checks. A log that was tampered with makes the constructor throw InvalidBlockException. A replay is vouched, so it ignores what the authority says now.

The caller owns the log and closes it. The waiting transactions are kept by a Mempool that the ledger creates itself, with room for 10,000 of them. A node is built on top of that, see Ledger consensus engine, and in a running system the driver builds the nodes through its NodeFactory. In the daemon that is LedgerNodes, and LedgerService builds the driver.

Ledger implements ConsensusApp, see Ledger consensus engine, and adds:

Method What it does
submit(Transaction), submit(Heartbeat) Empty when accepted, the reason otherwise
blocksFrom(from, max), hashAt(height) Read the log for peers, without waiting for a commit
status() The chain id, height and tip hash, as a Status
validators(), height(), tipHash(), chainId(), round() Where the chain stands
supply(), balanceOf(Address), stateRoot(), pending() What it holds
isProgressing(now, window) Whether more than two thirds of the validators sent a recent heartbeat

commit(block) is package-private: only LedgerNode commits. Every public method except blocksFrom and hashAt is synchronized; LedgerState itself is not thread-safe and relies on that.

The authority has four methods, all of them the node's policy:

Method Asked
due(time) When a block is proposed or work is looked for. The mints this node knows to be due, in a fixed order; the ledger leaves out the ones already made
authorizes(Mint, time, supply) When a mint is submitted, proposed or checked before a vote. The time is the node's clock on submit and the block's time otherwise, and the supply is that of the state the mint would be made on
authorizes(Claim) The same, for a claim
authorizes(Transfer) The same, for the fee a transfer pays

The last two have no default, so an authority that forgot one would not let anything through by accident. None of them is asked when a block that already carries a certificate is checked: the state is then vouched for, because more than two thirds of the validators asked their own authorities before they voted. The authority of the daemon is SporkLedgerAuthority, see Ledger in the daemon.

Errors and logging

Rejections that are expected, such as a bad transaction or block, are returned as a readable reason in an Optional<String>. An exception is thrown where the caller made a mistake or the data cannot be trusted: InvalidBlockException for a refused block in apply and commit; IllegalArgumentException for a malformed value or a second endorsement; CorruptLogException and CorruptSignStateException for a log or sign state that cannot be trusted; FileLockedException for a sign state held by another node; BlockLogBehindException for a log that lacks blocks the sign state has passed; and ArithmeticException where a sum would overflow, which a check turns into Could not be checked.

Logging is a warning when a torn record is dropped, one when a check could not be made, and what the driver reports when it halts or replaces a node. In the daemon, LedgerService logs the chain id at start, the wait for the snapshot, and once why the ledger is not running or has halted; SporkLedgerAuthority logs each cause of a refusal once, at debug level. A seed is never logged.

Tests

The tests use jqwik properties and examples, with Hamcrest assertions and a small Expect.assertThrows. Every test of the ledger module that touches a file uses a Jimfs file system.

Test Covers
AccountKeyTest, AddressTest, ReferenceTest Size, equality, order, both address kinds, copies, derived references
MintTest, VoteTest, ClaimTest, TransferTest, HeartbeatTest Encodings, and that every field is under the signature or the id
LegacySignatureTest Both key encodings, low s, a header that must match the key, ASCII only, a vector made by an independent implementation
TransactionCodecTest, BlockCodecTest, CertificateTest Round trips including the largest block, every truncation, trailing bytes, count limits
DigestsTest, MerkleRootTest, Ed25519Test Hashing, tree shape for odd and empty inputs, a published Ed25519 test vector
BlockTest, ValidatorSetTest, LedgerGenesisTest Header size, a hash that ignores the certificate, quorum for every set size, rotation by view, a chain id that commits to every field
MintScheduleTest The height-to-time rule, overflow and negative values
LedgerStateTest, LedgerClaimTest, LedgerTransferTest, LedgerFeeTest, LedgerConservationTest Mints, claims, transfers, fee payout, and that no money is made or lost
LedgerCertificateTest, LedgerLastCommitTest, LedgerVoteTest, LedgerRoundTest Exact quorum, views, the last commit, every vote rule, the end of a round, genesis validators never lost
LedgerClockTest, LedgerWorkTest, LedgerValidateTest, LedgerAuthorityTest, LedgerBlacklistTest Due mints, the 30-second limit, what counts as work, the supply passed to the authority, certified blocks that policy would refuse, a throwing authority
MempoolTest, MempoolTransferTest Slots, limits, the nonce window, replacing by fee, heartbeats and progress
LedgerTest, LedgerServeTest Propose, validate and commit end to end, restart from the log, serving blocks
BlockLogTest A torn tail at every cut, a zero tail, damage in the middle, an impossible length, a rolled-back write
ConsensusViewTest, ConsensusViewsTest, ConsensusRestoreTest One view, view changes, timers, locks, proofs, resting, restoring a saved state
ConsensusSimulationTest, LedgerNetworkTest Many nodes on a simulated network with delays, loss, partitions, crashes and restarts from disk
SignStateTest, SignStateFileTest, LedgerNodeTest, ChainGuardTest The format, every damaged byte, atomic save, the lock, save before send, a log behind the sign state
LedgerDriverTest, LedgerDriverSyncTest, LedgerDriverDoorTest, LedgerDriverNetworkTest, PeerLimiterTest, WireCodecTest Events, forwarding, scoring, halting, sync and promotion, queue limits, drivers on a virtual network

LedgerFixtures and CodecFixtures build keys, blocks and random valid values. FlakyChannel wraps a real file channel and fails a write after a chosen number of bytes; it is handed to the log through the package-private BlockLog.over. A simulated network, a virtual-time memory network and a manual scheduler drive the engine and the driver.

In the application, LedgerSyncNetworkTest runs real QUIC servers on one host and syncs, gossips and halts on a wrong checkpoint; NettyLedgerNetworkTest covers peer naming, ordering and limits; further tests cover the protocol gate, the pipeline and the options. LedgerFleetTest runs several daemons with their LedgerService in one process, see Ledger in the daemon.

What the implementation does not do yet

  • No REST endpoints or commands for the ledger itself: nothing reads LedgerService.status(), a balance or a block, and nothing submits a transaction from outside the node.
  • No bundled genesis. A node runs a ledger only with --ledger-genesis.
  • One folder for every chain. The block log and sign state sit in ledger/ of the data directory whatever the genesis, so a node moved to another genesis must have the old files moved away.
  • A node whose ledger did not start still offers the ledger protocol, and drops what its peers send.
  • Heartbeats go unused. isProgressing has no caller.
  • No penalty for equivocation. The evidence is recorded and lost at the next start.
  • Waiting transactions are not stored. A restart drops them.
  • Signed heights depend on the file. A sign state that is deleted or replaced by an older copy is not noticed, and the directory is not flushed on Windows or an in-memory file system.
  • A fork among an earlier validator set goes unseen. ChainGuard checks certificates against the current set only.
  • Rounds follow the traffic. With no empty blocks, a round of n blocks lasts as long as it takes to make them, and a change of validators waits for its end.
  • Everything is kept in memory. The log holds every block, the state every used reference, and a start replays every block and verifies every signature, twice for a validator.
  • Work is repeated. A block is checked with its certificate when the engine decides, again when the ledger commits it and a third time when the state applies it, and every waiting transaction is checked again afterwards. The authority decodes every entry of the mint spork each time it is asked.
  • No lock on the log file. Two processes could open the same block log.

In the source

  • ledger/src/main/java/org/unigrid/hedgehog/ledger/: the module, one package
  • ledger/src/main/java/org/unigrid/hedgehog/ledger/Ledger.java: the state, the waiting transactions and the log together
  • ledger/src/main/java/org/unigrid/hedgehog/ledger/ConsensusApp.java: the interface the engine asks the ledger through
  • ledger/src/main/java/org/unigrid/hedgehog/ledger/LedgerAuthority.java: the questions the node answers
  • ledger/src/main/java/org/unigrid/hedgehog/ledger/LedgerDriver.java: a node run against a network
  • application/src/main/java/org/unigrid/hedgehog/service/ledger/: the ledger in the daemon
  • ledger/src/test/java/org/unigrid/hedgehog/ledger/ and application/src/test/java/org/unigrid/hedgehog/service/ledger/: the tests and their fixtures

Each subpage ends with the files of its part.

Clone this wiki locally