- Go 96.4%
- Shell 3.2%
- HTML 0.3%
Upstream serves the validators' sync protocol on a real socket: real CommitBlocks of brisa's state types with whatever actions a test puts in, drops, and checksum confirmations (as an upstream relay sends them). StartRelay runs a real relay over it. So a protocol's tests can run the whole chain below the validators — relay, social nodes, children — with every link real and the drops and confirmations where the test wants them. The relay's own derivation of drops from the blocks needs the calendar and signed pools and is not exercised: a relay below an Upstream passes on what it is told. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|---|---|---|
| chain | ||
| cmd | ||
| crypto | ||
| deploy/hetzner | ||
| docker | ||
| docs | ||
| network | ||
| node | ||
| papirus | ||
| scripts | ||
| state | ||
| swell | ||
| testnet | ||
| util | ||
| .gitignore | ||
| brisa.go | ||
| docker-compose.yml | ||
| Dockerfile | ||
| go.mod | ||
| go.sum | ||
| LICENSE | ||
| Makefile | ||
| README.md | ||
| TESTNET.md | ||
Brisa
Brisa is a Byzantine Fault Tolerant blockchain with stake-weighted validator selection, window-based checksum consensus, and coordinated disaster recovery.
Building
make build # builds all binaries into the project root
make test # runs all tests
make clean # removes built binaries
Individual binaries:
make brisa-node
make relay-node
make gateway-node
make wallet
Node Types
Brisa networks consist of three node types. Each is a separate binary in cmd/.
| Node | Binary | Role |
|---|---|---|
| Validator | brisa-node |
Participates in BFT consensus, produces blocks |
| Relay | relay-node |
Distributes blocks from validators to clients |
| Gateway | gateway-node |
Accepts user actions, confirms them against validators |
Swell is the only consensus path, from a single validator up to N. An earlier
single-authority (Proof of Authority) binary, poa-server, was removed on
2026-08-01 once swell with one validator covered its ground. See
Sizing the validator set.
Sizing the validator set
Swell is Byzantine fault tolerant with a 3f+1 threshold — the BFT quorum rule
is weight > 2*TotalWeight/3 (swell/bft/ballot.go). With equally weighted
validators:
| Validators | Votes needed | Failures tolerated |
|---|---|---|
| 1 | 1 | 0 |
| 2 | 2 | 0 |
| 3 | 3 | 0 |
| 4 | 3 | 1 |
| 5 | 4 | 1 |
| 6 | 5 | 1 |
| 7 | 5 | 2 |
The counter-intuitive part: 2 or 3 validators are strictly worse than 1. They tolerate no failure at all — every node must vote — but there are now two or three machines that can die instead of one. The useful jumps are 1 → 4 and 4 → 7.
- Development, bootstrap, tests: 1 validator.
- Production with resilience: 4 validators, plus at least 1 relay for block distribution. The fourth node only has to vote, so it can be a small machine.
- Never: 2 or 3.
Running a single validator
Both pool knobs must be set — ValidateProtocol rejects a bft_pool_size
larger than num_validators, so leaving the default of 10 against
num_validators: 1 fails at startup rather than at the first window boundary.
window_slots must also stay at or above WindowToPoolRatio (20) ×
num_validators, which puts the floor at 20 slots.
node:
genesis: true # first start only
db_path: "./data"
protocol:
window_slots: 300 # >= 20 x num_validators
num_validators: 1
bft_pool_size: 1 # must not exceed num_validators
minimum_stake: 1000
block_reward: 10
validators: [] # no peers
The single validator is its own BFT quorum (weight > 2*1/3 needs 1 vote) and
its own checksum majority (threshold len(MintOrder)/2 + 1 = 1). Covered
end-to-end by TestIntegration_SingleValidator_TwoWindowBoundaries.
The 20-slot floor is a validation minimum, not a setting to use. Keep
window_slotsat the default of 300 whatevernum_validatorsis — a single validator gets no benefit from a shorter window and pays for it, becausewindow_slotsalso sizes the budget for the checkpoint state copy.Each window the chain deep-copies the state at the 50% milestone, and that copy has to be delivered in time for the naked checksum statement to reach a committed block header before the 90% milestone — roughly 40% of a window. The copy is paced at about 100 MiB/s by design, so the budget in bytes is
0.4 × window_slots × block_interval × 100 MiB/s. At the defaults (300 slots, 1s blocks) that is ~120s, comfortably over 7 GiB of state; at the 20-slot floor it drops to ~8s, and with sub-second blocks it goes away entirely.Missing it is not a crash and not silent corruption: the validator does not attest that window, so a lone validator finds no checksum majority and rolls back to the previous checkpoint, losing roughly a window and a half of blocks and trying again. That recovers a transient stall (a load spike, a long GC pause). It does not recover a state that is simply too large for the window configured — that repeats every window, logging a warning each time.
Keep the validator's deposit above
minimum_stake. The pool lottery skips any candidate below the bar. With several validators the others absorb it; with exactly one, the pool comes back empty and the chain ends — no height has a designated leader, so nothing can be proposed and nothing can restore the stake. Recovery is disaster recovery, not a deposit.The node does not die quietly. It finishes the current window, refuses to promote the empty pool at the boundary, and halts: block production stops but the web admin and the sync gateway stay up, reporting
halted: true, halt_reason: pool_exhaustedon/api/status, so the chain can still be read for recovery.The
ERRORat the 90% milestone is an alarm, not a save. By the time it can fire, the outcome is already decided: pool determination reads the state snapshot taken at the 50% checkpoint, so a deposit landing anywhere in the second half of the window is invisible to that lottery.
Note that TotalWeight counts slots, not nodes: the mint order is a slot
list and a validator appearing k times weighs k. With slots spread evenly the
arithmetic is the same; with an uneven spread, weight is what counts.
Clients should connect to relays, not to validators. Relays are the entry point and the read fan-out; validators stay lean.
Deploying a Validator Network
1. Generate keystores
On each validator machine:
./brisa-node -gen-keystore -keystore validator.json
# Enter a password, note the printed token (64-char hex)
2. Create configuration
Generate a sample config and edit it:
./brisa-node -gen-config # writes brisa-node.example.yaml
cp brisa-node.example.yaml brisa-node.yaml
Each validator's config lists all other validators:
node:
db_path: "./data"
genesis: false # true ONLY on the first node, first start
network:
consensus_port: 9000
sync_port: 9100 # defaults to consensus_port + 100
action_gateway: ":9001"
web_admin: ":8080"
credentials:
keystore: "./validator.json"
password_env: "BRISA_PASSWORD"
validators:
- token: "<validator-2-token>"
addr: "192.168.1.11:9000"
- token: "<validator-3-token>"
addr: "192.168.1.12:9000"
protocol:
window_slots: 300 # 5 min windows @ 1 block/s
num_validators: 15
bft_pool_size: 10
block_reward: 10
minimum_stake: 1000
3. Start the genesis node
Exactly one node starts with genesis: true to create the genesis block.
This records the genesis timestamp; all other nodes receive it via sync.
export BRISA_PASSWORD=<password>
./brisa-node -config brisa-node.yaml
After the first start, set genesis: false for all subsequent runs.
4. Start remaining validators
Each additional validator either:
- Syncs from a peer (recommended for first start):
node:
sync_peer: "192.168.1.10:9100"
sync_peer_token: "<genesis-node-token>"
- Or connects directly if the network is young enough that replay from genesis is fast.
export BRISA_PASSWORD=<password>
./brisa-node -config brisa-node.yaml
5. NTP clock correction
By default, validators query pool.ntp.org at startup for clock correction.
All validators must agree on block slot times, so NTP is strongly recommended.
./brisa-node -ntp-server pool.ntp.org # default
./brisa-node -no-ntp # use system clock (testing only)
Deploying a Relay Node
Relays distribute blocks from validators to downstream clients (gateways, social nodes, other relays). They do not participate in consensus.
./relay-node -example > relay.yaml
# Edit relay.yaml
export RELAY_PASSWORD=<password>
./relay-node -config relay.yaml
Minimal config:
relay:
listen: ":9200"
cache_size: 100
credentials:
keystore: "./relay.json"
password_env: "RELAY_PASSWORD"
generate: true
upstreams:
- token: "<validator-token>"
addr: "192.168.1.10:9100"
History node mode
Set data_dir to persist blocks to disk. With bootstrap_history: true, the
relay fetches missing history from upstreams at startup.
relay:
listen: ":9200"
cache_size: 200
data_dir: "./relay-blocks"
bootstrap_history: true
Size cache_size for at least 2 consensus windows (2 × window_slots) to
ensure rollbacks never reach disk.
Deploying a Gateway Node
Gateways accept user actions (transactions), forward them to validators, and confirm them by watching the block stream. They are the entry point for application clients.
./gateway-node -example > gateway.yaml
# Edit gateway.yaml
export GW_PASSWORD=<password>
./gateway-node -config gateway.yaml
gateway:
listen: ":9300"
min_confirmations: 1
confirmation_depth: 3
credentials:
keystore: "./gateway.json"
password_env: "GW_PASSWORD"
generate: true
validators:
- token: "<validator-token>"
sync_addr: "192.168.1.10:9100"
action_addr: "192.168.1.10:9001"
The gateway must be listed in each validator's authorized_nodes for action
submission to be accepted.
Network Topology
┌──────────┐
│ Validator│
┌────►│ 1 │◄────┐
│ └──────────┘ │
│ ▲ │
consensus consensus consensus
│ │ │
▼ ▼ ▼
┌──────────┐ ┌──────────┐ ┌──────────┐
│ Validator│ │ Validator│ │ Validator│
│ 2 │ │ 3 │ │ ... │
└────┬─────┘ └────┬─────┘ └────┬─────┘
│sync │sync │sync
▼ ▼ ▼
┌─────────────────────────────────┐
│ Relay Node(s) │
└──────────┬──────────┬───────────┘
│ │
┌────▼───┐ ┌───▼────┐
│Gateway │ │ Social │
│ Node │ │ Node │
└────────┘ └────────┘
Validators form a fully-connected BFT mesh on the consensus port. Each validator exposes a sync port for relay subscriptions. Relays fan out blocks to gateways, social nodes, and other relays.
Disaster Recovery
When the entire network stops (no BFT quorum can form), operators coordinate a restart out-of-band. Each operator agrees on:
- Snapshot hash — which snapshot to restart from
- Restart time — when consensus begins (RFC 3339)
- Validator pool — who participates in the new network
Add to each validator's config:
disaster_recovery:
snapshot_hash: "a1b2c3..."
restart_time: "2026-04-15T14:00:00Z"
pool:
- "<validator-1-token>"
- "<validator-2-token>"
- "<validator-3-token>"
Start each validator before restart_time. The node loads its local snapshot,
validates the hash, and waits until the agreed time to begin consensus.
Relays do not need DR configuration — they receive a SyncMsgDRReset from
validators on reconnect, clear their cache, and propagate the signal
downstream automatically.
After successful restart, remove the disaster_recovery section from config
for subsequent normal runs.
Recovery Levels
| Outage | Duration | Mechanism | Manual? |
|---|---|---|---|
| Single node down | Any | BFT tolerance — requires 4+ validators, see Sizing the validator set | No |
| Short network stall | < 1 window | Skip blocks + stagnation | No |
| Medium outage | > 1 window | Auto clock recovery (maybeAddRecoveryEntry) |
No |
| Checksum divergence | Window boundary | Rollback to last checkpoint | No |
| Full network stop | Any | Disaster recovery (coordinated restart) | Yes |
Further Reading
cmd/README.md— Detailed flag reference for all binariesdocs/brisa-spec.md— Brisa network specificationdocs/swell-spec.md— Swell consensus algorithmdocs/network-spec.md— Wire formats, transport and its limitsdocs/rollback-protocol.md— The forward-only rollbackswell/bft/README.md— BFT safety proof
docs/ carries the specifications and nothing else. Plans, surveys and
measurement write-ups are working notes: they record how a decision was reached
rather than what the protocol is, they go stale against the code by design, and
they are not part of what this repository publishes.