fix: read metadata and runtime version at the head, not the finalized block - #154
fix: read metadata and runtime version at the head, not the finalized block#154n13 wants to merge 1 commit into
Conversation
… block subxt pins a client's metadata and runtime version to the latest finalized block. QPoW finality trails the head by ~100 blocks, so for roughly 20 minutes after a runtime upgrade enacts the CLI keeps talking to the old runtime: - governance config reads stale. After Heisenberg enacted spec 148, tech-referenda config still listed only track 0 with the 144 decision deposit of 1000 UNIT, so the fast_upgrade track looked like it had not shipped. - calls and storage added by the upgrade appear absent. - transaction_version is wrong, which signs extrinsics the chain rejects. That matters here because 144 -> 148 moves it from 3 to 6. Re-point both at the head after connecting, matching every other read path in the CLI (#152 did the same for collect-rewards proofs). A failure to read them is an error rather than a fallback, since silently continuing would leave the client on finalized metadata -- the bug this fixes.
n13
left a comment
There was a problem hiding this comment.
Reviewer model: GPT Sol
Verdict (advisory): Request changes
Blocking findings:
-
src/chain/client.rs:149installs head metadata into the one sharedOnlineClient, but not every consumer reads the head.events --finalized,events --block,events --block-hash, block analysis, and SDK finalized paths pass older block hashes to Subxt, which still decodes them with this single head metadata snapshot. This is a concrete regression across an upgrade boundary. During review, Heisenberg's head was spec/tx 148/6 while finalized block 977072 was still 144/3 and the metadata payloads differed. Before this patch,events --finalizeddecoded all 10 events in block 977072. The exact PR binary, using spec-148 head metadata, fails on that same block after four events withCould not decode Phase, variant doesn't exist. Please keep finalized/historical reads on metadata from their target block (or separate the head-oriented client from block-specific clients) instead of globally assuming every caller targets the head. -
src/chain/client.rs:187-205does not capture one coherent head snapshot.state_getMetadataandstate_getRuntimeVersioneach resolve an omitted block argument independently, so an upgrade block becoming best between the two calls leaves old metadata paired with a new runtime version. The identity gate at lines 155-167 then makes a third unpinned version request and cannot detect that mismatch. Because this client does not run Subxt's runtime updater, the inconsistent pair persists for the command and can reproduce the missing-call or invalid-extrinsic behavior this patch is intended to eliminate. Capture the best block hash once, request both metadata and runtime version at that hash, validate that same version response, and add coverage for the upgrade-boundary case.
Non-blocking accuracy note: Subxt 0.44.3's LegacyBackend::current_runtime_version already calls state_getRuntimeVersion(None) at the head; its metadata fetch is the part pinned to the finalized hash. The comments and PR rationale should not claim both values were previously finalized.
Validation on exact head 250ef8a0138acdd5fc129b266f9867beec2b001f:
git diff --check- passed.taplo format --check --config taplo.toml- passed.cargo +nightly-2026-08-31 fmt --all -- --check- passed.cargo metadata --locked --no-deps --format-version 1- passed.SKIP_CIRCUIT_BUILD=1 cargo test --release --locked --lib chain::client::tests- 3 passed; the new retargeting path itself has no automated coverage.SKIP_CIRCUIT_BUILD=1 cargo clippy --all-targets --locked -- -D warnings- passed.- Live Heisenberg
tech-referenda config- passed and showed both spec-148 tracks, confirming the intended head-read fix. - Live Heisenberg historical-event reproduction at block 977072 (
0x705dadae...c685f5) - failed only after the client was retargeted to spec-148 metadata, as described above.
All current hosted checks pass, including Ubuntu/macOS builds and tests, examples, strict analysis/docs, formatting, security audit, and dependency cooldown.
Found live: after Heisenberg enacted spec 148,
quantus tech-referenda configstill showed only track 0 with the 144 decision deposit of 1000 UNIT, so it looked like thefast_upgradetrack had not shipped. It had — the CLI was reading the pre-upgrade runtime.Cause
OnlineClient::from_rpc_clientpins metadata and runtime version tolatest_finalized_block_ref(). QPoW finality trails the head by ~100 blocks, so for roughly 20 minutes after any runtime upgrade enacts the client keeps talking to the old runtime.Measured on Heisenberg at the time:
The client was already inconsistent with itself: the runtime-identity check calls
state_getRuntimeVersionwith no block argument, which answers at the head, while the metadata it kept was from the finalized block.Consequences
transaction_versionis wrong, which signs extrinsics the chain rejects. This is the one that actually bites: 144 → 148 moves it 3 → 6, andsigning_context()reads the client's cached runtime version to pick the FIPS 204 signing context.Change
After connecting, fetch metadata and runtime version at the head and install them via
set_metadata/set_runtime_version. Both RPCs answer at the head when given no block argument.This matches every other read path in the CLI — #152 made the same head-not-finalized correction for collect-rewards proofs, for the same reason.
A failure to read either is an error, not a fallback: silently continuing would leave the client on finalized metadata, which is precisely the bug being fixed.
Verification
Same command that exposed it, against live Heisenberg on 148:
Track #1 now appears and track #0 shows 148's deposit.
cargo test --release --lib— 328 passedSKIP_CIRCUIT_BUILD=1 cargo clippy --all-targets --locked -- -D warnings— cleancargo +nightly fmt --all -- --check— cleanNote on ordering
Independent of #150, which is still in review. This touches only client construction; #150 touches decoding. They do not overlap, but #150 is branched from an older main and will want a rebase.