Skip to content
Adam Waldenberg edited this page Oct 16, 2026 · 5 revisions

Ledger

Upcoming. This page describes the ledger-node branch. It is not part of a release, and details may change. A daemon started with --ledger runs the ledger, but no genesis comes with the node and nothing outside the node can read or use the ledger yet, see Not there yet.

The ledger is a small chain that Hedgehog nodes keep together: a record of account balances and of the validators that maintain it. Every block carries a certificate, the signatures of more than two thirds of the validators, so a node that holds the blocks can check the whole history on its own. The rules and the consensus engine are in their own Maven module, ledger. The application module depends on it, carries its messages over the node's QUIC connections and answers its questions from the Grid sporks.

The name is unrelated to the ledger entries of the Legacy chain snapshot, which hold the history of the earlier chain.

Page For
Running a ledger node Operators: the options, the genesis, the files and what the log says
Ledger sporks Board members: the sporks that decide what the ledger mints and takes
Ledger implementation Developers: the classes, byte layouts and checks, on six further pages

What it records

  • the balance of each account, where an account is an Ed25519 public key, or a 20-byte legacy address until its owner claims it;
  • the supply, and how much of the legacy snapshot is still unclaimed;
  • the number of transfers each account has made, which orders its next one;
  • the references of the mints and claims already made, so that none is made twice;
  • the fees waiting to be paid out;
  • the validator set, in the order that decides who proposes, and the set before it;
  • the votes cast in the current round;
  • the height, hash and time of the latest block, the tip.

Every block commits to all of this with a state root, a Merkle root over SHA-512. A validator signs a block only if it arrives at the same root, so two nodes that accept the same block hold the same state.

Where the money comes from

The chain starts from a genesis: a start time, the length of a round in blocks, the foundation validators, and the hash and total of a snapshot of the legacy chain's balances. Its hash is the chain id, which every signature on the chain covers, so nothing can be replayed on another chain. The supply starts at the snapshot total. There are two ways to get coins: claim a legacy balance, or be paid by a mint.

A claim moves the balance of a legacy address to an account key. The owner proves it with a signature from the old wallet, over a text that names this chain and the recipient:

claim <chain id, 128 hex characters> <recipient key, 64 hex characters>

The owner signs it with the wallet's signmessage "<legacy address>" "<text>". A claim carries no amount: it takes what the snapshot holds for the address, plus anything the ledger sent to that address before the claim. Once an address is claimed, later mints and transfers to it go to the claimer. A claim can be held back by a node's policy, for example for a blacklisted address.

A mint credits an amount to an account or a legacy address under a 32-byte reference that can be used once. A mint has no signature. Whether it may be made is the question the node answers, see below. The mints come from the entries of the mint storage spork, and none passes the maximum of the mint supply spork, see Ledger sporks. An entry for a legacy address sits at a block height of the legacy chain. This chain has no such heights, so a height becomes a time: the time of the last block of the snapshot plus one minute for every height after it. Entries at or below the snapshot's last height are already in the snapshot balances and never mint here. An entry for an account says its time in seconds instead. The reference of a mint is derived from the address and the number of the entry, so that an entry is made once whatever node proposes it.

A claim moves a legacy balance to an account, a mint credits a spork entry when it is due, and the fees of transfers go to the validators that signed the block before

Transactions

Kind What it does Authorised by
Mint Credits an amount to an account, under a reference that can be used once The node's authority, not a signature
Vote A validator's vote to add a key to the validator set, or to remove one, in the current round The voter's signature, which also covers the chain it is for
Claim Moves the balance of a legacy address to an account key A secp256k1 signature from the legacy wallet, and the node's authority
Transfer Sends an amount from an account to an account or legacy address, and pays a fee The sender's Ed25519 signature

A transfer carries a nonce, the number of transfers its sender has made before, so each is applied once and in order. A sender can have 16 transfers waiting, from its next nonce on, and replace a waiting one by paying a higher fee.

The authority. The ledger asks the node around it four questions: which mints are due now, may this mint be made, may this claim be taken, and does this transfer pay the fee this node demands. The answers are the node's own policy and may differ between nodes. They are asked only of what is not certified yet: a block that more than two thirds of the validators signed stands whatever the answers are, which is what keeps a change of policy from forking the chain. A node that does not know yet answers no, which only delays a block. The rules that every node must agree on, such as signatures, nonces, balances and the supply, are checked by the ledger itself. A node answers from its sporks: the mint storage and mint supply sporks for mints, the blacklist spork for claims and the fee spork for transfers. A spork that is not set, or that the node knows a newer version of, is a no.

Fees. The minimum fee is not a rule of the chain but a spork. The fees of a block collect in a pool and are paid in the next block, evenly to the validators that signed the block before it. The remainder goes one unit each to the first signers.

How a block is made

Validators agree on each block by a vote in views. The engine is a state machine that is told what happened and answers with messages to send, timers to set and blocks decided. It does not own a clock, a network or a thread.

sequenceDiagram
    participant S as Sender
    participant N as Nodes
    participant P as Proposer of the view
    participant V as Validators
    S->>N: transaction
    N->>N: check it and pass it on
    P->>V: proposal: a block
    V->>V: check it, prevote it to everyone
    Note over V: more than two thirds prevoted it: lock it
    V->>V: precommit it to everyone
    Note over V: more than two thirds precommitted it: a certificate
    V->>N: decision: the block and its certificate
    N->>N: write to the log, then change the state
Loading
  • One proposer per height and view. The validators take turns in the order of the set, and each new view moves on one key. A block holds 1 to 1000 transactions. With nothing waiting, no block is made, except that a mint that is due, or fees waiting to be paid out, count as work.
  • Checked before voting. A validator prevotes a block only if it follows the tip, comes from the proposer of its view, is not older than the tip, is dated no more than 30 seconds ahead of the validator's own clock, carries a valid certificate of the block before it, and has transactions that are all valid and lead to the state root it states. Otherwise it prevotes nil.
  • The certificate. A block is valid with the precommits of more than two thirds of the validators, each once. A precommit covers the chain, the height, the view and the block hash, so it is worth nothing in another view or on another chain. The block hash covers the header only, which includes the view, so a block has one identity however its signatures were gathered. The proposer need not be among the signers: a block proposed in one view can be committed in a later one after its proposer has gone. Each block also carries the certificate of the block before it, whose signers are paid the fees.
  • A certified block is trusted for the authority's questions. More than two thirds of the validators asked their authorities before they precommitted, so a node does not ask its own again. This is what lets a node replay old blocks after a restart, whatever its authority knows by then.
  • Commit. The block is written to disk before the state changes, so a crash between the two replays to the same state. Waiting transactions that the new state no longer allows are dropped.

The genesis hashes to the chain id; each block names the hash of the header before it and carries the certificate of the block before as its last commit

Up to 10,000 transactions wait for a block. Beyond that, new ones are refused and nothing is evicted.

When the proposer is absent. Every validator waits for a proposal for 3 seconds in the first view, and half a second longer in each later one. With no proposal they vote nil, and when more than two thirds have precommitted without agreeing on a block, they move on to the next view, whose proposer is the next key. A validator that learns that more than a third of the validators are in a later view jumps there at once.

Four validators: the proposer of view 0 is offline, the other three wait, vote nil and decide the block of B in view 1

What keeps it safe. A validator signs at most one proposal and one prevote and precommit in a view. It locks on a block when it precommits it, and leaves the lock only for a block that got a quorum of prevotes in a view no earlier than the lock's, proved by the proposal. A quiet chain rests with no timers and wakes when work arrives. The engine is described on Ledger consensus engine.

Not signing twice

Two blocks at one height in different views are legitimate, so the rule that matters is what a validator signed in each view. That is kept outside the ledger, in a sign state file: the proposals and ballots this validator has signed for the height now being decided, with its lock. It is written, forced to disk and swapped in atomically before any message leaves the node, so a restart re-sends what was signed and never signs something else. A crash leaves the old state or the new one.

  • The file carries a checksum, and a node refuses one that is damaged or that it could not have written.
  • A second node opening the same file gets an error: the file is locked while a node holds it.
  • If the block log is shorter than the sign state needs, the node refuses to start and tells the operator to restore the tail of the log from a peer. Nothing deletes the sign state.

The protection depends on the file surviving: a deleted or older copy of it is not noticed.

