- Go 99.1%
- Shell 0.5%
- Assembly 0.3%
Leader-to-validator block broadcasts travelled without an application-layer signature — only the hop-by-hop socket auth protected them, so any relay that forwarded a broadcast could have injected forged block content at the same height/hash. Carry the leader's Ed25519 signature over the block hash in the broadcast wire format and verify it against the round-0 leader token on receive. The receiver also caches the signature so, once BFT produces ProofOfConsensus, the sealed block can be assembled locally — skipping the round-trip FetchBlock the follower was doing on every height. HandleConsensusResult now prefers the cached broadcast, waits 50ms if it hasn't arrived yet, and only then falls back to FetchBlock. On localhost the BFT result and the leader's broadcast race in the microsecond range, so the wait lets the broadcast path win most heights. The short-circuit for `height <= LastCommitHeight` covers the observer-promotion window where the upstream-applied block and a concurrent BFT result would otherwise trip AddSealedBlock's InvalidHeight check. Also serialize Send+Read on peerConn via a new reqMu + sendAndRead helper: two concurrent Reads on the same BufferedChannel could strand one forever because the dispatcher only tracks a single waiter. This showed up as the proactive fetch and the consensus-result fetch fighting over the same leader connection. With the broadcast path carrying the seal, the fetch fallback is rarer but still races the proactive loop when it fires. Drops the double PutLargeByteArray wrap in the broadcast wire format that the receiver never unwrapped — the likely source of the intermittent "block broadcast hash mismatch" warning. TestEndToEnd_ObserverPromotion body now passes end-to-end. The remaining FAIL at timeout is the pre-existing Node.Stop() hang in wg.Wait (sync connection goroutines parked in conn.Read don't wake on stopChan close); that is a separate issue. |
||
|---|---|---|
| chain | ||
| cmd | ||
| crypto | ||
| docker | ||
| docs | ||
| network | ||
| node | ||
| papirus | ||
| scripts | ||
| state | ||
| swell | ||
| testnet | ||
| util | ||
| .gitignore | ||
| brisa.go | ||
| brisa_test.go | ||
| docker-compose.poa.yml | ||
| docker-compose.yml | ||
| Dockerfile | ||
| go.mod | ||
| go.sum | ||
| hqq | ||
| 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 poa-server
make wallet
Node Types
Brisa networks consist of four 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 |
| PoA | poa-server |
Single-authority node (development / private chains) |
A minimal production network needs 3+ validators (BFT requires 2/3+1) and at least 1 relay for block distribution.
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 (2/3+1 continue) | 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 algorithmswell/bft/README.md— BFT safety proofdocs/test-inventory.md— Test coverage inventory