From 250ef8a0138acdd5fc129b266f9867beec2b001f Mon Sep 17 00:00:00 2001 From: Nikolaus Heger Date: Fri, 4 Sep 2026 00:36:07 +0800 Subject: [PATCH] fix: read metadata and runtime version at the head, not the finalized 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. --- src/chain/client.rs | 70 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) diff --git a/src/chain/client.rs b/src/chain/client.rs index a72874a..2312e3b 100644 --- a/src/chain/client.rs +++ b/src/chain/client.rs @@ -140,6 +140,14 @@ impl QuantusClient { // Create SubXT client using the configured RPC client let client = OnlineClient::::from_rpc_client(rpc_client).await?; + // subxt pins metadata and runtime version to the latest *finalized* block. QPoW + // finality trails the head by a long way (~100 blocks), so a client left on that + // default reports the pre-upgrade runtime for ~20 minutes after an upgrade + // enacts: governance config reads stale, calls added by the upgrade look absent, + // and `transaction_version` is wrong, which signs extrinsics the chain rejects. + // Re-point both at the head, matching every other read path in the CLI (#152). + Self::retarget_to_head(&client, &ws_client, &display_node_url).await?; + // Reject non-Quantus / older-unsupported runtimes before encode/sign. Newer-than-table // Quantus specs are allowed with a warning (see validate_runtime_identity). if enforce_runtime_identity { @@ -164,6 +172,68 @@ impl QuantusClient { Ok(QuantusClient { client, rpc_client: ws_client, node_url: node_url.to_string() }) } + /// Re-point a freshly built client's metadata and runtime version at the chain head. + /// + /// Failing here would leave the client silently on finalized-block metadata, so a + /// lookup failure is an error rather than a fallback. + async fn retarget_to_head( + client: &OnlineClient, + ws_client: &WsClient, + display_node_url: &str, + ) -> Result<(), QuantusError> { + use codec::Decode; + use jsonrpsee::core::client::ClientT; + + // No block argument: both RPCs answer at the head. + let metadata_hex: String = ws_client + .request::("state_getMetadata", []) + .await + .map_err(|e| { + QuantusError::NetworkError(format!( + "Failed to fetch runtime metadata at the head from {display_node_url}: {e:?}" + )) + })?; + let metadata_bytes = hex::decode(metadata_hex.trim_start_matches("0x")).map_err(|e| { + QuantusError::NetworkError(format!("Runtime metadata is not valid hex: {e:?}")) + })?; + let metadata = subxt::Metadata::decode(&mut &metadata_bytes[..]).map_err(|e| { + QuantusError::NetworkError(format!("Failed to decode runtime metadata: {e:?}")) + })?; + + let version: serde_json::Value = ws_client + .request::("state_getRuntimeVersion", []) + .await + .map_err(|e| { + QuantusError::NetworkError(format!( + "Failed to fetch runtime version at the head from {display_node_url}: {e:?}" + )) + })?; + let field = |name: &str| -> Result { + version + .get(name) + .and_then(serde_json::Value::as_u64) + .and_then(|v| u32::try_from(v).ok()) + .ok_or_else(|| { + QuantusError::NetworkError(format!( + "Runtime version from {display_node_url} has no usable `{name}`" + )) + }) + }; + let runtime_version = subxt::client::RuntimeVersion { + spec_version: field("specVersion")?, + transaction_version: field("transactionVersion")?, + }; + + log_verbose!( + "📡 Using head runtime: spec {} / tx {}", + runtime_version.spec_version, + runtime_version.transaction_version + ); + client.set_metadata(metadata); + client.set_runtime_version(runtime_version); + Ok(()) + } + /// Get reference to the underlying SubXT client /// The FIPS 204 context the connected runtime verifies extrinsic signatures under. Read from /// the runtime version subxt already cached at connect, so this costs no RPC.