Validators and rounds

The validator set is fixed for a whole round. During the round a validator may vote to add or remove a key, and the last block of the round closes it: every change that more than two thirds of the set voted for takes effect from the next block, and the votes start over. Exactly two thirds is not enough.

  • A validator votes once per candidate and casts at most 64 votes in a round.
  • The genesis validators can never be removed. They stay on as the backup.
  • The set holds 1 to 1000 keys. Added keys go to the end of the proposing order.

Heartbeats

Blocks are made only when there is something to record, so a quiet chain has no new block for a long while. A heartbeat is a validator's signed statement that it is up and sees the current tip at a given time. A validator sends one every 10 seconds, and the chain counts as progressing while more than two thirds of the validators have a heartbeat within a chosen window.

A node keeps only the newest heartbeat of each validator, and only for the current tip. Committing a block discards them all. Heartbeats never enter a block or the state. They are gossiped and held, but nothing reads whether the chain is progressing yet.

Joining and catching up

A node that holds a validator key starts as an observer: it follows the chain and serves blocks but never signs. It votes only after a grace period of 30 seconds and once its ledger has caught up with what its peers claim.

  • Status. Nodes tell their peers where their chain stands every 10 seconds, after a decision and when a peer connects. A peer's claim counts only while it is repeated, and a peer that fails to deliver is doubted for a while.
  • Sync. A node that is behind asks one peer at a time for up to 16 blocks, checks each with its full certificate, and moves on to another peer when one is silent or sends something wrong.
  • A trusted checkpoint. An operator can name the block that a synced ledger must have. A block that differs from it, or two certified blocks at one height, stops the driver instead of letting the node follow a fork.
flowchart LR
    Replay[log replayed] --> Observer["Observer<br/>follows, syncs, serves"]
    Observer -->|"a key in the set,<br/>30 seconds, caught up"| Validator["Validator<br/>votes, signs, heartbeats"]
    Observer -->|"wrong checkpoint,<br/>fork, failed twice"| Halted["Halted<br/>serves its blocks"]
    Validator -->|"wrong checkpoint,<br/>fork, failed twice"| Halted
Loading

A validator is never turned back into an observer. A halted node no longer votes or follows, but peers can still sync the blocks it holds.

On the network

The ledger uses the node's existing QUIC connections and the same port, 52883, with no ports or channels of its own. A connection speaks the ledger only if both ends settled on the protocol hedgehog-ledger/0.0.1, which a node started with --ledger offers before hedgehog/0.0.4 and gridspork/0.0.4. A node without --ledger, or from a release before it, settles on the older protocol and is never treated as a ledger peer. There are six kinds of message: transaction, consensus, heartbeat, status, block request and block batch. The detail is on Ledger networking.

  • Gossip. A transaction, vote or heartbeat that a node takes in as new is passed on to at most 8 other peers, never back to the sender.
  • Limits. Every peer has a rate limit per kind of message, and the node has shared limits. A peer that sends what is plainly forged, or too much, collects points and is cut off for a minute. The limits protect the node and do not authenticate anyone: a local forwarder that hands every connection to the node under its own address gives each connection its own limits.

Storage

Blocks are stored in one append-only file. A record is the length of the block, a checksum of that length, a checksum of the block, and the block itself. A block counts as stored only once it has been forced to disk.

  • A torn last record is dropped. A crash can only damage the end of the file: a record cut short, or a tail of zeros. Opening the log cuts that off with a warning.
  • Damage elsewhere is refused. A record that fails its checksum in the middle of the file stops the log from opening and leaves the file untouched, since dropping one record would drop every block behind it. The checksum of the length is what tells the two cases apart.
  • A failed write is rolled back, so the next block does not land behind half a record.

The state is not stored. A node rebuilds it on start by replaying every block with all checks, so a log that was tampered with does not come up as a state. The files are described on Ledger storage.

In the daemon

A daemon started with --ledger runs the ledger beside everything else, and the node runs on without it whatever goes wrong with the ledger. It loads the genesis, waits for the bootstrap snapshot and checks that it is the one the genesis names, since every claim is judged against it. Then it replays its block log, joins as an observer and, given the key of a genesis validator, starts to vote once it has caught up.

