Skip to content

docs(operators): automated state-sync trust_hash extraction + block_id.hash warning - #59

Open
pete-csyn wants to merge 1 commit into
ripple:mainfrom
pete-csyn:docs/statesync-trust-hash-extraction
Open

docs(operators): automated state-sync trust_hash extraction + block_id.hash warning#59
pete-csyn wants to merge 1 commit into
ripple:mainfrom
pete-csyn:docs/statesync-trust-hash-extraction

Conversation

@pete-csyn

Copy link
Copy Markdown

Summary

The State Sync section of pages/operators/advanced/sync-options.md currently tells operators to copy a recent block height and hash from a block explorer by hand, then edit config.toml manually. 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 /block RPC response contains several "hash" fields. trust_hash must be block_id.hash. Operators who script the manual step on a minimal image without jq often use a greedy regex over the single-line JSON, which grabs the last "hash" (an app/header hash) instead of block_id.hash. The light client then rejects the anchor and the node crash-loops at startup with expected header's hash X, but got Y. This is easy to hit and hard to diagnose.

What changed

  • Option A (automated, recommended): a jq snippet that computes trust_height and extracts trust_hash via the explicit .result.block_id.hash path.
  • Option B (manual): the existing explorer method, retained.
  • An optional in-place sed to apply the computed values to [statesync].
  • A warning admonition: 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).

Single file, docs-only. The community state-sync scripts (Polkachu, Lavender.Five) already use the correct jq path; this brings the official docs in line and warns against the jq-less footgun.

Verified against the live XRPL EVM Testnet RPC during an exrpd v10.0.3 state-sync bring-up.

…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 vriveraPeersyst left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 -1 is structurally guaranteed, not luck. coretypes.ResultBlock declares BlockID before Block, and Go serialises struct fields in declaration order, so block_id.hash is always the first "hash": in the document. Worth stating in the docs — it turns a "seems to work" into a "cannot break".
  • The sed range 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 realistic config.toml and nothing outside the section was touched.
  • current height - 1000 is a sane default. Mainnet block time is ~5.7s, so that is ~95 minutes back — comfortably inside the 168h trust_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`.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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|" \

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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; }

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants