Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions integration-tests/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,10 +86,10 @@ Unable to map [u8; 32] to a lookup index
[16:36:17.240] INFO (xcm/24440): Connected relaychain 'Polkadot' with parachain 'Zeitgeist'
```

#### Test the upgrade to the WASM from `./target/release/wbuild/zeitgeist-runtime` on zombienet:
#### Test the upgrade to the WASM from `./target/release/wbuild/battery-station-runtime` on zombienet:

```bash
pnpm exec moonwall test zombienet_zeitgeist_upgrade --runInBand
pnpm exec moonwall test zombienet_battery_station_upgrade --runInBand
```

#### Test the upgrade to the WASM from `./target/release/wbuild/zeitgeist-runtime` on the live main-net fork using chopsticks:
Expand Down
6 changes: 3 additions & 3 deletions integration-tests/moonwall.config.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"scriptsDir": "scripts/",
"environments": [
{
"name": "zombienet_zeitgeist_upgrade",
"name": "zombienet_battery_station_upgrade",
"testFileDir": ["tests/rt-upgrade-zombienet"],
"runScripts": [
"build-node.sh",
Expand All @@ -17,10 +17,10 @@
"binPath": "../target/release/zeitgeist"
}
],
"rtUpgradePath": "../target/release/wbuild/zeitgeist-runtime/zeitgeist_runtime.compact.compressed.wasm",
"rtUpgradePath": "../target/release/wbuild/battery-station-runtime/battery_station_runtime.compact.compressed.wasm",
"type": "zombie",
"zombieSpec": {
"configPath": "./configs/zombieZeitgeist.json"
"configPath": "./configs/zombieBatteryStation.json"
}
},
"connections": [
Expand Down
1 change: 1 addition & 0 deletions integration-tests/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
"@polkadot/api": "16.5.2",
"@polkadot/keyring": "13.5.2",
"@polkadot/types": "16.5.2",
"@polkadot/util": "13.5.2",
"@polkadot/util-crypto": "13.5.2",
"@types/node": "22.10.2",
"debug": "4.3.4",
Expand Down
3 changes: 3 additions & 0 deletions integration-tests/pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ import {
} from "@moonwall/cli";
import { KeyringPair } from "@moonwall/util";
import { ApiPromise, Keyring } from "@polkadot/api";
import { u8aConcat } from "@polkadot/util";
import { blake2AsHex, xxhashAsU8a } from "@polkadot/util-crypto";
import { u8aToBigInt } from "@polkadot/util";
import fs from "node:fs";
import { RuntimeVersion } from "@polkadot/types/interfaces";

Expand Down Expand Up @@ -83,6 +86,12 @@ describeSuite({
const codeString = currentCode.toString();

const moonwallContext = await MoonwallContext.getContext();
const specVersion = (
paraApi.consts.system.version as unknown as RuntimeVersion
).specVersion.toNumber();
log(
`Parachain specVersion=${specVersion}, block=${blockNumberBefore}, rtUpgradePath=${moonwallContext.rtUpgradePath}`
);
log(
"Moonwall Context providers: " +
moonwallContext.providers.map((p) => p.name).join(", ")
Expand All @@ -109,16 +118,184 @@ describeSuite({
);
}

await context.upgradeRuntime({ from: alice, logger: log });
const txStatus = async (tx: any, label: string) =>
new Promise<void>((resolve, reject) => {
let unsubscribe: (() => void) | undefined;
tx.signAndSend(alice, (result: any) => {
if (result.dispatchError) {
// Dispatch errors won't throw, so surface them explicitly.
const errText = result.dispatchError.toString();
log(`${label} dispatchError=${errText}`);
reject(new Error(`${label} failed: ${errText}`));
unsubscribe?.();
return;
}
log(
`${label} status=${result.status?.type ?? "unknown"}, events=${result.events
?.map((ev: any) => `${ev.event.section}.${ev.event.method}`)
.join(",")}`
);
if (result.status?.isInBlock || result.status?.isFinalized) {
unsubscribe?.();
resolve();
}
})
.then((unsub: () => void) => {
unsubscribe = unsub;
})
.catch(reject);
});
Comment on lines +120 to +146
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Add timeout protection to prevent indefinite hangs.

The txStatus helper lacks timeout protection. If a transaction never reaches isInBlock or isFinalized status (due to network issues, dropped transactions, etc.), the promise will hang indefinitely, causing the test to timeout at the suite level rather than providing a clear failure reason.

Consider adding a timeout mechanism:

 const txStatus = async (tx: any, label: string) =>
   new Promise<void>((resolve, reject) => {
     let unsubscribe: (() => void) | undefined;
+    const timeout = setTimeout(() => {
+      unsubscribe?.();
+      reject(new Error(`${label} timed out after 60s`));
+    }, 60000);
     tx.signAndSend(alice, (result: any) => {
       if (result.dispatchError) {
         const errText = result.dispatchError.toString();
         log(`${label} dispatchError=${errText}`);
+        clearTimeout(timeout);
         reject(new Error(`${label} failed: ${errText}`));
         unsubscribe?.();
         return;
       }
       log(
         `${label} status=${result.status?.type ?? "unknown"}, events=${result.events
           ?.map((ev: any) => `${ev.event.section}.${ev.event.method}`)
           .join(",")}`
       );
       if (result.status?.isInBlock || result.status?.isFinalized) {
+        clearTimeout(timeout);
         unsubscribe?.();
         resolve();
       }
     })
       .then((unsub: () => void) => {
         unsubscribe = unsub;
       })
-      .catch(reject);
+      .catch((err) => {
+        clearTimeout(timeout);
+        reject(err);
+      });
   });
🤖 Prompt for AI Agents
In
integration-tests/tests/rt-upgrade-zombienet/test-zombienet-runtime-upgrade.ts
around lines 120-146, the txStatus promise can hang indefinitely because it
never rejects if a transaction never becomes InBlock/Finalized; add timeout
protection by starting a timer when the promise is created that rejects with a
descriptive timeout error after a configurable duration (e.g., 30s), ensure the
timer is cleared on resolve/reject, and call unsubscribe before rejecting on
timeout so the subscription is cleaned up; also ensure any caught errors clear
the timer and unsubscribe as well.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Is there a risk of indefinite hangs here if the TX fails to reach isInBlock or isFinalized?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Should be fixed by: #1459


const findCall = (callName: string) => {
for (const [section, calls] of Object.entries(paraApi.tx)) {
const typedCalls = calls as Record<string, any>;
if (typedCalls?.[callName]) {
return { call: typedCalls[callName], section };
}
}
return undefined;
};

const upgradeCallLocation = {
authorize: undefined as string | undefined,
enact: undefined as string | undefined,
};

const authorizeUpgradeResult = findCall("authorizeUpgrade");
if (authorizeUpgradeResult) {
upgradeCallLocation.authorize = authorizeUpgradeResult.section;
}

// On this SDK the enact call lives in frame-system as applyAuthorizedUpgrade.
const applyAuthorizedUpgradeResult = findCall("applyAuthorizedUpgrade");
if (applyAuthorizedUpgradeResult) {
upgradeCallLocation.enact = applyAuthorizedUpgradeResult.section;
}

const authorizeUpgrade = authorizeUpgradeResult?.call;
const applyAuthorizedUpgrade = applyAuthorizedUpgradeResult?.call;
const upgradeAvailable = authorizeUpgrade && applyAuthorizedUpgrade;

const upgradeSections = Object.keys(paraApi.tx).filter((section) =>
/parachain|upgrade|system/i.test(section)
);
log(`tx sections matching /parachain|upgrade|system/: ${upgradeSections.join(",")}`);

if (upgradeAvailable) {
// Zeitgeist runtime blocks `setCode`, so use the authorized upgrade flow.
const wasmHash = blake2AsHex(wasm);
log("Authorizing runtime upgrade via system.authorizeUpgrade");
const authorizeTx =
authorizeUpgrade.meta.args.length === 1
? authorizeUpgrade(wasmHash)
: authorizeUpgrade(wasmHash, true);
log(
`authorizeUpgrade located in section=${upgradeCallLocation.authorize}, args=${authorizeUpgrade.meta.args.length}`
);
await txStatus(
paraApi.tx.sudo.sudo(authorizeTx),
"authorizeUpgrade"
);

log("Waiting for validation function approval");
await context.waitBlock(2);

log("Enacting authorized upgrade");
log(
`applyAuthorizedUpgrade located in section=${upgradeCallLocation.enact}, args=${applyAuthorizedUpgrade.meta.args.length}`
);
await txStatus(
paraApi.tx.sudo.sudo(applyAuthorizedUpgrade(rtHex)),
"applyAuthorizedUpgrade"
);
} else {
throw new Error(
"Runtime upgrade calls missing in metadata; expected system.authorizeUpgrade/applyAuthorizedUpgrade"
);
}
Comment on lines +183 to +214
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Search for authorizeUpgrade and applyAuthorizedUpgrade definitions in runtime

# First, find runtime-related directories
echo "=== Locating runtime directories ==="
fd -t d "runtime" | head -20

echo -e "\n=== Searching for authorizeUpgrade definitions ==="
rg -n "authorizeUpgrade\|applyAuthorizedUpgrade" \
  --type rust \
  -B 2 -A 5 \
  2>/dev/null | head -100

Repository: zeitgeistpm/zeitgeist

Length of output: 338


🏁 Script executed:

#!/bin/bash
# Read the test file to understand the full context

echo "=== Reading test file (lines 150-240 for context) ==="
sed -n '150,240p' integration-tests/tests/rt-upgrade-zombienet/test-zombienet-runtime-upgrade.ts

echo -e "\n=== Checking Cargo.toml for SDK version ==="
find . -name "Cargo.toml" -path "*/integration-tests/*" | head -1 | xargs cat | grep -A 5 -B 5 "polkadot"

Repository: zeitgeistpm/zeitgeist

Length of output: 3696


🏁 Script executed:

#!/bin/bash
# Find integration-tests Cargo.toml and check SDK version

echo "=== Finding integration-tests Cargo.toml ==="
find . -path "*/integration-tests/Cargo.toml" -exec cat {} \;

echo -e "\n=== Searching for other meta.args.length patterns ==="
rg -n "meta\.args\.length" --type typescript --type javascript -B 2 -A 2

echo -e "\n=== Searching for waitBlock patterns to understand timing ==="
rg -n "waitBlock" --type typescript -B 2 -A 2 | head -50

Repository: zeitgeistpm/zeitgeist

Length of output: 314


🏁 Script executed:

#!/bin/bash
# Better approach to find integration-tests files

echo "=== Integration-tests directory structure ==="
ls -la integration-tests/ 2>/dev/null | head -20

echo -e "\n=== Finding Cargo.toml files ==="
find integration-tests -name "Cargo.toml" 2>/dev/null

echo -e "\n=== Search for meta.args.length (no type filter) ==="
rg -n "meta\.args\.length" -B 2 -A 2 | head -60

echo -e "\n=== Search for waitBlock (no type filter) ==="
rg -n "waitBlock" -B 2 -A 2 | head -60

Repository: zeitgeistpm/zeitgeist

Length of output: 5027


🏁 Script executed:

#!/bin/bash
# Check package.json for SDK versions and find waitBlock implementation

echo "=== Integration-tests package.json ==="
cat integration-tests/package.json

echo -e "\n=== Search for context.waitBlock definition/implementation ==="
rg -n "waitBlock" --type typescript -A 5 -B 2 | grep -A 10 "function\|const.*waitBlock\|=>" | head -40

echo -e "\n=== Search for moonwall context/framework files ==="
rg -n "class.*Context\|function.*context" integration-tests -B 2 -A 5 | head -60

Repository: zeitgeistpm/zeitgeist

Length of output: 1621


🏁 Script executed:

#!/bin/bash
# Search for test-zombienet-runtime-upgrade.ts full content to understand context better

echo "=== Full test file content (part 1 - imports and setup) ==="
head -100 integration-tests/tests/rt-upgrade-zombienet/test-zombienet-runtime-upgrade.ts

echo -e "\n=== Check for comments about timing or block requirements ==="
rg -n "block\|timing\|wait" integration-tests/tests/rt-upgrade-zombienet/test-zombienet-runtime-upgrade.ts -i | head -40

echo -e "\n=== Search for authorizeUpgrade in Polkadot docs or comments ==="
rg -n "authorizeUpgrade\|applyAuthorizedUpgrade\|SDK\|version" integration-tests/tests/rt-upgrade-zombienet/test-zombienet-runtime-upgrade.ts -i -B 1 -A 1

Repository: zeitgeistpm/zeitgeist

Length of output: 3789


🏁 Script executed:

#!/bin/bash
# Search for more context about the meta.args check and what it's handling

echo "=== Full context around meta.args.length check (lines 170-220) ==="
sed -n '170,220p' integration-tests/tests/rt-upgrade-zombienet/test-zombienet-runtime-upgrade.ts

echo -e "\n=== Search for any SDK or version-related comments in test ==="
rg -n "SDK\|version\|meta\|args" integration-tests/tests/rt-upgrade-zombienet/test-zombienet-runtime-upgrade.ts | head -30

Repository: zeitgeistpm/zeitgeist

Length of output: 2331


🌐 Web query:

Polkadot SDK 16.5.2 authorizeUpgrade signature args parameters

💡 Result:

Summary (@polkadot/api v16.5.2)

  • Extrinsic: api.tx.system.authorizeUpgrade(codeHash: H256)

    • Single argument: codeHash of type H256 (the runtime code hash).
    • Origin required: Root. [1][2]
  • Related extrinsics:

    • api.tx.system.authorizeUpgradeWithoutChecks(codeHash: H256) — same arg but skips safety checks. [1][2]
    • api.tx.system.applyAuthorizedUpgrade(code: Bytes) — provides the runtime WASM preimage (Bytes). [3]

Sources:

  • Polkadot JS extrinsics docs (system.authorizeUpgrade / authorizeUpgradeWithoutChecks). [1]
  • Substrate / Polkadot extrinsics listing (applyAuthorizedUpgrade / authorizeUpgrade). [3]
  • @polkadot/api (v16.5.2) package listing. [4]

🏁 Script executed:

#!/bin/bash
# Search for authorizeUpgrade definition in battery-station runtime

echo "=== Searching for authorizeUpgrade in battery-station runtime ==="
rg -n "authorizeUpgrade" runtime/battery-station -B 3 -A 3 --type rust

echo -e "\n=== Checking runtime/zeitgeist as well ==="
rg -n "authorizeUpgrade" runtime/zeitgeist -B 3 -A 3 --type rust

echo -e "\n=== Search for pallet_system config or extensions ==="
rg -n "pallet_system\|System" runtime/battery-station/src/lib.rs -B 2 -A 10 | head -80

Repository: zeitgeistpm/zeitgeist

Length of output: 229


🏁 Script executed:

#!/bin/bash
# Check battery-station runtime configuration

echo "=== Reading battery-station runtime/lib.rs ==="
wc -l runtime/battery-station/src/lib.rs

echo -e "\n=== Grep for System pallet config ==="
rg -n "impl.*frame_system\|frame_system::Config\|System" runtime/battery-station/src/lib.rs -B 2 -A 5 | head -100

echo -e "\n=== Check for any comments about upgrade or args ==="
rg -n "upgrade\|args\|variant" runtime/battery-station/src/lib.rs -i | head -30

Repository: zeitgeistpm/zeitgeist

Length of output: 243


🏁 Script executed:

#!/bin/bash
# Read the full battery-station runtime lib.rs

echo "=== Full battery-station/src/lib.rs ==="
cat -n runtime/battery-station/src/lib.rs

echo -e "\n=== Check runtime structure for other files ==="
find runtime/battery-station/src -type f -name "*.rs" | head -20

Repository: zeitgeistpm/zeitgeist

Length of output: 7574


🌐 Web query:

Polkadot runtime upgrade authorizeUpgrade applyAuthorizedUpgrade timing blocks requirements

💡 Result:

Short answer — how authorizeUpgrade + applyAuthorizedUpgrade work, timing and requirements:

  • authorizeUpgrade(code_hash) must be called with Root origin and simply records an authorized code hash (two‑phase flow useful when the WASM preimage is large or sent later). [1][2]
  • applyAuthorizedUpgrade(code) can be called by any origin; it supplies the WASM preimage for a previously authorized hash. If the authorization required version checks, applyAuthorizedUpgrade enforces that the spec_name is unchanged and spec_version increases. [1][2]
  • Timing: whether the new runtime is applied immediately in the same block or scheduled for a later block depends on the runtime’s OnSetCode implementation (T::OnSetCode). In many chains (including Polkadot/parachains) the OnSetCode implementation may schedule the actual set_code to a future block (so applyAuthorizedUpgrade may only enqueue the change). The system::set_code path likewise delegates to T::OnSetCode::set_code. [2][3]
  • Parachain note: parachains generally must use the authorize/apply_authorized two‑step flow to coordinate upgrades with the relay chain’s expectations (hence the authorize step is required for parachain upgrade workflows). [1][3]
  • Practical checks/requirements: authorizeUpgrade requires Root; applyAuthorizedUpgrade validates the preimage matches an authorized hash and (unless using the WithoutChecks variants) enforces spec/version checks; there are also set_code and without_checks variants if you need single‑step or skip safety checks. [1][2]

Sources: polkadot.js extrinsics docs (system.authorizeUpgrade / applyAuthorizedUpgrade). [1] Substrate frame_system implementation and OnSetCode behavior (set_code / set_code_without_checks / OnSetCode trait). [2][3]


The meta.args.length check is dead code; 2-block wait is reasonable but undocumented.

1. Lines 189-190 — Unnecessary conditional: The check for authorizeUpgrade.meta.args.length === 1 will always be true with Polkadot SDK v16.5.2, which defines authorizeUpgrade(codeHash: H256) with only one argument. The else branch (authorizeUpgrade(wasmHash, true)) is dead code. This appears to be legacy compatibility logic for older SDK versions. While not a functional issue, it creates unnecessary complexity and could confuse future maintainers.

2. Line 200 — 2-block wait is reasonable: The waitBlock(2) timing is appropriate for parachain upgrade workflows, as the OnSetCode implementation may schedule the actual code change for a future block. However, there is no explicit documentation in the code explaining why 2 blocks is chosen. The subsequent verification at lines 220–225 properly validates that the upgrade succeeded, so the wait is effective.

Consider:

  • Simplifying the conditional to remove the dead else branch (or add a comment explaining the version compatibility intent if intentional).
  • Adding a comment on line 199 explaining why 2 blocks is sufficient (e.g., "wait for OnSetCode scheduling and block finalization").
🤖 Prompt for AI Agents
integration-tests/tests/rt-upgrade-zombienet/test-zombienet-runtime-upgrade.ts
lines 183-214: the conditional that checks authorizeUpgrade.meta.args.length and
the else branch with authorizeUpgrade(wasmHash, true) is dead/legacy code for
current Polkadot SDK and should be simplified to always call
authorizeUpgrade(wasmHash); remove the unused else branch; additionally,
annotate the waitBlock(2) call (around line 199) with a brief comment like "wait
for OnSetCode scheduling and block finalization (2 blocks)" so the choice is
documented for future maintainers.


await context.waitBlock(2);
const blockNumberAfter = (
await paraApi.rpc.chain.getBlock()
).block.header.number.toNumber();
log(`Before: #${blockNumberBefore}, After: #${blockNumberAfter}`);
const codeAfter = (await paraApi.rpc.state.getStorage(":code"))?.toString();
log(
`Before: #${blockNumberBefore}, After: #${blockNumberAfter}, code changed=${
codeAfter !== codeString
}`
);
log(
`Code (before): ${codeString.slice(0, 10)}...${codeString.slice(-10)}, code (after): ${
codeAfter ? codeAfter.slice(0, 10) + "..." + codeAfter.slice(-10) : "undefined"
}`
);
expect(
blockNumberAfter,
"Block number did not increase"
).to.be.greaterThan(blockNumberBefore);
expect(codeAfter, "Runtime code should match upgraded wasm").to.equal(rtHex);
Comment on lines +220 to +235
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Add explicit null check before code comparison.

The optional chaining at line 220 could result in codeAfter being undefined. If storage retrieval fails, the assertion at line 235 would produce a confusing failure message ("Runtime code should match upgraded wasm") rather than indicating that the storage read failed.

         const codeAfter = (await paraApi.rpc.state.getStorage(":code"))?.toString();
+        expect(codeAfter, "Failed to read :code storage after upgrade").to.not.be.undefined;
         log(
           `Before: #${blockNumberBefore}, After: #${blockNumberAfter}, code changed=${
             codeAfter !== codeString
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const codeAfter = (await paraApi.rpc.state.getStorage(":code"))?.toString();
log(
`Before: #${blockNumberBefore}, After: #${blockNumberAfter}, code changed=${
codeAfter !== codeString
}`
);
log(
`Code (before): ${codeString.slice(0, 10)}...${codeString.slice(-10)}, code (after): ${
codeAfter ? codeAfter.slice(0, 10) + "..." + codeAfter.slice(-10) : "undefined"
}`
);
expect(
blockNumberAfter,
"Block number did not increase"
).to.be.greaterThan(blockNumberBefore);
expect(codeAfter, "Runtime code should match upgraded wasm").to.equal(rtHex);
const codeAfter = (await paraApi.rpc.state.getStorage(":code"))?.toString();
expect(codeAfter, "Failed to read :code storage after upgrade").to.not.be.undefined;
log(
`Before: #${blockNumberBefore}, After: #${blockNumberAfter}, code changed=${
codeAfter !== codeString
}`
);
log(
`Code (before): ${codeString.slice(0, 10)}...${codeString.slice(-10)}, code (after): ${
codeAfter ? codeAfter.slice(0, 10) + "..." + codeAfter.slice(-10) : "undefined"
}`
);
expect(
blockNumberAfter,
"Block number did not increase"
).to.be.greaterThan(blockNumberBefore);
expect(codeAfter, "Runtime code should match upgraded wasm").to.equal(rtHex);
🤖 Prompt for AI Agents
In
integration-tests/tests/rt-upgrade-zombienet/test-zombienet-runtime-upgrade.ts
around lines 220 to 235, the optional chaining when reading storage can yield
undefined and the subsequent equality assertion will give a misleading message;
add an explicit null/undefined check for codeAfter immediately after retrieving
it and fail with a clear error if storage read returned undefined (e.g.,
expect(codeAfter, "Failed to read :code storage after
upgrade").to.not.be.undefined), then proceed to compare the non-null codeAfter
value to rtHex using the existing equality assertion.

},
});

it({
id: "T03",
title: "Relay timestamp (from relay proof) is present and increases across blocks",
timeout: 120000,
test: async function () {
const relayTsStorageKey =
"0x54dbd40f5201dbc18b0eed4b2ecd9cc67e2cdf745d68eeb295336330e3a1a063";
Comment on lines +244 to +245
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Hardcoded storage key is fragile.

The storage key 0x54dbd40f5201dbc18b0eed4b2ecd9cc67e2cdf745d68eeb295336330e3a1a063 is hardcoded, making the code brittle to storage layout changes. Use the Polkadot API to derive the key dynamically.

Replace the hardcoded key with dynamic derivation:

-        const relayTsStorageKey =
-          "0x54dbd40f5201dbc18b0eed4b2ecd9cc67e2cdf745d68eeb295336330e3a1a063";
+        // Derive storage key: twox_128("RelayDataProvider") + twox_128("RelayTimestampNow")
+        const relayTsStorageKey = u8aConcat(
+          xxhashAsU8a("RelayDataProvider", 128),
+          xxhashAsU8a("RelayTimestampNow", 128)
+        );

This explains the previously imported but unused u8aConcat and xxhashAsU8a utilities.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const relayTsStorageKey =
"0x54dbd40f5201dbc18b0eed4b2ecd9cc67e2cdf745d68eeb295336330e3a1a063";
// Derive storage key: twox_128("RelayDataProvider") + twox_128("RelayTimestampNow")
const relayTsStorageKey = u8aConcat(
xxhashAsU8a("RelayDataProvider", 128),
xxhashAsU8a("RelayTimestampNow", 128)
);
🤖 Prompt for AI Agents
In
integration-tests/tests/rt-upgrade-zombienet/test-zombienet-runtime-upgrade.ts
around lines 245-246, replace the hardcoded storage key with a dynamic
derivation using the Polkadot API/utility functions: derive the storage key by
hashing the pallet name and storage item name (and any key params) with
xxhashAsU8a and concatenating with u8aConcat (or simply call
api.query.<pallet>.<storage>.key(...)) to produce the correct storage key at
runtime; update the code to use the previously imported u8aConcat and
xxhashAsU8a (or api.query.*.key) so the key adapts to storage layout changes
instead of remaining hardcoded.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Is it okay to hardcode this value? relayTsStorageKey

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

It's just a storage key called pallet runtime and storage item name RelayTimestampNow. If it were to change, we would know instantly by this test.

Exactly what we have in /Users/chralt/development/zeitgeist/runtime/common/src/relay_timestamp.rs:

struct RelayTimestampNowPrefix;
impl StorageInstance for RelayTimestampNowPrefix {
    const STORAGE_PREFIX: &'static str = "RelayTimestampNow";

    fn pallet_prefix() -> &'static str {
        "runtime"
    }
}
Screenshot 2025-12-03 at 10 15 02 Screenshot 2025-12-03 at 10 15 24


const readRelayTs = async (): Promise<bigint> => {
const raw = await paraApi.rpc.state.getStorage(relayTsStorageKey);
expect(raw, "RelayTimestampNow storage should exist").to.not.be.null;
const rawHex = raw?.toHex();
expect(rawHex, "RelayTimestampNow should decode to hex").to.not.be.undefined;
log(`RelayTimestampNow raw=${rawHex}`);
// Storage encodes u64 little-endian; decode explicitly.
const ts = u8aToBigInt(raw?.toU8a(true) ?? new Uint8Array(), true);
return ts;
};

let tsRelay1 = 0n;
let retries = 0;
while (tsRelay1 === 0n && retries < 5) {
log(`Attempt ${retries + 1}: reading RelayTimestampNow`);
tsRelay1 = await readRelayTs();
const rawDirect = await paraApi.rpc.state.getStorage(
relayTsStorageKey
);
log(`RelayTimestampNow direct RPC read: ${rawDirect?.toHex() ?? "null"}`);
if (tsRelay1 === 0n) {
await context.waitBlock(1);
}
retries++;
}

const tsPara1 = (await paraApi.query.timestamp.now()).toBigInt();
const block1 = (
await paraApi.rpc.chain.getBlock()
).block.header.number.toNumber();

expect(tsRelay1, "Initial relay timestamp should be non-zero").to.be.greaterThan(0n);
expect(
tsPara1,
"Parachain timestamp should mirror relay timestamp"
).to.equal(tsRelay1);

await context.waitBlock(2);

const tsRelay2 = await readRelayTs();
const tsPara2 = (await paraApi.query.timestamp.now()).toBigInt();
const block2 = (
await paraApi.rpc.chain.getBlock()
).block.header.number.toNumber();

expect(block2, "Block height should advance").to.be.greaterThan(block1);
expect(
tsRelay2,
"Relay timestamp should increase with new relay proofs"
).to.be.greaterThan(tsRelay1);
expect(tsPara2, "Parachain timestamp should mirror relay timestamp").to.equal(tsRelay2);
},
});
},
Expand Down