docs(operators): automated state-sync trust_hash extraction + block_id.hash warning - #59
Conversation
…ck_id.hash warning
The State Sync section instructs operators to copy a recent block height and
hash from an explorer by hand and edit config.toml manually. That manual step
is error-prone, and operators who script it on a minimal image (no jq) commonly
extract the wrong hash: a greedy regex over the single-line /block JSON grabs
the last "hash" (an app/header hash) instead of block_id.hash, and the node
crash-loops at startup with "expected header's hash X, but got Y".
Add a copy-paste automated snippet (jq, explicit .result.block_id.hash), an
optional in-place sed to apply the values, and a warning admonition explaining
that trust_hash must be block_id.hash — with a jq-less fallback that selects
the first hash explicitly (grep -oE '"hash":"[A-F0-9]{64}"' | head -1).
The existing manual explorer method is retained as Option B.
vriveraPeersyst
left a comment
There was a problem hiding this comment.
Took a pass at this at @oeggert's request. I ran every command in the PR against the live XRPL EVM Mainnet and Testnet Tendermint RPCs, and checked the config handling against CometBFT v0.38.21 — the version exrpd v10.1.0 actually pins.
The premise is right and this is worth having. Confirmed against fresh blocks on both networks: the greedy match really does return the wrong hash, and .result.block_id.hash is the correct path.
mainnet height=7398639 trust_height=7397639
block_id.hash = 3B1BAA36288E537DDF0FD31EC9B88240BDBBD43CD0681B30226FE6E45BE9BDBD
greedy sed returns = 1DA655E2EB16D78338D8A9F14742511CDCAF206DFAA53614E90F4283E7C3D89E
testnet height=8334540 trust_height=8333540
block_id.hash = 84612CCF82A69BF5F1566784C6ECE642A335088697A56A7B49A02D4A5CA10EE0
greedy sed returns = CB22FCCC92B9D63B7F349B55B27654A575DAF9976E24F7C1FA0588CAAD92E826
A couple of other things I checked that hold up:
head -1is structurally guaranteed, not luck.coretypes.ResultBlockdeclaresBlockIDbeforeBlock, and Go serialises struct fields in declaration order, soblock_id.hashis always the first"hash":in the document. Worth stating in the docs — it turns a "seems to work" into a "cannot break".- The
sedrange is correctly scoped./^\[statesync\]/,/^\[/does not self-terminate on its own start line, and it stops at[blocksync], which is what follows[statesync]in the default template. I ran it against a realisticconfig.tomland nothing outside the section was touched. current height - 1000is a sane default. Mainnet block time is ~5.7s, so that is ~95 minutes back — comfortably inside the 168htrust_period.
Three things I would fix before merging, detailed inline. The third one is the one I would not ship without: as written, following Option A plus the in-place sed produces a node that refuses to start.
| **Option B — manual.** Visit a blockchain explorer and read a recent block height and its corresponding block hash directly. | ||
|
|
||
| {% admonition type="warning" name="Use block_id.hash — the FIRST hash, not any hash" %} | ||
| The trust hash must be the block's **`block_id.hash`** (the `jq` path above), not the header hash or app hash. The `/block` response contains several `"hash"` fields. If you extract it without `jq` on a minimal image, a naive greedy match such as `sed 's/.*"hash":"\([A-F0-9]*\)".*/\1/'` grabs the **last** `"hash"` on the (single-line) JSON — an app/header hash — and the node crash-loops at startup with `expected header's hash X, but got Y`. Without `jq`, extract the first hash explicitly: `grep -oE '"hash":"[A-F0-9]{64}"' | head -1`. |
There was a problem hiding this comment.
Two problems in this warning, and the first one bites exactly the reader it is written for.
1. The jq-less fallback returns a string that is not a hash. grep -o prints the whole match, and the pattern includes the key and both quotes:
$ grep -oE '"hash":"[A-F0-9]{64}"' block.json | head -1
"hash":"3B1BAA36288E537DDF0FD31EC9B88240BDBBD43CD0681B30226FE6E45BE9BDBD"
An operator on a minimal image without jq — precisely who this line is for — pastes that into config.toml and gets an invalid trust_hash. It needs one more step:
grep -oE '"hash":"[A-F0-9]{64}"' | head -1 | grep -oE '[A-F0-9]{64}'2. "an app/header hash" misidentifies what you actually get. The greedy match returns result.block.last_commit.block_id.parts.hash — a PartSetHeader hash of the previous block. I confirmed this on both Mainnet and Testnet, and it is structurally fixed: Block ends in LastCommit, Commit is {Height, Round, BlockID, Signatures}, BlockID is {Hash, PartSetHeader}, PartSetHeader is {Total, Hash}, and CommitSig has no hash field — so the last "hash": in the response is always that one.
It also cannot be an app hash or a header hash: those keys serialise as "app_hash":, "last_commit_hash":, "validators_hash": and so on, and the pattern "hash":" requires the key to be exactly hash. Suggest rewording to name last_commit.block_id.parts.hash.
The consequence you describe — the crash-loop — is correct, so this is about the diagnosis, not the symptom.
| ```bash | ||
| CONFIG="$HOME/.exrpd/config/config.toml" | ||
| sed -i.bak \ | ||
| -e "/^\[statesync\]/,/^\[/ s|^enable = .*|enable = true|" \ |
There was a problem hiding this comment.
This sets enable = true but leaves rpc_servers at its shipped default of "", and CometBFT refuses to start in that state. From StateSyncConfig.ValidateBasic() in v0.38.21, the version exrpd v10.1.0 pins:
if cfg.Enable {
if len(cfg.RPCServers) == 0 {
return errors.New("rpc_servers is required")
}
if len(cfg.RPCServers) < 2 {
return errors.New("at least two rpc_servers entries is required")
}So an operator who follows Option A and then this block ends up with a node that will not boot. The prose above does tell them to set rpc_servers, but flipping enable = true inside the automated block makes it read as self-contained — and enable is not one of the "computed values" this block is introduced as applying.
Either add a fourth expression, e.g.
SNAP_RPC_SERVERS="$SNAP_RPC,$SNAP_RPC"
...
-e "/^\[statesync\]/,/^\[/ s|^rpc_servers = .*|rpc_servers = \"$SNAP_RPC_SERVERS\"|" \(duplicates are allowed, as the line above already notes), or drop enable = true from the sed so the block only does what it claims and the operator still has to open the file.
| SNAP_RPC="https://<trusted-rpc-endpoint>" | ||
|
|
||
| LATEST_HEIGHT=$(curl -s "$SNAP_RPC/block" | jq -r .result.block.header.height) | ||
| TRUST_HEIGHT=$((LATEST_HEIGHT - 1000)) |
There was a problem hiding this comment.
Minor, but it fails silently rather than loudly. If the RPC is unreachable or returns an error object, jq -r yields empty or null, and bash arithmetic treats both as 0:
LATEST_HEIGHT = ''
TRUST_HEIGHT = -1000
No error, exit status 0. The sed further down then happily writes trust_height = -1000 into config.toml, and the operator debugs a state-sync failure instead of a network failure. I hit this by accident while testing.
A one-line guard would cover it:
[ -n "$LATEST_HEIGHT" ] && [ "$LATEST_HEIGHT" != "null" ] || { echo "could not reach $SNAP_RPC"; exit 1; }
Summary
The State Sync section of
pages/operators/advanced/sync-options.mdcurrently tells operators to copy a recent block height and hash from a block explorer by hand, then editconfig.tomlmanually. This PR adds a correct, copy-paste automated path alongside the manual one, plus a warning about the single most common way state-sync bootstrapping fails.Motivation
The
/blockRPC response contains several"hash"fields.trust_hashmust beblock_id.hash. Operators who script the manual step on a minimal image withoutjqoften use a greedy regex over the single-line JSON, which grabs the last"hash"(an app/header hash) instead ofblock_id.hash. The light client then rejects the anchor and the node crash-loops at startup withexpected header's hash X, but got Y. This is easy to hit and hard to diagnose.What changed
jqsnippet that computestrust_heightand extractstrust_hashvia the explicit.result.block_id.hashpath.sedto apply the computed values to[statesync].trust_hashmust beblock_id.hash, with ajq-less fallback that selects the first hash explicitly (grep -oE '"hash":"[A-F0-9]{64}"' | head -1).Single file, docs-only. The community state-sync scripts (Polkachu, Lavender.Five) already use the correct
jqpath; this brings the official docs in line and warns against the jq-less footgun.Verified against the live XRPL EVM Testnet RPC during an
exrpdv10.0.3 state-sync bring-up.