Ledger implementation
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, and recovers the public key that made it, which the wallet does not print |
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 |
OutboundQueue |
What waits to be written to one stream, in two lanes with the votes first |
Beside them are MintEntry and LedgerText in model.bootstrap, the sporks LedgerBlacklist and LedgerFee
and the tracker SporkLag in model.spork, and LedgerOptions in command.option.
Reading and using the ledger from outside, see REST interface and Ledger command line
| Class | Job |
|---|---|
LedgerResource |
The REST resource under /ledger: state, account, block and the submit of a transaction. It reads the newest ledger through LedgerService.read and submits through LedgerService.submit |
LedgerInfo, LedgerAccount, LedgerBlock, LedgerTransactionView |
The JSON records of the answers, in server.rest.entity. A field that does not apply is left out |
LedgerSubmission, LedgerSubmitted |
The body of a submit, and the answer to it |
LedgerText |
How an address and an amount of coins are written and read, shared by the resource, the commands and the mint spork |
LedgerCommand and the six commands beside it |
hedgehog cli ledger info, balance, block, transfer, claim and vote, in command.cli.ledger |
LedgerClient, ChainOption |
The requests of the commands to a node and their exit codes, and the --chain 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
Driving the ledger
A ledger is put together by the caller, in three steps:
new LedgerState(genesis, authority), or with aSnapshotas a third argument;BlockLog.open(path), which creates the file and its directory if they are missing;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 throwInvalidBlockException. 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 |
block(height) |
One block of the log, empty below 1 and above the tip |
status() |
The chain id, height and tip hash, as a Status |
validators(), height(), tipHash(), chainId(), round(), roundEnds(), roundValidators() |
Where the chain stands: the set that signs blocks, the height and tip, the round of this node’s clock and when it ends, and the set that the round’s votes are judged by |
supply(), balanceOf(Address), stateRoot(), pending() |
What it holds |
nonceOf(AccountKey), claimedBy(LegacyAddress), claimable(LegacyAddress), mintedTo(Address) |
What the outside needs to build a transaction and show an account: the next nonce, who claimed an address, what a claim would move, and the sum of the mints made to an address |
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 five 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, minted) |
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. The supply, and the sum of the mints made to the recipient so far as the mints named it, are those 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 |
leastNewAccountAmount() |
After authorizes(Transfer) said yes: the least amount that a transfer to an address the ledger holds no account for must carry. 0 means no least amount |
authorizes(Claim) and authorizes(Transfer) have no default, so an authority that forgot one would not let
anything through by accident; leastNewAccountAmount() defaults to 0.
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.
The REST resource is tested by LedgerResourceTest on real ledgers, with every status of every endpoint, the
limit of 16 waiting submits and the body limit. The commands are tested by LedgerReadCommandsTest,
LedgerTransferCommandTest, LedgerClaimCommandTest, LedgerVoteCommandTest and LedgerClientTest against
a fake node, and LedgerTextTest covers how addresses and amounts are read.
LedgerCommandsAcceptanceTest runs the commands against the REST servers of a fleet of daemons, with a
transfer, a claim signed as the legacy wallet signs it, a vote and a node without a ledger.
What the implementation does not do yet
- No bundled genesis. A node runs a ledger only with
--ledger-genesis. - A node whose ledger did not start still offers the ledger protocol. It drops what its peers send, and answers a request for blocks with an empty batch at once.
- Heartbeats go unused.
isProgressinghas no caller. - No penalty for equivocation. The evidence is recorded and lost at the next start.
- Waiting transactions are not stored. A restart of the process drops them. A node that replaces another in the same process takes over its waiting transactions, so a promotion keeps 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.
ChainGuardchecks certificates against the current set only. - 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.
- 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 packageledger/src/main/java/org/unigrid/hedgehog/ledger/Ledger.java: the state, the waiting transactions and the log togetherledger/src/main/java/org/unigrid/hedgehog/ledger/ConsensusApp.java: the interface the engine asks the ledger throughledger/src/main/java/org/unigrid/hedgehog/ledger/LedgerAuthority.java: the questions the node answersledger/src/main/java/org/unigrid/hedgehog/ledger/LedgerDriver.java: a node run against a networkapplication/src/main/java/org/unigrid/hedgehog/service/ledger/: the ledger in the daemonapplication/src/main/java/org/unigrid/hedgehog/server/rest/LedgerResource.java: the/ledgerendpoints, with their records inserver/rest/entity/application/src/main/java/org/unigrid/hedgehog/command/cli/ledger/: the commands ofhedgehog cli ledgerapplication/src/main/java/org/unigrid/hedgehog/model/bootstrap/LedgerText.java: addresses and coin amounts as people write themledger/src/test/java/org/unigrid/hedgehog/ledger/andapplication/src/test/java/org/unigrid/hedgehog/service/ledger/: the tests and their fixtures
Each subpage ends with the files of its part.