diff --git a/ts-tests/configs/chopsticks-fork.yml b/ts-tests/configs/chopsticks-fork.yml new file mode 100644 index 0000000000..8ee5bea107 --- /dev/null +++ b/ts-tests/configs/chopsticks-fork.yml @@ -0,0 +1,113 @@ +# Input config for scripts/gen-chopsticks-fork.ts. +# +# This is NOT consumed by Chopsticks directly — the generator connects to the +# endpoints below, fetches the `prefetch-storages` items for ONE netuid, and +# writes a slim, self-contained config to tmp/chopsticks-fork-slim.yml that +# Chopsticks can fork in ~1 min instead of ~15. + +endpoint: + - wss://entrypoint-finney.opentensor.ai:443 + - wss://bittensor-finney.api.onfinality.io/public-wss + +mock-signature-host: true # Allows you to sign as any account. +allow-unresolved-imports: true # Allows host functions not available in smoldot to be skipped. + +import-storage: + Sudo: + Key: 5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY # Alice + System: + Account: + - + - + - 5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY # Alice + - providers: 1 + data: + free: 1000000000000000 + - + - + - 5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty # Bob + - providers: 1 + data: + free: 1000000000000000 + SubtensorModule: + AdminFreezeWindow: "0x0000" + NetworkRateLimit: 0 + NetworkLockReductionInterval: 1 + $removePrefix: ['NetworksAdded'] + NetworksAdded: + - - - 0 + - true + - - - 1 + - true + +prefetch-storages: + - SubtensorModule.MaxBurn + - SubtensorModule.MinBurn + - SubtensorModule.Burn + - SubtensorModule.BurnHalfLife + - SubtensorModule.NetworksAdded + - SubtensorModule.MechanismCountCurrent + - SubtensorModule.TimelockedWeightCommits + - SubtensorModule.RevealPeriodEpochs + - SubtensorModule.Tempo + - SubtensorModule.SubtokenEnabled + - SubtensorModule.NetworkRegistrationAllowed + - SubtensorModule.FirstEmissionBlockNumber + - SubtensorModule.SubnetTaoFlow + - SubtensorModule.SubnetEmaTaoFlow + - SubtensorModule.SubnetMovingPrice + - SubtensorModule.SubnetMechanism + - SubtensorModule.SubnetAlphaIn + - SubtensorModule.SubnetAlphaOut + - SubtensorModule.SubnetTAO + - SubtensorModule.SubnetVolume + - SubtensorModule.PendingRootAlphaDivs + - SubtensorModule.PendingServerEmission + - SubtensorModule.PendingValidatorEmission + - SubtensorModule.PendingOwnerCut + - SubtensorModule.BlocksSinceLastStep + - SubtensorModule.EmaPriceHalvingBlocks + - SubtensorModule.RootClaimableThreshold + - SubtensorModule.StakingColdkeys + - SubtensorModule.RootClaimable + - SubtensorModule.RootClaimType + - SubtensorModule.StakingColdkeysByIndex + - SubtensorModule.StakingHotkeys + - SubtensorModule.ChildkeyTake + - SubtensorModule.LastRateLimitedBlock + - SubtensorModule.AdjustmentAlpha + - SubtensorModule.ActivityCutoff + - SubtensorModule.AdjustmentInterval + - SubtensorModule.Kappa + - SubtensorModule.Rho + - SubtensorModule.ImmunityPeriod + - SubtensorModule.MaxAllowedValidators + - SubtensorModule.MaxWeightsLimit + - SubtensorModule.MinAllowedWeights + - SubtensorModule.MinDifficulty + - SubtensorModule.MaxDifficulty + - SubtensorModule.Difficulty + - SubtensorModule.TargetRegistrationsPerInterval + - SubtensorModule.MaxRegistrationsPerBlock + - SubtensorModule.RegistrationsThisInterval + - SubtensorModule.LastAdjustmentBlock + - SubtensorModule.WeightsVersionKey + - SubtensorModule.WeightsSetRateLimit + - SubtensorModule.LiquidAlphaOn + - SubtensorModule.CommitRevealWeightsEnabled + - SubtensorModule.SubnetworkN + - SubtensorModule.MaxAllowedUids + - SubtensorModule.LastUpdate + - SubtensorModule.Active + - SubtensorModule.ValidatorPermit + - SubtensorModule.Keys + - SubtensorModule.Uids + - Swap.ScrapReservoirAlpha + - Swap.SwapBalancer + - Swap.FeeRate + - Swap.PalSwapInitialized +# Balancer-model Swap storages (SwapBalancer/FeeRate/PalSwapInitialized above) are +# per-netuid maps (keyed by u16) read during block production. The generator skips +# any prefetch item the live chain lacks, so items not yet present on finney are inert. +# NOTE: keep this comment AFTER the last list item — the generator's YAML parser treats +# a blank/comment line inside the list as its end, dropping everything after it. diff --git a/ts-tests/moonwall.config.json b/ts-tests/moonwall.config.json index cc5667af9f..1a57fa52d0 100644 --- a/ts-tests/moonwall.config.json +++ b/ts-tests/moonwall.config.json @@ -37,6 +37,34 @@ ] } }, + { + "name": "chopsticks_fork", + "timeout": 300000, + "testFileDir": ["suites/chopsticks_fork"], + "runScripts": [ + "gen-chopsticks-fork.ts configs/chopsticks-fork.yml tmp/chopsticks-fork-slim.yml" + ], + "foundation": { + "type": "chopsticks", + "launchSpec": [ + { + "name": "subtensor", + "configPath": "tmp/chopsticks-fork-slim.yml", + "buildBlockMode": "manual", + "allowUnresolvedImports": true, + "newBlockTimeout": 120000, + "wasmOverride": "../target/release/wbuild/node-subtensor-runtime/node_subtensor_runtime.compact.compressed.wasm" + } + ] + }, + "connections": [ + { + "name": "node", + "type": "polkadotJs", + "endpoints": ["ws://127.0.0.1:8000"] + } + ] + }, { "name": "zombienet_staking", "timeout": 600000, diff --git a/ts-tests/pnpm-lock.yaml b/ts-tests/pnpm-lock.yaml index d92a6b9cd6..d985437c33 100644 --- a/ts-tests/pnpm-lock.yaml +++ b/ts-tests/pnpm-lock.yaml @@ -53,6 +53,10 @@ importers: ps-node: specifier: 0.1.6 version: 0.1.6 + optionalDependencies: + '@polkadot-api/descriptors': + specifier: file:.papi/descriptors + version: file:.papi/descriptors(polkadot-api@1.19.2(postcss@8.5.8)(rxjs@7.8.2)(tsx@4.21.0)(yaml@2.8.2)) devDependencies: '@acala-network/chopsticks': specifier: 1.2.3 @@ -135,10 +139,6 @@ importers: yargs: specifier: 18.0.0 version: 18.0.0 - optionalDependencies: - '@polkadot-api/descriptors': - specifier: file:.papi/descriptors - version: file:.papi/descriptors(polkadot-api@1.19.2(postcss@8.5.8)(rxjs@7.8.2)(tsx@4.21.0)(yaml@2.8.2)) packages: @@ -210,28 +210,24 @@ packages: engines: {node: '>= 10'} cpu: [arm64] os: [linux] - libc: [glibc] '@ast-grep/napi-linux-arm64-musl@0.40.5': resolution: {integrity: sha512-/qKsmds5FMoaEj6FdNzepbmLMtlFuBLdrAn9GIWCqOIcVcYvM1Nka8+mncfeXB/MFZKOrzQsQdPTWqrrQzXLrA==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - libc: [musl] '@ast-grep/napi-linux-x64-gnu@0.40.5': resolution: {integrity: sha512-DP4oDbq7f/1A2hRTFLhJfDFR6aI5mRWdEfKfHzRItmlKsR9WlcEl1qDJs/zX9R2EEtIDsSKRzuJNfJllY3/W8Q==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - libc: [glibc] '@ast-grep/napi-linux-x64-musl@0.40.5': resolution: {integrity: sha512-BRZUvVBPUNpWPo6Ns8chXVzxHPY+k9gpsubGTHy92Q26ecZULd/dTkWWdnvfhRqttsSQ9Pe/XQdi5+hDQ6RYcg==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - libc: [musl] '@ast-grep/napi-win32-arm64-msvc@0.40.5': resolution: {integrity: sha512-y95zSEwc7vhxmcrcH0GnK4ZHEBQrmrszRBNQovzaciF9GUqEcCACNLoBesn4V47IaOp4fYgD2/EhGRTIBFb2Ug==} @@ -288,28 +284,24 @@ packages: engines: {node: '>=14.21.3'} cpu: [arm64] os: [linux] - libc: [musl] '@biomejs/cli-linux-arm64@1.9.4': resolution: {integrity: sha512-fJIW0+LYujdjUgJJuwesP4EjIBl/N/TcOX3IvIHJQNsAqvV2CHIogsmA94BPG6jZATS4Hi+xv4SkBBQSt1N4/g==} engines: {node: '>=14.21.3'} cpu: [arm64] os: [linux] - libc: [glibc] '@biomejs/cli-linux-x64-musl@1.9.4': resolution: {integrity: sha512-gEhi/jSBhZ2m6wjV530Yy8+fNqG8PAinM3oV7CyO+6c3CEh16Eizm21uHVsyVBEB6RIM8JHIl6AGYCv6Q6Q9Tg==} engines: {node: '>=14.21.3'} cpu: [x64] os: [linux] - libc: [musl] '@biomejs/cli-linux-x64@1.9.4': resolution: {integrity: sha512-lRCJv/Vi3Vlwmbd6K+oQ0KhLHMAysN8lXoCI7XeHlxaajk06u7G+UsFSO01NAs5iYuWKmVZjmiOzJ0OJmGsMwg==} engines: {node: '>=14.21.3'} cpu: [x64] os: [linux] - libc: [glibc] '@biomejs/cli-win32-arm64@1.9.4': resolution: {integrity: sha512-tlbhLk+WXZmgwoIKwHIHEBZUwxml7bRJgk0X2sPyNR3S93cdRq6XulAZRQJ17FYGGzWne0fgrXBKpl7l4M87Hg==} @@ -1128,42 +1120,36 @@ packages: engines: {node: '>= 10.0.0'} cpu: [arm] os: [linux] - libc: [glibc] '@parcel/watcher-linux-arm-musl@2.5.6': resolution: {integrity: sha512-Ve3gUCG57nuUUSyjBq/MAM0CzArtuIOxsBdQ+ftz6ho8n7s1i9E1Nmk/xmP323r2YL0SONs1EuwqBp2u1k5fxg==} engines: {node: '>= 10.0.0'} cpu: [arm] os: [linux] - libc: [musl] '@parcel/watcher-linux-arm64-glibc@2.5.6': resolution: {integrity: sha512-f2g/DT3NhGPdBmMWYoxixqYr3v/UXcmLOYy16Bx0TM20Tchduwr4EaCbmxh1321TABqPGDpS8D/ggOTaljijOA==} engines: {node: '>= 10.0.0'} cpu: [arm64] os: [linux] - libc: [glibc] '@parcel/watcher-linux-arm64-musl@2.5.6': resolution: {integrity: sha512-qb6naMDGlbCwdhLj6hgoVKJl2odL34z2sqkC7Z6kzir8b5W65WYDpLB6R06KabvZdgoHI/zxke4b3zR0wAbDTA==} engines: {node: '>= 10.0.0'} cpu: [arm64] os: [linux] - libc: [musl] '@parcel/watcher-linux-x64-glibc@2.5.6': resolution: {integrity: sha512-kbT5wvNQlx7NaGjzPFu8nVIW1rWqV780O7ZtkjuWaPUgpv2NMFpjYERVi0UYj1msZNyCzGlaCWEtzc+exjMGbQ==} engines: {node: '>= 10.0.0'} cpu: [x64] os: [linux] - libc: [glibc] '@parcel/watcher-linux-x64-musl@2.5.6': resolution: {integrity: sha512-1JRFeC+h7RdXwldHzTsmdtYR/Ku8SylLgTU/reMuqdVD7CtLwf0VR1FqeprZ0eHQkO0vqsbvFLXUmYm/uNKJBg==} engines: {node: '>= 10.0.0'} cpu: [x64] os: [linux] - libc: [musl] '@parcel/watcher-win32-arm64@2.5.6': resolution: {integrity: sha512-3ukyebjc6eGlw9yRt678DxVF7rjXatWiHvTXqphZLvo7aC5NdEgFufVwjFfY51ijYEWpXbqF5jtrK275z52D4Q==} @@ -1640,79 +1626,66 @@ packages: resolution: {integrity: sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==} cpu: [arm] os: [linux] - libc: [glibc] '@rollup/rollup-linux-arm-musleabihf@4.59.0': resolution: {integrity: sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==} cpu: [arm] os: [linux] - libc: [musl] '@rollup/rollup-linux-arm64-gnu@4.59.0': resolution: {integrity: sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==} cpu: [arm64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-arm64-musl@4.59.0': resolution: {integrity: sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==} cpu: [arm64] os: [linux] - libc: [musl] '@rollup/rollup-linux-loong64-gnu@4.59.0': resolution: {integrity: sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==} cpu: [loong64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-loong64-musl@4.59.0': resolution: {integrity: sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==} cpu: [loong64] os: [linux] - libc: [musl] '@rollup/rollup-linux-ppc64-gnu@4.59.0': resolution: {integrity: sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==} cpu: [ppc64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-ppc64-musl@4.59.0': resolution: {integrity: sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==} cpu: [ppc64] os: [linux] - libc: [musl] '@rollup/rollup-linux-riscv64-gnu@4.59.0': resolution: {integrity: sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==} cpu: [riscv64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-riscv64-musl@4.59.0': resolution: {integrity: sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==} cpu: [riscv64] os: [linux] - libc: [musl] '@rollup/rollup-linux-s390x-gnu@4.59.0': resolution: {integrity: sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==} cpu: [s390x] os: [linux] - libc: [glibc] '@rollup/rollup-linux-x64-gnu@4.59.0': resolution: {integrity: sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==} cpu: [x64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-x64-musl@4.59.0': resolution: {integrity: sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==} cpu: [x64] os: [linux] - libc: [musl] '@rollup/rollup-openbsd-x64@4.59.0': resolution: {integrity: sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==} @@ -3388,7 +3361,6 @@ packages: engines: {node: '>= 10'} cpu: [arm64] os: [linux] - libc: [glibc] napi-maybe-compressed-blob-linux-x64-gnu@0.0.11: resolution: {integrity: sha512-JKY8KcZpQtKiL1smMKfukcOmsDVeZaw9fKXKsWC+wySc2wsvH7V2wy8PffSQ0lWERkI7Yn3k7xPjB463m/VNtg==} @@ -5571,7 +5543,7 @@ snapshots: '@effect/sql': 0.48.6(@effect/experimental@0.57.11(@effect/platform@0.93.8(effect@3.19.19))(effect@3.19.19))(@effect/platform@0.93.8(effect@3.19.19))(effect@3.19.19) '@effect/workflow': 0.15.2(@effect/experimental@0.57.11(@effect/platform@0.93.8(effect@3.19.19))(effect@3.19.19))(@effect/platform@0.93.8(effect@3.19.19))(@effect/rpc@0.72.2(@effect/platform@0.93.8(effect@3.19.19))(effect@3.19.19))(effect@3.19.19) '@inquirer/prompts': 8.3.0(@types/node@25.3.5) - '@moonwall/types': 5.18.3(@polkadot/api-base@16.5.4)(@polkadot/api@16.5.4)(@polkadot/keyring@14.0.1(@polkadot/util-crypto@14.0.1(@polkadot/util@14.0.1))(@polkadot/util@14.0.1))(@polkadot/types@16.5.4)(@polkadot/util-crypto@14.0.1(@polkadot/util@14.0.1))(@polkadot/util@14.0.1)(@types/debug@4.1.12)(@vitest/ui@3.2.4)(chokidar@3.6.0)(encoding@0.1.13)(jsdom@23.2.0)(postcss@8.5.8)(rxjs@7.8.2)(tsx@4.21.0)(typescript@5.8.3)(yaml@2.8.2)(zod@3.25.76) + '@moonwall/types': 5.18.3(@polkadot/api-base@16.5.4)(@polkadot/api@16.5.4)(@polkadot/keyring@14.0.1(@polkadot/util-crypto@14.0.1(@polkadot/util@14.0.1))(@polkadot/util@14.0.1))(@polkadot/types@16.5.4)(@polkadot/util-crypto@14.0.1(@polkadot/util@14.0.1))(@polkadot/util@14.0.1)(@types/debug@4.1.12)(@vitest/ui@3.2.4(vitest@3.2.4(@types/debug@4.1.12)(@types/node@25.3.5)(@vitest/ui@3.1.3)(jsdom@23.2.0)(tsx@4.21.0)(yaml@2.8.2)))(chokidar@3.6.0)(encoding@0.1.13)(jsdom@23.2.0)(postcss@8.5.8)(rxjs@7.8.2)(tsx@4.21.0)(typescript@5.8.3)(yaml@2.8.2)(zod@3.25.76) '@moonwall/util': 5.18.3(@polkadot/api-base@16.5.4)(@polkadot/api-derive@16.5.4)(@polkadot/api@16.5.4)(@polkadot/keyring@14.0.1(@polkadot/util-crypto@14.0.1(@polkadot/util@14.0.1))(@polkadot/util@14.0.1))(@polkadot/rpc-provider@16.5.4)(@polkadot/types-codec@16.5.4)(@polkadot/types@16.5.4)(@polkadot/util-crypto@14.0.1(@polkadot/util@14.0.1))(@polkadot/util@14.0.1)(@types/debug@4.1.12)(@types/node@25.3.5)(chokidar@3.6.0)(encoding@0.1.13)(jsdom@23.2.0)(postcss@8.5.8)(rxjs@7.8.2)(tsx@4.21.0)(typescript@5.8.3)(yaml@2.8.2)(zod@3.25.76) '@octokit/rest': 22.0.1 '@polkadot/api': 16.5.4 @@ -5584,7 +5556,7 @@ snapshots: '@polkadot/util-crypto': 14.0.1(@polkadot/util@14.0.1) '@types/react': 19.2.7 '@types/tmp': 0.2.6 - '@vitest/ui': 3.2.4(vitest@3.2.4) + '@vitest/ui': 3.2.4(vitest@3.2.4(@types/debug@4.1.12)(@types/node@25.3.5)(@vitest/ui@3.1.3)(jsdom@23.2.0)(tsx@4.21.0)(yaml@2.8.2)) '@zombienet/orchestrator': 0.0.113(@polkadot/util@14.0.1)(@types/node@25.3.5)(chokidar@3.6.0) '@zombienet/utils': 0.0.30(@types/node@25.3.5)(chokidar@3.6.0)(typescript@5.8.3) arkregex: 0.0.4 @@ -5609,7 +5581,7 @@ snapshots: tiny-invariant: 1.3.3 tmp: 0.2.5 viem: 2.41.2(typescript@5.8.3)(zod@3.25.76) - vitest: 3.2.4(@types/debug@4.1.12)(@types/node@25.3.5)(@vitest/ui@3.2.4)(jsdom@23.2.0)(tsx@4.21.0)(yaml@2.8.2) + vitest: 3.2.4(@types/debug@4.1.12)(@types/node@25.3.5)(@vitest/ui@3.2.4(vitest@3.2.4(@types/debug@4.1.12)(@types/node@25.3.5)(@vitest/ui@3.1.3)(jsdom@23.2.0)(tsx@4.21.0)(yaml@2.8.2)))(jsdom@23.2.0)(tsx@4.21.0)(yaml@2.8.2) web3: 4.16.0(encoding@0.1.13)(typescript@5.8.3)(zod@3.25.76) web3-providers-ws: 4.0.8 ws: 8.19.0 @@ -5667,7 +5639,7 @@ snapshots: - utf-8-validate - zod - '@moonwall/types@5.18.3(@polkadot/api-base@16.5.4)(@polkadot/api@16.5.4)(@polkadot/keyring@14.0.1(@polkadot/util-crypto@14.0.1(@polkadot/util@14.0.1))(@polkadot/util@14.0.1))(@polkadot/types@16.5.4)(@polkadot/util-crypto@14.0.1(@polkadot/util@14.0.1))(@polkadot/util@14.0.1)(@types/debug@4.1.12)(@vitest/ui@3.2.4)(chokidar@3.6.0)(encoding@0.1.13)(jsdom@23.2.0)(postcss@8.5.8)(rxjs@7.8.2)(tsx@4.21.0)(typescript@5.8.3)(yaml@2.8.2)(zod@3.25.76)': + '@moonwall/types@5.18.3(@polkadot/api-base@16.5.4)(@polkadot/api@16.5.4)(@polkadot/keyring@14.0.1(@polkadot/util-crypto@14.0.1(@polkadot/util@14.0.1))(@polkadot/util@14.0.1))(@polkadot/types@16.5.4)(@polkadot/util-crypto@14.0.1(@polkadot/util@14.0.1))(@polkadot/util@14.0.1)(@types/debug@4.1.12)(@vitest/ui@3.2.4(vitest@3.2.4(@types/debug@4.1.12)(@types/node@25.3.5)(@vitest/ui@3.1.3)(jsdom@23.2.0)(tsx@4.21.0)(yaml@2.8.2)))(chokidar@3.6.0)(encoding@0.1.13)(jsdom@23.2.0)(postcss@8.5.8)(rxjs@7.8.2)(tsx@4.21.0)(typescript@5.8.3)(yaml@2.8.2)(zod@3.25.76)': dependencies: '@polkadot/api': 16.5.4 '@polkadot/api-base': 16.5.4 @@ -5681,7 +5653,7 @@ snapshots: ethers: 6.16.0 polkadot-api: 1.19.2(postcss@8.5.8)(rxjs@7.8.2)(tsx@4.21.0)(yaml@2.8.2) viem: 2.41.2(typescript@5.8.3)(zod@3.25.76) - vitest: 3.2.4(@types/debug@4.1.12)(@types/node@24.12.0)(@vitest/ui@3.2.4)(jsdom@23.2.0)(tsx@4.21.0)(yaml@2.8.2) + vitest: 3.2.4(@types/debug@4.1.12)(@types/node@24.12.0)(@vitest/ui@3.2.4(vitest@3.2.4(@types/debug@4.1.12)(@types/node@25.3.5)(@vitest/ui@3.1.3)(jsdom@23.2.0)(tsx@4.21.0)(yaml@2.8.2)))(jsdom@23.2.0)(tsx@4.21.0)(yaml@2.8.2) web3: 4.16.0(encoding@0.1.13)(typescript@5.8.3)(zod@3.25.76) transitivePeerDependencies: - '@edge-runtime/vm' @@ -5717,7 +5689,7 @@ snapshots: '@moonwall/util@5.18.3(@polkadot/api-base@16.5.4)(@polkadot/api-derive@16.5.4)(@polkadot/api@16.5.4)(@polkadot/keyring@14.0.1(@polkadot/util-crypto@14.0.1(@polkadot/util@14.0.1))(@polkadot/util@14.0.1))(@polkadot/rpc-provider@16.5.4)(@polkadot/types-codec@16.5.4)(@polkadot/types@16.5.4)(@polkadot/util-crypto@14.0.1(@polkadot/util@14.0.1))(@polkadot/util@14.0.1)(@types/debug@4.1.12)(@types/node@25.3.5)(chokidar@3.6.0)(encoding@0.1.13)(jsdom@23.2.0)(postcss@8.5.8)(rxjs@7.8.2)(tsx@4.21.0)(typescript@5.8.3)(yaml@2.8.2)(zod@3.25.76)': dependencies: '@inquirer/prompts': 8.3.0(@types/node@25.3.5) - '@moonwall/types': 5.18.3(@polkadot/api-base@16.5.4)(@polkadot/api@16.5.4)(@polkadot/keyring@14.0.1(@polkadot/util-crypto@14.0.1(@polkadot/util@14.0.1))(@polkadot/util@14.0.1))(@polkadot/types@16.5.4)(@polkadot/util-crypto@14.0.1(@polkadot/util@14.0.1))(@polkadot/util@14.0.1)(@types/debug@4.1.12)(@vitest/ui@3.2.4)(chokidar@3.6.0)(encoding@0.1.13)(jsdom@23.2.0)(postcss@8.5.8)(rxjs@7.8.2)(tsx@4.21.0)(typescript@5.8.3)(yaml@2.8.2)(zod@3.25.76) + '@moonwall/types': 5.18.3(@polkadot/api-base@16.5.4)(@polkadot/api@16.5.4)(@polkadot/keyring@14.0.1(@polkadot/util-crypto@14.0.1(@polkadot/util@14.0.1))(@polkadot/util@14.0.1))(@polkadot/types@16.5.4)(@polkadot/util-crypto@14.0.1(@polkadot/util@14.0.1))(@polkadot/util@14.0.1)(@types/debug@4.1.12)(@vitest/ui@3.2.4(vitest@3.2.4(@types/debug@4.1.12)(@types/node@25.3.5)(@vitest/ui@3.1.3)(jsdom@23.2.0)(tsx@4.21.0)(yaml@2.8.2)))(chokidar@3.6.0)(encoding@0.1.13)(jsdom@23.2.0)(postcss@8.5.8)(rxjs@7.8.2)(tsx@4.21.0)(typescript@5.8.3)(yaml@2.8.2)(zod@3.25.76) '@polkadot/api': 16.5.4 '@polkadot/api-derive': 16.5.4 '@polkadot/keyring': 14.0.1(@polkadot/util-crypto@14.0.1(@polkadot/util@14.0.1))(@polkadot/util@14.0.1) @@ -5726,7 +5698,7 @@ snapshots: '@polkadot/types-codec': 16.5.4 '@polkadot/util': 14.0.1 '@polkadot/util-crypto': 14.0.1(@polkadot/util@14.0.1) - '@vitest/ui': 3.2.4(vitest@3.2.4) + '@vitest/ui': 3.2.4(vitest@3.2.4(@types/debug@4.1.12)(@types/node@25.3.5)(@vitest/ui@3.1.3)(jsdom@23.2.0)(tsx@4.21.0)(yaml@2.8.2)) arkregex: 0.0.4 bottleneck: 2.19.5 chalk: 5.6.2 @@ -5740,7 +5712,7 @@ snapshots: semver: 7.7.4 tiny-invariant: 1.3.3 viem: 2.41.2(typescript@5.8.3)(zod@3.25.76) - vitest: 3.2.4(@types/debug@4.1.12)(@types/node@25.3.5)(@vitest/ui@3.2.4)(jsdom@23.2.0)(tsx@4.21.0)(yaml@2.8.2) + vitest: 3.2.4(@types/debug@4.1.12)(@types/node@25.3.5)(@vitest/ui@3.2.4(vitest@3.2.4(@types/debug@4.1.12)(@types/node@25.3.5)(@vitest/ui@3.1.3)(jsdom@23.2.0)(tsx@4.21.0)(yaml@2.8.2)))(jsdom@23.2.0)(tsx@4.21.0)(yaml@2.8.2) web3: 4.16.0(encoding@0.1.13)(typescript@5.8.3)(zod@3.25.76) ws: 8.19.0 yargs: 18.0.0 @@ -7135,7 +7107,7 @@ snapshots: tinyrainbow: 2.0.0 vitest: 3.2.4(@types/debug@4.1.12)(@types/node@25.3.5)(@vitest/ui@3.1.3)(jsdom@23.2.0)(tsx@4.21.0)(yaml@2.8.2) - '@vitest/ui@3.2.4(vitest@3.2.4)': + '@vitest/ui@3.2.4(vitest@3.2.4(@types/debug@4.1.12)(@types/node@25.3.5)(@vitest/ui@3.1.3)(jsdom@23.2.0)(tsx@4.21.0)(yaml@2.8.2))': dependencies: '@vitest/utils': 3.2.4 fflate: 0.8.2 @@ -9965,7 +9937,7 @@ snapshots: tsx: 4.21.0 yaml: 2.8.2 - vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.12.0)(@vitest/ui@3.2.4)(jsdom@23.2.0)(tsx@4.21.0)(yaml@2.8.2): + vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.12.0)(@vitest/ui@3.2.4(vitest@3.2.4(@types/debug@4.1.12)(@types/node@25.3.5)(@vitest/ui@3.1.3)(jsdom@23.2.0)(tsx@4.21.0)(yaml@2.8.2)))(jsdom@23.2.0)(tsx@4.21.0)(yaml@2.8.2): dependencies: '@types/chai': 5.2.3 '@vitest/expect': 3.2.4 @@ -9993,7 +9965,7 @@ snapshots: optionalDependencies: '@types/debug': 4.1.12 '@types/node': 24.12.0 - '@vitest/ui': 3.2.4(vitest@3.2.4) + '@vitest/ui': 3.2.4(vitest@3.2.4(@types/debug@4.1.12)(@types/node@25.3.5)(@vitest/ui@3.1.3)(jsdom@23.2.0)(tsx@4.21.0)(yaml@2.8.2)) jsdom: 23.2.0 transitivePeerDependencies: - jiti @@ -10053,7 +10025,7 @@ snapshots: - tsx - yaml - vitest@3.2.4(@types/debug@4.1.12)(@types/node@25.3.5)(@vitest/ui@3.2.4)(jsdom@23.2.0)(tsx@4.21.0)(yaml@2.8.2): + vitest@3.2.4(@types/debug@4.1.12)(@types/node@25.3.5)(@vitest/ui@3.2.4(vitest@3.2.4(@types/debug@4.1.12)(@types/node@25.3.5)(@vitest/ui@3.1.3)(jsdom@23.2.0)(tsx@4.21.0)(yaml@2.8.2)))(jsdom@23.2.0)(tsx@4.21.0)(yaml@2.8.2): dependencies: '@types/chai': 5.2.3 '@vitest/expect': 3.2.4 @@ -10081,7 +10053,7 @@ snapshots: optionalDependencies: '@types/debug': 4.1.12 '@types/node': 25.3.5 - '@vitest/ui': 3.2.4(vitest@3.2.4) + '@vitest/ui': 3.2.4(vitest@3.2.4(@types/debug@4.1.12)(@types/node@25.3.5)(@vitest/ui@3.1.3)(jsdom@23.2.0)(tsx@4.21.0)(yaml@2.8.2)) jsdom: 23.2.0 transitivePeerDependencies: - jiti diff --git a/ts-tests/scripts/gen-chopsticks-fork.ts b/ts-tests/scripts/gen-chopsticks-fork.ts new file mode 100644 index 0000000000..4df1fc670e --- /dev/null +++ b/ts-tests/scripts/gen-chopsticks-fork.ts @@ -0,0 +1,465 @@ +#!/usr/bin/env tsx +/** + * Generates a slim Chopsticks config that forks finney for a SINGLE netuid. + * + * Vanilla Chopsticks forking of finney is unusably slow (~15 min) because the + * `prefetch-storages` list makes Chopsticks lazily live-fetch enormous maps + * (Keys, StakingHotkeys, ...) the first time block production touches them. + * + * Given an input Chopsticks YAML with a `prefetch-storages` list, this script: + * 1. Connects to the configured WS endpoints (a finney archive node). + * 2. Finds the first non-root registered netuid (or uses --netuid ). + * 3. For each prefetch storage item, introspects its key structure via metadata + * and fetches only data for that netuid from the live chain. + * 4. Writes a new Chopsticks YAML with those values baked into import-storage and + * NO prefetch-storages, plus a self-contained `block:` field — so Chopsticks + * starts in ~1 min and only processes one subnet. + * + * It also zeroes emission-flow storages so the forked subnet's alpha price does + * not drift during a test (mirrors the upstream relayer generator). + * + * Output: and a sibling with {blockNumber, netuid, hotkey}. + * + * This is run by moonwall as a pre-test step (the chopsticks_fork env's `runScripts`), + * so it regenerates the slim config from the live chain on every `moonwall test` run. + * + * The subnet selection flags also have env equivalents, because moonwall's static + * `runScripts` entry can't take CLI args at `moonwall test` time: + * --netuid | FORK_NETUID= + * --random-subnet | FORK_RANDOM_SUBNET=1 + * + * Usage: + * tsx scripts/gen-chopsticks-fork.ts [--netuid ] [--random-subnet] + */ + +import { ApiPromise, WsProvider } from "@polkadot/api"; +import { readFileSync, writeFileSync } from "node:fs"; + +// ── CLI args (subnet selection also reads env, for the moonwall runScripts path) ─ + +const args = process.argv.slice(2); +const inputPath = args[0]; +const outputPath = args[1]; + +const truthy = (v: string | undefined): boolean => v === "1" || v === "true"; + +const netuidFlagIdx = args.indexOf("--netuid"); +const forcedNetuid = + netuidFlagIdx !== -1 + ? Number.parseInt(args[netuidFlagIdx + 1], 10) + : process.env.FORK_NETUID + ? Number.parseInt(process.env.FORK_NETUID, 10) + : null; +const randomSubnet = args.includes("--random-subnet") || truthy(process.env.FORK_RANDOM_SUBNET); + +if (!inputPath || !outputPath) { + console.error( + "Usage: tsx scripts/gen-chopsticks-fork.ts [--netuid ] [--random-subnet]" + ); + process.exit(1); +} + +const metaPath = outputPath.replace(/\.yml$/, ".meta.json"); + +// ── Minimal YAML parser (only what we need from the chopsticks config) ──────── + +function parseSimpleYaml(text: string): { endpoints: string[]; prefetchStorages: string[] } { + // We only need `endpoint` and `prefetch-storages` from the input. + const endpoints: string[] = []; + const prefetchStorages: string[] = []; + + let inEndpoint = false; + let inPrefetch = false; + + for (const raw of text.split("\n")) { + const line = raw.replace(/#.*$/, "").trimEnd(); + if (!line.trim()) { + inEndpoint = false; + inPrefetch = false; + continue; + } + + if (/^endpoint:/.test(line)) { + inEndpoint = true; + inPrefetch = false; + const inline = line.replace(/^endpoint:\s*/, "").trim(); + if (inline && !inline.startsWith("-")) endpoints.push(inline.replace(/^['"]|['"]$/g, "")); + continue; + } + if (/^prefetch-storages:/.test(line)) { + inPrefetch = true; + inEndpoint = false; + continue; + } + if (/^[a-zA-Z]/.test(line)) { + inEndpoint = false; + inPrefetch = false; + } + + if (inEndpoint && /^\s*-\s/.test(line)) { + endpoints.push( + line + .replace(/^\s*-\s*/, "") + .replace(/^['"]|['"]$/g, "") + .trim() + ); + } + if (inPrefetch && /^\s*-\s/.test(line)) { + prefetchStorages.push(line.replace(/^\s*-\s*/, "").trim()); + } + } + + return { endpoints, prefetchStorages }; +} + +// ── Storage introspection ───────────────────────────────────────────────────── + +type StorageKind = { kind: "plain" } | { kind: "map"; keyCount: number }; + +function getStorageKind(api: ApiPromise, pallet: string, name: string): StorageKind { + const query = (api.query as any)[pallet]?.[name]; + if (!query) throw new Error(`Storage not found: ${pallet}.${name}`); + + const meta = query.creator.meta; + if (meta.type.isPlain) return { kind: "plain" }; + if (meta.type.isMap) return { kind: "map", keyCount: meta.type.asMap.hashers.length }; + return { kind: "plain" }; +} + +// ── Value serialisation ─────────────────────────────────────────────────────── + +// Convert a polkadot-js Codec to a YAML-safe primitive. +function toYamlValue(codec: any): unknown { + // toJSON gives human-readable output; large integers come back as hex strings. + return codec.toJSON(); +} + +// ── YAML emission helpers ───────────────────────────────────────────────────── + +function indent(n: number): string { + return " ".repeat(n); +} + +// Serialise a JS value to inline YAML (single line, no block scalars). +function yamlInline(val: unknown): string { + if (val === null) return "null"; + if (val === true) return "true"; + if (val === false) return "false"; + if (typeof val === "number") return String(val); + if (typeof val === "bigint") return String(val); + if (typeof val === "string") { + if (/^(true|false|null|~|\d)/.test(val) || /[:#\[\]{},&*!|>'"%@`]/.test(val) || val.includes("\n")) { + return JSON.stringify(val); + } + return val; + } + if (Array.isArray(val)) { + return `[${val.map(yamlInline).join(", ")}]`; + } + if (typeof val === "object" && val !== null) { + const pairs = Object.entries(val as Record) + .map(([k, v]) => `${k}: ${yamlInline(v)}`) + .join(", "); + return `{${pairs}}`; + } + return String(val); +} + +// Emit a map entry with any number of keys: `- [[key1, key2, ...], value]` +function emitEntry(depth: number, keys: unknown[], value: unknown): string { + const lines = keys.map((k, i) => + i === 0 ? `${indent(depth)}- - - ${yamlInline(k)}` : `${indent(depth)} - ${yamlInline(k)}` + ); + lines.push(`${indent(depth)} - ${yamlInline(value)}`); + return lines.join("\n"); +} + +// ── Fetch helpers ───────────────────────────────────────────────────────────── + +async function fetchStorageEntries( + api: ApiPromise, + pallet: string, + name: string, + netuid: number, + kind: StorageKind +): Promise { + const query = (api.query as any)[pallet][name]; + const lines: string[] = []; + + if (kind.kind === "plain") { + // Plain storage is a global scalar — not expressible as an iterable map entry. + // Skip it; Chopsticks falls back to the remote fork value for these items. + return []; + } + + if (kind.keyCount === 1) { + // Zero out emission flow storage so tests don't inherit mainnet emission rates. + if (name === "subnetTaoFlow") { + // Zero raw flow so the subnet gets zero share of block emission. + lines.push(`${indent(2)}${name}:`); + lines.push(emitEntry(3, [netuid], 0)); + return lines; + } + if (name === "subnetEmaTaoFlow") { + // Empty array + $removePrefix removes all stored EMA entries so historical + // EMA can't drive emissions during the test. + lines.push(`${indent(2)}${name}: []`); + return lines; + } + if (name === "firstEmissionBlockNumber") { + // Clearing this (None) makes the subnet fail get_subnets_to_emit_to, so no + // fresh TAO block emission is assigned and the per-block swap path never fires. + lines.push(`${indent(2)}${name}: []`); + return lines; + } + if ( + name === "pendingServerEmission" || + name === "pendingValidatorEmission" || + name === "pendingOwnerCut" || + name === "pendingRootAlphaDivs" + ) { + // Zero queued alpha emissions snapshotted from mainnet so on_initialize + // doesn't distribute them on the first block and push the price up. + lines.push(`${indent(2)}${name}:`); + lines.push(emitEntry(3, [netuid], 0)); + return lines; + } + lines.push(`${indent(2)}${name}:`); + lines.push(emitEntry(3, [netuid], toYamlValue(await query(netuid)))); + return lines; + } + + if (name === "lastRateLimitedBlock") { + // Keyed by RateLimitKey enum, not by netuid. Migrations iterate iter_keys() + // over every entry (one per staker) causing chopsticks to live-fetch them all. + // Seeding empty makes the migration's iter_keys() return nothing instantly. + lines.push(`${indent(2)}${name}: []`); + return lines; + } + + // keyCount >= 2: prefix scan on first key = netuid; use all decoded args as keys + const entries: [any, any][] = await query.entries(netuid); + if (entries.length === 0) { + // An empty iterable array is required — a bare key with no value becomes YAML + // null and Chopsticks crashes with "storage is not iterable". + lines.push(`${indent(2)}${name}: []`); + return lines; + } + lines.push(`${indent(2)}${name}:`); + for (const [storageKey, val] of entries) { + const keys = (storageKey.args as any[]).map((a) => toYamlValue(a)); + lines.push(emitEntry(3, keys, toYamlValue(val))); + } + return lines; +} + +// ── Main ────────────────────────────────────────────────────────────────────── + +async function main() { + const inputText = readFileSync(inputPath, "utf-8"); + const { endpoints, prefetchStorages } = parseSimpleYaml(inputText); + + if (endpoints.length === 0) throw new Error("No endpoints found in input YAML"); + if (prefetchStorages.length === 0) throw new Error("No prefetch-storages found in input YAML"); + + console.log(`Endpoints: ${endpoints.join(", ")}`); + console.log(`Prefetch items: ${prefetchStorages.length}`); + + // Connect — try each endpoint in order until one works + let api: ApiPromise | null = null; + let blockNumber = 0; + for (const ep of endpoints) { + process.stdout.write(`Connecting to ${ep}...`); + const provider = new WsProvider(ep, 0); + try { + await new Promise((res, rej) => { + const timer = setTimeout(() => rej(new Error("timeout")), 15_000); + provider.on("connected", () => { + clearTimeout(timer); + res(); + }); + provider.on("error", () => { + clearTimeout(timer); + rej(new Error("ws error")); + }); + provider.connect().catch(rej); + }); + api = await ApiPromise.create({ provider, noInitWarn: true }); + const header = await api.rpc.chain.getHeader(); + blockNumber = header.number.toNumber(); + console.log(" connected"); + break; + } catch { + console.log(" failed, trying next"); + await provider.disconnect().catch(() => {}); + } + } + if (!api) throw new Error("Could not connect to any endpoint"); + + // Find target netuid + let netuid: number; + if (forcedNetuid !== null) { + netuid = forcedNetuid; + console.log(`Using forced netuid=${netuid}`); + } else { + const entries = (await (api.query.subtensorModule as any).networksAdded.entries()) as any[]; + const netuids = entries + .filter(([, v]: [any, any]) => v.toPrimitive() === true) + .map(([k]: [any, any]) => k.args[0].toNumber() as number) + .filter((n: number) => n > 0) + .sort((a: number, b: number) => a - b); + if (netuids.length === 0) throw new Error("No non-root subnets found"); + if (randomSubnet) { + netuid = netuids[Math.floor(Math.random() * netuids.length)]; + console.log(`Picked netuid=${netuid} (random, from ${netuids.length} non-root subnets)`); + } else { + netuid = netuids[0]; + console.log(`Picked netuid=${netuid} (first non-root)`); + } + } + + // Group prefetch items by pallet + const byPallet = new Map(); + for (const item of prefetchStorages) { + const dot = item.indexOf("."); + if (dot === -1) { + console.warn(`Skipping malformed prefetch item: ${item}`); + continue; + } + const pallet = item.slice(0, dot); + const name = item.slice(dot + 1); + if (!byPallet.has(pallet)) byPallet.set(pallet, []); + byPallet.get(pallet)?.push(name); + } + + // Extract plain scalar overrides from the static import-storage for a pallet. + // These are lines like ` Key: value` that are NOT list entries or directives. + // We carry them into the generated section so they survive the prefetch rewrite. + function parsePlainOverrides(palletName: string): string[] { + const lines: string[] = []; + let inImportStorage = false; + let inTargetPallet = false; + for (const raw of inputText.split("\n")) { + const stripped = raw.replace(/#.*$/, "").trimEnd(); + if (/^import-storage:/.test(stripped)) { + inImportStorage = true; + continue; + } + if (!inImportStorage) continue; + if (/^[a-zA-Z]/.test(raw) && !/^\s/.test(raw)) { + inImportStorage = false; + continue; + } + if (!stripped.trim()) continue; + const palletMatch = raw.match(/^ {2}([A-Za-z][A-Za-z0-9]*):/); + if (palletMatch) { + inTargetPallet = palletMatch[1] === palletName; + continue; + } + if (!inTargetPallet) continue; + const m = stripped.match(/^ {4}([A-Za-z][A-Za-z0-9]*):\s*(\S.*)$/); + if (m && !/^[-\[{]/.test(m[2])) { + lines.push(`${indent(2)}${m[1]}: ${m[2]}`); + } + } + return lines; + } + + // Build import-storage sections per pallet + const palletSections: string[] = []; + for (const [pallet, names] of Array.from(byPallet)) { + const palletCamel = pallet.charAt(0).toLowerCase() + pallet.slice(1); + const mapNames: string[] = []; // names that are maps (need $removePrefix) + + const entryLines: string[] = []; + + for (const name of names) { + const nameCamel = name.charAt(0).toLowerCase() + name.slice(1); + process.stdout.write(` Fetching ${pallet}.${name}...`); + try { + const kind = getStorageKind(api, palletCamel, nameCamel); + if (kind.kind === "map") mapNames.push(name); + const lines = await fetchStorageEntries(api, palletCamel, nameCamel, netuid, kind); + entryLines.push(...lines); + console.log(` ok (${kind.kind}${kind.kind === "map" ? `, keys=${kind.keyCount}` : ""})`); + } catch (e) { + console.log(` SKIPPED (${(e as Error).message})`); + } + } + + const plainOverrides = parsePlainOverrides(pallet); + + const removePrefix = + mapNames.length > 0 + ? `${indent(2)}$removePrefix:\n${mapNames.map((n) => `${indent(3)}- ${n}`).join("\n")}\n` + : ""; + + palletSections.push( + `${indent(1)}${pallet}:\n${removePrefix}${plainOverrides.join("\n")}${plainOverrides.length > 0 ? "\n" : ""}${entryLines.join("\n")}` + ); + } + + // Preserve everything already in import-storage that isn't from prefetch pallets. + const prefetchPallets = new Set(Array.from(byPallet.keys())); + const staticImportLines: string[] = []; + let inImportStorage = false; + let currentPalletIsFromPrefetch = false; + + for (const raw of inputText.split("\n")) { + if (/^import-storage:/.test(raw)) { + inImportStorage = true; + continue; + } + if (inImportStorage) { + if (/^[a-zA-Z]/.test(raw) && !/^\s/.test(raw)) { + inImportStorage = false; + continue; + } + const palletMatch = raw.match(/^ {2}([A-Za-z][A-Za-z0-9]*):/); + if (palletMatch) { + currentPalletIsFromPrefetch = prefetchPallets.has(palletMatch[1]); + } + if (!currentPalletIsFromPrefetch) staticImportLines.push(raw); + } + } + + const outputLines = [ + "# Auto-generated by gen-chopsticks-fork.ts", + `# Source: ${inputPath}`, + `# Netuid: ${netuid}`, + "", + "endpoint:", + ...endpoints.map((e) => ` - ${e}`), + "", + // Pin the fork block so this config is self-contained (moonwall does not pass --block). + `block: ${blockNumber}`, + "", + "mock-signature-host: true", + "allow-unresolved-imports: true", + "", + "import-storage:", + ...staticImportLines.filter((l) => l.trim()), + ...palletSections, + "", + "# prefetch-storages intentionally omitted — all data is baked into import-storage above", + ]; + + const output = `${outputLines.join("\n")}\n`; + writeFileSync(outputPath, output); + console.log(`\nWritten to ${outputPath}`); + + // Pick an existing registered hotkey from UID 0 on the target subnet. + const hotkeyCodec = await (api.query.subtensorModule as any).keys(netuid, 0); + const hotkey: string = hotkeyCodec.toString(); + console.log(`Picked existing hotkey at uid=0: ${hotkey}`); + + writeFileSync(metaPath, JSON.stringify({ blockNumber, netuid, hotkey }, null, 2)); + console.log(`Written meta to ${metaPath}`); + + await api.disconnect(); +} + +main().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/ts-tests/suites/chopsticks_fork/README.md b/ts-tests/suites/chopsticks_fork/README.md new file mode 100644 index 0000000000..c7ab4f0ef0 --- /dev/null +++ b/ts-tests/suites/chopsticks_fork/README.md @@ -0,0 +1,94 @@ +# Chopsticks mainnet-fork tests + +These tests fork **live finney** with [Chopsticks](https://github.com/AcalaNetwork/chopsticks), +apply your locally-built runtime, and run extrinsics (currently an `addStake` and a +balance transfer) against +real mainnet state for a single subnet. + +Vanilla Chopsticks forking of finney is unusably slow (~15 min) because the chain's +storage maps are enormous and Chopsticks lazily live-fetches them during block +production. `scripts/gen-chopsticks-fork.ts` works around this: it connects to a +finney archive node, fetches storage **for one netuid only**, and bakes it into a +slim, self-contained Chopsticks config that forks in ~1 min. + +## Prerequisites + +1. **Build the runtime wasm** (non-fast / release profile). Building the node also + produces the runtime wasm as a byproduct: + + ```bash + cargo build --release -p node-subtensor + ``` + + The fork uses `target/release/wbuild/node-subtensor-runtime/node_subtensor_runtime.compact.compressed.wasm` + via `--wasm-override` (see `moonwall.config.json` → env `chopsticks_fork`). + +2. **Network access** to a finney archive endpoint (configured in + `configs/chopsticks-fork.yml`). This is why the suite is **not** part of the CI + matrix — run it locally / on demand. + +## Run + +```bash +cd ts-tests +pnpm install + +# Regenerates the slim config from the live chain, then runs the suite: +pnpm moonwall test chopsticks_fork +``` + +Moonwall runs the config generator as a **pre-test step** (the env's `runScripts`) +before the fork is launched, so the config is rebuilt fresh on every run and every +test in the suite shares it: + +1. **Pre-test (moonwall `runScripts`):** `gen-chopsticks-fork.ts configs/chopsticks-fork.yml tmp/chopsticks-fork-slim.yml` + - Connects to the live chain and writes `tmp/chopsticks-fork-slim.yml` (slim config, + fork `block:` pinned) and `tmp/chopsticks-fork-slim.meta.json` + (`{ blockNumber, netuid, hotkey }`). +2. **Launch + test (moonwall):** starts Chopsticks from the slim config with + `--wasm-override`, then runs the tests in this directory. + +### Targeting a specific subnet + +By default the generator picks the first non-root registered netuid. The selection +flags also have env equivalents, because moonwall's `runScripts` entry is static and +can't take CLI args at `moonwall test` time: + +| Flag (manual generator run) | Env (moonwall pre-test path) | +|---|---| +| `--netuid ` | `FORK_NETUID=` | +| `--random-subnet` | `FORK_RANDOM_SUBNET=1` | + +```bash +# Drive the moonwall pre-test step via env in one shot: +FORK_NETUID=5 pnpm moonwall test chopsticks_fork + +# Or generate manually first (e.g. to inspect the config), then run: +pnpm tsx scripts/gen-chopsticks-fork.ts configs/chopsticks-fork.yml tmp/chopsticks-fork-slim.yml --netuid 5 +pnpm moonwall test chopsticks_fork +``` + +The test reads the chosen netuid + an existing registered hotkey from +`tmp/chopsticks-fork-slim.meta.json`, so it always matches the generated config. + +## How the runtime upgrade is applied + +The fork boots **directly on your locally-built runtime** via Chopsticks +`--wasm-override` (`launchSpec.wasmOverride`). This replaces `:code` in the forked +state with no on-chain `spec_version` check, and `Executive::on_runtime_upgrade` +runs any bundled migrations against real mainnet state on the first produced block. +This mirrors the proven approach used in the `relayer` repo. + +> **Alternative — real on-chain `setCode`:** moonwall's chopsticks context exposes +> `context.upgradeRuntime()`, which performs a true `system.applyAuthorizedUpgrade`. +> That path enforces `spec_version` **strictly greater** than mainnet's, so it only +> works when your branch has bumped `spec_version`. To use it, add +> `"rtUpgradePath": "../target/release/wbuild/.../node_subtensor_runtime.compact.compressed.wasm"` +> to the `chopsticks_fork` foundation, drop `wasmOverride`, and call +> `await context.upgradeRuntime()` in the test's `beforeAll`. + +## Adding tests + +Add more `*.ts` files to this directory using `foundationMethods: "chopsticks"`. +Useful context helpers: `context.polkadotJs()`, `context.createBlock()`, +`context.setStorage(...)`, `context.keyring.alice`. diff --git a/ts-tests/suites/chopsticks_fork/test-add-stake.ts b/ts-tests/suites/chopsticks_fork/test-add-stake.ts new file mode 100644 index 0000000000..5713fc2ba9 --- /dev/null +++ b/ts-tests/suites/chopsticks_fork/test-add-stake.ts @@ -0,0 +1,86 @@ +import { beforeAll, describeSuite, expect } from "@moonwall/cli"; +import type { KeyringPair } from "@moonwall/util"; +import type { ApiPromise } from "@polkadot/api"; +import { readFileSync } from "node:fs"; +import { resolve } from "node:path"; + +// ── Read meta written by scripts/gen-chopsticks-fork.ts ─────────────────────── +// +// The slim config and this meta file are produced by the generator before the +// chopsticks fork is launched (see `pnpm moonwall test chopsticks_fork`). The meta tells +// us which netuid was forked and an existing registered hotkey on that subnet. + +const meta = JSON.parse(readFileSync(resolve(process.cwd(), "tmp/chopsticks-fork-slim.meta.json"), "utf-8")) as { + blockNumber: number; + netuid: number; + hotkey: string; +}; + +const STAKE_AMOUNT = 200n * 1_000_000_000n; // 200 TAO (9 decimals) + +// AlphaV2 stake is stored as a U64F64 fixed-point ({ bits } or { mantissa, exponent } +// depending on metadata). For a before/after comparison we only need a monotonic +// integer, so collapse whatever representation comes back to a BigInt. +function alphaToBigint(value: any): bigint { + const json = value?.toJSON?.() ?? value; + if (json == null) return 0n; + if (typeof json === "number" || typeof json === "string") return BigInt(json); + if (typeof json === "object") { + if ("bits" in json) return BigInt(json.bits); + const mantissa = BigInt(json.mantissa ?? 0); + const exponent = BigInt(json.exponent ?? 0); + return exponent >= 0n ? mantissa * 10n ** exponent : mantissa / 10n ** -exponent; + } + return BigInt(json); +} + +async function getAlphaStake(api: ApiPromise, hotkey: string, coldkey: string, netuid: number): Promise { + const value = await (api.query.subtensorModule as any).alphaV2(hotkey, coldkey, netuid); + return alphaToBigint(value); +} + +describeSuite({ + id: "CHOP_FORK_ADD_STAKE", + title: "Chopsticks finney fork — runtime upgrade (wasm-override) + addStake", + foundationMethods: "chopsticks", + testCases: ({ it, context, log }) => { + let api: ApiPromise; + let alice: KeyringPair; + + beforeAll(async () => { + api = context.polkadotJs(); + alice = context.keyring.alice; + + log( + `Forked finney at block ${meta.blockNumber}, netuid=${meta.netuid}, ` + + `hotkey=${meta.hotkey}. Runtime version: ${api.runtimeVersion.specVersion.toString()}` + ); + + // The fork boots on the locally-built runtime via --wasm-override. Produce + // the first block so Executive::on_runtime_upgrade runs any migrations + // bundled in that runtime against the real mainnet state before we test. + await context.createBlock(); + }); + + it({ + id: "T01", + title: "addStake increases the coldkey's alpha stake on the forked subnet", + test: async () => { + const coldkey = alice.address; + + const stakeBefore = await getAlphaStake(api, meta.hotkey, coldkey, meta.netuid); + log(`alpha stake before: ${stakeBefore}`); + + await api.tx.subtensorModule.addStake(meta.hotkey, meta.netuid, STAKE_AMOUNT).signAndSend(alice); + + // Seal the block; createChopsticksBlock throws on any ExtrinsicFailed event. + await context.createBlock(); + + const stakeAfter = await getAlphaStake(api, meta.hotkey, coldkey, meta.netuid); + log(`alpha stake after: ${stakeAfter}`); + + expect(stakeAfter > stakeBefore, "addStake should increase alpha stake").toBe(true); + }, + }); + }, +}); diff --git a/ts-tests/suites/chopsticks_fork/test-balance-transfer.ts b/ts-tests/suites/chopsticks_fork/test-balance-transfer.ts new file mode 100644 index 0000000000..1289c9ff43 --- /dev/null +++ b/ts-tests/suites/chopsticks_fork/test-balance-transfer.ts @@ -0,0 +1,53 @@ +import { beforeAll, describeSuite, expect } from "@moonwall/cli"; +import type { KeyringPair } from "@moonwall/util"; +import type { ApiPromise } from "@polkadot/api"; + +// Second suite in the chopsticks_fork env. It shares the SAME pre-generated fork +// config (tmp/chopsticks-fork-slim.yml) that moonwall builds once via the env's +// `runScripts` step — so this file existing and passing alongside test-add-stake.ts +// confirms the pre-generation works and is reused across every test in the suite. +// +// Alice and Bob are both pre-funded in configs/chopsticks-fork.yml (import-storage). + +const TRANSFER_AMOUNT = 100n * 1_000_000_000n; // 100 TAO (9 decimals) + +async function getFreeBalance(api: ApiPromise, address: string): Promise { + const account = (await api.query.system.account(address)) as any; + return BigInt(account.data.free.toString()); +} + +describeSuite({ + id: "CHOP_FORK_BALANCE_TRANSFER", + title: "Chopsticks finney fork — balance transfer", + foundationMethods: "chopsticks", + testCases: ({ it, context, log }) => { + let api: ApiPromise; + let alice: KeyringPair; + let bob: KeyringPair; + + beforeAll(async () => { + api = context.polkadotJs(); + alice = context.keyring.alice; + bob = context.keyring.bob; + }); + + it({ + id: "T01", + title: "transferKeepAlive moves balance from Alice to Bob", + test: async () => { + const bobBefore = await getFreeBalance(api, bob.address); + log(`Bob free balance before: ${bobBefore}`); + + await api.tx.balances.transferKeepAlive(bob.address, TRANSFER_AMOUNT).signAndSend(alice); + + // Seal the block; createChopsticksBlock throws on any ExtrinsicFailed event. + await context.createBlock(); + + const bobAfter = await getFreeBalance(api, bob.address); + log(`Bob free balance after: ${bobAfter}`); + + expect(bobAfter - bobBefore, "Bob should receive exactly the transferred amount").toBe(TRANSFER_AMOUNT); + }, + }); + }, +});