Baklava

Run a node, join the set

Running a node and voting are two different things. The first is a command; the second costs a stake and takes effect at the next epoch.

1. Keys

A node holds two: an Ed25519 identity the network layer presents, and a BLS key that signs votes and evaluates the sortition VRF. One keystore holds both.

keys
./baklava keygen -keystore node.json -member node-card.json

The member card is the public half — the thing a genesis file names. You do not need it to join an existing chain; it matters when you are starting one.

2. Run

follow the chain
./baklava run \
    -genesis genesis.json \
    -keystore node.json \
    -db node.db \
    -peer 14b49efa2d4ec817a78ff56f71889648153ff24a4577ecb7ba1ef21fa8d5a49c@seed.baklava.network:26656 \
    -listen 0.0.0.0:26656 \
    -rpc 127.0.0.1:26657

It will connect to the seed and catch up one block at a time. That is not a second, laxer validation path: a fetched block goes through exactly what a proposed one goes through, so catching up verifies every certificate on the way.

That is the common case and it stops working in one situation: a node that has been off for longer than its peers keep block bodies is asking for heights nobody holds any more. The way back in is a snapshot — a recent state, downloaded and checked against the state_root the header claims for it — and it needs -trust. That check proves the state matches the header; it cannot prove the header is one the network agreed on, and a node with no history has no way to find out. So the one thing that cannot be proved is named by the person running the node, once.

-observeFollow the chain without voting, even once registered. The honest way to run for a while before taking a seat.
-rpcWhere to answer queries; empty for none. Keep it on the loopback — see the query surface.
-retentionHow far back block bodies are kept; ninety days by default. -archive keeps everything.
-unstake-on-exitLeave the participant registry on the way out, waiting up to -unstake-wait for it to commit. Off by default, and never in a service unit that restarts — see below.
-trustThe node id a snapshot may be installed from. No default: without it, a node that has fallen further behind than its peers keep says so and stops trying rather than adopting a state nobody vouched for.

Open the peer port, keep the RPC port shut

Peers reach each other on 26656 and that has to be reachable. The RPC surface has no authentication and no TLS on purpose — a wrong answer is caught by the proof it fails, not by knowing who sent it — so it belongs behind a proxy or behind nothing at all.

3. Register, and vote

Voting power is not bought. A stake opens the gate; what decides the draw is weight = maturity × reliability, where maturity is log2 of how long you have been registered and reliability is how often you turned up when drawn. The stake amount does not enter it.

register
./baklava stake \
    -keystore node.json \
    -genesis genesis.json \
    -rpc https://rpc.baklava.network

The stake is moved, not marked: it is debited from your account and credited to a pool with no private key. A number beside an account somebody can still spend from is not collateral. The threshold is read from the chain, not from a flag — on this network it is 1 BKLV.

Register with the same keystore the node runs with. The registry is keyed by the address the Ed25519 key derives, and the BLS key in the payload is the one that will vote; registering one node from another's keys enters a member nobody can act as.

You will not vote today

The participant set is frozen for the length of an epoch — 500 blocks here — and it is frozen from the state at the last block of the previous one. So a registration that lands now takes effect when the next epoch opens, not immediately.

Your first weight will also be near zero, because maturity is log2 of an age. That is the design working: the apprenticeship quota keeps a seat for newcomers so the draw is reachable, and seniority is earned rather than bought.

4. Check that it took

console
> /participant <your address>
> /committee <epoch>
> /status

5. Leaving, and what misbehaviour costs

A stake that can never be recovered is not collateral, it is a fee. So there is a way out, and time between asking and being paid.

console
> /unstake

This does not return the stake. It marks the member as leaving, which keeps them out of every set frozen from then on — they finish the epoch they are in, because its set was already frozen and a certificate names its signers by an index into it — and the money comes back on its own at the first epoch close after 90 days. There is no way to cancel it, and asking twice is refused rather than restarting the clock.

The delay is the whole point. A node that could sign a bad block and withdraw in the same breath would leave nothing to take, so the stake stays where it is for exactly as long as evidence can still be produced against it.

Leaving on the way out

A node that is simply switched off keeps its seat. The entry stays, the frozen set still counts it toward the quorum, and nothing on the chain can compel the transaction it never sent — which is how a small network walks into the halt below. So baklava run takes -unstake-on-exit: on Ctrl-C or SIGTERM the node signs its own departure, keeps running until it is in a committed block, and only then closes the database.

The waiting is the feature and not the manners. A node small enough for its departure to matter is usually the node that has to seal it, so a departure that was only broadcast would die in the mempool of the process that was about to exit. What it does not wait for is the freeze: the mark takes the member out of the next frozen set, and the one in force still counts them until the epoch turns. On a network small enough to notice, stay up to the boundary.

Never in a service unit that restarts

