From f03fbaced0cd4990cac7dfdb84a8077450b35bc4 Mon Sep 17 00:00:00 2001 From: 2xburnt <169301814+2xburnt@users.noreply.github.com> Date: Fri, 24 Jul 2026 15:02:24 -0500 Subject: [PATCH 1/2] fix(blocks): treat a malformed blocks/latest response as a failed poll fetchLatest trusts the response shape. An endpoint that answers 200 with something other than a block - an error body, a rate-limit page, a proxy interstitial - is stored as `latest`. Its missing chain_id then fails this check: if (!this.earliest || this.earliest?...chain_id != this.latest?...chain_id) { this.earliest = this.latest; this.recents = []; } That branch exists to detect a chain switch, but a junk payload trips it identically, so `earliest` and `recents` are wiped. The visible effect is the average block time snapping back to the 1000ms placeholder and the recent-blocks list emptying, intermittently, with nothing surfaced to the user. Observed against a flaky endpoint: the average block time read 1679 -> 6117 -> 1000 -> 3844 ms against a true 4060 ms, with recents dropping 4 -> 1. With the payload validated it converged to 3957 ms and neither value reset, while a third of responses were still being rejected as malformed. --- src/stores/useBaseStore.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/stores/useBaseStore.ts b/src/stores/useBaseStore.ts index 6c26bea205..0a17a2a5f4 100644 --- a/src/stores/useBaseStore.ts +++ b/src/stores/useBaseStore.ts @@ -79,7 +79,13 @@ export const useBaseStore = defineStore('baseStore', { async fetchLatest() { if (!this.hasRpc) return this.latest; try { - this.latest = await this.blockchain.rpc?.getBaseBlockLatest(); + const latest = await this.blockchain.rpc?.getBaseBlockLatest(); + // A malformed 200 (an error body, a rate-limit page) would otherwise be + // stored as `latest`; its missing chain_id then trips the reset below and + // wipes earliest/recents, resetting the average block time to the 1000ms + // placeholder. Treat it as a failed poll instead. + if (!latest?.block?.header?.height) throw new Error('malformed blocks/latest payload'); + this.latest = latest; this.connected = true; } catch (error) { console.error('Error fetching latest block:', error); From 3950bf69c388fe40e16ba980883b2dcba2b1f62f Mon Sep 17 00:00:00 2001 From: 2xburnt <169301814+2xburnt@users.noreply.github.com> Date: Fri, 24 Jul 2026 17:11:48 -0500 Subject: [PATCH 2/2] fix(blocks): require chain_id as well as height in the payload guard The guard only checked `block.header.height`, so a partial response carrying a height but no chain_id still passed and was assigned to `latest`. The chain_id comparison immediately below then saw undefined against the previous chain, reset `earliest` and `recents`, and reproduced exactly the state loss this change is meant to prevent. Thanks @chatgpt-codex-connector for spotting it - the original comment even described that mechanism while the condition missed it. Verified by serving 13 consecutive 200s shaped as `{ block: { header: { height: "999999" } } }` with chain_id omitted: earliest stays at 32184677, chain_id stays cosmoshub-4, recents keeps growing, and the partial is never stored as `latest`. --- src/stores/useBaseStore.ts | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/src/stores/useBaseStore.ts b/src/stores/useBaseStore.ts index 0a17a2a5f4..f71d50de1e 100644 --- a/src/stores/useBaseStore.ts +++ b/src/stores/useBaseStore.ts @@ -80,11 +80,19 @@ export const useBaseStore = defineStore('baseStore', { if (!this.hasRpc) return this.latest; try { const latest = await this.blockchain.rpc?.getBaseBlockLatest(); - // A malformed 200 (an error body, a rate-limit page) would otherwise be - // stored as `latest`; its missing chain_id then trips the reset below and - // wipes earliest/recents, resetting the average block time to the 1000ms + // A malformed 200 - an error body, a rate-limit page, a truncated + // response - would otherwise be stored as `latest`. The chain_id + // comparison below would then see undefined against the previous chain, + // wipe earliest/recents and reset the average block time to the 1000ms // placeholder. Treat it as a failed poll instead. - if (!latest?.block?.header?.height) throw new Error('malformed blocks/latest payload'); + // + // chain_id is checked as well as height: it is specifically the missing + // chain_id that trips that reset, so a partial payload carrying a height + // but no chain_id would otherwise still cause the state loss this guards. + const header = latest?.block?.header; + if (!header?.height || !header?.chain_id) { + throw new Error('malformed blocks/latest payload'); + } this.latest = latest; this.connected = true; } catch (error) {