SP-WRB Block Node Runbook
Overview
A special-purpose Block Node (the "Tier-0" or "SP-BN") is a Block Node co-located with the WRB CLI whose sole job is to ingest Wrapped Record Blocks (WRBs) produced by the CLI and re-serve them, along with the address-book history they were signed under and the TSS data, to Council-operated Tier-1 Block Nodes.
The CLI feeds the SP-BN through two ingest paths that cover different block ranges:
Historical backfill — the bulk of the archive is loaded directly onto the SP-BN's
archivePV without going through the gRPC Publish API. The CLI'sblocks bulk-loadcommand (or atarstream overkubectl exec) copies wrapped-block zips into the on-pod historic directory, andBlockFileHistoricPluginpicks them up on the next pod start. This is how the majority of blocks arrive on a fresh SP-BN.Live push — once the archive is caught up, the CLI's
days live-sequential --push-enabledstreams newly-wrapped blocks over the normal Publish API. This carries the tail-of-chain going forward.
Tier-1 nodes pull blocks, address books, and TSS data through the Block Node's standard backfill and status paths.
The address-book history is a first-class serving responsibility, not just a local verification concern. Every Tier-1 that reads WRBs from an SP-BN must verify each historical block against the address book that was in effect for that block, so it must first fetch the same range-keyed history from the SP-BN before it can start reading blocks. The SP-BN both loads its own range-keyed history at startup and serves that history to downstream Tier-1s — a standard Tier-1 needs only a single current bootstrap roster.
This runbook covers the SP-BN-specific deployment and operational steps. For the CLI-side work that feeds the SP-BN, see the WRB CLI runbook; for the generic pre-cutover BN checks (health, hardware, storage sizing) see Preparing Your Block Node for WRB Cutover. The end-to-end design lives at sp-wrb-bn-design.md.
Audience: operators standing up or maintaining an SP-BN (Solo/GKE, self-hosted Kubernetes, or local kind).
Table of Contents
Prerequisites
A running Kubernetes cluster reachable via
kubectl(Solo/GKE, kind, on-prem, etc.).kubectlandhelmon your PATH, current context pointed at the target cluster.The CLI shaded jar for a release at or after
v0.40.1— earlier releases pre-date the ranged address-book history loader described below. Downloadblock-stream-tools-<tag>.jarfrom the Block Node release page; no repo clone or Gradle build is required. Pick a tag that matches (or trails) the chart / image tag you plan to deploy — the CLI's wrap output format must be understood by the BN.Address-book history JSON produced by the CLI's
mirror generateAddressBook*command (see wrb-cli-runbook.md § Generate Address Book History).A directory of wrapped-block zips produced by the CLI's
blocks wrap/days live-sequential(see wrb-cli-runbook.md § Wrap Record Files).The CLI-produced
tss-bootstrap-roster.jsonon the operator host.
Version alignment matters. The BN main container image, the chart's init-container images, and the chart-templated startup command all reference the same version. Bumping one without the others (e.g.
kubectl set imagewhile leaving the chart on the old version) leaves the pod looking for/opt/hiero/block-node/app-<oldver>/bin/appinside an image that shipsapp-<newver>/bin/app, and the container exits immediately. Always upgrade viahelm upgrade, never viakubectl set imageon a Helm-managed StatefulSet.
Deployment
The recommended path is Solo Provisioner (solo-provisioner), which bootstraps the on-box Kubernetes cluster, provisions the PVs, installs the chart, and reconciles configuration changes against a persisted state file. Both the previewnet and testnet SP-BNs are deployed and maintained this way. Raw Helm is documented in § Deploying without Solo Provisioner as a fallback for local kind and clusters where Solo Provisioner is not available.
Prerequisites for the Solo Provisioner path
solo-provisionerinstalled on the target host (see the Solo Weaver docs).A provisioner config YAML (profile, chart reference, storage paths, sizes) — one per network. Example:
~/testnet-lfh-provisioner-config.yaml.A chart values overlay YAML (LFH preset, LoadBalancer, plugin ports). Start from
charts/block-node-server/values-overrides/lfh-values.yamland adapt for your network.
The LoadBalancer block in the values overlay must set includePluginPorts: true so the publish (40984), subscribe (40980), block-access (40981), status (40982), and health (40983) plugin ports are all exposed alongside the aggregate port (40840). Without it the external LB only publishes 40840 and clients pinning individual ports (e.g. LiveSequential's --push-bn-port) will be unable to reach the BN.
Install
The provisioner bootstraps the single-node cluster (cri-o, kubelet, kubeadm, cilium, MetalLB, helm) on first invocation, then deploys the chart. Subsequent changes to either YAML are applied via reconfigure rather than reinstall:
reconfigure re-runs the helm upgrade with the new values and rolls the pod. It is the correct tool for tasks like adding includePluginPorts: true after an initial install, or bumping the chart version pin.
Confirm the pod reaches 1/1 Ready before continuing:
Persistent volume layout
A default install provisions five PVCs (per-pod, via volumeClaimTemplates):
application-state-storage-*
RSA/roster bootstrap, TSS parameters, verification state.
archive-storage-*
Historical archives — the target of the bulk-load path (§5).
live-storage-*
Live-received blocks pending archival.
plugins-storage-*
Version-specific plugin JARs resolved by the resolve-plugins init.
logging-storage-*
Per-pod log storage.
Do not delete PVCs to reset state on static-PV setups. Deleting the PVC releases the PV; on Retain reclaim (the default for hand-provisioned PVs) the PV stays with the old data attached and refuses to rebind to a freshly-recreated PVC. Wipe file contents with a short-lived pod that mounts the existing PVC instead — see the Wipe recipe in Troubleshooting.
Deploying without Solo Provisioner
For local kind clusters or environments where Solo Provisioner is not available, the chart can be installed directly with Helm. The operator is then responsible for the cluster, PVs, and LoadBalancer that Solo Provisioner would otherwise manage.
Confirm the pod reaches 1/1 Ready:
The same includePluginPorts: true requirement applies to the values overlay when a LoadBalancer is used. All later sections of this runbook (address-book bootstrap, TSS file move, backfill, live push) apply unchanged regardless of which deployment path was used.
Address-book history bootstrap
The BN verifies WRBs against the address book that was current at the block being verified — not just today's roster. Operators supply that history by converting the CLI's per-era address-book JSON into the BN's block-range-keyed format and placing the result at the RSA bootstrap path.
Convert with the CLI
Key options (--help for the full list):
-i / --input
The CLI-produced addressBookHistory.json.
-o / --output
Roster history JSON output. Default: /opt/hiero/block-node/application-state/rsa-bootstrap-roster.json.
--block-times-file
block_times.bin used to translate consensus times to block numbers. Default: metadata/block_times.bin.
The output is a RangedAddressBookHistory — an ordered list of [startBlock, endBlock] eras, each carrying a NodeAddressBook. It replaces the single-book NodeAddressBook JSON that a Tier-1 BN would normally load.
Invariants the output must satisfy
The BN's roster loader (AddressBookHistoryLookup) enforces:
Ordered ascending by
startBlock.Non-overlapping — no two eras cover the same block.
Open-ended last entry — the final era uses
endBlock = 0to cover all future blocks ≥startBlock.Non-empty — an empty list causes the BN to fail at startup.
The convert tool produces a compliant file. If you hand-edit, re-run convert-address-book-history or verify by loading the file into a temporary BN and watching startup logs.
Place the file on the BN
Config key
The BN reads the roster from app.state.rsaBootstrapFilePath (default /opt/hiero/block-node/application-state/rsa-bootstrap-roster.json). The same key and same file path accept either a single-book NodeAddressBook JSON or a ranged RangedAddressBookHistory JSON — the loader auto-detects. This means bootstrapping a fresh SP-BN and upgrading an existing Tier-1 to historical-verification behavior use the same file swap; no config change is required.
Mirror-Node-driven maintenance (T6)
Once bootstrapped, the BN keeps its address-book history current by polling Mirror Node for new address-book changes and appending eras as they land. Enable it via the RSA-roster plugin config (all keys are roster.bootstrap.rsa.*):
roster.bootstrap.rsa.mirrorNodeBaseUrl
Mirror Node REST endpoint. Empty disables the maintenance loop.
""
roster.bootstrap.rsa.mnInitialQueryIntervalMillis
First-poll interval.
5_000
roster.bootstrap.rsa.mnSubsequentQueryIntervalMillis
Steady-state poll interval.
60_000
roster.bootstrap.rsa.mirrorNodePageSize
Page size for the changes query.
100
For the SP-BN role, point mirrorNodeBaseUrl at the Mirror Node covering the same network as your CLI (https://mainnet.mirrornode.hedera.com, https://testnet.mirrornode.hedera.com, etc.).
TSS bootstrap file move
The BN's TSS peer retrieval (used by Tier-1 nodes via RosterBootstrapTssPlugin.queryPeerTssData()) requires a single operator step: place the CLI-produced tss-bootstrap-roster.json at the BN's application-state path.
Critical: stop the BN before the copy, restart after. The BN can and does rewrite this file during normal operation; a concurrent write from an operator races the BN and can leave a corrupt file.
Do not write directly to /mnt/wrb-operations/wrappedBlocks/tss-bootstrap-roster.json if that path is bind-mounted into the BN — the BN treats the destination as owned state, and a shared write path races.
For long-lived SP-BN deployments, consider folding the copy into the deploy script that installs or upgrades the Helm chart so the file is placed atomically alongside the pod bring-up.
Historical backfill
Historical WRBs are bulk-copied into the BN's on-disk historic archive while the BN is stopped (or, in practice, while it is running — the BN re-scans on its next restart). A single script handles staging the wrapped-block zips and streaming them into the pod:
The script (backfill-wrb-to-bn.sh) is env-var configurable for non-default deployments:
BN_KUBE_CONTEXT
kubectl context to use.
current context
BN_NAMESPACE
Namespace of the BN release.
block-node
BN_STATEFULSET
StatefulSet name.
block-node-block-node-server
BN_POD
Pod name.
${BN_STATEFULSET}-0
BN_CONTAINER
Container name in the pod.
block-node-server
HISTORIC_MOUNT_PATH
On-pod historic dir.
/opt/hiero/block-node/data/historic
STAGING_DIR
Local staging dir.
/tmp/bn-backfill-<pid>
CLI_JAR
Path to the CLI shadow jar.
hunts for tools-*-all.jar next to script
READY_TIMEOUT
Seconds to wait for pod-ready after restart.
300
SKIP_ROLLOUT
true to skip the auto-rollout after staging.
false
What it does, in order:
Waits for the BN pod to be
Ready.Stages the wrapped-block source into a local temp dir via
blocks bulk-load.Streams the staged files into
HISTORIC_MOUNT_PATHinside the target pod viatar | kubectl exec.Triggers a StatefulSet rollout so
BlockFileHistoricPluginre-scans and picks up the copied files.Waits for the pod to come back
Ready, then cleans up the staging dir.
Verify
Once the pod is back Ready, confirm the BN sees the new blocks:
Expected: firstAvailableBlock and lastAvailableBlock show real block numbers, not the UINT64_MAX sentinel 18446744073709551615.
Resume
blocks bulk-load skips files already present in the destination. Re-running the script after a partial copy (or after adding new wrapped-block zips to the source dir) copies only the new files.
Corrupt-zip handling
BlockFileHistoricPlugin in 0.40.1+ auto-quarantines corrupt zips it finds during startup scan, moving them to <historic>/corrupted/…. A single quarantine event can still trip the plugin's first-vs-latest invariant check during that same startup (IllegalStateException: First zipped block number [0] cannot be greater than the latest zipped block number [-1]); the second startup usually succeeds because the corrupt file is out of the scan path. If it persists past two restarts, see Troubleshooting → invariant violation on start.
Live push
After backfill, keep the SP-BN current by pushing each freshly-produced WRB from the CLI's live wrap pipeline. This runs indefinitely alongside the CLI's normal days live-sequential loop.
Push flags
--push-enabled
Enable push. Without this flag the wrap loop runs as-normal, no push traffic.
--push-bn-host
SP-BN host (cluster-internal DNS, LoadBalancer external IP, or bind-mount address).
--push-bn-port
SP-BN's BlockStreamPublishService port (publish stream). Standard: 40984.
--push-bn-status-port
SP-BN's BlockNodeService port (status queries). Required in split-port deployments. Standard: 40982.
--push-queue-capacity
Backpressure queue depth between the CLI wrap thread and the push worker. Default: 32.
In single-port deployments where all API services share one port, omit --push-bn-status-port — it defaults to --push-bn-port.
Startup behaviour
At startup, the tool queries the SP-BN's serverStatus on --push-bn-status-port to establish a watermark. Startup log will show one of:
or
An empty BN advertises its watermark as UINT64_MAX (18446744073709551615) which the tool recognises and pushes from block 0. This is the expected first-run behaviour against a fresh SP-BN.
If the tool fails at startup with QueryFailedException: queryLastAvailableBlock failed against <host>:<port>, the message names the exact port that failed. Common causes:
The BN pod isn't
Readyyet — checkkubectl get pods -n block-node.The status port is different from the publish port (split-port deployment) and
--push-bn-status-portwas not supplied.A network policy or firewall blocks the operator host from the status port.
Steady-state signals
While pushing, the CLI logs a compact per-100-block summary line:
On the SP-BN side, publisher-sourced blocks appear as:
with matching closeBlock Completed blocks <N> entries. If you see Handler <id> is ending mid-block <N> bursts, the CLI is not emitting the mandatory end_of_block marker after each block. Upgrade the CLI shadow jar — this was fixed in the same release that added --push-bn-status-port.
Serving Tier-1 nodes
Tier-1 BNs consume from the SP-BN through two existing mechanisms — no SP-BN-side configuration change is required for either:
Block backfill — Tier-1 BNs' existing backfill plugin pulls blocks over the standard subscriber stream. Point their
blockNodeSourcesPathconfig at the SP-BN endpoint.TSS retrieval — Tier-1 BNs'
RosterBootstrapTssPlugin.queryPeerTssData()calls the SP-BN'sserverStatusAPI to pull TSS data from the roster the operator placed above.
Publish the SP-BN's reachable endpoint to Council Tier-1 operators. For a standard split-port deployment publish:
Host: cluster LoadBalancer external IP (or an operator-controlled DNS record fronting it).
Publish port:
40984(BlockStreamPublishService).Status port:
40982(BlockNodeService).Subscriber port:
40980(BlockStreamSubscribeService).Block-access port:
40981(BlockAccessService).Health port:
40983(/healthz/readyz,/healthz/livez).
See network-ports-and-protocols.md for the authoritative port table and firewall guidance.
Monitoring
Roster-history-specific metrics (in addition to the standard BN publisher / verification metrics):
blocknode:roster_eras_loaded
Number of [startBlock, endBlock] eras loaded at startup.
blocknode:roster_entries_loaded
Total NodeAddress entries summed across all eras.
blocknode:roster_load_duration_ms
Startup load time in ms.
Sanity checks:
roster_eras_loadedshould equal the number of eras in yourrsa-bootstrap-roster.json. A1after a converted history load usually means the loader fell back to single-book mode — check the file's actual contents.roster_load_duration_msshould be small (single-digit to low-double-digit ms) even for histories with tens of eras.
Recommended alerts (implementer choice on threshold):
BN tip stall —
lastAvailableBlockdoesn't advance forTseconds whiledays live-sequential --push-enabledis running. Points at either a stuck publisher on the CLI side or a rejected stream on the BN side.Verification failure "no address book" — a WRB fell outside every era in the loaded history. Points at a history gap; extend or convert-and-replace the roster file.
Troubleshooting
Invariant violation on start
Cause: BlockFileHistoricPlugin computed a "first block" (found at least one zip on disk) but came back with latest = -1 (no zip parseable as the highest). Common triggers:
Mixed on-disk layout after a backfill that wrote both a
historic-plugin-bulk-load-state.jsonand shard-nestedNNN/NNN/NNN/NNN/*.blk.zipfiles. Different enumerators saw different views; the invariant check fails.Corrupt zip that got quarantined mid-scan on the first startup, leaving no readable zip in the shard that produced the "first" reading.
Ownership mismatch — files copied as
root:rootwhile the container runs as UID2000; one enumerator canlist()names, another can'topen()the zips.
Recovery:
Restart the pod once. In 0.40.1+ the auto-quarantine typically self-heals on the second start.
If it doesn't, wipe the archive PVC (see Wipe recipe) and re-run backfill from a fresh wrap output. Do not re-run backfill on top of a partial layout.
Wipe recipe
Static-PV deployments must wipe file contents rather than deleting PVCs. Pattern using a short-lived pod:
An example wipe-pod.yaml mounting application-state, archive, live at /app-state, /archive, /live and running find /app-state /archive /live -mindepth 1 -delete is the minimum shape — adjust PVC names to your release.
No BlockMessagingFacility provided at startup
Cause: the plugins-storage PVC has a JAR set left over from a previous image version, and the resolve-plugins init container doesn't overwrite it. The main container's SPI scan then finds mis-matched (or missing) plugin classes for the current image version.
Fix: wipe plugins-storage using the recipe above and restart. On version bumps, always include plugins-storage in the wipe.
PVs stuck Released after PVC delete
If you delete PVCs on a static-PV setup (Retain reclaim policy is default), the PVs keep the stale claimRef and refuse to rebind to newly-recreated PVCs. Recovery:
Repeat for each stranded PV, then the pending PVCs will bind and the pod can start. Note that the on-disk data is preserved. If the goal was to wipe state, follow up with the wipe recipe after the PVs re-bind.
Wrap tool: Block counter out of sync with day block number
The CLI's days wrap / days live-sequential persists its counter to wrappedBlocks/streamingMerkleTree.bin. If the counter falls behind the day-metadata alignment (missing days in compressedDays/, corrupt day archive, day file re-downloaded with different content), the tool aborts before wrapping.
Fix: delete the stale state files and re-run:
If instead the message is Day archives found without matching entries in day_blocks.json, regenerate metadata via mirror extractDayBlock before re-running wrap.
Image / chart mismatch
Symptoms:
Container exits with
/bin/bash: line 1: /opt/hiero/block-node/app-<X>/bin/app: No such file or directory.kubectl describe pod …shows init containers on image tagXwhile main container is on tagY.
Cause: kubectl set image bumped only the main container; the chart's init containers and startup command still template version X. Reconciler on Helm-managed StatefulSets reverts the image change on the next sync.
Fix: bump the whole chart, not just the image. On the operator host:
Reference
File paths
/opt/hiero/block-node/application-state/rsa-bootstrap-roster.json
BN (app.state.*)
RSA roster — single-book or RangedAddressBookHistory JSON; loader auto-detects.
/opt/hiero/block-node/application-state/tss-parameters.bin
BN
TSS parameters; managed by the BN, do not hand-edit.
/opt/hiero/block-node/data/historic/
BN
On-disk historic archive; target of the bulk-load path.
/mnt/wrb-operations/wrappedBlocks/tss-bootstrap-roster.json
CLI
CLI-produced TSS bootstrap; copy into BN application-state.
wrappedBlocks/addressBookHistory.json
CLI
CLI-produced address-book history; input to convert-address-book-history.
Ports
Standard split-port deployment (Solo-provisioner defaults):
40980
BlockStreamSubscribeService
40981
BlockAccessService
40982
BlockNodeService (status)
40983
Health (/healthz/readyz, /healthz/livez)
40984
BlockStreamPublishService
Config keys
app.state.rsaBootstrapFilePath
/opt/hiero/block-node/application-state/rsa-bootstrap-roster.json
RSA / roster-history file path.
roster.bootstrap.rsa.mirrorNodeBaseUrl
""
Mirror Node base URL; empty disables T6 maintenance.
roster.bootstrap.rsa.mnInitialQueryIntervalMillis
5000
First-poll interval for MN maintenance.
roster.bootstrap.rsa.mnSubsequentQueryIntervalMillis
60000
Steady-state poll interval for MN maintenance.
Related docs
Design: sp-wrb-bn-design.md
CLI: wrb-cli-runbook.md
Reset / upgrade: resetting-and-upgrading-the-block-node.md
Glossary: glossary.md — WRB, TSS, roster terminology.
Last updated