Option Meaning
--ledger Run the ledger. Off by default
--ledger-genesis <file> The genesis of the chain, a JSON file
--ledger-key <file> The validator's seed, made by hedgehog util ledger-key-generate <file>. Without it the node never signs
--ledger-trusted <height>:<hash> The block a synced ledger must have, with the hash as 128 hexadecimal characters

How to run one, the genesis format and what the log says are on Running a ledger node.

Not there yet

  • A genesis that comes with the node. A ledger runs only on a genesis given with --ledger-genesis.
  • REST endpoints and commands to read balances, send a transfer, claim, vote or follow the chain. The node holds the answers, but nothing outside it can ask yet.
  • A use for heartbeats. Nothing reads whether the chain is progressing.
  • A consequence for signing twice. The engine records a validator that signs two different ballots in one view, but nothing penalises it.
  • Lasting waiting transactions. The transactions that wait for a block are lost on restart.
  • Less in memory. The log holds every block, the state every used reference, and a start replays every block and verifies every signature.

Key facts

Fact Value
Account key Ed25519 public key, 32 bytes
Legacy address 20 bytes, the hash of a secp256k1 public key
Signature Ed25519, 64 bytes
Hash SHA-512, 64 bytes
Block header 308 bytes
Transactions per block 1 to 1000, and 0 while fees wait to be paid
Encoded mint, vote, claim and transfer 74 or 62, 138, 131 or 163, and 154 or 142 bytes
Largest encoded block 355,322 bytes
Validators 1 to 1000
Signatures a block needs n * 2 / 3 + 1 of n validators: 3 of 4, 5 of 7, 67 of 100
Votes per validator per round 64
Waiting transactions 10,000, and 16 per sender from its next nonce
Proposal wait 3 seconds in the first view, plus half a second for each later one
Clock limit when voting 30 seconds ahead of the validator's clock
Heartbeat and status interval 10 seconds
Grace period before voting 30 seconds
Blocks per sync request 16
Log record header 12 bytes: the length and two CRC32C checksums
Maven module ledger, artifact hedgehog-ledger
Protocol hedgehog-ledger/0.0.1, on the QUIC port 52883, offered only with --ledger
Sporks it reads Mint storage 1000, mint supply 1010, ledger blacklist 1040, ledger fee 1050
Files ledger/blocks.log and, for a validator, ledger/sign.state in the data directory

In the source

  • ledger/src/main/java/org/unigrid/hedgehog/ledger/Ledger.java: ties the state, the waiting transactions and the block log together
  • ledger/src/main/java/org/unigrid/hedgehog/ledger/LedgerState.java: every rule for transactions, blocks, certificates and rounds
  • ledger/src/main/java/org/unigrid/hedgehog/ledger/LedgerAuthority.java: the questions the node answers
  • ledger/src/main/java/org/unigrid/hedgehog/ledger/MintSchedule.java: a spork height turned into a time
  • ledger/src/main/java/org/unigrid/hedgehog/ledger/Consensus.java: the voting engine
  • ledger/src/main/java/org/unigrid/hedgehog/ledger/LedgerNode.java: ties the engine to a ledger and its sign state
  • ledger/src/main/java/org/unigrid/hedgehog/ledger/LedgerDriver.java: runs a node against a network: gossip, sync, promotion and limits
  • ledger/src/main/java/org/unigrid/hedgehog/ledger/Block.java: the header, the transactions and the certificates
  • ledger/src/main/java/org/unigrid/hedgehog/ledger/BlockLog.java: the append-only file and its recovery
  • ledger/src/main/java/org/unigrid/hedgehog/ledger/SignStateFile.java: the file of what was signed
  • application/src/main/java/org/unigrid/hedgehog/service/ledger/LedgerService.java: runs the ledger in the daemon
  • application/src/main/java/org/unigrid/hedgehog/service/ledger/SporkLedgerAuthority.java: answers the ledger from the sporks
  • application/src/main/java/org/unigrid/hedgehog/service/ledger/LedgerGateway.java: carries the ledger over the node's connections
  • application/src/main/java/org/unigrid/hedgehog/service/ledger/LedgerSetup.java: checks the ledger options at start

Clone this wiki locally