Repository navigation
Ledger
Upcoming. This page describes the
ledger-nodebranch. It is not part of a release, and details may change. A daemon started with--ledgerruns 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 |
- 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.
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.
| 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.
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
- 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.
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.
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.
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.
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.
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.
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
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.
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.
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.
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.
-
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.
| 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 |
-
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
Using Hedgehog
Code
Network
Releases
Upcoming