A binary upgrade that restarts the node would leave the registry every single time, and the entry that comes back is a new one: seniority restarts from zero and nothing credits an address for time already served. The flag belongs on a one-off manual run at the end of a node's life. Somebody going offline for a few days should do nothing at all — stay registered, and take the reliability an intermittent schedule honestly earns.

What is taken, and for what

Absence costs nothing extra: reliability already prices it, continuously and with nobody reporting anything — a member who is drawn and does not turn up watches their weight fall at the rate they were expected to contribute. What is slashed is the one thing that can be proved rather than observed: signing two different blocks at one height and round.

It costs the stake and the registry entry with it. The entry is the punishment and the stake is a formality — weight is maturity times reliability and the stake never enters it, so at 1 BKLV the token deters nobody, while however long you had been turning up is the whole of what you owned. Re-entering means a new stake and a maturity that starts at zero.

Half of the stake goes to whoever proved it. Nodes do this without being asked: two conflicting votes are only ever seen together by a node that was there when they arrived — individual votes never reach the chain, a certificate carries an aggregate — so evidence exists only while somebody is running and paying attention, and it is published as an ordinary transaction.

Nobody can be made to sign two blocks. An honest node cannot be baited into this and does not have to do anything to avoid it, beyond not running two nodes on one key.

In force here, and that is newer than it sounds

Leaving and slashing are rule version 2. A chain's rule version is derived from its height and its upgrade schedule — never carried in a header, so no proposer can state one — and baklava-testnet-5 schedules version 5 at height 1, so every block but the genesis block has been under it or something later.

Its predecessor could not say that. baklava-testnet-2 started with an empty schedule, and until recently nothing could add to one: the table was written at genesis and that was the end of it. So every rule touching a state root meant a new chain. That is no longer true — see Upgrades and readiness — but the mechanism which changed it is itself a version 2 rule, so reaching it took one last new genesis. This is that chain.

A binary that is too old stops, and says so

If the schedule reaches a version your binary does not implement, the node refuses to execute that block rather than computing a state root under a rule it does not have. It prints the height and the version it will need on the way up, so the day it stops is a day you knew about.

That is the same decision as the handshake refusing a peer on another wire version, and for the same reason: a node that kept running would quietly compute the wrong answer and reject every block the rest of the network agreed on.

6. If the network stops

A quorum is ⌊2C/3⌋+1, so a set that loses more than a third of itself for good stops committing blocks and no message between the survivors gets it going again. Nothing can compel the missing member to unstake, because by definition nothing is running on that machine to be compelled, and no protocol can tell “gone for good” from “partitioned, back in five minutes” from inside.

The way out is a new chain derived from the old one's state rather than typed by hand. Stop the node first — the derivation takes the database's exclusive lock — and hand it a card from everybody who is still there.

restart
./baklava restart     -db node.db     -genesis genesis.json     -out genesis-next.json     -chain-id baklava-testnet-5-next     -member node-card.json

The new registry is exactly the cards handed in, and that is deliberate: a proof of possession is checked when a member registers and then thrown away, so the chain cannot write one out again. Only the key holder can make one, which turns the list into an affirmative list of the living rather than a list of the dead — nobody is dropped by name, and nobody can be dropped by an oversight. A member who does not turn up is not punished: they keep their account, their stake comes back into it, and they can register again once the chain runs.

Everything else about a carried member comes from the chain and not from the card, including both halves of their weight. Seniority survives because the join time does; reliability survives because the vote count and the expected-draw total are carried with it. A restart that reset everybody to unproven would be handing the most generous possible reading to exactly the members whose absence caused the halt.

It refuses the first time, and that is the design

Some BKLV on a halted chain sits at addresses the new one would have no transition able to reach: the fee pool the last block filled, and each swap pool's reserve. A pool's reserve belongs to whoever holds its shares, and a share leaf sits at a hash of the pool and the holder — so the state knows exactly how much is in the pool and not one address with a claim on it. No tool can hand it back.

So the command names every such balance and stops. Decide, then say so with -unreachable treasury or -unreachable forfeit. Either way it prints the old supply, the new one, and every Krumb of the difference, because every Krumb has to come out carried, disposed of or burned before anything is written.

What does not survive: the header chain, the light client anchor, and the whole third table — tokens, collections, listings, pools and open proposals. Derive the new anchor with baklava anchor -genesis genesis-next.json, and check the genesis hash the command printed against everybody else's before anyone starts.

Running it as a service

A service has no terminal, so the passphrase comes from a file only that user can read, and the unit has to give the node room to shut down: it closes the database on SIGTERM, and a block and the state it produced are written in one transaction. A hard kill is the one shutdown that can cost a resync.

The unit file this network's own node runs under, and the reasoning behind each line of it, is deploy/README.md.

Next

What a node will answer, and how to check it.