wbft
wbft is a consensus-only implementation of the WBFT protocol in Go, written
from the WBFT specification. It plays the role that CometBFT (Tendermint)
plays for Cosmos applications: it runs the consensus state machine, verifies
headers and seals, keeps the validator set and epochs, signs safely across
restarts, and reports what happened, while the application behind a narrow
interface executes blocks and owns chain state.
The goals are:
- Conformance. Every normative requirement of Part A of the specification
is implemented by identifiable code (
// Spec: WBFT-... comments) and
checked by unit tests, conformance vectors or simulations.
- Interoperability. In its default profile a
wbft node keeps the wire
format (istanbul/100, message RLP) and block format (Ethereum header plus
WBFT extra data) of go-stablenet and can run in the same network as
go-stablenet nodes.
- Replayability. The consensus core is a pure state machine: no clocks,
goroutines, randomness or I/O. Recorded inputs replay to the same outputs.
- Observability. Consensus events, participation records and raw message
journals are exposed for analysis instead of being discarded.
Block storage, synchronisation, devp2p, the EVM, governance and transaction
rules are not part of wbft; applications provide them through the
interfaces of package app, chain/validator/source, mempool and p2p/transport.
Nodes derive an independent effective proposer configuration at startup: an
omitted policy selects RoundRobin (0), and explicit base or transition policies
must select RoundRobin (0) or Sticky (1). The original application JSON and rule
declarations remain unchanged. Production quorum and early round-change
thresholds use integer arithmetic, including at large committee sizes; an empty
committee retains a quorum of one. These defaults are reported as
checked_proposer_policy and integer_quorum with source profile.
types.ParseChainConfig, Config.CheckProposerPolicy, validator.QuorumSize
and validator.InFPlusOneWindow retain their explicit reference semantics.
Status
Milestone W1 (pure modules) is complete: crypto/keccak,
crypto/ecdsa, crypto/bls, types (heights and rounds as
arbitrary-precision integers, chain configuration and config_at),
codec/rlp and codec (extra data, headers, blocks, the four consensus
messages, signing payloads and hashes), internal/refsort (the reference
implementation's sort), chain/validator (quorum, proposer selection, epoch
schedule, validator-set lookup), chain/epoch (next-epoch computation, candidate
order, shuffle), chain/validator/source (authority snapshots and their
cache) and chain/header (proposal construction, seal writing and merging, proposal, header,
batch and light verification) pass the conformance vectors of their handlers.
Milestone W2 (consensus core, write-ahead log, private validator, in
simulation) covers:
consensus: the pure state machine with the reference behaviour. One
Step takes one input (start, stop, new head, request, message, replay,
timer expiry) and returns the outputs to execute (messages to sign and
send, relays, scheduled inputs, timer requests, build requests, commits,
message outcomes, events). It reads the outside only through Env;
Vars exposes the state variables and Snapshot the extra seals for
proposal building.
consensus/inputlog: the encoding of core inputs, Env answers and step
outputs shared by the write-ahead log and the message journal.
consensus/wal: a segmented append-only log with CRC-framed records, synced
appends and repair of a damaged tail.
consensus/privval: the signer on a go-stablenet node key file (the BLS key is
derived from it), per-kind sign rules that refuse a second value for a
view across restarts, and the sign floor for a node that takes over a key
without its sign record.
consensus/runner: the consensus goroutine, bounded per-peer receive
queues with receive-check workers and a slot table, the timer scheduler
with generations, signing and logging of own messages, the commit
goroutine, and the replay of the write-ahead log at start.
p2p/transport: the transport interface, the deduplication caches with the
gossip target choice, and the frame verdicts DecodeFrame,
CheckOutbound and StoppedEngineAction for the application adapters.
observe/event and observe/journal: the event vocabulary with a JSON
Lines writer, the schema of the event records
(observe/event/schema/events-v1.json, JSON Schema 2020-12, which the
simulator and the node tests check every record they write against), and
the message journal (a store of its own with pruning by
height and size). cmd/wbft-journal reports on a journal directory
(stat), checks it (verify), prunes it (prune) and exports it, also
while a node writes it: as a bundle (export, a tar file of the segments
that cover a range of heights with a manifest, which the analyzer and
cmd/wbft-replay read) or as the R-01 frame dump that wbft-inspector
reads (export --format r01).
observe/metrics: the metric registry (counters, gauges, histograms with
labels) and its Prometheus text export, on the standard library. A node
derives the metrics its event records carry (height and round, round
changes, timer expiries, messages by code and outcome, dropped inbound
messages, finalize durations, evidence), reads the pool sizes, the
receive-queue bytes and the core's backlog (wbft_backlog_messages, from
the snapshot of the last step) when gathered (GaugeFunc), counts sends left out
by the recent cache (wbft_send_suppressed_total) and times the
write-ahead log's fsyncs by method (wbft_wal_fsync_seconds, through
wal.Options.Now and Synced), privval refusals by message code
(runner.Deps.Refused) and parent snapshots missing from the authority
cache by context (preprepare, header_only, build). A head announced
before its snapshot and a PRE-PREPARE whose parent snapshot is missing
are also HEALTH records (snapshot_missing_at_head,
snapshot_missing_at_preprepare). Each new head's header gives the
per-validator series wbft_validator_seals_total{validator, type} and
wbft_validator_missed_proposals_total{validator}, always under
validator="total" and per address for a set of at most 64; the event
records give wbft_validator_message_delay_seconds{validator, code},
each source's first message of a code in a view less the round's start
(off above 64 validators); a validator absent from the sets for more
than two epochs loses its series (DeleteWhere); Node.Metrics()
returns the registry for the application to serve or read.
observe/logcat: log levels by module (observe.md 7.1). A node logs each
part under a fixed module name (node, consensus.round, mempool, ...)
at the level of Config.Log (a base level and per-module exceptions,
off to trace); an unknown module refuses the start. A disabled call
makes no record. The runner writes one line of the fixed message catalog
(logcat.EventEntry) per event record, under consensus.round (round
progress, timers, decisions; info for engine start/stop and finalized
blocks) or consensus.msg (messages, outcomes, evidence); log settings
do not change a run (TestLogLevelsDoNotChangeRun). The settings in force go to a LOG_CONFIG event and a
log line at start and on Node.SetLogLevels, with the application's
settings that name no module (Config.LogUnmapped, field unmapped); wbft_logLevels reads them,
and admin_wbftSetLogLevels (Node.AdminAPIs, for IPC or an
authenticated endpoint only) changes them. logcat.BuildProfile (and
cmd/wbft-logprofile) gives the inspector's log profile: which line
(message, level, module) is which event kind, for logs received without
the event stream; its ID, wbft@<digest>, is wbft_nodeInfo.logProfile
and NODE_START.log_profile.
conformance/stepdriver: the driver of the steps vectors, which a
transport adapter can reuse with its own frame stage, and RunTrace, which
replays a message journal through the core (cmd/wbft-replay).
conformance/sim: a deterministic multi-node simulator with a fake
application and a fake transport, crash and disk faults, and a scenario
bundle.
Nodes run the reference behaviour with two restart-safety rules: the
write-ahead log replay and the sign rules of the private validator (with
the core's guard against a second round-0 proposal, and the core's mark on
a ROUND-CHANGE whose prepared pair the bad-block rule released, which lets
the private validator sign it). Conformance vectors run
with every optional behaviour off.
Milestone W3a (node and application interface) is in progress:
app: the application boundary: the Application interface with its
optional extensions, the Consensus service interface, the request and
response types and the error values.
node: the node assembly. Start reads the application's Info, parses
and checks the chain configuration, opens the write-ahead log and the sign
state, sets the sign floor of a taken-over key (TakeoverGuard; without
it a floor no signature followed is cleared, and NODE_START.sign_floor
reports set, kept or cleared), runs the
start-up handshake with the application head (finalizing a decided block
again when needed), loads the authority snapshot of the head and starts the
core at head + 1 with the restart-safety rules. A validator keeps a
message journal in <data dir>/journal unless Config.Journal.Disabled
is set; cmd/wbft-replay replays it. The node implements the
Consensus service (proposal fields, epoch information, header
verification, head and synchronisation notifications) on top of the header
and epoch rules.
mempool: the transaction pool. Validity is decided by the application's
admission hook; the pool keeps per-sender nonce lists (executable and
waiting), de-duplicates, bounds its size, rechecks after every new head
before later admissions, and hands out proposals through an iterator that
the builder reports to (skip a transaction or drop a sender). Ordering is
a plugin; the built-in fifo orders senders by arrival and each sender by
nonce. The node starts the pool when the application gives an admission
hook and refuses an unknown ordering name.
rpc: the read-only wbft namespace (wbft_nodeInfo,
wbft_consensusState, wbft_peers) over a backend the node implements,
and a JSON-RPC over HTTP handler for applications without an RPC server.
examples/kvstore: an example application, a replicated key-value store.
It implements the application interface (building proposals from the
pool, executing them in proposal verification and at finalization,
storing blocks, the authority source), announces blocks and lets a node
that fell behind fetch them from its peers. wbft-kvstore init writes a
local network and wbft-kvstore start runs one node with an HTTP API;
make devnet (scripts/kvstore-devnet.sh) runs four processes, submits
transactions, stops and restarts a node and checks that it catches up.
Transactions are not signed: it is a development tool.
p2p/devnet: a TCP transport for development networks and tests, with a
consensus channel (through the frame stage) and an application channel.
Peers are identified by the address they claim; it is not for production
networks.
Layout
Directories group packages by layer. Lower layers never import higher ones,
and the lint rules below enforce the direction.
| Layer |
Directory |
Packages |
Responsibility |
| Primitives |
types, crypto, codec |
types, crypto/keccak, crypto/ecdsa, crypto/bls, codec, codec/rlp |
Basic types and chain configuration, hashing, secp256k1 and BLS12-381, encoding, signing payloads and hashes |
| Chain rules |
chain |
chain/validator, chain/validator/source, chain/epoch, chain/header |
Quorums, validator sets and proposers, authority source interface, epoch computation, header and proposal rules |
| Consensus |
consensus |
consensus, consensus/inputlog, consensus/wal, consensus/privval, consensus/runner |
The pure state machine, input encoding for WAL and journal, write-ahead log, private validator, the runtime around the core |
| Network |
p2p |
p2p/transport, p2p/devnet |
Transport interface, deduplication and frame verdicts for the application adapters; a development transport |
| Observation |
observe |
observe, observe/event, observe/journal, observe/metrics, observe/logcat, observe/participation |
Event vocabulary, message journal, metrics, logging, participation records |
| Node |
node, app, mempool, rpc, storage |
node, app, mempool, rpc, storage/kv |
Node assembly, application boundary, transaction pool, RPC, key-value store |
| Examples |
examples |
examples/kvstore, examples/kvstore/cmd/wbft-kvstore |
An example application and its command for local networks |
| Conformance |
conformance |
conformance/stepdriver, conformance/sim |
The step driver for vectors and traces, the deterministic simulator |
| Commands |
cmd |
cmd/wbft-vector-adapter, cmd/wbft-replay, cmd/wbft-journal, cmd/wbft-logprofile |
Conformance vector adapter, journal replay helper, journal inspection and pruning, inspector log profile |
internal holds helpers that are not part of the API: internal/refsort
(the reference-compatible sort), internal/fsys (the file system interface
with an in-memory implementation), internal/faultpoint (fault injection
under the wbft_faults build tag), internal/snetpartb (the StableNet
execution-side header rules used by the vector adapter and headerscan) and
internal/trace (traceability data).
Developer tools live in the separate module tools/ so that their
dependencies never enter the wbft module graph:
| Path |
Purpose |
tools/lint/coredet |
go/analysis analyzer for the determinism rules that linters cannot express |
tools/lint/heightlow |
Analyzer for the 64-bit truncation sites of heights and rounds: every use of RefLow64, RefLowInt64 or RefLow32 carries //wbft:low64 HH-nn |
tools/tracegen |
Builds the requirement traceability matrix |
tools/headerscan |
Verifies the headers of a running network over JSON-RPC |
tools/chainfetch |
Downloads raw blocks and receipts of block ranges over JSON-RPC, for replay and development |
internal/trace/owners.yaml |
Owner table read by tracegen: requirement ID to package and symbols |
internal/trace/wbft-spec.ref |
Commit of wbft-spec that CI checks out |
internal/trace/baseline.tsv |
Committed baseline of the matrix (requirement IDs and vector handlers) that CI compares against |
Dependencies
wbft may depend only on the Go standard library and these modules; CI
rejects anything else (.golangci.yml, scripts/check-deps.sh):
| Module |
Used by |
github.com/ethereum/go-ethereum v1.17.x (rlp, crypto, common only) |
types, codec/rlp, codec, crypto/keccak, crypto/ecdsa |
github.com/supranational/blst |
crypto/bls |
github.com/holiman/uint256 |
chain/validator/source, chain/header, mempool, the simulator's fake application |
github.com/cockroachdb/pebble |
storage/kv |
github.com/prometheus/client_golang |
observe/metrics/prom (reserved; observe/metrics writes the text format itself) |
github.com/BurntSushi/toml |
node |
golang.org/x/sys is linked as a dependency of the go-ethereum crypto
package only; no wbft package may import it. The go-ethereum packages
core/*, consensus/*, eth/* and p2p are forbidden. wbft is built
and tested against upstream go-ethereum v1.17.x only (.github/workflows/ci.yml).
Build, test and lint
Requirements: Go 1.26.8 or newer (both modules), a C
toolchain (cgo is required: BLS12-381 through blst and the secp256k1 of
go-ethereum; a build without cgo stops with a compile error), and
golangci-lint v2.
make build # go build in both modules
make test # go vet and go test in both modules
make lint # golangci-lint, coredet, heightlow and the dependency allow-list
make lint-negative # proves the lint rules reject violating code
make sim # simulation tests (WBFT_SIM_SEEDS seeds per scenario)
make sim-full # the scenario bundle with 10000 seeds per scenario
make faults # crashes at every fault point (build tag wbft_faults)
The same steps without make:
go build ./... && go vet ./... && go test ./...
go -C tools test ./...
golangci-lint run ./...
go -C tools build -o ../bin/coredet ./lint/coredet/cmd/coredet && bin/coredet ./...
go -C tools build -o ../bin/heightlow ./lint/heightlow/cmd/heightlow && \
bin/heightlow -require "$(tr -d ' ' < scripts/heightlow-rows.txt | paste -sd, -)" ./...
scripts/check-deps.sh
Lint rules
- Boundaries (depguard): the module allow-list above;
chain/header does
not import consensus or node; the pure modules do not import consensus,
node, app, p2p, storage or the observers other than
observe/event; consensus does not import app, p2p/transport,
consensus/privval or consensus/wal; only types and chain/epoch use
internal/refsort.
- Determinism of the core (
consensus, consensus/inputlog) and the pure
modules (crypto/*, internal/refsort, types, codec, codec/rlp,
chain/validator, chain/epoch, chain/header), test files excepted:
- depguard forbids
math/rand, crypto/rand, os, net, syscall and
runtime (and sync in the core);
- forbidigo forbids clock calls (
time.Now, timers, time.Sleep, ...),
unstable sorts (sort.Slice, sort.Sort, slices.SortFunc) and
math.FMA;
coredet forbids go, select and channel operations (in pure modules
except header.VerifyHeaders) and requires //wbft:unordered <reason> on
every direct range over a map.
- Heights and rounds: heights and rounds are arbitrary-precision
integers; the reference implementation reads only their low 64 (or 32)
bits at some places, and
wbft does the same at those places.
heightlow requires //wbft:low64 HH-nn on every use of a truncating
accessor outside types, where HH-nn is a site label that groups the
uses by the reference code they reproduce, and requires at least one use
for every label listed in scripts/heightlow-rows.txt.
Reference sort
internal/refsort holds a copy of the reference implementation's sort so
that candidate and transition orderings match it; see
internal/refsort/testdata/gen to regenerate the fixture.
Vector adapter
cmd/wbft-vector-adapter runs the implementation under a conformance vector
runner using the adapter protocol wbft-vector/1 of the specification
(A-11 §3.4): one JSON object per line on standard input and output, protocol
messages only on standard output, diagnostics on standard error.
make adapter
bin/wbft-vector-adapter -version
# Run the vectors of the specification with its reference runner:
python3 <wbft-spec>/spec/tools/vectorgen/check_adapter.py <wbft-spec>/spec/vectors -- bin/wbft-vector-adapter
In a conformance run all optional behaviour switches are off and
hello.improvements is empty. The adapter announces in its hello the
handlers it implements: crypto/*, encoding/*, validators/*,
timers/*, state_machine/*, network/receive_outcome, header/* and
chain/config_at. The steps handlers run the core through
conformance/stepdriver without a frame stage, so network cases whose
verdict belongs to the transport adapter (framing, the legacy code, size
limits, empty payloads) are answered unsupported here and by the
application's adapter there. Handlers of the execution layer are answered by
the adapters of the application repositories. The header handlers decide the execution-side steps of header
verification (uncle hash, gas limit, fork times, base fee) with a stand-in of
the application hook for the StableNet presets (internal/snetpartb). The adapter exits with status 0 after bye, 1 when
its input ends without bye, and 2 on a protocol error.
conformance/stepdriver can also run the steps cases the way a node runs
them: the core with consensus.RestartSafety and own messages signed by a
private validator (Options.Improvements, Options.PrivVal,
Options.SignFloor). TestVectorsWithRestartSafety checks that every
steps case then sends and records exactly the reference output, and
TestVectorsWithSignFloor that a node which took over its key signs
nothing at the first height.
scripts/cross-arch-vectors.sh runs the vectors whose results rest on
binary64 arithmetic (validators/quorum, timers/round_timeout) and writes
one line per case with the adapter's result. CI runs it on amd64 and arm64
(determinism-cross-arch) and fails when a case fails on either
architecture or when the two result files differ.
scripts/cross-arch-vectors.sh <wbft-spec>/spec bin/wbft-vector-adapter vectors.txt
Configuration time units
The chain configuration's requestTimeoutSeconds, blockPeriodSeconds,
maxRequestTimeoutSeconds and allowedFutureBlockTime are in whole seconds.
For example, requestTimeoutSeconds: 2 sets a two-second base timeout.
types.NewConfig and height transitions convert that base to the internal
types.Params.RequestTimeoutMs value of 2000; they do not interpret the
JSON value as milliseconds. Use 2, rather than 2000, for this example.
An absent or zero base request timeout does not set a value. A zero request
timeout in a transition inherits the earlier value. A missing maximum inherits
the earlier cap, while an explicit maxRequestTimeoutSeconds: 0 removes it.
The kvstore example configures a two-second base timeout. Simulation timing
overrides below also take seconds and retain their own positive-value checks.
These unit descriptions do not add conversion range checks or change existing
timeout arithmetic, genesis files or stored chain configurations.
Simulation and journal replay
conformance/sim runs N nodes on a manual clock with the real runner,
private validator, write-ahead log and journal on an in-memory file system
that keeps only synced data across a crash. The seed determines the run:
two runs of one scenario write identical event files and journals. Every
run checks agreement, that no honest node signs two values in one view,
progress, the header rules of every stored block, and that a write-ahead
log replay never differs from the original run. Fault points (build tag
wbft_faults) crash a node between two durable steps.
Scheduled engine_stop and engine_start actions pause consensus on the same
runner while the node's application, block synchronisation and transport remain
live. The signer, WAL, journal, event writer and node clock survive the pause;
engine start uses the application's current head and configured replay policy.
Control actions wait in source order behind a blocked manual finalization, and
a full node stop or crash discards pending engine controls. These actions model
an observed engine lifecycle; they do not infer synchronisation decisions.
NodeSpec.Timing optionally overrides RequestTimeoutSeconds,
BlockPeriodSeconds and MaxRequestTimeoutSeconds for one node, including
its later restarts. Nil fields inherit normalized Scenario.Params; an
explicit zero maximum removes the timeout cap. Explicit timeout and block
period values must be positive. Supplied seconds must fit a time.Duration
when added to the simulation horizon. The existing round timeout arithmetic
is unchanged. Each node's runner, fake application, header verification and
journal use the same resolved configuration. Header verification results are
shared only among nodes with identical configurations. Inputs are copied at
startup, so later changes to caller-owned timing pointers do not alter a run.
Different block periods can cause nodes to reject each other's headers; these
are reported as header violations rather than hidden by a shared cache.
Scenario.Params.RuleActivations preserves chain-wide consensus rule activations
in the simulator's generated chain configuration. Values are exact non-negative
decimal strings, including heights beyond 64 bits; missing rules are disabled.
The official chain configuration parser refuses unsupported rule names. Nil or
empty declarations retain the original default configuration and outputs.
Node profiles and timing overrides do not change these activations. Every node's
runner, fake application and header checks use the configured rules, and actual
journal segments retain their activation heights and configuration digest across
restarts. Declarations are copied at startup so caller-owned map changes cannot
change an executing scenario.
Scenario.Params.AllowedFutureBlockTime optionally preserves the chain's static
future-block allowance. Nil keeps the original default representation; a
supplied value, including zero, is retained across timing overrides and restarts
without narrowing the chain configuration's unsigned integer domain.
NodeSpec.Processing optionally declares service samples by receive-check or
core-input phase, input kind and message code. An explicit worker count bounds
receive checks independently of the single reserved core input. Samples use
the node's seeded generator, retaining their order and multiplicity. Nil and
all-zero profiles preserve the original execution and evidence bytes without
extra random draws. Unlisted work classes keep zero cost.
Lifecycle steps and WAL replay retain their existing synchronous execution.
The optional node.Config.NextHeight (or runner.Config.NextHeight) takes
runner.NextHeightLimits with four explicit positive limits: total entries,
owned raw bytes, entries per peer, and entries per recovered outer signer.
Nil preserves the original receiver. The lane authenticates only the outer
signature of exactly the next sequence, then verifies the entire message
against the active validator set when that sequence becomes current. Raw-byte
reservations survive queued work, authentication, WAL persistence and core
consumption; they do not bound process heap, CPU time or WAL disk usage.
Enabling the lane requires WAL replay and a compatible binary on later starts.
Journal segments declare this receive profile and its limits separately from
consensus improvement flags. A deferred_message input retains inline message
bytes and the first writer-run/capture pair, including after process restart and
WAL replay. wbft-replay reports deferred_origin separately from functional
replay: missing or pruned captures leave the inline input usable, without
substituting a matching frame from a later run. Receipt times belong to the
original writer's clock. Timed and scheduled replay require a verified source
in the same writer run; cross-run clock comparison remains unavailable.
sim.NodeSpec.NextHeight declares the same explicit limits and persists the
receive profile in every incarnation. Nil preserves existing execution bytes
and random draws. Enabled lanes require WAL replay and atomic receive/core
processing; positive pre-dispatch service samples remain unsupported. JSON
receive limits use decimal strings to retain the full unsigned integer domain.
The pre_dispatch_service/1 policy reserves an input, occupies its worker for
the sampled time, then executes the existing operation atomically. Core state,
environment lookups, timer arms and outputs are evaluated at dispatch after
service; the service interval is not a measured core step. Applying a recorded
entry-to-completion sample here is an explicit approximation, not reconstruction
of internal CPU, signing, WAL or output milestones. The startup record retains
the declaration with decimal-string nanoseconds. It does not certify that a
sample, worker count or unlisted zero cost was measured on a physical node.
Application inputs, head notifications and timer callbacks can arrive while
core work waits. Already queued expiries survive later cancellation. Ordered
engine controls and a clean node stop wait behind reserved core work, including
a restart requested before that work finishes. A crash invalidates callbacks
of the old incarnation, and old engine receive checks cannot affect a new run.
Work extending beyond the simulation horizon remains incomplete. The manual
runner requires a caller-serialized completion scheduler; production runners
reject these callbacks and retain their existing goroutine implementation.
The per-node application model keeps independent request scheduling by default.
Setting NodeSpec.BuildQueuePolicy to "serial-latest" selects one worker and
one latest pending request. BuildDoneResult must explicitly be "failed" or
"submitted": it defines the fake executor's response to Done at the sampled
assembly return. Done signals cancellation without freeing the worker, while
Stop lets an active executor return before a clean restart. A crash drops that
executor without inventing a completion. Each request retains its original
run's event writer and clock.
NODE_START.application_model records the queue, completion and scheduler
assumptions. The shared callback scheduler orders deadlines and equal-deadline
insertion; worker polling then selects Done before a pending replacement.
conformance/buildqueue exposes explicit choices for ambiguous selections.
Callers must validate the selected executor and scheduling assumptions against
execution evidence before fitting. Legacy miner queues require a separate model.
WBFT_SIM_SEEDS=100 go test -run TestBundle ./conformance/sim/
go test -tags wbft_faults -run TestCrashAtFaultPoints ./conformance/sim/
go test -run TestReplayDeterminism ./conformance/stepdriver/
# Replay one node journal of a simulator export:
bin/wbft-replay -journal <dir>/<node> -chain <dir>/chain.rlp
# Inspect, check and prune a node's journal (JSON output):
bin/wbft-journal stat --dir <data dir>/journal
bin/wbft-journal verify --dir <data dir>/journal
bin/wbft-journal prune --dir <data dir>/journal --keep-heights 100000
# Bundle of heights 100 to 200 (two warm-up heights before), and its replay
# when it starts at the start of a writer run:
bin/wbft-journal export --dir <data dir>/journal --out node.tar --from 100 --to 200
bin/wbft-replay -journal node.tar -chain <chain.rlp>
# Explicit approximation when the bundle omits the recorded engine start:
# bin/wbft-replay -journal node.tar -chain <chain.rlp> -warmup-from 98 -compare-from 100
# R-01 frame dump (frames-<run>.jsonl and payloads/) of heights 100 to 200:
bin/wbft-journal export --dir <data dir>/journal --out <dir> --format r01 --from 100 --to 200
tools/headerscan checks the headers of a running StableNet network with
the header rules of wbft. It fetches headers over JSON-RPC, one request at
a time and at most five per second, and verifies every header of the scanned
ranges with header.VerifyHeader (header-only mode with seal checks; the
steps that need the parent state are skipped) and with header.VerifyLight.
It also checks that the block hash computed by wbft equals the hash the
node reports. The ranges are blocks 0 to 100 and, for every fork block F
after genesis, blocks F to F + 100 (-span).
make headerscan
bin/headerscan -rpc https://api.test.stablenet.network
The built-in preset is the StableNet testnet (chain ID 8283, Boho at block
14 408 500); -config and -forks select another network. Every rejected
header is printed with its number, hash, failing step and error; the exit
status is 1 when a header is rejected. CI does not run the scan, since it
depends on a public endpoint.
Traceability matrix
make tracegen
bin/tracegen -spec <spec-dir> -vectors <spec-dir>/vectors \
-owners internal/trace/owners.yaml -code . -prefix WBFT- -format md
tracegen reads the requirement IDs from the specification chapters (or a
list file such as speclint output with -requirements), the meta.yaml of
every vector case, the owner table and the // Spec: / // Covers: comments
in the code, and writes one row per requirement: owner, symbols, code and test
references, vector handlers and case count. References to unknown IDs are
reported on standard error; -strict turns them into a failure.
Coverage check
-check decides, for the chapters it names, whether every requirement that
owners.yaml gives to a wbft package (no class) is implemented and has
evidence. A requirement passes when
- every package in its
owner field exists in the module,
- its
symbols field is not empty and every symbol is declared in the
module (pkg.Name, pkg.Type.Method or pkg.Type.Field, where pkg is
a package name; an unqualified name is looked up in the owner packages),
- a
// Spec: ID comment sits in a non-test file of one of its owner
packages, and
- a vector handler that cites it is implemented by the adapter (its cases
then run in the conformance step, which fails on any failed case), or,
when no implemented handler cites it, a test file carries a
// Covers: ID comment next to the test that checks it.
make adapter tracegen
bin/tracegen -spec <wbft-spec>/spec -vectors <wbft-spec>/spec/vectors \
-owners internal/trace/owners.yaml -code . -prefix WBFT- -o /dev/null \
-check A-01,A-02,A-03,A-04,A-08 -adapter bin/wbft-vector-adapter
Every gap is printed with its kind (owner, symbols, code,
evidence) and the exit status is 1 when there is one. CI runs the check
for chapters A-01, A-02, A-03, A-04 and A-08 after the conformance vectors.
Baseline
internal/trace/baseline.tsv records two columns of the matrix: the
requirement IDs of the public specification and, for each, the sorted vector
handlers that cite it. The traceability matrix step of CI (enabled by the
repository variable WBFT_SPEC_REPOSITORY) runs tracegen with
-baseline internal/trace/baseline.tsv and fails when a requirement was
added or removed or when the handler column of a requirement changed. The
differences are printed one per line (+ new requirement, - removed
requirement, ~ changed handlers with the added and removed handlers).
CI checks out the specification at the commit recorded in
internal/trace/wbft-spec.ref, so a change merged into wbft-spec does not
affect wbft until this file is updated. To move to a newer specification
revision, update wbft-spec.ref, regenerate the baseline against that
revision and commit both in one pull request:
make tracegen
bin/tracegen -spec <wbft-spec>/spec -vectors <wbft-spec>/spec/vectors \
-owners internal/trace/owners.yaml -code . -prefix WBFT- -o /dev/null \
-write-baseline internal/trace/baseline.tsv
git diff internal/trace/baseline.tsv
Review the diff of baseline.tsv in the pull request: every added or removed
ID and every added or removed handler should be explained by the
specification or vector change it follows. A removed handler or a
requirement that lost all its vectors is a coverage loss and needs a reason.
License
wbft is licensed under the GNU Lesser General Public License, version 3
or (at your option) any later version (SPDX: LGPL-3.0-or-later,
LICENSE). The LGPL supplements the GNU General Public License
v3.0 (COPYING).
Chain rules
Chain-level rules come only from the genesis chain configuration's
wbftRules.consensusRules. The supported keys are rejectNumberJump,
minimalSealerBitmap, stableCandidateSort, firstProposerDiligenceCap,
committedEpochDiligence, replayPerBlockPolicy, epochInfoInProposal and
blsRandao. Each key is disabled when missing or null; a
non-negative JSON integer sets its activation height. Node profiles and local options do not override
these values. Unknown consensus or feature rule keys refuse startup.
types.Config.Rules reads activation using full heights. Header rules
select activation from parent.Number + 1, including during proposal
and batch verification.
NODE_START, configuration logs and wbft_nodeInfo expose the configured
activation heights and the rules digest.
minimalSealerBitmap checks the representation of present aggregate seals.
From its activation height, prepared and committed seal bitmaps must omit
trailing zero bytes. Previous seals use the parent's own activation context,
so they are checked starting with the next child of an activation block.
These checks also apply when signature verification is omitted for a
proposal. Optional absent seals stay absent; ordinary seal-presence and BLS
checks remain separate. Wire decoding preserves the supplied representation.
For a scheduled rule, deciding a non-minimal previous bitmap requires the
grandparent header. Header verification consults the batch and stored
history. A light caller supplies LightInputs.Grandparent; missing context
remains undecided. Canonical previous bitmaps need no additional context.
committedEpochDiligence scores the fully committed window from the previous
actual epoch boundary L through the parent of the current boundary e. Its
length is e-L, including after a schedule transition. The current block's
own round and carried signer entries enter the next window; prepared re-proposals
therefore retain the same epoch information and signing digest.
Each historical block uses its own proposer policy and authority order.
Selection starts from the parent's Coinbase, or the zero address for genesis.
All attempts through the successor header's hash-bound PrevRound count,
including full rotations; the historical header's own Round and current seals
are node-local and do not affect accounting. For the final historical parent,
the current candidate supplies this previous-round field. It is already part
of the prepared proposal hash and stays fixed during re-proposal.
The final-round proposer receives credit for carried previous signatures.
Capacity uses the parent authority's size, and signing participation uses
actual membership in that authority. Genesis and its child provide no previous
certificate observations. A candidate without eligible signing observations
keeps its prior score; a candidate absent from L starts at the default score.
The score combines the observed signing and proposal fractions with its prior,
weighted by eligible slots over ten times the window length. Arithmetic retains
full precision and rejects invalid ranges rather than clamping.
This rule replaces the legacy score calculation when active.
firstProposerDiligenceCap continues to affect the legacy calculation only;
stableCandidateSort independently controls ranking in either mode. Header,
EpochInfo, wire and signature formats do not change. The activation is bound
to the actual parent's full height plus one.
replayPerBlockPolicy changes only the legacy epoch score replay. Each
historical block uses the proposer policy configured at its own full height,
and the first child of genesis starts from the zero proposer address. The
existing bounded search for each recorded Coinbase and score weighting stay
in place. Activation uses the actual hash-bound parent plus one before epoch
classification; an active call must supply that exact height.
committedEpochDiligence retains its separate window and accounting when both
rules are enabled. Missing, null and future activations preserve legacy replay.
The digest is Keccak-256 of the canonical JSON object containing
anzeon.wbft, transitions and every wbftRules group. Object keys are
sorted, array order is preserved, missing selected sections are null,
and numbers use an exact decimal coefficient without trailing zeroes
plus a base-ten exponent (omitted when zero). No floating-point conversion
is used; 100, 100.0 and 1e2 have the same digest representation.
Activation values themselves must be integer tokens, not exponent notation.
Replay protocol hello includes the embedded WBFT module version. Simulator
journal segments use the same version resolver. A locally replaced dependency
without version metadata remains unknown, and a modified main module carries
a -modified suffix. These labels do not prove a matching clean core revision.
Trace replay refuses journal gap records instead of claiming matches across
lost inputs. Protocol errors include a bounded code: input_gap for missing
segment/step continuity or explicit journal loss, and replay_error otherwise.
Readable steps emitted before an error are only a prefix. Independent payload
completeness checks are still required when step inputs contain inline bytes.
Received-frame replay references
Adapters implementing transport.IOObserver retain the observer clock's
monotonic and wall-clock endpoints around transport I/O. Writes end at the
underlying write return; reads end after consuming the payload and before
validation or queue delivery. A read interval may include waiting for a frame.
The existing callback timestamp remains unchanged. Optional journal intervals
and R-01 io fields are absent when the adapter did not measure them. Wall-clock
regressions are preserved. Interval overlap neither proves a clock fault nor
establishes a corrected one-way latency; clock agreement and message pairing
still need independent evidence. R-01 outgoing frames retain explicit write
results when recorded, including successful writes.
Adapters that support transport.CaptureObserver reserve a run-local capture
ID before Receiver.Offer and report it with the original received frame after
the offer. transport.Inbound.Capture carries only journal provenance. The
runner stores peer journal steps as captured_message references; the WAL
continues to store the original inline input for recovery. Legacy adapters keep
their existing inline journal steps without claiming a frame association.
The journal reader resolves references with a bounded, position-only index,
including frames recorded after their steps. Source runs, peer mappings, frame
stage delivery and message codes must agree. Missing, ambiguous or truncated
captures are refused in normal closed replay. Older journal layouts still
read; new capture metadata does not change wire or block encodings.
wbft-replay -omit-missing-captures (protocol
omit_missing_captures: true) explicitly models intentional input loss. The
missing received input is not fed to the core; its actual empty output list is
compared at the original recorded position. Each omitted step is labelled and
the protocol summary marks the run counterfactual. Invalid or ambiguous
references are not treated as intentional omissions. App answers are never
invented; an unrecorded application call reports env_exhausted.
Replay execution identity
Core-step events include engine_run alongside their step number. The writer
run, engine generation and step identify the corresponding journal input,
including when a restarted engine reuses step numbers. Older events may omit
the engine generation and require an explicitly unambiguous association.
Trace replay loads each new writer's recorded configuration and requires its
engine start before feeding its steps. A changed configuration within the same
writer is refused. Recorded application answers are matched by call kind and
argument key within their source step; repeated answers for the same key are
consumed in recorded order and never reused. Missing answers remain failures.
Protocol steps and structured step errors include the source writer_run,
engine_run and step; callers do not need to parse diagnostic text.
Recorded application answer context
The default -answer-scope step preserves closed replay: application responses
are consumed once by call kind and argument key from their original step.
Explicit -answer-scope context permits a missing local answer to be found in
other recorded steps of the same writer, engine, head number/hash and validator
context. Local original responses take precedence. Cross-step lookup refuses
conflicting original responses, including a conflict whose other response has
already been consumed. Absent answers remain env_exhausted; conflicts are
env_ambiguous and source/context changes are env_source_mismatch.
stepdriver.NewAnswerPool reads the retained source separately; RunTrace
validates the recorded configuration and step response fingerprint. Each replay
gets a new consumption session. Successful calls retain their original writer,
engine, step and one-based call index. The pool is bounded to one million source
records, 100000 steps, 100000 application answers and 16 MiB encoded responses.
Journal sequence gaps, duplicate coordinates and unsupported response kinds are
refused. Head lookups verify both the full number and hash, including fallbacks.
This option permits counterfactual application lookup; it does not reconstruct
unrecorded application execution or the reactions of other nodes. CLI returns 3
and protocol results mark the mode counterfactual even if all outputs match.
Hello advertises context_answers_v1; replay requests use answer_scope, steps
carry context_answers and answer_sources, and completion echoes the scope.
The validity boundary of an open-loop experiment must still be handled by its caller.
Bounded replay warmup
Closed trace replay still requires a recorded engine start. Callers can explicitly
set TraceOptions.Warmup with a positive FromHeight and a later
CompareFromHeight (at most 10000 heights apart) to process a retained prefix
whose original start is absent. The first available step at FromHeight must
have a verified recorded head and a valid validator set. The driver starts a
fresh core there; its synthetic initial outputs are neither executed nor
reported as an original step. Backlog, prior views and unknown timer generations
are not reconstructed. Journal gaps, missing captures, discontinuous steps,
missing application answers and unavailable prefix heads still fail closed.
Every subsequent result carries Approximate, and approximate steps before the
comparison height also carry Warmup. Matching outputs alone do not establish
original reproduction. An actual recorded start restores its original timer
generations and clears the approximation marker. Available complete histories
use the normal exact path even when the fallback is configured.
wbft-replay accepts -warmup-from N -compare-from M with -journal. It returns
exit 3 when approximate results were emitted, separately from exact success (0),
different outputs (1) and failed/unavailable replay (2). Zero replayed steps are
not success. The wbft-replay/1 hello advertises bounded_warmup_v1; clients must
check it before sending warmup_from_height and compare_from_height.
Protocol steps expose approximate and warmup, and completion reports separate
approximate_steps and warmup_steps totals. Warmup failures use
warmup_unavailable with structured source coordinates when available.
TraceOptions.Schedule and wbft-replay -input-schedule FILE apply one explicit
drop, delay, or swap to a recorded external input identified by writer,
engine and original step. Delay places that input after other_step; swap
exchanges two external inputs only when recorded wire stamps prove that both
were available before the earlier position. Inline transport timestamps with an
unknown journal clock origin cannot prove that boundary. Internal inputs and
lifecycle boundaries cannot be moved by these operations.
Source continuity is validated before altered execution. Original step and
journal sequence coordinates remain distinct from execution order and slot
coordinates. Source bytes and receipt timestamps are not rewritten. A drop
compares an actual empty output list at its original position. Reserved
self-deliveries, replays, requests and timer inputs must match outputs actually
produced by prior replayed handlers; unavailable reservations are retained as
empty positions with reservation_unavailable after the change. They are not
implicitly executed. Unprocessed newly reserved events remain pending at the
end of the finite input tape.
Experiments require explicit context application answers and a complete recorded
start; missing application responses remain failures. Moved inputs read the
recorded execution slot's head and actual validator context, rather than reusing
an old source-step head. No signing, sending, application execution, timer-length
change, or automatic reaction of other nodes is performed here. Changed timer
policies and open-loop validity boundaries require the caller's recorded evidence
and simulator scheduling integration. CLI returns 3 for a completed experiment
even when every output matches. Protocol hello advertises input_schedule_v1,
requests use input_schedule, steps retain scheduled_input source/slot fields
and suppression_reason, and completion echoes the experiment with
counterfactual: true. Scheduled source buffering is limited to one million
records, 100000 steps and 256 MiB encoded step inputs and responses.
Shared replay clock and timer reservations
conformance/manualclock is the callback scheduler used by the deterministic
simulator. It orders equal deadlines by insertion order, preserves cancellation
identity, and delivers callbacks at their deadlines when advanced. It provides
the runner clock interface; time reversal, deadline overflow and reentrant
advancement are refused. runner.TimerQueue exposes the existing runner timer
implementation, including full timeout context, generation replacement,
cancellation before expiry, and preservation of already-queued expiries.
New live runner journal steps include a measured StepRec.Entry monotonic/wall
clock pair. StepRec.Mono remains the completion stamp. Entry time zero is a
known measurement; absence in older records and reconstructed WAL steps is
unknown. A trailing optional RLP entry field preserves the old encoding when
absent. Current readers accept both shapes; readers built before the extension
must be upgraded before consuming new steps with entry metadata.
These are the timing and scheduling prerequisites for timer-policy replay.
Recorded-input schedule experiments still use their finite recorded timer tape;
they do not yet regenerate timer inputs under changed duration parameters.
Measured timer operation journal
New node and simulator journals declare Core.TimerTrace = 1 in a trailing
optional segment field and include timer records (kind 8). Readers must be
upgraded before reading these journals. The absent capability preserves the
legacy segment encoding and does not enable timer recording. Custom writers
opt in through TimerTraceEnabled.
The runner measures scheduler mutations under the queue mutex. A sequence
orders equal monotonic timestamps. Records include arm/replacement, actual
cancellation, the cancellation request even when every timer has expired,
callback queue insertion, dequeue and lifecycle clear. Each arm keeps its
writer/engine/step/output cause, entry-based deadline and full encoded timeout
context. Output indices are one-based; zero identifies lifecycle, dequeue or
WAL restoration rather than an invented core output. Queued callbacks retain
the arm's original cause and never inherit the currently running step.
Operation timestamps are distinct from step entry and completion. In particular,
a callback can queue during processing before a later cancellation, and that
expiry remains available for dequeue. Sequence and full-context records allow
this ordering to be checked without choosing a guessed cancellation instant.
Timer input bytes count toward journal queue limits; loss remains explicit as
gap records. Segment rotation and journal bundles preserve the records and
capability. These observations are prerequisites for timer-policy execution,
not a claim that every journal can already be used for what-if replay.
Timer policy experiments
wbft-replay -journal DIR -chain FILE -answer-scope context -timer-policy POLICY
regenerates timeout inputs from actual ArmTimer/CancelTimers outputs through
runner.TimerQueue. Policy JSON uses decimal string seconds, for example
{"request_timeout_seconds":"2","max_request_timeout_seconds":"30"}.
Unknown fields, duplicates, numeric JSON values, zero base timeout and duration
overflow are refused. The original configuration and height transitions remain
intact apart from these explicit parameters.
A complete source with measured step entry timing is required. The source must declare measured timer operation tracing; legacy operation
clocks remain unavailable. The untouched
baseline must reproduce outputs and timer contexts before any experiment step
is emitted. Internal Schedule inputs are regenerated in runner FIFO order.
Recorded signing results can be reused only for identical full Broadcast requests,
writer/engine/head context and recorded signing order; nested signatures remain
unchanged. Missing or conflicting signing results and new application questions
stop execution. This does not execute a new signer or block builder.
The explicit execution model is recorded-external-slots/zero-generated-processing/recorded-signing-results/1.
External processing slots and their recorded busy intervals are fixed. Generated
handlers have zero modeled processing cost. Recorded wall anchors are used at
source slots, with monotonic extrapolation between them. These are conditional
open-loop assumptions, not proof of physical execution or another node's response.
All experiment results remain counterfactual, including identical policy values.
The finite source horizon is preserved and remaining outputs are not run beyond it.
Hello advertises timed_inputs_v1. Use timed_replay with timer_policy and
answer_scope:"context"; partial ranges, missing-input omission, warmup, input
schedule combinations and vars are refused. Replies use timed_step and
timed_done. Execution/source coordinates, monotonic nanoseconds and policy
seconds are decimal strings. Generated inputs have no source step; timer arm,
causal execution parent and reused signing evidence are distinct fields. Errors
retain execution and available source/causal coordinates. CLI exits 3 for a
finished experiment and 2 for a refused or incomplete run. These experiments
still need analyzer storage, UI and explicit open-loop validity boundaries.
The baseline uses measured arm/cancel operation times and observed callback
queue times through the official scheduler. Complete operation sequence,
source-output joins, FIFO dequeue and full timeout contexts are validated before
emitting results. Missing or conflicting timing evidence is refused. Changed
policy execution uses the deterministic clock and ordinary FIFO Pop. Reused
application questions require complete output and writer/engine/head/validator
context identity; original question source and output index remain explicit.
Completion reports each writer horizon and pending internal, queued and armed
timers. A recorded Stop applies its measured runtime cleanup; no future tail is
manufactured.
Timed replay helpers declare timed_state_v1 and return execution_state
from the core snapshot after each execution slot. Height and round are decimal
strings while the core runs; a stopped core returns null for both. Suppressed
original internal slots report the unchanged execution state, never the source
input view. This state is counterfactual metadata, not a canonical chain fact.
Timed helpers declaring timed_actions_v1 return output_actions and
recorded_output_actions independently. Every action retains its one-based
output index and SHA256 of the complete encoded output. Network descriptors
separate unsigned broadcast requests, recorded signing results/failures and
actual relay bytes. Signing evidence points to both the original producer
output and its later signing input. Application build/commit requests retain
exact original-question locations and full decimal-string views; no payload,
signature or block hash is exported. Eligible recipients describe validator
membership intent, not observed transmission or delivery.
parent_output identifies the precise parent execution output that reserved a
regenerated input, including a broadcast awaiting unrecorded signing results.
Generated executions have no invented original output slot. A changed timer
fingerprint does not imply changed network bytes. No action descriptor proves
an arrival time or establishes other nodes' counterfactual reactions.
Application build observations
BUILD_HANDOVER identifies each request actually passed to the application by
build_id, writer run, full height and round, parent hash and nanosecond wait.
The ID counter is shared across engine restarts of one node. The optional
app.BuildRequest.Observation records BUILD_STARTED after the builder's wait
and BUILD_FINISHED for submission, replacement, cancellation, unavailable
inputs, a changed head, or failure. Request copies share a single observer.
Assembly intervals use actual start/finish boundaries; they do not subtract an
assumed wait from a nearby proposal. submitted means the application called
SubmitProposal, not that the core accepted the block. Applications without
observer support and missing/dropped endpoints remain incomplete evidence.
The existing BUILD_REQUEST and PROPOSAL_SUBMITTED records are preserved.
Build callbacks retain the writer and clock of their handover, including when
another node run starts before a late completion. Closed original writers leave
missing evidence rather than moving records into a later run.
RELAY_TARGETS records the actual local dedup operation in its engine/step and
output position: attached-validator candidates, selected send targets and peers
excluded by their recent cache. It includes empty selections and explicitly
unknown validator context. These are local target-selection observations;
selected targets do not prove successful delivery or receipt.
Cache operation sources and queued attempts
New node journals declare Core.CacheTrace = 1 and record initially empty
cache instances plus every inbound or gossip mutation under the actual Dedup
mutex. Each instance has its own operation sequence. Records retain code, key,
payload hash, pre-operation hits, actual candidates, targets and suppressed
peers. Cache rules, capacities, payloads, signatures, consensus options and WAL
behavior are unchanged. Declaration clocks are unavailable; operation clocks
are measured at the actual mutation.
transport.AttemptSender and transport.AttemptObserver are optional queue
metadata interfaces. A recording owner and enqueue generation keep a delayed
write attached to its original cache decision. The opaque owner is compared
only within the process, never serialized as a pointer or nonce. Observer
replacement or a legacy adapter leaves identity explicitly unavailable. An
attempt sequence identifies a call; it does not replace cache mutex order,
transport completion time or peer delivery evidence.
Both development and application adapters must preserve this metadata in their
queues. Development streams register their peer identity once per observer
lifetime before the first observed frame, including observers installed after
a connection already exists. Stream registration is shared by read/write
callbacks and retained separately for each actual connection.
R01 exports retain complete cache context outside frame height filters.
cache_trace closes available instance/operation counts and marks incomplete
snapshots; cache_instance declares the empty state and real capacities;
cache_operation preserves the mutation and its original journal coordinates.
When scheduler and cache capabilities share a segment record, one
scheduler_trace row contains both declarations with the original raw sequence.
Outgoing send_attempt and suppression cache_decision metadata point to their
exact decision sequence, segment and offset. Missing context stays unavailable;
a successful write of a suppressed decision is retained for independent failure
checks rather than removed. Export refuses conflicting identities, invalid
bindings and source changes between its two passes.
The RLP capability and attempt fields are optional tails. Absent capabilities
preserve older layouts and R01 output. Readers must understand the new record
kind and closed metadata before consuming new cache-enabled journals. Source
coordinate authentication is a separate audit of retained raw bytes; these
records alone do not establish remote delivery or complete network coverage.
Application sync control observations
New node journals declare Core.SyncTrace = 1 and record submitted sync
states separately from their actual handling by the node. Request identities
and processing identities are distinct: the existing latest-state queue can
supersede requests or handle one request more than once. The first-success
guard and the existing engine stop/start policy are unchanged.
Each processed record retains the selected state, the guard and running state
before/after handling, the actual action, and whether its control call failed.
Only an actual engine control call gets event writer cursors. These are next
sequence numbers sampled immediately before and after that call: the raw event
range is [event_before, event_after). An empty range is retained, including
failed calls; timestamps do not establish these boundaries. A missing writer,
sequence exhaustion, dropped records or missing context remains incomplete
evidence. Observation uses the existing bounded asynchronous journal queue.
R01 exports retain sync_control rows outside frame height filters and keep
their original journal sequence, segment and offset. sync_trace reports the
available request/processing counts and source completeness. Capabilities from
one segment share one declaration row with its original sequence. A processed
row links to its exact submitted request only when the complete retained source
establishes that identity and state; physical enqueue order need not match the
logical request order. Export refuses ambiguous identities, unsupported trace
versions and source changes between its two passes. Source completeness does
not certify the sync policy or an external downloader result.
The sync capability is an optional trailing RLP field; readers of older records
preserve their original bytes. Readers must support the additional record kind
and export metadata before consuming sync-enabled journals. Consensus inputs,
WAL records, application sync state messages and wire payloads are unchanged.
consensusRules.epochInfoInProposal checks epoch information using candidates supplied by the parent authority snapshot. It requires committedEpochDiligence at the same or an earlier height so that a prepared proposal keeps its epoch information and digest across round changes. The execution layer must select parent-state candidates consistently for building, snapshots and execution checks; SDK applications require sourceRules.candidatesFromParentState at or before activation. Missing or null activations preserve the existing behavior.
Snapshots distinguish missing candidate input from a known empty list and bind it to the parent's hash and full height. Proposal verification rejects unavailable input and mismatched epoch information. Header-only synchronization checks known input but can defer absent parent state to the mandatory execution check; this is not complete state validation from headers alone. Light verification reports CANNOT_DECIDE for an activated epoch when only headers are available. Non-epoch headers cannot carry epoch information once the rule is active.
The deterministic simulator uses the same parent-state candidate schedule when this rule is active. Its LightChecks result retains CANNOT_DECIDE observations separately from successful state-bound header checks, including the checked block identity and reason.
Local import monitoring
node.Config.Improvements accepts explicit local behavior names. The default
remains the existing restart-safety set. Unknown or repeated names are rejected;
these settings cannot disable restart safety and do not change header validity,
chain configuration or wire payloads. Enabled names are reported with source
node in NODE_START and wbft_nodeInfo; journal metadata retains the exact mask.
commit_watchdog resets the round timer after a block is decided. While import
is pending and the block is not marked bad, expiry renews the monitor without
changing rounds. Successful import cancels it and waits for the new-head event.
A bad block still permits round recovery, and the existing F+1 path remains
available. A slow application can therefore keep this node at the decided view
longer than the default behavior.
Normal progress is tested with all-default, all-enabled and mixed nodes. A
separate deterministic case with every import taking longer than the round
timeout records an existing progress boundary: the all-enabled configuration
progresses, while the default and half-enabled configurations do not. That
boundary is retained explicitly and is not a mixed-deployment liveness claim.
advance_on_commit_failure separately advances to the next round on a failed
import of the current decided block. Results must match both its hash and its
full height/round; only the first result is consumed. Results from another view,
unbound historical results and queued superseded timer expiries cannot change
the current view. These options are individually opt-in and do not declare that
every behavior of an improved profile is implemented.
The runner appends the decided view to import-result input records only when
one of these options is enabled. Older two-field input records retain their
original encoding. Readers of new opted-in journals and WALs must understand
the bound result format. Ordinary node startup still uses its durable commit
handshake before starting or replaying the consensus engine.
Local height and backlog ordering
reset_height_state initializes every new height at round zero, including a
catch-up observed while processing a queued round timeout or an F+1 round change.
It clears the previous prepared certificate and round-change set, requests a
round-zero proposal and suppresses the old height's target-round broadcast. The
requested round remains in the round-entry audit record. Same-height round
changes and already queued retry timers retain their existing behavior.
scan_ready_backlog scans past messages that are still future for the current
phase, releasing ready messages in their existing priority order. Future entries
keep their order and duplicate slots; membership checks and queue limits remain
in effect. Both options are off by default and use the existing named node
configuration, reporting and exact journal mask. They do not change header
validity, chain configuration or message encodings.
cancel_stale_retries independently ignores retry timer expiries after
cancellation, replacement or a change of the full height/round view. A queued
expiry can therefore remain in the runner's recorded timer queue without
causing a stale round-change broadcast or rearming a cancelled retry. Current
active retries still send and rearm normally. This option is off by default;
it uses the existing named settings and exact journal mask. Timer input
encodings, durable WAL and header validity remain unchanged.
follow_higher_rounds applies the existing F+1 round-following rule whenever
at least its threshold of distinct higher-round senders is available. This
allows a later valid round-change message to advance a node when previously
retained messages have already moved the count above that threshold. It keeps
the existing F+1-before-proposal evaluation order and lowest-higher-round
selection. The option is off by default; it can skip rounds sooner than the
default behavior and does not change header validity or wire encodings.
bind_prepared_certificate independently keeps the prepared certificate with
its locked block and round. It carries that certificate across same-height
rounds and clears it with the pair on release or a new height. The original
certificate lifetime remains available when the option is disabled.
drop_unbound_extra_seals suppresses relay of late votes when there is no
current or prior proposal against which to check their seals. Late votes with
a matching target retain their existing storage and relay behavior.
resume_round_proposals relays a round-change quorum even when the proposer
is still waiting for its builder. A later proposal request repeats the same
quorum and justification checks for that round, with a prepared block taking
priority. It does not send twice after a successful proposal broadcast.
These three options are independent, disabled by default, and preserve the
existing header, message and WAL formats and mandatory restart safeguards.
refresh_round_certificates selects the highest prepared certificate from
the currently stored round-change messages whenever a sender replaces its
entry. It retains the existing certificate eligibility checks and uses sender
address order to resolve equal prepared rounds. The default retains the
original increasing-only cache and first-arrival selection for ties.
clear_current_extra_seals clears only the current height's extra seals when
the application releases a bad prepared block. Surviving prior-height entries
remain available; a prior entry already overwritten by a newer vote cannot
be recovered. Ordinary height-transition cleanup keeps its existing behavior.
Both options are independent and off by default, with the existing named
configuration and journal mask and no header, message or WAL format change.
validate_round_proposals is an independent, default-off local option.
Before reproposing a prepared block, it calls proposal validation after the
existing quorum and justification checks. A normal future timestamp waits
on a distinct local round_proposal timer; failure does not select a builder
fallback. Local rejection records use local_reproposal, without claiming
an inbound PRE-PREPARE or physical frame.
The option extends local timer/checkpoint records to a fourth generation.
Default configurations retain the three reference timer generations and
cancellation outputs. New readers can restore three-generation checkpoints;
older readers do not support the additional local timer. Header and network
message encodings are unchanged. These checks do not establish execution
validity, physical provenance or a complete improved profile.
cap_initial_round_timeout is a default-off local option. When the base
request timeout exceeds a positive cap and both duration products fit normally,
the first round also uses the cap. The public reference timeout function remains
unchanged; overflow in the millisecond-base and second-cap duration products
keeps its reference path. Earlier configuration-unit conversion is outside this
local policy. This changes local timing,
not header or network message encoding, and does not establish every schedule
or complete-profile acceptance. Existing optional-behavior observations retain
their scope rather than becoming reference conformance passes.
Local future proposal timing
use_future_time_allowance and limit_future_proposal_wait are independent,
default-off local settings. The first applies inherited nonzero transition
allowances at the full height and retries a future proposal at its timestamp
minus that allowance. Full and light header checks select this policy
explicitly; a missing light-verifier clock remains missing. Ordinary
seconds-to-nanoseconds timestamp and allowance products use the selected
calculation. Other values retain the reference path for both the header check
and retry target.
The second rejects an ordinary positive future wait longer than the current
view's positive round timeout. It uses the same initial-cap selection as the
round timer and compares timeout lengths, without estimating elapsed time.
Equal or shorter waits retain their received or local candidate provenance.
Local candidate checks run only when validate_round_proposals is enabled.
An excessive wait records future_proposal_wait_exceeded with its closed
origin and exact decimal duration metadata; it does not arm a shortened wait.
Disabled reference queries and arithmetic remain available. These settings
do not change the header or network message encoding, reconstruct earlier
configuration conversion, or establish every clock schedule or complete
profile. The event schema adds the explicit wait-limit observation. Existing
protected analysis servers are not upgraded by selecting a node setting.
Local round budgets and deadlines
require_initial_round_budget checks each effective configuration at height
zero and every distinct transition height before node or simulation startup.
For ordinary positive duration products, the selected round-zero timeout must
exceed the block period. An enabled initial-round cap participates in this
comparison. Equal-height transitions are checked after their final application,
with full heights and existing inheritance rules. Other arithmetic retains its
prior path. This necessary condition does not reserve application or network
processing time. Direct core callers invoke CheckInitialRoundBudget at their
configuration boundary.
preserve_round_deadline keeps an already-live round timer when accepting a
proposal in that round. It still cancels retry and future-proposal timers; an
absent round timer uses the existing reservation path. Entering another round
continues to reserve a new timer. A proposal arriving near the original deadline
therefore has less time to complete than under the reference reset behavior.
Both options are independent and default off. They change local startup or
timing behavior without changing header, message or WAL encodings. Existing
reference functions and disabled configurations retain their behavior. These
checks do not establish every schedule or a complete improved profile.
Keeping peers during a local engine pause
keep_stopped_peers is a default-off local setting. Consensus frames that
arrive while the engine is paused receive DROP_SILENT without a transport
disconnect request. They enter neither the receive queue nor the known cache.
The existing synchronising drop path is retained. Normal engine restart
restores the usual receive checks; dropped frames are not replayed on restart.
The setting is reported through the existing improvement selection and recorded
core option mask. It changes no header, message, WAL or journal encoding.
Reference stopped-engine observations keep their existing conformance meaning.
Normal signature and Stop/Start controls do not establish every remote lifecycle
or complete-profile acceptance.
Versioned BLS reveals
wbftRules.consensusRules.blsRandao=A selects BLS reveals starting at the
positive activation height A. It requires rejectNumberJump at or before A.
The checked parent's full height plus one selects the format in proposal,
import and light verification. Missing or null activation keeps ECDSA reveals;
there is no signature-length fallback after activation. Genesis is unchanged.
The message is keccak256(RLP(["WBFT_RANDAO_BLS_V1", be_min(chainID), be_min(number)])), signed with the proposer's existing BLS key and ciphersuite.
The verifier uses that height's registered validator key, including at epoch
boundaries. A builder needs the optional header.BLSRandaoSigner extension;
the built-in file signer implements the corresponding typed extension without
changing vote-signing state or the legacy signer interface. The builder checks
the produced signature against the registered key before returning a proposal.
Reveals are compressed G2 signatures (96 bytes). A verifier needs the same
activation configuration and validator history as the chain.
The parent-mix XOR hash-of-reveal calculation is unchanged. A unique reveal for
the registered key and message does not eliminate block withholding or provide
a general unbiased randomness beacon. This is a consensus format change with
an additional BLS verification per checked header; it supplies no production
activation default and never rewrites stored blocks.
bind_fresh_proposal_author is an independent, default-off local option. After
normal proposer and round-change justification checks, a fresh proposal must
have the authenticated PRE-PREPARE sender as its Coinbase. A prepared block may
retain its original author across later rounds only with a distinct-member
PREPARE quorum for the same full sequence, earlier prepared round and block
hash. The local send and receive paths apply the same check. The block and its
Coinbase are never rewritten.
Select the same option on validators that should enforce this acceptance
policy. Existing header imports, standalone header checks and fee distribution
remain unchanged; this option does not reconstruct historical proposal authors
from headers. The node reports the selected option in its normal runtime and
journal metadata, allowing replay to preserve the selected behavior.
Prior-head seal provenance
The optional bind_prior_head local improvement retains the adopted header's
historical sealing authority separately from the successor validator set. It
requires the matching WAL replay profile and is disabled by default. See
prior-head seal provenance for recovery and reader scope.
Validator startup availability
The experimental, default-off defer_validator_start option waits for an
available successor validator set before adopting a new height. Its optional
durable profile has finite history limits and strict replay requirements. See
validator startup availability for configuration,
normal lifecycle behavior, budget exhaustion and reader limitations.
Verified previous seals
The default-off check_epoch_info_presence option checks whether epoch information
is present exactly at the configured full-height epoch boundaries. It runs after
header decoding and before seals, independently of the stronger parent-candidate
chain rule. Header-only verification does not require a parent state snapshot for
this presence check. Direct light callers select LightInputs.CheckEpochInfoPresence
for the same early check; the default light procedure retains its existing order.
This local selection changes no block or signature format.
The default-off verify_merged_prev_seals option checks both previous seal
aggregates during proposal assembly, falling back only to a verified original
parent aggregate. It can be selected with or without bind_prior_head. See
verified previous seals for historical
validator binding, failure behavior and recorded replay scope.
Runner terminal queue closure
Runner.Close stops admission through Submit, NewHead and PeerConnected,
waits for its existing workers, and disposes of remaining application inputs.
An application finalization already in progress keeps its ordinary return and
notification path. A later Start is refused. A rejected manual Close with
pending work leaves admission unchanged; ordinary Stop remains restartable.
Optional application queue sources retain version 1 for append, pop, coalesce
and Stop clear. The terminal close operation alone uses version 2. Its
independent check binds the last original core Stop and clear, refuses later
queue mutation, and does not claim physical or complete schedule termination.
Older readers refuse the new operation; absent Stop evidence stays unavailable.
Chain-bound ROUND-CHANGE signing
The common chain rule wbftRules.consensusRules.roundChangeDomain selects a
chain-bound signing profile at a positive full activation height. Omission
or null keeps the existing behavior. For example:
{"wbftRules":{"consensusRules":{"roundChangeDomain":3}}}
An activated ROUND-CHANGE signs the canonical RLP list
["wbft/round-change-domain", 1, chainID, genesisHash, legacySigningPayload].
The final element is a byte string containing the legacy signing payload.
Each ROUND-CHANGE uses its own full sequence, including those nested inside
a PRE-PREPARE. Other outer message signatures, BLS seals, RANDAO and wire
layouts retain their existing interpretation. The domain does not distinguish
forks sharing the same chain ID and genesis.
A Node derives this immutable profile only after checking its actual genesis
and chain configuration. Low-level callers must supply an explicitly trusted
genesis through types.DomainForConfig, then use
codec.SigningPayloadWithDomain and consensus.RecoverWithDomain as
appropriate. Recovery alone is not validator membership or independent chain
authentication. Existing naked signing/recovery APIs remain legacy-only.
A configured domain requires its profile even before the activation height.
It does not enable a local improvement flag or validator-start deferral.
Selected live runners require strict functional WAL replay and matching core,
signer and transport profiles. Domain-only history has finite default bounds
of 64 MiB and 65,536 records; existing explicit validator-history overrides
still require validator-start deferral. Budget exhaustion is not an unbounded
persistence guarantee.
This profile requires absent or matching domain-bound signing state. A fresh
signer persists format 2 before returning its first signature, including before
activation. Existing format 1 state is refused even when logically empty;
other chain/genesis/activation profiles are also refused. There is no implicit
migration, reset or deletion. Use a fresh isolated data directory for an
initial deployment; this change supplies no operating-network state migration
procedure. Matching profiles support ordinary stop/reopen. Domain WAL history
uses a separate format 6; existing format 5 and legacy history are not silently
converted. Read-only profile checks precede selected startup writes.
Journal segments and core-step/signer sources retain explicit versioned domain
declarations. Readers distinguish unsupported or missing context from malformed
supported evidence. R01 cannot retain the domain and refuses selected input
before legacy own-message indexing or output, including when height filters
exclude it. Keep original journals or supported original-segment bundles;
verify and stat check their supported recorded metadata, not external chain
authority. Unknown future profiles produce structured unavailable results in
the journal CLI. Independent physical/full-runtime source restoration remains
unavailable for selected profiles; ordinary same-profile runtime reopen tests
do not establish crash or power-loss recovery.