diff --git a/.github/workflows/benchmark.yaml b/.github/workflows/benchmark.yaml index c6eea22c..18fa5466 100644 --- a/.github/workflows/benchmark.yaml +++ b/.github/workflows/benchmark.yaml @@ -64,6 +64,23 @@ on: description: The value to set to the `RUST_LOG` environment variable when the benchmarks run required: true default: info,alloy=off + profile-watched-txs: + description: Enable per-opcode profiling of sampled watched txs. Requires `network == westend`. + required: true + type: boolean + default: false + profile-samples-per-step-path: + description: Profiler — number of txs to sample per step path (K). K=3 picks first / middle / last + required: false + default: "3" + profile-concurrency: + description: Profiler — concurrent `trace_tx` RPC calls + required: false + default: "4" + profile-step-limit: + description: Profiler — per-tx tracer step cap. 0 disables the cap + required: false + default: "5000" override-repetition-count: type: number description: A way for the repetition count to be overridden. This is only used when we want to quickly run the CI to check if it works, please do not use it otherwise @@ -77,6 +94,28 @@ permissions: contents: write jobs: + # ============================================================================================ + # Validate Inputs: Fail fast on incompatible toggle combinations before any heavy build kicks off + # ============================================================================================ + + validate-inputs: + name: Validate Workflow Inputs + runs-on: ubuntu-latest + steps: + - name: Profiling requires the westend network + env: + PROFILE_ENABLED: ${{ inputs.profile-watched-txs }} + NETWORK: ${{ inputs.network }} + run: | + if [ "$PROFILE_ENABLED" = "true" ] && [ "$NETWORK" != "westend" ]; then + echo "::error::profile-watched-txs requires network=westend." \ + "The --cfg revive_debug gate that flips is_execution_tracing_enabled() on" \ + "at runtime only exists on asset-hub-westend-runtime;" \ + "asset-hub-polkadot has no equivalent gate, so profiling silently produces" \ + "empty traces. Re-run with network=westend, or disable profile-watched-txs." + exit 1 + fi + # ============================================================================================ # Build Jobs: Building the binaries we need and making sure that they're cached and available # ============================================================================================ @@ -84,6 +123,7 @@ jobs: build-retester: name: Build and Cache Retester Binaries runs-on: parity-large-persistent-new + needs: validate-inputs steps: - name: Checkout This Repository uses: actions/checkout@v4 @@ -105,6 +145,7 @@ jobs: build-chainspec-generator: name: Build and Cache Runtime's Chainspec Generator runs-on: parity-large-persistent-new + needs: validate-inputs steps: - name: Checkout This Repository uses: actions/checkout@v4 @@ -131,6 +172,7 @@ jobs: build-polkadot-sdk: name: Build and Cache the Polkadot SDK Dependencies runs-on: parity-large-persistent-new + needs: validate-inputs steps: - name: Checkout This Repository uses: actions/checkout@v4 @@ -150,12 +192,13 @@ jobs: polkadot-parachain-bin pallet-revive-eth-rpc env: | - ${{ inputs.use-jit && 'RUSTFLAGS=-Awarnings --cfg revive_jit' || 'RUSTFLAGS=-Awarnings' }} - WASM_BUILD_RUSTFLAGS=-Clink-arg=--allow-undefined + RUSTFLAGS=-Awarnings${{ inputs.use-jit && ' --cfg revive_jit' || '' }}${{ inputs.profile-watched-txs && ' --cfg revive_debug' || '' }} + WASM_BUILD_RUSTFLAGS=-Clink-arg=--allow-undefined${{ inputs.profile-watched-txs && ' --cfg revive_debug' || '' }} build-resolc: name: Build and Cache Resolc Binary runs-on: parity-large-persistent-new + needs: validate-inputs steps: - name: Checkout This Repository uses: actions/checkout@v4 @@ -291,8 +334,8 @@ jobs: polkadot-parachain-bin pallet-revive-eth-rpc env: | - ${{ inputs.use-jit && 'RUSTFLAGS=-Awarnings --cfg revive_jit' || 'RUSTFLAGS=-Awarnings' }} - WASM_BUILD_RUSTFLAGS=-Clink-arg=--allow-undefined + RUSTFLAGS=-Awarnings${{ inputs.use-jit && ' --cfg revive_jit' || '' }}${{ inputs.profile-watched-txs && ' --cfg revive_debug' || '' }} + WASM_BUILD_RUSTFLAGS=-Clink-arg=--allow-undefined${{ inputs.profile-watched-txs && ' --cfg revive_debug' || '' }} - name: Set artifact name id: names env: @@ -348,6 +391,10 @@ jobs: env: OVERRIDE_REPETITION_COUNT: ${{ inputs.override-repetition-count }} TEST_SPECIFIER: ${{ matrix.test.specifier }} + PROFILE_ENABLED: ${{ inputs.profile-watched-txs }} + PROFILE_SAMPLES: ${{ inputs.profile-samples-per-step-path }} + PROFILE_CONCURRENCY: ${{ inputs.profile-concurrency }} + PROFILE_STEP_LIMIT: ${{ inputs.profile-step-limit }} run: | export TMPDIR="$PWD/workdir" @@ -357,6 +404,15 @@ jobs: additional_args+=(--benchmark.repetition-count-override "$OVERRIDE_REPETITION_COUNT") fi + if [ "$PROFILE_ENABLED" = "true" ]; then + additional_args+=( + --benchmark.profile-watched-txs + --benchmark.profile-samples-per-step-path "$PROFILE_SAMPLES" + --benchmark.profile-concurrency "$PROFILE_CONCURRENCY" + --benchmark.profile-step-limit "$PROFILE_STEP_LIMIT" + ) + fi + retester benchmark \ --log-format json \ --test "$TEST_SPECIFIER" \ diff --git a/Cargo.lock b/Cargo.lock index ee7fcf33..19b4a6a3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -134,7 +134,7 @@ checksum = "25db5bcdd086f0b1b9610140a12c59b757397be90bd130d8d836fc8da0815a34" dependencies = [ "alloy-primitives", "num_enum", - "strum", + "strum 0.27.2", ] [[package]] @@ -584,7 +584,7 @@ dependencies = [ "derive_more 2.1.1", "rand 0.8.5", "serde", - "strum", + "strum 0.27.2", ] [[package]] @@ -1360,7 +1360,26 @@ dependencies = [ "digest 0.10.7", "rand_chacha 0.3.1", "sha2 0.10.9", - "w3f-ring-proof", + "w3f-ring-proof 0.0.2", + "zeroize", +] + +[[package]] +name = "ark-vrf" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b9bd02dbd2f282fe742d51f681adb2745a79dc8a025bc8d16cac9b255d55bf7" +dependencies = [ + "ark-bls12-381 0.5.0", + "ark-ec 0.5.0", + "ark-ed-on-bls12-381-bandersnatch", + "ark-ff 0.5.0", + "ark-serialize 0.5.0", + "ark-std 0.5.0", + "digest 0.10.7", + "generic-array", + "sha2 0.10.9", + "w3f-ring-proof 0.0.8", "zeroize", ] @@ -1882,9 +1901,9 @@ checksum = "230c5f1ca6a325a32553f8640d31ac9b49f2411e901e427570154868b46da4f7" [[package]] name = "binary-merkle-tree" -version = "16.1.0" +version = "16.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95c9f6900c9fd344d53fbdfb36e1343429079d73f4168c8ef48884bf15616dbd" +checksum = "a6d867f1ffee8b07e7bee466f4f33a043b91f868f5c7b1d22d8a02f86e92bee8" dependencies = [ "hash-db", "log", @@ -1915,6 +1934,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "90dbd31c98227229239363921e60fcf5e558e43ec69094d46fc4996f08d1d5bc" dependencies = [ "bitcoin_hashes 0.14.1", + "rand 0.8.5", + "rand_core 0.6.4", "serde", "unicode-normalization", ] @@ -3532,9 +3553,9 @@ dependencies = [ [[package]] name = "ethereum-standards" -version = "0.1.2" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5bb19a698ceb837a145395f230f1ee1c4ec751bc8038dfc616a669cfb4a01de" +checksum = "b156dce6f705a22ab532719e50ff5f7b3fe449b0f8bdeecc515436bf3a435446" dependencies = [ "alloy-core", ] @@ -3823,10 +3844,11 @@ checksum = "28dd6caf6059519a65843af8fe2a3ae298b14b80179855aeb4adc2c1934ee619" [[package]] name = "frame-benchmarking" -version = "44.0.1" +version = "48.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83680108c981eb72fe2098feda8fc9f832fd68190c99634adb79107115c2a5e9" +checksum = "bac5bf601c69cb27aee945517b723c2df4ecfc0bdd7b498f04998307d7ce5c13" dependencies = [ + "anyhow", "frame-support", "frame-support-procedural", "frame-system", @@ -3836,13 +3858,13 @@ dependencies = [ "paste", "scale-info", "serde", - "sp-api 38.0.0", - "sp-application-crypto 42.0.0", - "sp-core", - "sp-io 42.0.0", - "sp-runtime 43.0.0", - "sp-runtime-interface 31.0.0", - "sp-storage", + "sp-api 42.0.0", + "sp-application-crypto 46.0.0", + "sp-core 41.0.0", + "sp-io 46.0.0", + "sp-runtime 47.0.0", + "sp-runtime-interface 35.0.0", + "sp-storage 23.0.0", "static_assertions", ] @@ -3890,14 +3912,15 @@ dependencies = [ [[package]] name = "frame-support" -version = "44.0.0" +version = "47.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b853a8724d2ac1096e480a1fa6d1279697b774d098251817a964f3dfd8ba0036" +checksum = "bba1bf34af600d7b60fb75d6326dd15faf2e0c5e7f0e54f930562ef90ab94e78" dependencies = [ "aquamarine", "array-bytes", "binary-merkle-tree", "bitflags 1.3.2", + "derive-where", "docify", "environmental", "frame-metadata", @@ -3911,30 +3934,30 @@ dependencies = [ "scale-info", "serde", "serde_json", - "sp-api 38.0.0", + "sp-api 42.0.0", "sp-arithmetic", - "sp-core", + "sp-core 41.0.0", "sp-crypto-hashing-proc-macro", - "sp-debug-derive", - "sp-genesis-builder 0.19.0", - "sp-inherents 38.0.0", - "sp-io 42.0.0", - "sp-metadata-ir", - "sp-runtime 43.0.0", + "sp-debug-derive 15.0.0", + "sp-genesis-builder 0.23.0", + "sp-inherents 42.0.0", + "sp-io 46.0.0", + "sp-metadata-ir 0.13.0", + "sp-runtime 47.0.0", "sp-staking", - "sp-state-machine 0.47.0", + "sp-state-machine 0.51.0", "sp-std", - "sp-tracing 18.0.0", - "sp-trie", - "sp-weights", + "sp-tracing 19.0.0", + "sp-trie 44.0.0", + "sp-weights 34.0.0", "tt-call", ] [[package]] name = "frame-support-procedural" -version = "35.0.0" +version = "38.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c481996abeb9027d9a4d62d0c2cb4115c0ee6ef3120ad234fa2776b6313a4ed4" +checksum = "673819ac0c7bee44a653ed5b1161a767c886f21b3a468db536e5d6d9d31c42ed" dependencies = [ "Inflector", "cfg-expr", @@ -3977,9 +4000,9 @@ dependencies = [ [[package]] name = "frame-system" -version = "44.0.0" +version = "47.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "198d131ab22ecad578902c725d364cd3c02437da589451bf5a9744a4ac17af6e" +checksum = "77d1fc13a3ff43f0f1b4c5232910356779dd391676b4d7b77861b0286e2eeb08" dependencies = [ "cfg-if", "docify", @@ -3988,11 +4011,11 @@ dependencies = [ "parity-scale-codec", "scale-info", "serde", - "sp-core", - "sp-io 42.0.0", - "sp-runtime 43.0.0", - "sp-version 41.0.0", - "sp-weights", + "sp-core 41.0.0", + "sp-io 46.0.0", + "sp-runtime 47.0.0", + "sp-version 45.0.0", + "sp-weights 34.0.0", ] [[package]] @@ -7014,9 +7037,9 @@ dependencies = [ [[package]] name = "pallet-revive" -version = "0.11.3" +version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "995f1338e2254b7fe1b8b90ca2e14d40d069619e72a72cee3e8cd6faf67d821a" +checksum = "b77d1b5539765351b93b8468eb087bbee646327023f45ea044d930fa856a99a9" dependencies = [ "alloy-consensus", "alloy-core", @@ -7042,8 +7065,8 @@ dependencies = [ "pallet-transaction-payment", "parity-scale-codec", "paste", - "polkavm 0.30.0", - "polkavm-common 0.30.0", + "polkavm 0.33.1", + "polkavm-common 0.33.0", "rand 0.8.5", "revm", "ripemd", @@ -7051,24 +7074,24 @@ dependencies = [ "scale-info", "serde", "serde_json", - "sp-api 38.0.0", + "sp-api 42.0.0", "sp-arithmetic", "sp-consensus-aura", "sp-consensus-babe", "sp-consensus-slots", - "sp-core", - "sp-io 42.0.0", - "sp-runtime 43.0.0", - "sp-version 41.0.0", + "sp-core 41.0.0", + "sp-io 46.0.0", + "sp-runtime 47.0.0", + "sp-version 45.0.0", "substrate-bn", - "subxt-signer 0.43.0", + "subxt-signer", ] [[package]] name = "pallet-revive-fixtures" -version = "0.8.0" +version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "426606804d83efc7f3e1e7a7e9c33561d2b2832b9de61d643f7c057a28ba3d08" +checksum = "4efb1cd2d84727d34254c1fb1eac34d77d94a812c59b8bf1d34dbd23de2b1b4b" dependencies = [ "alloy-core", "anyhow", @@ -7077,16 +7100,16 @@ dependencies = [ "pallet-revive-uapi", "polkavm-linker", "serde_json", - "sp-core", - "sp-io 42.0.0", + "sp-core 41.0.0", + "sp-io 46.0.0", "toml 0.8.23", ] [[package]] name = "pallet-revive-proc-macro" -version = "0.6.0" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed97af646322cfc2d394c4737874bf6df507d25dd421a2939304eee02d89c742" +checksum = "1e819e77baf254cdb41a2a521eb871ee708e0a0197cd1db9a49d68ca646c085d" dependencies = [ "proc-macro2", "quote", @@ -7095,9 +7118,9 @@ dependencies = [ [[package]] name = "pallet-revive-uapi" -version = "0.9.0" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d0bf9c852c4426130520d546fe9ea0d932914c42ed7ae2970b5e428a3efe7e1" +checksum = "4c0cef0a48517471543a404f6ec497671b1ef1b6e78774ac24aee353527c896d" dependencies = [ "alloy-core", "bitflags 1.3.2", @@ -7111,9 +7134,9 @@ dependencies = [ [[package]] name = "pallet-transaction-payment" -version = "44.0.0" +version = "48.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5f607dd5778666d8440e75373db3c63a36bbfad63d2931a0eae8c051e515488" +checksum = "9041abf1095150239fec5bc98c9c28b50708e5633392f9ca1b3df9f0f03d2d72" dependencies = [ "frame-benchmarking", "frame-support", @@ -7122,8 +7145,8 @@ dependencies = [ "parity-scale-codec", "scale-info", "serde", - "sp-io 42.0.0", - "sp-runtime 43.0.0", + "sp-io 46.0.0", + "sp-runtime 47.0.0", ] [[package]] @@ -7478,16 +7501,16 @@ dependencies = [ [[package]] name = "polkavm" -version = "0.30.0" +version = "0.33.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4323d016144b2852da47cee55ca5fc33dfe7517be1f52395759f247ecc5695f6" +checksum = "d90ece49c68657299648e20469517e22c6ec38321307bb14a69c27a33927a491" dependencies = [ "libc", "log", "picosimd", - "polkavm-assembler 0.30.0", - "polkavm-common 0.30.0", - "polkavm-linux-raw 0.30.0", + "polkavm-assembler 0.33.0", + "polkavm-common 0.33.0", + "polkavm-linux-raw 0.33.0", ] [[package]] @@ -7501,9 +7524,9 @@ dependencies = [ [[package]] name = "polkavm-assembler" -version = "0.30.0" +version = "0.33.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3a873fa7ace058d6507debf5fccb1d06bd3279f5b35dbaf70dc7fe94a6c415c" +checksum = "00010f7924647dbf6f468d85d0fcfe4c3587cfb4557ef13f3682dbece8fd57f0" dependencies = [ "log", ] @@ -7523,11 +7546,20 @@ name = "polkavm-common" version = "0.30.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed1b408db93d4f49f5c651a7844682b9d7a561827b4dc6202c10356076c055c9" +dependencies = [ + "picosimd", +] + +[[package]] +name = "polkavm-common" +version = "0.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e44a9487003cf5b9fc4462bbcf105cc37d5d9b18b40edf5ed50dd20ed1fdb27" dependencies = [ "blake3", "log", "picosimd", - "polkavm-assembler 0.30.0", + "polkavm-assembler 0.33.0", ] [[package]] @@ -7548,6 +7580,15 @@ dependencies = [ "polkavm-derive-impl-macro 0.30.0", ] +[[package]] +name = "polkavm-derive" +version = "0.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ef966bc8518a66ce12d4edb73f2c4094cae72bb23258bc9e9b2802cc9d6cd79" +dependencies = [ + "polkavm-derive-impl-macro 0.33.0", +] + [[package]] name = "polkavm-derive-impl" version = "0.26.0" @@ -7572,6 +7613,18 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "polkavm-derive-impl" +version = "0.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0c2166ad71dd7f51dcdd0d91b70d408a8b3610fa6e94d8202dd4b7185607181" +dependencies = [ + "polkavm-common 0.33.0", + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "polkavm-derive-impl-macro" version = "0.26.0" @@ -7592,6 +7645,16 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "polkavm-derive-impl-macro" +version = "0.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7ac2ac8ec5b938e249fa97b5ebb1e6fa47000c81a25eba6bf0f13edb8d430e4" +dependencies = [ + "polkavm-derive-impl 0.33.0", + "syn 2.0.117", +] + [[package]] name = "polkavm-linker" version = "0.30.0" @@ -7616,9 +7679,9 @@ checksum = "28919f542476f4158cc71e6c072b1051f38f4b514253594ac3ad80e3c0211fc8" [[package]] name = "polkavm-linux-raw" -version = "0.30.0" +version = "0.33.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "604b23cdb201979304449f53d21bfd5fb1724c03e3ea889067c9a3bf7ae33862" +checksum = "42063d4a1c52e569f7794df27dab3e19c9fa8946184023257bdbb43eb4a94be5" [[package]] name = "polling" @@ -8485,7 +8548,7 @@ dependencies = [ "schemars 1.2.0", "semver 1.0.27", "serde", - "strum", + "strum 0.27.2", "tokio", "tracing", ] @@ -8526,7 +8589,7 @@ dependencies = [ "serde", "serde_json", "serde_with", - "strum", + "strum 0.27.2", "temp-dir", ] @@ -8598,7 +8661,7 @@ dependencies = [ "serde_json", "serde_with", "serde_yaml_ng", - "sp-core", + "sp-core 38.1.0", "sp-runtime 43.0.0", "subxt 0.43.1", "temp-dir", @@ -8616,9 +8679,11 @@ dependencies = [ "alloy", "anyhow", "futures", + "hex", "pallet-revive", "revive-common", "revive-dt-common", + "revm-bytecode", "serde_json", "subxt 0.43.1", "tokio", @@ -8667,7 +8732,7 @@ dependencies = [ "revive-dt-report", "serde", "serde_json", - "strum", + "strum 0.27.2", ] [[package]] @@ -9281,7 +9346,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "01733879c581defda6f49ff4076033c675d7127bfab6fd0bd0e6cf10696d0564" dependencies = [ "log", - "sp-core", + "sp-core 38.1.0", "sp-wasm-interface 24.0.0", "thiserror 1.0.69", ] @@ -9304,7 +9369,7 @@ dependencies = [ "serde", "serde_json", "sp-blockchain", - "sp-core", + "sp-core 38.1.0", "sp-crypto-hashing", "sp-genesis-builder 0.20.0", "sp-io 43.0.0", @@ -9342,13 +9407,13 @@ dependencies = [ "sp-api 39.0.0", "sp-blockchain", "sp-consensus", - "sp-core", + "sp-core 38.1.0", "sp-database", - "sp-externalities", + "sp-externalities 0.30.0", "sp-runtime 44.0.0", "sp-state-machine 0.48.0", - "sp-storage", - "sp-trie", + "sp-storage 22.0.0", + "sp-trie 41.1.1", "substrate-prometheus-endpoint", ] @@ -9365,12 +9430,12 @@ dependencies = [ "sc-executor-wasmtime", "schnellru", "sp-api 39.0.0", - "sp-core", - "sp-externalities", + "sp-core 38.1.0", + "sp-externalities 0.30.0", "sp-io 43.0.0", "sp-panic-handler", "sp-runtime-interface 32.0.0", - "sp-trie", + "sp-trie 41.1.1", "sp-version 42.0.0", "sp-wasm-interface 24.0.0", "tracing", @@ -9458,7 +9523,7 @@ dependencies = [ "smallvec", "sp-arithmetic", "sp-blockchain", - "sp-core", + "sp-core 38.1.0", "sp-runtime 44.0.0", "substrate-prometheus-endpoint", "thiserror 1.0.69", @@ -9536,7 +9601,7 @@ dependencies = [ "parity-scale-codec", "serde", "sp-blockchain", - "sp-core", + "sp-core 38.1.0", "sp-runtime 44.0.0", "thiserror 1.0.69", ] @@ -10481,55 +10546,55 @@ dependencies = [ [[package]] name = "sp-api" -version = "38.0.0" +version = "39.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d91062b6183f20a6c5fb02d055eeacb4791c8ad32fa1d280c75c0b29aa74acf" +checksum = "2cc9635cc2a860eff0b2d8b05ba217085c8292f41793f9cadfd931dc54976c00" dependencies = [ "docify", "hash-db", "log", "parity-scale-codec", "scale-info", - "sp-api-proc-macro 24.0.0", - "sp-core", - "sp-externalities", - "sp-metadata-ir", - "sp-runtime 43.0.0", - "sp-runtime-interface 31.0.0", - "sp-state-machine 0.47.0", - "sp-trie", - "sp-version 41.0.0", + "sp-api-proc-macro 25.0.0", + "sp-core 38.1.0", + "sp-externalities 0.30.0", + "sp-metadata-ir 0.12.3", + "sp-runtime 44.0.0", + "sp-runtime-interface 32.0.0", + "sp-state-machine 0.48.0", + "sp-trie 41.1.1", + "sp-version 42.0.0", "thiserror 1.0.69", ] [[package]] name = "sp-api" -version = "39.0.0" +version = "42.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2cc9635cc2a860eff0b2d8b05ba217085c8292f41793f9cadfd931dc54976c00" +checksum = "8977f83b4672cff5fff35a89a042e925d02f2a7dbfd63b7f4d12c80a2581f16b" dependencies = [ "docify", "hash-db", "log", "parity-scale-codec", "scale-info", - "sp-api-proc-macro 25.0.0", - "sp-core", - "sp-externalities", - "sp-metadata-ir", - "sp-runtime 44.0.0", - "sp-runtime-interface 32.0.0", - "sp-state-machine 0.48.0", - "sp-trie", - "sp-version 42.0.0", + "sp-api-proc-macro 27.0.0", + "sp-core 41.0.0", + "sp-externalities 0.32.0", + "sp-metadata-ir 0.13.0", + "sp-runtime 47.0.0", + "sp-runtime-interface 35.0.0", + "sp-state-machine 0.51.0", + "sp-trie 44.0.0", + "sp-version 45.0.0", "thiserror 1.0.69", ] [[package]] name = "sp-api-proc-macro" -version = "24.0.0" +version = "25.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8124c25cffbde85d2ef5978fa710bb900d89c368821e04d59040788a0ece3e25" +checksum = "7d832cd107113d389340dc80a632330fe7ed7d20f3db50aeeb6abe40e23b6f4e" dependencies = [ "Inflector", "blake2", @@ -10542,9 +10607,9 @@ dependencies = [ [[package]] name = "sp-api-proc-macro" -version = "25.0.0" +version = "27.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d832cd107113d389340dc80a632330fe7ed7d20f3db50aeeb6abe40e23b6f4e" +checksum = "32c95e3a2cadf60408a228d175d10bbacacdd2b1cd4a15115bc97e8d667ab0eb" dependencies = [ "Inflector", "blake2", @@ -10564,7 +10629,7 @@ dependencies = [ "parity-scale-codec", "scale-info", "serde", - "sp-core", + "sp-core 38.1.0", "sp-io 42.0.0", ] @@ -10577,15 +10642,28 @@ dependencies = [ "parity-scale-codec", "scale-info", "serde", - "sp-core", + "sp-core 38.1.0", "sp-io 43.0.0", ] +[[package]] +name = "sp-application-crypto" +version = "46.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb47c341b52ca668e68929c982c9f4698b467012e1760b71f5be6cf8cf0fb49a" +dependencies = [ + "parity-scale-codec", + "scale-info", + "serde", + "sp-core 41.0.0", + "sp-io 46.0.0", +] + [[package]] name = "sp-arithmetic" -version = "28.0.0" +version = "28.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5f4755af7cc57f4a2a830e134b403fc832caa5d93dacb970ffc7ac717f38c40" +checksum = "4e06588d1c43f60b9bb7f989785689842cc4cc8ce0e19d1c47686b1ff5fe9548" dependencies = [ "docify", "integer-sqrt", @@ -10608,7 +10686,7 @@ dependencies = [ "schnellru", "sp-api 39.0.0", "sp-consensus", - "sp-core", + "sp-core 38.1.0", "sp-database", "sp-runtime 44.0.0", "sp-state-machine 0.48.0", @@ -10633,45 +10711,45 @@ dependencies = [ [[package]] name = "sp-consensus-aura" -version = "0.44.0" +version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11ae4c25ce19f4b0527d26a2d4225c3ddc1fcf0b4dfc8d1f02f874ecfa64eb7d" +checksum = "d7b60bc0893c412a3ad7413bdff09773a726e480cf72ea2948d3353006ab7277" dependencies = [ "async-trait", "parity-scale-codec", "scale-info", - "sp-api 38.0.0", - "sp-application-crypto 42.0.0", + "sp-api 42.0.0", + "sp-application-crypto 46.0.0", "sp-consensus-slots", - "sp-inherents 38.0.0", - "sp-runtime 43.0.0", + "sp-inherents 42.0.0", + "sp-runtime 47.0.0", "sp-timestamp", ] [[package]] name = "sp-consensus-babe" -version = "0.44.0" +version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63e4b6de91c8151b91bd43f9291fbe8f543ca82cbdb19fff71bda6961c6b7802" +checksum = "9e0380bb40464600f232d8c7d1fe87c51066b348e3d937897ca99793f91277e7" dependencies = [ "async-trait", "parity-scale-codec", "scale-info", "serde", - "sp-api 38.0.0", - "sp-application-crypto 42.0.0", + "sp-api 42.0.0", + "sp-application-crypto 46.0.0", "sp-consensus-slots", - "sp-core", - "sp-inherents 38.0.0", - "sp-runtime 43.0.0", + "sp-core 41.0.0", + "sp-inherents 42.0.0", + "sp-runtime 47.0.0", "sp-timestamp", ] [[package]] name = "sp-consensus-slots" -version = "0.44.0" +version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac5030ea234ed6b31c089df51f9029bd5f8ab9560b83a24133df4b2f966379a3" +checksum = "cf724d2985e75f4e93be6627c8b64febd9565d1ed7908656757a309ad94bc15a" dependencies = [ "parity-scale-codec", "scale-info", @@ -10685,7 +10763,7 @@ version = "38.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "707602208776d0e19d4269bb3f68c5306cacbdfabbb2e4d8d499af7b907bb0a3" dependencies = [ - "ark-vrf", + "ark-vrf 0.1.1", "array-bytes", "bitflags 1.3.2", "blake2", @@ -10715,10 +10793,58 @@ dependencies = [ "serde", "sha2 0.10.9", "sp-crypto-hashing", - "sp-debug-derive", - "sp-externalities", + "sp-debug-derive 14.0.0", + "sp-externalities 0.30.0", "sp-std", - "sp-storage", + "sp-storage 22.0.0", + "ss58-registry", + "substrate-bip39", + "thiserror 1.0.69", + "tracing", + "w3f-bls", + "zeroize", +] + +[[package]] +name = "sp-core" +version = "41.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "571e6da2db8de83fa2a6f9fab2a86befdcd004c90cfa19c641b072d3f322a60d" +dependencies = [ + "ark-vrf 0.5.1", + "array-bytes", + "bip39", + "bitflags 1.3.2", + "blake2", + "bounded-collections", + "bs58", + "dyn-clone", + "ed25519-zebra", + "futures", + "hash-db", + "hash256-std-hasher", + "impl-serde", + "itertools 0.11.0", + "k256", + "libsecp256k1", + "log", + "merlin", + "parity-scale-codec", + "parking_lot 0.12.5", + "paste", + "primitive-types 0.13.1", + "rand 0.8.5", + "scale-info", + "schnorrkel", + "secp256k1 0.28.2", + "secrecy 0.8.0", + "serde", + "sha2 0.10.9", + "sp-crypto-hashing", + "sp-debug-derive 15.0.0", + "sp-externalities 0.32.0", + "sp-std", + "sp-storage 23.0.0", "ss58-registry", "substrate-bip39", "thiserror 1.0.69", @@ -10773,6 +10899,18 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "sp-debug-derive" +version = "15.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d61809bf52be994e4d0a0485bb18a78509ed185e1418736c1ff9011bd1528999" +dependencies = [ + "proc-macro-warning", + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "sp-externalities" version = "0.30.0" @@ -10781,20 +10919,18 @@ checksum = "30cbf059dce180a8bf8b6c8b08b6290fa3d1c7f069a60f1df038ab5dd5fc0ba6" dependencies = [ "environmental", "parity-scale-codec", - "sp-storage", + "sp-storage 22.0.0", ] [[package]] -name = "sp-genesis-builder" -version = "0.19.0" +name = "sp-externalities" +version = "0.32.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e16e1046045e47124c09a9c9c03bfd1933926d67512aa1e66b778b81e51f4bb" +checksum = "ccab635fdf03594e8bec6213457df38389ad54ac15ce12fea22e9a88ba039c2f" dependencies = [ + "environmental", "parity-scale-codec", - "scale-info", - "serde_json", - "sp-api 38.0.0", - "sp-runtime 43.0.0", + "sp-storage 23.0.0", ] [[package]] @@ -10810,31 +10946,44 @@ dependencies = [ "sp-runtime 44.0.0", ] +[[package]] +name = "sp-genesis-builder" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2cee8b7ba45f4b2d8a5720248e024489daf58a9b795ac3e24ac9a697938b0254" +dependencies = [ + "parity-scale-codec", + "scale-info", + "serde_json", + "sp-api 42.0.0", + "sp-runtime 47.0.0", +] + [[package]] name = "sp-inherents" -version = "38.0.0" +version = "39.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d91ae44bf5232bff4e1a804b8eda9cecbf56921c0d67699f7b638db4ea1b776" +checksum = "2522693c705c1245ef8dbdbcf09d7cc6b139f0184d5e0a46856c546666b494d7" dependencies = [ "async-trait", "impl-trait-for-tuples", "parity-scale-codec", "scale-info", - "sp-runtime 43.0.0", + "sp-runtime 44.0.0", "thiserror 1.0.69", ] [[package]] name = "sp-inherents" -version = "39.0.0" +version = "42.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2522693c705c1245ef8dbdbcf09d7cc6b139f0184d5e0a46856c546666b494d7" +checksum = "4a298a87658bf4420b179fb15ee45ed885d77b53dc9627b1e858c16e1705417f" dependencies = [ "async-trait", "impl-trait-for-tuples", "parity-scale-codec", "scale-info", - "sp-runtime 44.0.0", + "sp-runtime 47.0.0", "thiserror 1.0.69", ] @@ -10853,14 +11002,14 @@ dependencies = [ "polkavm-derive 0.26.0", "rustversion", "secp256k1 0.28.2", - "sp-core", + "sp-core 38.1.0", "sp-crypto-hashing", - "sp-externalities", - "sp-keystore", + "sp-externalities 0.30.0", + "sp-keystore 0.44.1", "sp-runtime-interface 31.0.0", "sp-state-machine 0.47.0", "sp-tracing 18.0.0", - "sp-trie", + "sp-trie 41.1.1", "tracing", "tracing-core", ] @@ -10880,14 +11029,41 @@ dependencies = [ "polkavm-derive 0.26.0", "rustversion", "secp256k1 0.28.2", - "sp-core", + "sp-core 38.1.0", "sp-crypto-hashing", - "sp-externalities", - "sp-keystore", + "sp-externalities 0.30.0", + "sp-keystore 0.44.1", "sp-runtime-interface 32.0.0", "sp-state-machine 0.48.0", "sp-tracing 19.0.0", - "sp-trie", + "sp-trie 41.1.1", + "tracing", + "tracing-core", +] + +[[package]] +name = "sp-io" +version = "46.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc8f8415b05edacb24866790c540e29ff967250faffda9996709c2d48f0f6ca4" +dependencies = [ + "bytes", + "docify", + "ed25519-dalek", + "libsecp256k1", + "log", + "parity-scale-codec", + "polkavm-derive 0.33.0", + "rustversion", + "secp256k1 0.28.2", + "sp-core 41.0.0", + "sp-crypto-hashing", + "sp-externalities 0.32.0", + "sp-keystore 0.47.0", + "sp-runtime-interface 35.0.0", + "sp-state-machine 0.51.0", + "sp-tracing 19.0.0", + "sp-trie 44.0.0", "tracing", "tracing-core", ] @@ -10900,8 +11076,20 @@ checksum = "8a5c0b829014afc22e992be2c198f2677592db43267fc218e9f3207dbbfb6fbb" dependencies = [ "parity-scale-codec", "parking_lot 0.12.5", - "sp-core", - "sp-externalities", + "sp-core 38.1.0", + "sp-externalities 0.30.0", +] + +[[package]] +name = "sp-keystore" +version = "0.47.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74fead48d67f4374903b78624bca25fbf15f95a79d8dbefdecd407bb01544c3d" +dependencies = [ + "parity-scale-codec", + "parking_lot 0.12.5", + "sp-core 41.0.0", + "sp-externalities 0.32.0", ] [[package]] @@ -10925,6 +11113,18 @@ dependencies = [ "scale-info", ] +[[package]] +name = "sp-metadata-ir" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb285f8d35b3fbb553e31c66182c25985717526f21920a08d23fdca88e3bdd1e" +dependencies = [ + "derive-where", + "frame-metadata", + "parity-scale-codec", + "scale-info", +] + [[package]] name = "sp-panic-handler" version = "13.0.2" @@ -10956,11 +11156,11 @@ dependencies = [ "simple-mermaid", "sp-application-crypto 42.0.0", "sp-arithmetic", - "sp-core", + "sp-core 38.1.0", "sp-io 42.0.0", "sp-std", - "sp-trie", - "sp-weights", + "sp-trie 41.1.1", + "sp-weights 33.2.0", "tracing", "tuplex", ] @@ -10986,11 +11186,43 @@ dependencies = [ "simple-mermaid", "sp-application-crypto 43.0.0", "sp-arithmetic", - "sp-core", + "sp-core 38.1.0", "sp-io 43.0.0", "sp-std", - "sp-trie", - "sp-weights", + "sp-trie 41.1.1", + "sp-weights 33.2.0", + "tracing", + "tuplex", +] + +[[package]] +name = "sp-runtime" +version = "47.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00f3c9491215e9d640c05ea651723f6834699f1088aa53043dc9badeb6177935" +dependencies = [ + "binary-merkle-tree", + "bytes", + "docify", + "either", + "hash256-std-hasher", + "impl-trait-for-tuples", + "log", + "num-traits", + "parity-scale-codec", + "paste", + "rand 0.8.5", + "scale-info", + "serde", + "simple-mermaid", + "sp-application-crypto 46.0.0", + "sp-arithmetic", + "sp-core 41.0.0", + "sp-io 46.0.0", + "sp-std", + "sp-trie 44.0.0", + "sp-weights 34.0.0", + "strum 0.26.3", "tracing", "tuplex", ] @@ -11006,10 +11238,10 @@ dependencies = [ "parity-scale-codec", "polkavm-derive 0.26.0", "primitive-types 0.13.1", - "sp-externalities", - "sp-runtime-interface-proc-macro", + "sp-externalities 0.30.0", + "sp-runtime-interface-proc-macro 20.0.0", "sp-std", - "sp-storage", + "sp-storage 22.0.0", "sp-tracing 18.0.0", "sp-wasm-interface 23.0.0", "static_assertions", @@ -11025,15 +11257,34 @@ dependencies = [ "impl-trait-for-tuples", "parity-scale-codec", "polkavm-derive 0.26.0", - "sp-externalities", - "sp-runtime-interface-proc-macro", + "sp-externalities 0.30.0", + "sp-runtime-interface-proc-macro 20.0.0", "sp-std", - "sp-storage", + "sp-storage 22.0.0", "sp-tracing 19.0.0", "sp-wasm-interface 24.0.0", "static_assertions", ] +[[package]] +name = "sp-runtime-interface" +version = "35.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1263162adb7ffe06c762467495dca536794667abe24567c7a00d5edd2b3e727c" +dependencies = [ + "bytes", + "impl-trait-for-tuples", + "parity-scale-codec", + "polkavm-derive 0.33.0", + "sp-externalities 0.32.0", + "sp-runtime-interface-proc-macro 21.0.0", + "sp-std", + "sp-storage 23.0.0", + "sp-tracing 19.0.0", + "sp-wasm-interface 25.0.0", + "static_assertions", +] + [[package]] name = "sp-runtime-interface-proc-macro" version = "20.0.0" @@ -11048,18 +11299,32 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "sp-runtime-interface-proc-macro" +version = "21.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7d9650b5186fd32bf5198adfe08d8fecc76f2ebe8a8927f28aaf2a537d75b2e" +dependencies = [ + "Inflector", + "expander", + "proc-macro-crate 3.4.0", + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "sp-staking" -version = "40.0.0" +version = "44.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa9945ce70bbfb9b1c876f94a81017915bc932a576b8a9735b88aabfa01ea4e5" +checksum = "bd5fbc654e344fe2e89574058e3c4b7cb75fa3a72f6fc6e34d55bce74fb9f978" dependencies = [ "impl-trait-for-tuples", "parity-scale-codec", "scale-info", "serde", - "sp-core", - "sp-runtime 43.0.0", + "sp-core 41.0.0", + "sp-runtime 47.0.0", ] [[package]] @@ -11074,13 +11339,13 @@ dependencies = [ "parking_lot 0.12.5", "rand 0.8.5", "smallvec", - "sp-core", - "sp-externalities", + "sp-core 38.1.0", + "sp-externalities 0.30.0", "sp-panic-handler", - "sp-trie", + "sp-trie 41.1.1", "thiserror 1.0.69", "tracing", - "trie-db", + "trie-db 0.30.0", ] [[package]] @@ -11095,13 +11360,34 @@ dependencies = [ "parking_lot 0.12.5", "rand 0.8.5", "smallvec", - "sp-core", - "sp-externalities", + "sp-core 38.1.0", + "sp-externalities 0.30.0", "sp-panic-handler", - "sp-trie", + "sp-trie 41.1.1", "thiserror 1.0.69", "tracing", - "trie-db", + "trie-db 0.30.0", +] + +[[package]] +name = "sp-state-machine" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3789d2895faa6e5d0a88c34c19b8ca7f3fdd9823729bde83743fd13668aaf08a" +dependencies = [ + "hash-db", + "log", + "parity-scale-codec", + "parking_lot 0.12.5", + "rand 0.8.5", + "smallvec", + "sp-core 41.0.0", + "sp-externalities 0.32.0", + "sp-panic-handler", + "sp-trie 44.0.0", + "thiserror 1.0.69", + "tracing", + "trie-db 0.31.0", ] [[package]] @@ -11120,19 +11406,32 @@ dependencies = [ "parity-scale-codec", "ref-cast", "serde", - "sp-debug-derive", + "sp-debug-derive 14.0.0", +] + +[[package]] +name = "sp-storage" +version = "23.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5e5c9fb0016b49765f89d23e4869ee616e1317de8f75b59babb721ccc27d2af" +dependencies = [ + "impl-serde", + "parity-scale-codec", + "ref-cast", + "serde", + "sp-debug-derive 15.0.0", ] [[package]] name = "sp-timestamp" -version = "38.0.0" +version = "42.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57897783f3ae2b0630196f767194d9f753759305a5266fc2e0522e920733df0a" +checksum = "bee8bc7031359911e9c2099dcd5f28844cbf7150d92a17917be5ee5bca183a35" dependencies = [ "async-trait", "parity-scale-codec", - "sp-inherents 38.0.0", - "sp-runtime 43.0.0", + "sp-inherents 42.0.0", + "sp-runtime 47.0.0", "thiserror 1.0.69", ] @@ -11179,20 +11478,46 @@ dependencies = [ "rand 0.8.5", "scale-info", "schnellru", - "sp-core", - "sp-externalities", + "sp-core 38.1.0", + "sp-externalities 0.30.0", + "substrate-prometheus-endpoint", + "thiserror 1.0.69", + "tracing", + "trie-db 0.30.0", + "trie-root", +] + +[[package]] +name = "sp-trie" +version = "44.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f4d4aa0fec3c80f98762cae84ad7dda64c16dcd37f1b97c34c9d3402023119f" +dependencies = [ + "ahash", + "foldhash 0.1.5", + "hash-db", + "hashbrown 0.15.5", + "memory-db", + "nohash-hasher", + "parity-scale-codec", + "parking_lot 0.12.5", + "rand 0.8.5", + "scale-info", + "schnellru", + "sp-core 41.0.0", + "sp-externalities 0.32.0", "substrate-prometheus-endpoint", "thiserror 1.0.69", "tracing", - "trie-db", + "trie-db 0.31.0", "trie-root", ] [[package]] name = "sp-version" -version = "41.0.0" +version = "42.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e0d7b57b6577ddab5b363c2d6e9d49609749e041ee50e7232ecb413bc1cfa3f" +checksum = "633ea19da3ec057d449af667099072daa4e99900984f304b96f4c2ee15aeecc7" dependencies = [ "impl-serde", "parity-scale-codec", @@ -11200,7 +11525,7 @@ dependencies = [ "scale-info", "serde", "sp-crypto-hashing-proc-macro", - "sp-runtime 43.0.0", + "sp-runtime 44.0.0", "sp-std", "sp-version-proc-macro", "thiserror 1.0.69", @@ -11208,17 +11533,18 @@ dependencies = [ [[package]] name = "sp-version" -version = "42.0.0" +version = "45.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "633ea19da3ec057d449af667099072daa4e99900984f304b96f4c2ee15aeecc7" +checksum = "45a409b2e4cf147fc24ef158b3c6659738e7dca70838350309a3dc0cd59a81bc" dependencies = [ "impl-serde", "parity-scale-codec", "parity-wasm", "scale-info", "serde", + "sp-core 41.0.0", "sp-crypto-hashing-proc-macro", - "sp-runtime 44.0.0", + "sp-runtime 47.0.0", "sp-std", "sp-version-proc-macro", "thiserror 1.0.69", @@ -11262,6 +11588,18 @@ dependencies = [ "wasmtime", ] +[[package]] +name = "sp-wasm-interface" +version = "25.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d29d5c802dbb12ce5efe240f1284842d285249b87530bb7729ddae2647c6b18" +dependencies = [ + "anyhow", + "impl-trait-for-tuples", + "log", + "parity-scale-codec", +] + [[package]] name = "sp-weights" version = "33.2.0" @@ -11274,7 +11612,22 @@ dependencies = [ "serde", "smallvec", "sp-arithmetic", - "sp-debug-derive", + "sp-debug-derive 14.0.0", +] + +[[package]] +name = "sp-weights" +version = "34.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "364f93051b3b243c6221e51965e2bef465605f1de169a2ebd126fba2576dc3d9" +dependencies = [ + "bounded-collections", + "parity-scale-codec", + "scale-info", + "serde", + "smallvec", + "sp-arithmetic", + "sp-debug-derive 15.0.0", ] [[package]] @@ -11362,13 +11715,35 @@ version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" +[[package]] +name = "strum" +version = "0.26.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" +dependencies = [ + "strum_macros 0.26.4", +] + [[package]] name = "strum" version = "0.27.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf" dependencies = [ - "strum_macros", + "strum_macros 0.27.2", +] + +[[package]] +name = "strum_macros" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "rustversion", + "syn 2.0.117", ] [[package]] @@ -11385,9 +11760,9 @@ dependencies = [ [[package]] name = "substrate-bip39" -version = "0.6.0" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca58ffd742f693dc13d69bdbb2e642ae239e0053f6aab3b104252892f856700a" +checksum = "d93affb0135879b1b67cbcf6370a256e1772f9eaaece3899ec20966d67ad0492" dependencies = [ "hmac 0.12.1", "pbkdf2", @@ -11744,36 +12119,6 @@ dependencies = [ "url", ] -[[package]] -name = "subxt-signer" -version = "0.43.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a9bd240ae819f64ac6898d7ec99a88c8b838dba2fb9d83b843feb70e77e34c8" -dependencies = [ - "base64 0.22.1", - "bip32", - "bip39", - "cfg-if", - "crypto_secretbox", - "hex", - "hmac 0.12.1", - "keccak-hash", - "parity-scale-codec", - "pbkdf2", - "regex", - "schnorrkel", - "scrypt", - "secp256k1 0.30.0", - "secrecy 0.10.3", - "serde", - "serde_json", - "sha2 0.10.9", - "sp-crypto-hashing", - "subxt-core 0.43.0", - "thiserror 2.0.18", - "zeroize", -] - [[package]] name = "subxt-signer" version = "0.44.3" @@ -11781,11 +12126,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1bdcc9159fdcc81aca0f71f0c8c77829a671f7348958fc77fb2fb320ed59a13a" dependencies = [ "base64 0.22.1", + "bip32", "bip39", "cfg-if", "crypto_secretbox", "hex", "hmac 0.12.1", + "keccak-hash", "parity-scale-codec", "pbkdf2", "regex", @@ -12574,6 +12921,18 @@ dependencies = [ "smallvec", ] +[[package]] +name = "trie-db" +version = "0.31.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7795f2df2ef744e4ffb2125f09325e60a21d305cc3ecece0adeef03f7a9e560" +dependencies = [ + "hash-db", + "log", + "rustc-hex", + "smallvec", +] + [[package]] name = "trie-root" version = "0.18.0" @@ -12917,6 +13276,20 @@ dependencies = [ "merlin", ] +[[package]] +name = "w3f-pcs" +version = "0.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ea1046a1deb6d26c34ba2d1f1bab4222d695d126502ee765f80b021753cb674" +dependencies = [ + "ark-ec 0.5.0", + "ark-ff 0.5.0", + "ark-poly 0.5.0", + "ark-serialize 0.5.0", + "ark-std 0.5.0", + "merlin", +] + [[package]] name = "w3f-plonk-common" version = "0.0.2" @@ -12930,7 +13303,23 @@ dependencies = [ "ark-std 0.5.0", "getrandom_or_panic", "rand_core 0.6.4", - "w3f-pcs", + "w3f-pcs 0.0.2", +] + +[[package]] +name = "w3f-plonk-common" +version = "0.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30408cda37b81bd7257319942584c794c5784d00d749757bc664656749a1472a" +dependencies = [ + "ark-ec 0.5.0", + "ark-ff 0.5.0", + "ark-poly 0.5.0", + "ark-serialize 0.5.0", + "ark-std 0.5.0", + "getrandom_or_panic", + "rand_core 0.6.4", + "w3f-pcs 0.0.5", ] [[package]] @@ -12945,8 +13334,24 @@ dependencies = [ "ark-serialize 0.5.0", "ark-std 0.5.0", "ark-transcript", - "w3f-pcs", - "w3f-plonk-common", + "w3f-pcs 0.0.2", + "w3f-plonk-common 0.0.2", +] + +[[package]] +name = "w3f-ring-proof" +version = "0.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cbfc4cb881a934e6f33c25927bf955d0cb18e52b94528bbc5fa28dddedb4cd1" +dependencies = [ + "ark-ec 0.5.0", + "ark-ff 0.5.0", + "ark-poly 0.5.0", + "ark-serialize 0.5.0", + "ark-std 0.5.0", + "ark-transcript", + "w3f-pcs 0.0.5", + "w3f-plonk-common 0.0.7", ] [[package]] @@ -14286,9 +14691,9 @@ dependencies = [ "serde", "serde_json", "sha2 0.10.9", - "sp-core", + "sp-core 38.1.0", "subxt 0.44.3", - "subxt-signer 0.44.3", + "subxt-signer", "thiserror 1.0.69", "tokio", "tracing", @@ -14352,7 +14757,7 @@ dependencies = [ "futures", "lazy_static", "subxt 0.44.3", - "subxt-signer 0.44.3", + "subxt-signer", "tokio", "zombienet-configuration", "zombienet-orchestrator", @@ -14375,7 +14780,7 @@ dependencies = [ "regex", "reqwest", "serde_json", - "sp-core", + "sp-core 38.1.0", "thiserror 1.0.69", "tokio", "tracing", diff --git a/Cargo.toml b/Cargo.toml index ba95e999..b4dfc450 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -93,7 +93,8 @@ revive-common = { version = "0.3.0" } revive-differential = { version = "0.3.0" } zombienet-sdk = { version = "0.4.11" } -pallet-revive = { version = "0.11.3", default-features = true } +pallet-revive = { version = "0.18.0", default-features = true } +revm-bytecode = { version = "6.2.2", default-features = false, features = ["std"] } [profile.bench] inherits = "release" diff --git a/assets/benchmark-report.html b/assets/benchmark-report.html index bcda73a2..bfc6c1fc 100644 --- a/assets/benchmark-report.html +++ b/assets/benchmark-report.html @@ -429,7 +429,7 @@ /* ── Summary Cards ── */ .summary-cards { display: grid; - grid-template-columns: repeat(3, 1fr); + grid-template-columns: repeat(4, 1fr); gap: 16px; margin-bottom: 40px; } @@ -523,6 +523,29 @@ font-size: 14px; } + .weight-per-tx-cell { + display: inline-grid; + grid-template-columns: auto auto; + column-gap: 10px; + row-gap: 2px; + align-items: baseline; + line-height: 1.2; + } + + .weight-per-tx-cell .weight-label { + font-size: 9.5px; + color: var(--color-text-muted); + text-transform: uppercase; + letter-spacing: 0.06em; + font-weight: 600; + justify-self: start; + } + + .weight-per-tx-cell .weight-value-secondary { + font-size: 10.5px; + color: var(--color-text-muted); + } + /* ── Charts Area ── */ .charts-area { display: flex; @@ -833,6 +856,171 @@ vertical-align: middle; } + .opcode-profile-caption { + font-size: 12px; + color: var(--color-text-muted, #6b6b6b); + line-height: 1.5; + padding: 8px 14px 12px; + margin-bottom: 6px; + border-bottom: 1px solid var(--color-border-light, #ececec); + } + .opcode-profile-caption code { + background: rgba(0, 0, 0, 0.04); + padding: 1px 5px; + border-radius: 3px; + font-size: 11px; + } + + /* Opcode Profile section — reuses contract-card / contract-table base. */ + + .opcode-profile-grid { + display: grid; + grid-template-columns: 1fr; + gap: 16px; + } + + .opcode-profile-card .contract-card-header { + gap: 8px; + } + + .opcode-profile-summary { + display: flex; + align-items: center; + gap: 8px; + flex-wrap: wrap; + padding: 8px 16px; + background: var(--color-surface-alt); + border-bottom: 1px solid var(--color-border-light); + font-family: var(--font-mono); + font-size: 11px; + color: var(--color-text-secondary); + } + + .opcode-profile-summary b { + color: var(--color-text-primary); + font-weight: 600; + } + + .opcode-table .opcode-name { + font-family: var(--font-mono); + font-size: 11px; + color: var(--color-text-primary); + white-space: nowrap; + } + + .opcode-table .opcode-num { + font-family: var(--font-mono); + font-size: 11px; + text-align: right; + color: var(--color-text-secondary); + white-space: nowrap; + } + + .opcode-table .opcode-share-pct { + margin-left: 4px; + color: var(--color-text-muted); + font-weight: 400; + } + + .opcode-table th[data-sort-key] { + cursor: pointer; + user-select: none; + } + + .opcode-table th[data-sort-key]:hover { + background: rgba(0, 0, 0, 0.04); + } + + .opcode-table .sort-arrow { + display: inline-block; + width: 10px; + margin-left: 4px; + font-size: 9px; + color: var(--color-text-muted); + } + + .opcode-truncated { + padding: 8px 16px; + font-family: var(--font-mono); + font-size: 10px; + color: var(--color-text-muted); + border-top: 1px solid var(--color-border-light); + } + + /* ── Profile coverage: step-limit chip in the opcode-profile card header ── */ + .contract-card-header .cov-chip { margin-left: auto; align-self: center; } + .cov-chip { display: inline-flex; align-items: center; gap: 5px; font-size: 11px; font-weight: 600; + padding: 3px 9px; border-radius: 999px; white-space: nowrap; line-height: 1.3; } + .cov-chip-ok { color: var(--color-success); background: var(--color-success-bg); } + .cov-chip-hit { color: var(--color-error); background: var(--color-error-bg); } + .cov-chip-na { color: var(--color-text-muted); background: var(--color-surface); } + + .opcode-step-charts { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 12px; + background: var(--color-surface); + border: 1px solid var(--color-border); + border-radius: var(--radius); + box-shadow: var(--shadow-sm); + margin-bottom: 16px; + overflow: hidden; + } + + .opcode-step-chart-card { + padding: 12px 14px 8px; + border-right: 1px solid var(--color-border-light); + } + + .opcode-step-chart-card:last-child { + border-right: none; + } + + .opcode-step-chart-title { + font-family: var(--font-heading); + font-size: 10px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.08em; + color: var(--color-text-muted); + margin-bottom: 4px; + } + + .opcode-step-chart { + height: 240px; + width: 100%; + } + + .opcode-cross-platform-card { + background: var(--color-surface); + border: 1px solid var(--color-border); + border-radius: var(--radius); + box-shadow: var(--shadow-sm); + padding: 12px 14px 8px; + margin-bottom: 16px; + } + + .opcode-cross-chart { + height: 320px; + width: 100%; + } + + .opcode-stacked-card { + padding: 10px 16px 4px; + border-bottom: 1px solid var(--color-border-light); + } + + .opcode-stacked-chart { + height: 240px; + width: 100%; + } + + @media (max-width: 900px) { + .opcode-step-charts { grid-template-columns: 1fr; } + .opcode-step-chart-card { border-right: none; border-bottom: 1px solid var(--color-border-light); } + .opcode-step-chart-card:last-child { border-bottom: none; } + } + .contract-platform-cell { display: flex; align-items: center; @@ -1223,6 +1411,10 @@ .chart-row-3 { grid-template-columns: 1fr 1fr; } } + @media (max-width: 1100px) { + .summary-cards { grid-template-columns: repeat(2, 1fr); } + } + @media (max-width: 768px) { .summary-cards { grid-template-columns: 1fr; } .contracts-grid { grid-template-columns: 1fr; } @@ -1294,6 +1486,7 @@

Benchmark Report
+
@@ -1401,6 +1594,21 @@

Benchmark Report +
+ Opcode Profile + +
+
+
+ Values are sums over K × steps sampled txs + (K = 3, steps = 13). +
+
+
+
Step Details @@ -1505,6 +1713,46 @@

Benchmark ReportBenchmark ReportBenchmark Report { + if (!params || params.length === 0) return ''; + const idx = params[0].dataIndex; + const stepPath = xData[idx]; + let html = `step ${escapeHtml(stepPath)}`; + for (const { platform, color, rollups } of platformRollups) { + const r = rollups.find(x => x.step_path === stepPath); + if (!r) continue; + const topName = r.top_op_key ? opKeyToName(r.top_op_key) : '—'; + html += + `
` + + `${escapeHtml(platform)}` + + (r.failedAny ? ' (failed)' : '') + + `   K=${r.K}` + + `   min=${fmtFn(r[fieldMin])}` + + `   mean=${fmtFn(r[fieldMean])}` + + `   max=${fmtFn(r[fieldMax])}` + + `   top=${escapeHtml(topName)}`; + } + return html; + }, + }, + series: allSeries, + }); + setTimeout(() => chart.resize(), 0); +} + +/** + * Group a single platform's per-tx opcode breakdowns by step_path, picking + * the workload-wide top-N opcodes as named series and rolling the rest into + * an "Other" bucket. Returns: + * { topOps: [op_key, ...], + * rollups: [{ step_path, byOp: { op_key → ref_time }, other: ref_time }, ...] } + */ +function computeStepOpcodeRollups(txProfiles, topN, metric = 'ref_time') { + const totKey = 'total_' + metric; + const unKey = 'unattributed_' + metric; + // Pass 1 — workload-wide opcode totals to pick the named series. + const opcodeTotals = {}; + for (const t of txProfiles || []) { + for (const op of (t.opcodes || [])) { + opcodeTotals[op.op_key] = (opcodeTotals[op.op_key] || 0) + Number(op[totKey] || 0); + } + } + const topOps = Object.entries(opcodeTotals) + .sort((a, b) => b[1] - a[1]) + .slice(0, topN) + .map(e => e[0]); + const topSet = new Set(topOps); + + // Pass 2 — group by step, splitting into top-N buckets vs "Other". + const byStep = {}; + for (const t of txProfiles || []) { + const sp = t.step_path; + if (!byStep[sp]) byStep[sp] = { step_path: sp, byOp: {}, other: 0, unattributed: 0 }; + const g = byStep[sp]; + // Weight consumed but not attributed to any captured step (residual) — so the + // stacked bar totals weight_consumed and the coverage gap is visible inline. + g.unattributed += Number(t[unKey] || 0); + for (const op of (t.opcodes || [])) { + const v = Number(op[totKey] || 0); + if (topSet.has(op.op_key)) { + g.byOp[op.op_key] = (g.byOp[op.op_key] || 0) + v; + } else { + g.other += v; + } + } + } + const rollups = Object.values(byStep).sort((a, b) => + String(a.step_path).localeCompare(String(b.step_path), undefined, { numeric: true }) + ); + return { topOps, rollups }; +} + +// Palette for opcode segments — distinct hues, repeated if needed. Last +// entry is reserved for "Other" so it's visually neutral. +const OPCODE_SEGMENT_COLORS = [ + '#2563eb', '#dc2626', '#16a34a', '#d97706', '#7c3aed', + '#0891b2', '#e11d48', '#65a30d', '#9333ea', '#0d9488', +]; +const OPCODE_OTHER_COLOR = '#9ca3af'; + +/** + * Stacked bar per step showing the per-opcode composition of each step's + * ref_time for a single platform. + */ +function renderStepOpcodeBreakdownChart(chartId, txProfiles, topN = 8, metric = 'ref_time') { + const chart = getOrInitChart(chartId, null); + const fmt = metric === 'proof_size' ? fmtOpcodeProofSize : fmtOpcodeRefTime; + const { topOps, rollups } = computeStepOpcodeRollups(txProfiles, topN, metric); + + if (rollups.length === 0) { + chart.setOption({}, true); + return; + } + + // Horizontal orientation: one row per step. Size the container to the row + // count so each row stays legible instead of being crushed into a fixed + // height (the CSS height is only a pre-init fallback). + const el = document.getElementById(chartId); + if (el) { + el.style.height = `${Math.max(200, rollups.length * 26 + 96)}px`; + } + + const catData = rollups.map(r => r.step_path); + const series = topOps.map((opKey, i) => ({ + name: opKeyToName(opKey), + type: 'bar', + stack: 'total', + itemStyle: { color: OPCODE_SEGMENT_COLORS[i % OPCODE_SEGMENT_COLORS.length] }, + data: rollups.map(r => r.byOp[opKey] || 0), + emphasis: { focus: 'series' }, + })); + // "Other" bucket if any step had non-top-N contribution + const anyOther = rollups.some(r => r.other > 0); + if (anyOther) { + series.push({ + name: 'Other', + type: 'bar', + stack: 'total', + itemStyle: { color: OPCODE_OTHER_COLOR }, + data: rollups.map(r => r.other), + emphasis: { focus: 'series' }, + }); + } + // "Not profiled" residual (weight_consumed − Σ steps) as a final hatched + // segment, so each bar totals the step's consumed ref_time and the profiling + // gap reads inline. Hatched (not just gray) to stay distinct from "Other". + const anyUnattr = rollups.some(r => (r.unattributed || 0) > 0); + if (anyUnattr) { + series.push({ + name: 'Not profiled', + type: 'bar', + stack: 'total', + itemStyle: { + color: '#dcdcd6', + decal: { + symbol: 'rect', + color: 'rgba(0,0,0,0.30)', + dashArrayX: [1, 0], + dashArrayY: [3, 4], + rotation: -Math.PI / 4, + }, + }, + data: rollups.map(r => r.unattributed || 0), + emphasis: { focus: 'series' }, + }); + } + + chart.setOption({ + grid: { left: 60, right: 16, top: 40, bottom: 64, containLabel: true }, + legend: { + type: 'scroll', + top: 4, + textStyle: { fontSize: 9 }, + itemWidth: 14, + itemHeight: 8, + }, + xAxis: { + type: 'value', + name: metric, + nameTextStyle: { fontSize: 10, color: '#6b6b6b' }, + axisLabel: { fontSize: 9, color: '#6b6b6b', formatter: fmt }, + splitLine: { lineStyle: { color: '#f0f0f0' } }, + }, + yAxis: { + type: 'category', + inverse: true, + data: catData, + axisLabel: { fontSize: 9, color: '#6b6b6b' }, + axisTick: { alignWithLabel: true }, + axisLine: { lineStyle: { color: '#d0d0d0' } }, + }, + tooltip: { + trigger: 'axis', + axisPointer: { type: 'shadow' }, + backgroundColor: 'rgba(255,255,255,0.98)', + borderColor: '#e3e2df', + borderWidth: 1, + textStyle: { fontSize: 11 }, + formatter: (params) => { + if (!params || params.length === 0) return ''; + const idx = params[0].dataIndex; + const stepPath = catData[idx]; + const total = params.reduce((s, p) => s + Number(p.value || 0), 0); + let html = `step ${escapeHtml(stepPath)}` + + `
total: ${fmt(total)}`; + params.slice() + .sort((a, b) => Number(b.value || 0) - Number(a.value || 0)) + .forEach(p => { + const v = Number(p.value || 0); + if (v === 0) return; + const pct = total > 0 ? (v / total * 100).toFixed(1) : '0'; + html += `
${p.marker} ${escapeHtml(p.seriesName)}: ${fmt(v)} (${pct}%)`; + }); + return html; + }, + }, + series, + }); + setTimeout(() => chart.resize(), 0); +} + +/** + * Cross-platform grouped bar chart by **category**. Single-VM categories + * (Memory/Stack/Arith/ControlFlow on EVM; VM overhead/Immutables on PVM) + * are folded into a `VM-exclusive` bucket so the comparison is symmetric. + * Categories with zero contribution from every platform are omitted. + */ +function renderCrossPlatformOpcodeComparison(chartId, platformsData, metric = 'ref_time', forcedLabels = null) { + const chart = getOrInitChart(chartId, null); + const field = metric === 'proof_size' ? 'total_proof_size' : 'total_ref_time'; + const fmtFn = metric === 'proof_size' ? fmtOpcodeProofSize : fmtOpcodeRefTime; + const axisName = metric === 'proof_size' ? 'proof_size' : 'ref_time'; + + const VM_EXCLUSIVE_CATEGORIES = new Set([ + 'Memory', 'Stack', 'Arithmetic & Logic', 'Control flow', + 'VM overhead', 'Immutables', + ]); + const VM_EXCLUSIVE_LABEL = 'VM-exclusive'; + + // Re-aggregate from tx_profiles (uncapped), not opcode_profile.opcodes — + // the latter has a top-N "Other" bucket that would pollute Other here. + const perPlatformCatTotal = new Map(); + const perPlatformCatBreakdown = new Map(); + const seenCats = new Set(); + + for (const { platform, txProfiles } of platformsData) { + const catTotals = {}; + const catContribs = {}; // cat → { name → metric value } + for (const tx of (txProfiles || [])) { + for (const op of (tx.opcodes || [])) { + let cat = categoryFor(op.op_key); + if (VM_EXCLUSIVE_CATEGORIES.has(cat)) cat = VM_EXCLUSIVE_LABEL; + const v = Number(op[field] || 0); + catTotals[cat] = (catTotals[cat] || 0) + v; + seenCats.add(cat); + if (!catContribs[cat]) catContribs[cat] = {}; + const name = opKeyToName(op.op_key); + catContribs[cat][name] = (catContribs[cat][name] || 0) + v; + } + } + const catParts = {}; + for (const cat of Object.keys(catContribs)) { + catParts[cat] = Object.entries(catContribs[cat]) + .map(([name, v]) => ({ name, v })) + .sort((a, b) => b.v - a.v); + } + perPlatformCatTotal.set(platform, catTotals); + perPlatformCatBreakdown.set(platform, catParts); + } + + // `forcedLabels` keeps the ref_time and proof_size charts row-aligned + // so bar widths match across the pair. + const orderedCategories = OP_CATEGORIES + .filter(cat => !VM_EXCLUSIVE_CATEGORIES.has(cat)) + .concat(VM_EXCLUSIVE_LABEL); + const labels = forcedLabels + ? forcedLabels + : orderedCategories.filter(cat => seenCats.has(cat) && + platformsData.some(({ platform }) => (perPlatformCatTotal.get(platform)[cat] || 0) > 0) + ); + + const series = platformsData.map(({ platform, color }) => { + const totals = perPlatformCatTotal.get(platform); + return { + name: platform, + type: 'bar', + itemStyle: { color }, + data: labels.map(cat => totals[cat] || 0), + emphasis: { focus: 'series' }, + }; + }); + + chart.setOption({ + grid: { left: 16, right: 16, top: 40, bottom: 56, containLabel: true }, + legend: { + type: 'plain', + top: 4, + data: platformsData.map(p => p.platform), + textStyle: { fontSize: 10 }, + itemWidth: 14, + itemHeight: 8, + }, + xAxis: { + type: 'value', + name: axisName, + nameLocation: 'middle', + nameGap: 28, + nameTextStyle: { fontSize: 10, color: '#6b6b6b' }, + axisLabel: { fontSize: 9, color: '#6b6b6b', formatter: fmtFn }, + splitLine: { lineStyle: { color: '#f0f0f0' } }, + }, + yAxis: { + type: 'category', + data: labels, + inverse: true, + axisLabel: { fontSize: 10, color: '#6b6b6b' }, + axisTick: { alignWithLabel: true }, + axisLine: { lineStyle: { color: '#d0d0d0' } }, + }, + tooltip: { + trigger: 'axis', + axisPointer: { type: 'shadow' }, + backgroundColor: 'rgba(255,255,255,0.98)', + borderColor: '#e3e2df', + borderWidth: 1, + textStyle: { fontSize: 11 }, + formatter: (params) => { + if (!params || params.length === 0) return ''; + const idx = params[0].dataIndex; + const cat = labels[idx]; + let html = `${escapeHtml(cat)}`; + params.forEach(p => { + const v = Number(p.value || 0); + const platform = p.seriesName; + if (v <= 0) { + html += `
${p.marker} ${escapeHtml(platform)}: not observed`; + return; + } + html += `
${p.marker} ${escapeHtml(platform)}: ${fmtFn(v)}`; + // Top-3 contributors for this (platform, category) + const parts = (perPlatformCatBreakdown.get(platform) || {})[cat] || []; + const top = parts.slice(0, 3); + if (top.length > 0) { + const tot = parts.reduce((s, x) => s + x.v, 0); + const lines = top.map(p2 => { + const pct = tot > 0 ? (p2.v / tot * 100).toFixed(0) : '0'; + return `   ${escapeHtml(p2.name)} (${pct}%)`; + }); + html += '
' + lines.join('
'); + } + }); + return html; + }, + }, + series, + }); + setTimeout(() => chart.resize(), 0); +} + +/** + * Hydrate one `.opcode-table-sortable` with click-to-sort. Opcode data and + * the active sort live on the table's data-* attributes. + */ +function hydrateOpcodeTable(table) { + const opcodes = JSON.parse(table.dataset.opcodes || '[]'); + const totalRt = Number(table.dataset.totalRt) || 0; + const totalPs = Number(table.dataset.totalPs) || 0; + const tbody = table.querySelector('tbody'); + const headers = table.querySelectorAll('th[data-sort-key]'); + + function compareBy(key, dir) { + return (a, b) => { + let av, bv; + if (key === 'op_key') { + av = opKeyToName(a.op_key); + bv = opKeyToName(b.op_key); + } else { + av = Number(a[key] || 0); + bv = Number(b[key] || 0); + } + const cmp = av < bv ? -1 : av > bv ? 1 : 0; + return dir === 'asc' ? cmp : -cmp; + }; + } + + function renderRows() { + const sortKey = table.dataset.sortKey; + const sortDir = table.dataset.sortDir; + const sorted = [...opcodes].sort(compareBy(sortKey, sortDir)); + + tbody.innerHTML = sorted.slice(0, OPCODE_TABLE_TOP_N).map(op => { + const name = opKeyToName(op.op_key); + const rt = Number(op.total_ref_time || 0); + const ps = Number(op.total_proof_size || 0); + const rtPct = totalRt > 0 ? (rt / totalRt * 100).toFixed(2) : '0.00'; + const psPct = totalPs > 0 ? (ps / totalPs * 100).toFixed(2) : '0.00'; + return ` + ${escapeHtml(name)} + ${Number(op.sample_count || 0).toLocaleString()} + ${fmtOpcodeRefTime(rt)} (${rtPct}%) + ${fmtOpcodeProofSize(ps)} (${psPct}%) + `; + }).join(''); + + headers.forEach(h => { + const arrow = h.querySelector('.sort-arrow'); + if (!arrow) return; + arrow.textContent = (h.dataset.sortKey === sortKey) + ? (sortDir === 'asc' ? '▲' : '▼') + : ''; + }); + } + + headers.forEach(h => { + h.addEventListener('click', () => { + const k = h.dataset.sortKey; + if (table.dataset.sortKey === k) { + table.dataset.sortDir = table.dataset.sortDir === 'asc' ? 'desc' : 'asc'; + } else { + table.dataset.sortKey = k; + table.dataset.sortDir = (k === 'op_key') ? 'asc' : 'desc'; + } + renderRows(); + }); + }); + + renderRows(); +} + +/** + * Step-limit status chip for one platform's opcode profile: how many of the N + * sampled txs hit the `profile-step-limit` cap (a hit truncates the trace tail, + * whose weight then lands in `unattributed`). Per-sample step counts are in the + * hover title. The profiled-vs-unattributed split itself is shown inline in the + * per-step stacked bars (the "Not profiled" segment), so this only builds the chip. + */ +function renderCoverageBlock(profile) { + const tps = (profile.tx_profiles || []).filter(t => Number(t.weight_consumed_ref_time || 0) > 0); + if (!tps.length) return ''; + const cap = reportData?.context?.Benchmark?.benchmark_run?.profile_step_limit ?? null; + // Per-sample captured step counts, so we can report how many of the N samples + // actually hit the cap — an average would hide a partial truncation (some + // samples truncated, some not). + const capsHit = tps.map(t => (t.opcodes || []).reduce((s, o) => s + Number(o.sample_count || 0), 0)); + const nSamples = capsHit.length; + const maxC = Math.max(...capsHit); + const hitCount = cap != null ? capsHit.filter(c => c >= cap).length : 0; + + // Sampled txs can be different transactions with genuinely different step + // counts, so the chip reports status + how close the worst sample got to the + // cap (max), never a single conflated count. Per-sample counts live in the + // hover title. + const plur = nSamples === 1 ? 'sample' : 'samples'; + const perSampleTitle = `Steps captured per sample: ${capsHit.map(c => c.toLocaleString()).join(' · ')}` + + (cap != null ? ` (limit ${cap.toLocaleString()})` : ''); + let chip; + if (cap == null) { + chip = `${nSamples} ${plur} · up to ${maxC.toLocaleString()} steps`; + } else if (hitCount === 0) { + chip = `✓ Complete capture · ${nSamples} ${plur} · max ${maxC.toLocaleString()} / ${cap.toLocaleString()} steps`; + } else { + const scope = hitCount === nSamples ? `all ${nSamples} ${plur}` : `${hitCount} of ${nSamples} ${plur}`; + chip = `⚠ Step limit reached · ${scope} truncated · ${cap.toLocaleString()}-step limit`; + } + + return chip; +} + +function renderOpcodeProfileSection(modeData, enabledPlatforms) { + const group = document.getElementById('opcodeProfileGroup'); + const grid = document.getElementById('opcodeProfileGrid'); + + // Dispose cached ECharts instances bound to the about-to-be-discarded + // DOM — otherwise the next init renders into a detached node. + for (const id of Object.keys(echartsInstances)) { + if (id.startsWith('opcode')) { + echartsInstances[id].dispose(); + delete echartsInstances[id]; + } + } + + const platformExecution = modeData.execReport.platform_execution || {}; + const platformsWithProfile = enabledPlatforms.filter(p => { + const execInfo = platformExecution[p]; + return execInfo && execInfo.opcode_profile; + }); + + if (platformsWithProfile.length === 0) { + group.classList.add('hidden'); + grid.innerHTML = ''; + return; + } + group.classList.remove('hidden'); + + // Catalog is identical across platforms; install from the first that has one. + const firstWithCatalog = platformsWithProfile + .map(p => platformExecution[p].opcode_profile.opcode_catalog) + .find(c => c && (Object.keys(c.evm || {}).length || Object.keys(c.pvm || {}).length)); + installOpcodeCatalog(firstWithCatalog); + + // Compute per-platform rollups up front so the unified chart and the + // per-platform card stats see the same data. + const platformRollups = platformsWithProfile.map(platform => ({ + platform, + color: getColor(modeData.platforms.indexOf(platform)), + rollups: computeStepRollups(platformExecution[platform].opcode_profile.tx_profiles), + })); + const anyRollups = platformRollups.some(p => p.rollups.length > 0); + + const slugify = s => String(s).replace(/[^a-zA-Z0-9_-]/g, '_'); + const stackedChartIds = []; // [{platform, chartId, tx_profiles}] + + const cards = platformsWithProfile.map(platform => { + const profile = platformExecution[platform].opcode_profile; + const color = getColor(modeData.platforms.indexOf(platform)); + const total = profile.opcodes.reduce((s, o) => s + Number(o.total_ref_time || 0), 0); + const rollupsForPlatform = platformRollups.find(p => p.platform === platform).rollups; + const stackedId = `opcodeStackedByStep_${slugify(platform)}`; + const stackedPsId = `opcodeStackedByStepPs_${slugify(platform)}`; + if ((profile.tx_profiles || []).length > 0) { + stackedChartIds.push({ platform, chartId: stackedId, txProfiles: profile.tx_profiles, metric: 'ref_time' }); + stackedChartIds.push({ platform, chartId: stackedPsId, txProfiles: profile.tx_profiles, metric: 'proof_size' }); + } + + const opcodeTotalRefTime = profile.opcodes.reduce( + (sum, op) => sum + Number(op.total_ref_time || 0), 0); + const opcodeTotalProofSize = profile.opcodes.reduce( + (sum, op) => sum + Number(op.total_proof_size || 0), 0); + const opcodesAttr = JSON.stringify(profile.opcodes).replace(/'/g, '''); + + const truncated = profile.opcodes.length > OPCODE_TABLE_TOP_N + ? `
+ ${profile.opcodes.length - OPCODE_TABLE_TOP_N} more opcode${profile.opcodes.length - OPCODE_TABLE_TOP_N === 1 ? '' : 's'} (see report.json)
` + : ''; + + const coverageChip = renderCoverageBlock(profile); + const stackedBlock = (profile.tx_profiles || []).length > 0 + ? `
+
ref_time by opcode (top 8 + Other + Not profiled)
+
+
+
+
proof_size by opcode (top 8 + Other + Not profiled)
+
+
` + : ''; + + return `
+
+ + ${escapeHtml(platform)} + opcode profile + ${coverageChip} +
+
+ ${profile.sampled_tx_count} sampled tx${profile.sampled_tx_count === 1 ? '' : 's'} + · + ${profile.block_count} block${profile.block_count === 1 ? '' : 's'} traced + ${profile.failed_count > 0 ? `·${profile.failed_count} failed` : ''} + · + Σ ref_time: ${fmtOpcodeRefTime(total)} + · + ${rollupsForPlatform.length} step${rollupsForPlatform.length === 1 ? '' : 's'} +
+ ${stackedBlock} + + + + + + + + +
OpcodeCountΣ ref_timeΣ proof_size
+ ${truncated} +
`; + }).join(''); + + const chartsHtml = anyRollups + ? `
+
+
Per-step ref_time (min / mean / max)${platformRollups.length > 1 ? ' — by platform' : ''}
+
+
+
+
Per-step proof_size (min / mean / max)${platformRollups.length > 1 ? ' — by platform' : ''}
+
+
+
` + : ''; + + const crossHtml = platformsWithProfile.length > 1 + ? `
+
Workload-wide ref_time by category — by platform
+
+
+
+
Workload-wide proof_size by category — by platform
+
+
` + : ''; + + grid.innerHTML = chartsHtml + crossHtml + cards; + + grid.querySelectorAll('.opcode-table-sortable').forEach(hydrateOpcodeTable); + + if (anyRollups) { + const nonEmpty = platformRollups.filter(p => p.rollups.length > 0); + renderMultiPlatformStepBandChart('opcodeStepRefTime', nonEmpty, 'ref_time'); + renderMultiPlatformStepBandChart('opcodeStepProofSize', nonEmpty, 'proof_size'); + } + + if (platformsWithProfile.length > 1) { + // Aggregate from tx_profiles (uncapped): opcode_profile.opcodes has a + // top-N "Other" bucket that would taint the category view. + const crossData = platformsWithProfile.map(platform => ({ + platform, + color: getColor(modeData.platforms.indexOf(platform)), + txProfiles: platformExecution[platform].opcode_profile.tx_profiles, + })); + // Union of non-zero categories across both metrics — keeps the two + // charts row-aligned so bar widths match. + const VM_EXCLUSIVE_CATEGORIES = new Set([ + 'Memory', 'Stack', 'Arithmetic & Logic', 'Control flow', + 'VM overhead', 'Immutables', + ]); + const VM_EXCLUSIVE_LABEL = 'VM-exclusive'; + const seen = new Set(); + for (const { txProfiles } of crossData) { + for (const tx of (txProfiles || [])) { + for (const op of (tx.opcodes || [])) { + let cat = categoryFor(op.op_key); + if (VM_EXCLUSIVE_CATEGORIES.has(cat)) cat = VM_EXCLUSIVE_LABEL; + const rt = Number(op.total_ref_time || 0); + const ps = Number(op.total_proof_size || 0); + if (rt > 0 || ps > 0) seen.add(cat); + } + } + } + const orderedCats = OP_CATEGORIES + .filter(c => !VM_EXCLUSIVE_CATEGORIES.has(c)) + .concat(VM_EXCLUSIVE_LABEL); + const sharedLabels = orderedCats.filter(c => seen.has(c)); + + renderCrossPlatformOpcodeComparison('opcodeCrossPlatformBars', crossData, 'ref_time', sharedLabels); + renderCrossPlatformOpcodeComparison('opcodeCrossPlatformProofBars', crossData, 'proof_size', sharedLabels); + } + + for (const job of stackedChartIds) { + renderStepOpcodeBreakdownChart(job.chartId, job.txProfiles, 8, job.metric); + } +} + // ═══════════════════════════════════════════════════════════════════════════ // Summary Cards // ═══════════════════════════════════════════════════════════════════════════ @@ -2854,10 +3945,16 @@

Benchmark ReportBenchmark Report + ref_time${r} + proof_size${p} + `; + }; - function buildCard(id, label, accentColor, getValue, bestPlatform, fmt) { + function buildCard(id, label, accentColor, getValue, bestPlatform, fmt, hideNull = false) { const el = document.getElementById(id); el.style.borderTopColor = accentColor; - const rows = enabledPlatforms.map(p => { - const v = getValue(p); - const isBest = p === bestPlatform && v != null; - const color = getColor(modeData.platforms.indexOf(p)); - return ` -
- - - ${escapeHtml(p)} - - ${v != null ? fmt(v) : '\u2014'} -
`; - }).join(''); + const visible = hideNull + ? enabledPlatforms.filter(p => getValue(p) != null) + : enabledPlatforms; + const rows = visible.length === 0 + ? '
No data available
' + : visible.map(p => { + const v = getValue(p); + const isBest = p === bestPlatform && v != null; + const color = getColor(modeData.platforms.indexOf(p)); + return ` +
+ + + ${escapeHtml(p)} + + ${v != null ? fmt(v) : '\u2014'} +
`; + }).join(''); el.innerHTML = `
${label}
${rows}
`; } - buildCard('summaryTps', 'Effective TPS', '#6366f1', p => stats[p]?.effectiveTps, bestTps, fmtTps); - buildCard('summaryUtil', 'Peak Utilization', '#10b981', p => stats[p]?.maxUtil, bestUtil, fmtUtil); - buildCard('summaryTime', 'Running Time', '#f59e0b', p => stats[p]?.runningTime, bestTime, fmtTime); + buildCard('summaryTps', 'Effective TPS', '#6366f1', p => stats[p]?.effectiveTps, bestTps, fmtTps); + buildCard('summaryWeightPerTx', 'Weight / tx', '#a855f7', p => { + const s = stats[p]; + if (s == null || (s.refTimePerTx == null && s.proofSizePerTx == null)) return null; + return { refTime: s.refTimePerTx, proofSize: s.proofSizePerTx }; + }, null, fmtWeight, /* hideNull */ true); + buildCard('summaryUtil', 'Peak Utilization', '#10b981', p => stats[p]?.maxUtil, bestUtil, fmtUtil); + buildCard('summaryTime', 'Running Time', '#f59e0b', p => stats[p]?.runningTime, bestTime, fmtTime); document.getElementById('summaryCards').classList.remove('hidden'); } diff --git a/assets/revive_metadata.scale b/assets/revive_metadata.scale index 461d0710..f6f2d40f 100644 Binary files a/assets/revive_metadata.scale and b/assets/revive_metadata.scale differ diff --git a/crates/config/src/lib.rs b/crates/config/src/lib.rs index a07bdd6c..42c7c83c 100644 --- a/crates/config/src/lib.rs +++ b/crates/config/src/lib.rs @@ -198,6 +198,33 @@ mod context { /// which is a case where the benchmark isn't what's being done, but the program is being /// tested in one way or another. pub repetition_count_override: Option, + + /// Profile watched transactions by capturing per-opcode execution traces + /// after the benchmark completes. Substrate platforms only. + #[arg(long = "benchmark.profile-watched-txs", default_value_t = false)] + pub profile_watched_txs: bool, + + /// Number of samples to trace per unique step path. Samples are picked + /// at evenly-spaced iteration indices and tend to co-locate in the same + /// blocks, so the effective number of `trace_block` calls is roughly + /// this value times the number of top-level Repeat blocks. + #[arg(long = "benchmark.profile-samples-per-step-path", default_value_t = 5)] + pub profile_samples_per_step_path: usize, + + /// Trace every watched transaction instead of sampling. Significantly + /// heavier; useful when investigating a small workload in depth. + #[arg(long = "benchmark.profile-all", default_value_t = false)] + pub profile_all: bool, + + /// Maximum in-flight `trace_block` runtime API calls per platform. + #[arg(long = "benchmark.profile-concurrency", default_value_t = 4)] + pub profile_concurrency: usize, + + /// Safety cap on the number of `ExecutionStep` entries captured per + /// transaction. Maps to `ExecutionTracerConfig::limit`. Set to 0 for + /// no cap. + #[arg(long = "benchmark.profile-step-limit", default_value_t = 100_000)] + pub profile_step_limit: u64, } /// Configuration for the export-genesis target platform. diff --git a/crates/core/src/differential_benchmarks/driver.rs b/crates/core/src/differential_benchmarks/driver.rs index 87585a8e..4e0d6d18 100644 --- a/crates/core/src/differential_benchmarks/driver.rs +++ b/crates/core/src/differential_benchmarks/driver.rs @@ -1081,21 +1081,35 @@ where *gas_estimate } None => { - let gas_estimate = timeout(Duration::from_secs(5 * 60), async { - let mut interval = interval(Duration::from_millis(200)); - loop { - interval.tick().await; - match provider.estimate_gas(transaction.clone()).await { - Ok(gas_estimate) => break gas_estimate, - Err(err) => { - warn!(?err, "Failed to get the gas estimate, retrying"); - continue; + let gas_estimate = if node.substrate_rpc_client().is_some() { + // JIT-safe path: a single `ReviveApi_eth_transact` dry-run. + // The node's binary-search `eth_estimateGas` reuses warm + // pricing under the PVM JIT and underestimates, which makes + // the eth-rpc retry loop below stall to the 5-minute deadline + // and every polkavm workload time out. + node.estimate_gas_via_transact(transaction.clone()) + .await + .context("Failed to estimate gas via ReviveApi_eth_transact")? + } else { + timeout(Duration::from_secs(5 * 60), async { + let mut interval = interval(Duration::from_millis(200)); + loop { + interval.tick().await; + match provider.estimate_gas(transaction.clone()).await { + Ok(gas_estimate) => break gas_estimate, + Err(err) => { + warn!( + ?err, + "Failed to get the gas estimate, retrying" + ); + continue; + } } } - } - }) - .await - .context("Failed to get the gas estimate")?; + }) + .await + .context("Failed to get the gas estimate")? + }; write_guard.insert(step_path.clone(), gas_estimate); info!( estimated_gas = gas_estimate, diff --git a/crates/core/src/differential_benchmarks/entry_point.rs b/crates/core/src/differential_benchmarks/entry_point.rs index ff3b0984..c3bd0837 100644 --- a/crates/core/src/differential_benchmarks/entry_point.rs +++ b/crates/core/src/differential_benchmarks/entry_point.rs @@ -3,6 +3,9 @@ use crate::internal_prelude::*; use crate::differential_benchmarks::Driver; +use crate::differential_benchmarks::{ + SamplingMode, aggregate_to_summary, build_block_jobs, execute_trace_jobs, sample_watched_txs, +}; /// Handles the differential testing executing it according to the information defined in the /// context @@ -147,6 +150,9 @@ pub async fn handle_differential_benchmarks( }; let transaction_finder = TransactionFinder::new(provider.clone(), substrate_provider.clone()); + // Cheap Arc-clone — kept so we can resolve sampled tx hashes to + // their substrate blocks after the driver consumes the original. + let transaction_finder_for_profiler = transaction_finder.clone(); let (watcher, watcher_tx) = Watcher::new( platform_information .node @@ -245,8 +251,66 @@ pub async fn handle_differential_benchmarks( } let _ = driver_rtn.context("Driver failed")?; - watcher_rtn.context("Watcher failed")?; + let watcher_outcome = watcher_rtn.context("Watcher failed")?; inclusion_rtn.context("Inclusion watcher failed")?; + + if context.benchmark_run.profile_watched_txs { + let mode = if context.benchmark_run.profile_all { + SamplingMode::All + } else { + SamplingMode::Sample(context.benchmark_run.profile_samples_per_step_path) + }; + let sampled_hashes = + sample_watched_txs(&watcher_outcome.transaction_registration_information, mode); + let samples: Vec<(TxHash, StepPath)> = sampled_hashes + .into_iter() + .filter_map(|h| { + watcher_outcome + .transaction_registration_information + .get(&h) + .map(|(sp, _)| (h, sp.clone())) + }) + .collect(); + let watched_tx_count = watcher_outcome.transaction_registration_information.len(); + let sampled_count = samples.len(); + + let jobs = build_block_jobs(&transaction_finder_for_profiler, samples) + .await + .context("Failed to build profiler trace jobs")?; + let block_count = jobs.len() as u32; + + let profiles = execute_trace_jobs( + platform_information.node, + jobs, + context.benchmark_run.profile_step_limit, + context.benchmark_run.profile_concurrency, + ) + .await; + + let top_opcode = profiles + .iter() + .flat_map(|p| p.opcodes.first()) + .max_by_key(|op| op.total_ref_time) + .map(|op| op.op_key.as_string()) + .unwrap_or_else(|| "".to_string()); + + info!( + %platform_identifier, + watched_tx_count, + sampled_count, + block_count, + profile_count = profiles.len(), + top_opcode = %top_opcode, + "Profiler: workload complete" + ); + + let summary = aggregate_to_summary(profiles, block_count); + test_definition + .reporter + .execution_specific_reporter(0usize, platform_identifier) + .report_opcode_profile_completed_event(summary) + .context("Failed to send opcode_profile_completed event to reporter")?; + } } } diff --git a/crates/core/src/differential_benchmarks/mod.rs b/crates/core/src/differential_benchmarks/mod.rs index ada495bc..7e77563d 100644 --- a/crates/core/src/differential_benchmarks/mod.rs +++ b/crates/core/src/differential_benchmarks/mod.rs @@ -3,6 +3,7 @@ mod entry_point; mod execution_state; mod inclusion_watcher; mod transaction_finder; +mod tx_profiler; mod watcher; pub use driver::*; @@ -10,4 +11,5 @@ pub use entry_point::*; pub use execution_state::*; pub use inclusion_watcher::*; pub use transaction_finder::*; +pub use tx_profiler::*; pub use watcher::*; diff --git a/crates/core/src/differential_benchmarks/tx_profiler.rs b/crates/core/src/differential_benchmarks/tx_profiler.rs new file mode 100644 index 00000000..63b2e0cd --- /dev/null +++ b/crates/core/src/differential_benchmarks/tx_profiler.rs @@ -0,0 +1,429 @@ +//! Watched-tx opcode profiler. +//! +//! The pipeline runs in two passes so that per-workload state (the +//! [`TransactionFinder`]) doesn't have to survive past its workload's +//! lifetime: +//! +//! 1. **At the end of each `(platform, workload)`** (finder still alive): +//! sample tx hashes per [`StepPath`] via [`sample_watched_txs`], resolve +//! each sampled hash to `(tx_index_in_block, substrate_block)` via the +//! finder, group by block hash → [`PerBlockTraceJob`]s. +//! 2. **At the end of all benchmarks** (nodes still alive): execute the +//! accumulated jobs via [`execute_trace_jobs`] against each platform's +//! node, aggregating to [`TxProfile`]s. + +use std::collections::HashMap; + +use futures::stream::{self, StreamExt}; +use indexmap::IndexMap; +use revive_dt_node_interaction::opcode_profile::{OpcodeCatalog, TxProfile}; +use revive_dt_node_interaction::revive_metadata::runtime_types::pallet_revive::evm::api::debug_rpc_types::ExecutionTracerConfig; +use revive_dt_report::{ + AggregatedOpcode, OpcodeCatalogWire, OpcodeEntryWire, OpcodeProfileSummary, TxProfileWire, +}; +use subxt::utils::H256 as SubxtH256; +use tracing::warn; + +use crate::differential_benchmarks::transaction_finder::TransactionFinder; +use crate::internal_prelude::*; + +/// How to choose which watched transactions become profiler input. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum SamplingMode { + /// Pick at most `k` transactions per unique `StepPath`, at evenly-spaced + /// indices through that step path's submission order. + Sample(usize), + /// Use every watched transaction (CPU-heavy; opt-in via `--benchmark.profile-all`). + All, +} + +/// Sample tx hashes for profiling from the watcher's per-workload submission map. +/// +/// The watcher returns an `IndexMap` that preserves +/// submission order. We: +/// 1. Group hashes by `StepPath`, preserving submission order within each group. +/// 2. For `Sample(k)`: pick `k` evenly-spaced indices per group — positions +/// `⌊(N − 1) · i / (k − 1)⌋` for `i ∈ 0..k`. Captures cold (i=0), +/// steady-state (middle), and end-state (i=N−1). If `N ≤ k`, return all. +/// 3. For `All`: return every key. +pub fn sample_watched_txs( + transactions: &IndexMap, + mode: SamplingMode, +) -> Vec { + if let SamplingMode::All = mode { + return transactions.keys().copied().collect(); + } + let SamplingMode::Sample(k) = mode else { + unreachable!() + }; + if k == 0 || transactions.is_empty() { + return Vec::new(); + } + + let mut by_step: IndexMap<&StepPath, Vec> = IndexMap::new(); + for (tx_hash, (step_path, _)) in transactions { + by_step.entry(step_path).or_default().push(*tx_hash); + } + + let mut out = Vec::new(); + for (_, group) in by_step { + sample_one_group(&group, k, &mut out); + } + out +} + +fn sample_one_group(group: &[TxHash], k: usize, out: &mut Vec) { + let n = group.len(); + if n == 0 { + return; + } + if n <= k { + out.extend_from_slice(group); + return; + } + if k == 1 { + out.push(group[0]); + return; + } + for i in 0..k { + let idx = (n - 1) * i / (k - 1); + out.push(group[idx]); + } +} + +/// Build [`ExecutionTracerConfig`] with our defaults plus a CLI-controlled +/// step cap. Pass `step_limit = 0` to disable the cap. +pub fn execution_tracer_config(step_limit: u64) -> ExecutionTracerConfig { + ExecutionTracerConfig { + enable_memory: false, + disable_stack: true, + disable_storage: true, + enable_return_data: false, + disable_syscall_details: false, + limit: if step_limit == 0 { + None + } else { + Some(step_limit) + }, + memory_word_limit: 0, + } +} + +/// One block to trace, plus the `tx_index → (tx_hash, step_path)` map for +/// tagging traces back to samples. +pub struct PerBlockTraceJob { + pub block_hash: SubxtH256, + pub watched_indices: HashMap, +} + +/// Resolve sampled hashes to per-block trace jobs. Must be called while the +/// [`TransactionFinder`] is still alive. Samples whose finder lookup yields +/// no substrate block (non-substrate platforms) are silently skipped. +pub async fn build_block_jobs( + finder: &TransactionFinder, + samples: Vec<(TxHash, StepPath)>, +) -> Result> { + let mut by_block: HashMap = HashMap::new(); + + for (tx_hash, step_path) in samples { + let (tx_index, block_arc) = finder.find(tx_hash).await; + let Some(substrate_block) = block_arc.substrate_block.as_ref() else { + continue; + }; + let block_hash = substrate_block.hash(); + + by_block + .entry(block_hash) + .or_insert_with(|| PerBlockTraceJob { + block_hash, + watched_indices: HashMap::new(), + }) + .watched_indices + .insert(tx_index as u32, (tx_hash, step_path)); + } + Ok(by_block.into_values().collect()) +} + +/// Execute trace jobs against a node, aggregating each watched tx to a +/// [`TxProfile`]. Concurrency is bounded by `concurrency`. Per-block failures +/// are logged and skipped — the function returns whatever profiles succeed. +pub async fn execute_trace_jobs( + node: &dyn NodeApi, + jobs: Vec, + step_limit: u64, + concurrency: usize, +) -> Vec { + let concurrency = concurrency.max(1); + + let samples: Vec<(SubxtH256, u32, TxHash, StepPath)> = jobs + .into_iter() + .flat_map(|job| { + let block_hash = job.block_hash; + job.watched_indices + .into_iter() + .map(move |(tx_index, (tx_hash, step_path))| { + (block_hash, tx_index, tx_hash, step_path) + }) + }) + .collect(); + + let pending = samples + .into_iter() + .map(|(block_hash, tx_index, tx_hash, step_path)| { + let trace_future = + node.trace_execution_tx(block_hash, tx_index, execution_tracer_config(step_limit)); + async move { + match trace_future.await { + Ok(Some(execution_trace)) => Some(TxProfile::from_execution_trace( + tx_hash, + step_path, + &execution_trace, + )), + Ok(None) => { + warn!( + ?block_hash, + tx_index, "trace_execution_tx returned None; skipping sample" + ); + None + } + Err(err) => { + warn!( + ?block_hash, + tx_index, + ?err, + "trace_execution_tx failed; skipping sample" + ); + None + } + } + } + }); + + stream::iter(pending) + .buffer_unordered(concurrency) + .filter_map(|opt| async move { opt }) + .collect() + .await +} + +/// Maximum number of distinct opcode rows kept in the aggregated rollup. +/// Anything beyond rolls into an "Other" bucket. Keeps report.json bounded +/// (a single trace can have hundreds of distinct PVM syscalls + EVM opcodes). +const OPCODE_TOP_N: usize = 64; + +/// Aggregate a workload's `Vec` into a wire-ready +/// `OpcodeProfileSummary` for the report. +pub fn aggregate_to_summary(profiles: Vec, block_count: u32) -> OpcodeProfileSummary { + let sampled_tx_count = profiles.len(); + let failed_count = profiles.iter().filter(|p| p.failed).count(); + + let mut by_op: HashMap = HashMap::new(); + for profile in &profiles { + for opcode in &profile.opcodes { + let key = opcode.op_key.as_string(); + let entry = by_op.entry(key).or_insert((0, 0, 0)); + entry.0 += opcode.count; + entry.1 += opcode.total_ref_time; + entry.2 += opcode.total_proof_size; + } + } + + let mut sorted: Vec<(String, u64, u128, u128)> = by_op + .into_iter() + .map(|(k, (c, rt, ps))| (k, c, rt, ps)) + .collect(); + sorted.sort_by(|a, b| b.2.cmp(&a.2).then_with(|| a.0.cmp(&b.0))); + + let mut opcodes: Vec = sorted + .iter() + .take(OPCODE_TOP_N) + .map(|(op_key, count, rt, ps)| AggregatedOpcode { + op_key: op_key.clone(), + sample_count: *count, + total_ref_time: *rt, + total_proof_size: *ps, + }) + .collect(); + + if sorted.len() > OPCODE_TOP_N { + let (count, rt, ps) = sorted + .iter() + .skip(OPCODE_TOP_N) + .fold((0u64, 0u128, 0u128), |acc, (_, c, rt, ps)| { + (acc.0 + c, acc.1 + rt, acc.2 + ps) + }); + opcodes.push(AggregatedOpcode { + op_key: "Other".to_string(), + sample_count: count, + total_ref_time: rt, + total_proof_size: ps, + }); + } + + let tx_profiles: Vec = profiles + .into_iter() + .map(|p| { + let opcodes = p + .opcodes + .into_iter() + .map(|o| AggregatedOpcode { + op_key: o.op_key.as_string(), + sample_count: o.count, + total_ref_time: o.total_ref_time, + total_proof_size: o.total_proof_size, + }) + .collect(); + TxProfileWire { + tx_hash: p.tx_hash, + step_path: p.step_path, + failed: p.failed, + gas_used: p.gas_used, + weight_consumed_ref_time: p.weight_consumed_ref_time, + weight_consumed_proof_size: p.weight_consumed_proof_size, + base_call_weight_ref_time: p.base_call_weight_ref_time, + base_call_weight_proof_size: p.base_call_weight_proof_size, + unattributed_ref_time: p.unattributed_ref_time, + unattributed_proof_size: p.unattributed_proof_size, + opcodes, + } + }) + .collect(); + + OpcodeProfileSummary { + sampled_tx_count, + block_count, + failed_count, + opcodes, + tx_profiles, + opcode_catalog: opcode_catalog_wire(), + } +} + +fn opcode_catalog_wire() -> OpcodeCatalogWire { + let catalog = OpcodeCatalog::current(); + let to_wire = |m: std::collections::BTreeMap<_, _>| { + m.into_iter() + .map( + |(byte, entry): (u8, revive_dt_node_interaction::opcode_profile::OpcodeEntry)| { + ( + byte.to_string(), + OpcodeEntryWire { + name: entry.name, + category: entry.category.to_string(), + }, + ) + }, + ) + .collect() + }; + OpcodeCatalogWire { + evm: to_wire(catalog.evm), + pvm: to_wire(catalog.pvm), + category_order: catalog + .category_order + .iter() + .map(|s| s.to_string()) + .collect(), + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn mk_hash(tag: u8) -> TxHash { + let mut bytes = [0u8; 32]; + bytes[0] = tag; + TxHash::from(bytes) + } + + fn path(indices: &[usize]) -> StepPath { + StepPath::new( + indices + .iter() + .copied() + .map(StepIdx::new) + .collect::>(), + ) + } + + fn submissions(items: Vec<(u8, &[usize])>) -> IndexMap { + let mut m = IndexMap::new(); + for (tag, p) in items { + m.insert(mk_hash(tag), (path(p), ())); + } + m + } + + #[test] + fn all_mode_returns_every_key_in_order() { + let m = submissions(vec![(1, &[0]), (2, &[0]), (3, &[1])]); + let out = sample_watched_txs(&m, SamplingMode::All); + assert_eq!(out, vec![mk_hash(1), mk_hash(2), mk_hash(3)]); + } + + #[test] + fn k_zero_returns_empty() { + let m = submissions(vec![(1, &[0]), (2, &[0])]); + assert!(sample_watched_txs(&m, SamplingMode::Sample(0)).is_empty()); + } + + #[test] + fn smaller_than_k_returns_all_for_group() { + // Group has 3 entries, k=5 → return all 3 + let m = submissions(vec![(1, &[0]), (2, &[0]), (3, &[0])]); + let out = sample_watched_txs(&m, SamplingMode::Sample(5)); + assert_eq!(out, vec![mk_hash(1), mk_hash(2), mk_hash(3)]); + } + + #[test] + fn k_one_picks_first_per_group() { + let m = submissions(vec![(1, &[0]), (2, &[0]), (3, &[0]), (4, &[1]), (5, &[1])]); + let out = sample_watched_txs(&m, SamplingMode::Sample(1)); + assert_eq!(out, vec![mk_hash(1), mk_hash(4)]); + } + + #[test] + fn evenly_spaced_indices() { + // Group of 9 entries (indices 0..=8), k=5 + // Expected positions: ⌊(8 * i) / 4⌋ for i ∈ 0..5 = 0, 2, 4, 6, 8 + let entries: Vec<(u8, &[usize])> = (1u8..=9).map(|i| (i, &[0usize] as &[usize])).collect(); + let m = submissions(entries); + let out = sample_watched_txs(&m, SamplingMode::Sample(5)); + assert_eq!( + out, + vec![ + mk_hash(1), // idx 0 + mk_hash(3), // idx 2 + mk_hash(5), // idx 4 + mk_hash(7), // idx 6 + mk_hash(9), // idx 8 + ] + ); + } + + #[test] + fn multiple_groups_each_sampled_independently() { + // step_path [0]: 5 entries (1..=5), step_path [1]: 5 entries (6..=10) + // k=3 → positions ⌊(4 * i) / 2⌋ = 0, 2, 4 + let entries: Vec<(u8, &[usize])> = (1..=5) + .map(|i| (i, &[0usize] as &[usize])) + .chain((6..=10).map(|i| (i, &[1usize] as &[usize]))) + .collect(); + let m = submissions(entries); + let out = sample_watched_txs(&m, SamplingMode::Sample(3)); + assert_eq!( + out, + vec![ + // step_path [0] + mk_hash(1), // idx 0 + mk_hash(3), // idx 2 + mk_hash(5), // idx 4 + // step_path [1] + mk_hash(6), // idx 0 + mk_hash(8), // idx 2 + mk_hash(10), // idx 4 + ] + ); + } +} diff --git a/crates/core/src/differential_benchmarks/watcher.rs b/crates/core/src/differential_benchmarks/watcher.rs index 837512aa..64093803 100644 --- a/crates/core/src/differential_benchmarks/watcher.rs +++ b/crates/core/src/differential_benchmarks/watcher.rs @@ -138,7 +138,7 @@ impl Watcher { /// Finally, the watcher reports all observed block information, per-block transaction counts, /// and per-transaction metadata (submission time, block inclusion time) to the reporter. #[allow(irrefutable_let_patterns)] - pub fn run(mut self) -> FrameworkFuture> { + pub fn run(mut self) -> FrameworkFuture> { Box::pin(async move { // We start by waiting for the `StartEvent` which informs us of the first block that we want // to watch for. Any event that we receive before this `StartEvent` is ignored as the driver @@ -498,11 +498,21 @@ impl Watcher { let _ = self.reporter.report_block_mined_event(block); } - Ok(()) + Ok(WatcherOutcome { + transaction_registration_information, + }) }) } } +/// What a successful [`Watcher::run`] yields: the registration map collected +/// from `WatcherEvent::SubmittedTransaction`s, in submission order. The opcode +/// profiler consumes this for per-step-path sampling. +#[derive(Debug)] +pub struct WatcherOutcome { + pub transaction_registration_information: IndexMap, +} + #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] pub enum WatcherEvent { /// Informs the watcher that it should begin watching for the blocks mined by the platforms. diff --git a/crates/node-interaction/Cargo.toml b/crates/node-interaction/Cargo.toml index d5207858..c94ed3f5 100644 --- a/crates/node-interaction/Cargo.toml +++ b/crates/node-interaction/Cargo.toml @@ -16,7 +16,9 @@ revive-dt-common = { workspace = true } alloy = { workspace = true } anyhow = { workspace = true } futures = { workspace = true } +hex = { workspace = true } pallet-revive = { workspace = true } +revm-bytecode = { workspace = true } serde_json = { workspace = true } subxt = { workspace = true } tokio = { workspace = true } diff --git a/crates/node-interaction/src/lib.rs b/crates/node-interaction/src/lib.rs index 9eae3a01..6762132f 100644 --- a/crates/node-interaction/src/lib.rs +++ b/crates/node-interaction/src/lib.rs @@ -1,5 +1,7 @@ //! This crate implements all node interactions. +pub mod opcode_profile; + pub mod prelude { pub use crate::NodeApi; pub use crate::revive_metadata; @@ -136,6 +138,134 @@ pub trait NodeApi { }) } + /// Trace one extrinsic via `state_callRecorded` (not subxt's plain + /// `state_call`) so `ProofSizeExt` is registered and post-dispatch + /// reclaim works. + fn trace_execution_tx( + &self, + block_hash: subxt::utils::H256, + tx_index: u32, + config: revive_metadata::runtime_types::pallet_revive::evm::api::debug_rpc_types::ExecutionTracerConfig, + ) -> FrameworkFuture< + Result< + Option< + revive_metadata::runtime_types::pallet_revive::evm::api::debug_rpc_types::ExecutionTrace, + >, + >, + >{ + use pallet_revive::codec::{Decode, Encode}; + use revive_metadata::runtime_types::pallet_revive::evm::api::debug_rpc_types::{ + Trace, TracerType, + }; + + let rpc_client_future = self.substrate_rpc_client(); + Box::pin(async move { + let rpc_client = rpc_client_future + .context( + "trace_execution_tx requires a substrate RPC client; \ + this node is not substrate-based", + )? + .await + .context("Failed to get the substrate RPC client")?; + + let extra_args = (tx_index, TracerType::ExecutionTracer(Some(config))).encode(); + + let mut params = subxt::ext::subxt_rpcs::client::RpcParams::new(); + params + .push(block_hash) + .context("Failed to encode block_hash for state_callRecorded")?; + params + .push("ReviveApi_trace_tx") + .context("Failed to encode method name for state_callRecorded")?; + params + .push(format!("0x{}", hex::encode(&extra_args))) + .context("Failed to encode extra_args for state_callRecorded")?; + + let hex_result: String = rpc_client + .request("state_callRecorded", params) + .await + .context("state_callRecorded(ReviveApi_trace_tx) failed")?; + let result_bytes = hex::decode(hex_result.strip_prefix("0x").unwrap_or(&hex_result)) + .context("state_callRecorded returned non-hex bytes")?; + let trace: Option< + revive_metadata::runtime_types::pallet_revive::evm::api::debug_rpc_types::Trace, + > = Decode::decode(&mut result_bytes.as_slice()) + .context("Failed to SCALE-decode trace_tx response")?; + + Ok(trace.and_then(|t| match t { + Trace::Execution(execution_trace) => Some(execution_trace), + Trace::Call(_) | Trace::Prestate(_) => None, + })) + }) + } + + /// Estimate gas via a single `ReviveApi_eth_transact` dry-run instead of the + /// node's binary-search `eth_estimateGas`. + /// + /// The binary-search estimator reuses warm pricing after the first (cold) dry + /// run under the PVM JIT, underestimating gas; `eth_transact` performs a single + /// dry run with no supplied gas (the runtime caps it at the block gas limit) and + /// returns the actual `eth_gas` required. This is the JIT-safe path and mirrors + /// what `main` selects via `allowed_estimation_methods`. + /// + /// Requires a substrate RPC client; callers should only use this for substrate + /// based nodes (e.g. zombienet) and fall back to eth-rpc estimation otherwise. + fn estimate_gas_via_transact( + &self, + transaction: TransactionRequest, + ) -> FrameworkFuture> { + use pallet_revive::codec::{Decode, Encode}; + + let rpc_client_future = self.substrate_rpc_client(); + Box::pin(async move { + let rpc_client = rpc_client_future + .context( + "estimate_gas_via_transact requires a substrate RPC client; \ + this node is not substrate-based", + )? + .await + .context("Failed to get the substrate RPC client")?; + + // Convert the alloy transaction request into a revive `GenericTransaction` + // via a JSON round-trip (their serde representations line up). + let tx_json = serde_json::to_value(&transaction) + .context("Failed to serialize transaction request to JSON")?; + let generic_tx: pallet_revive::evm::GenericTransaction = + serde_json::from_value(tx_json).context( + "Failed to convert transaction request into a revive GenericTransaction", + )?; + + let args = generic_tx.encode(); + + let mut params = subxt::ext::subxt_rpcs::client::RpcParams::new(); + params + .push("ReviveApi_eth_transact") + .context("Failed to encode method name for state_call")?; + params + .push(format!("0x{}", hex::encode(&args))) + .context("Failed to encode args for state_call")?; + + let hex_result: String = rpc_client + .request("state_call", params) + .await + .context("state_call(ReviveApi_eth_transact) failed")?; + let result_bytes = hex::decode(hex_result.strip_prefix("0x").unwrap_or(&hex_result)) + .context("state_call returned non-hex bytes")?; + + let info: core::result::Result< + pallet_revive::EthTransactInfo, + pallet_revive::EthTransactError, + > = Decode::decode(&mut result_bytes.as_slice()) + .context("Failed to SCALE-decode eth_transact response")?; + let info = info + .map_err(|err| anyhow::anyhow!("ReviveApi_eth_transact failed: {err:?}"))?; + + let eth_gas = u64::try_from(info.eth_gas) + .map_err(|err| anyhow::anyhow!("eth_gas does not fit in u64: {err}"))?; + Ok(eth_gas) + }) + } + /// Returns the state diff of the transaction hash in the [TransactionReceipt]. fn state_diff(&self, tx_hash: TxHash) -> FrameworkFuture> { let provider = self.provider(); diff --git a/crates/node-interaction/src/opcode_profile.rs b/crates/node-interaction/src/opcode_profile.rs new file mode 100644 index 00000000..14472bd0 --- /dev/null +++ b/crates/node-interaction/src/opcode_profile.rs @@ -0,0 +1,428 @@ +//! Opcode-profile types and aggregation. +//! +//! Two responsibilities: +//! 1. [`OpcodeCatalog`] snapshots upstream byte→name tables (revm for EVM, +//! pallet-revive for PVM) and tags each with an editorial category. Carried +//! in the report so consumers don't ship their own opcode tables. +//! 2. [`TxProfile::from_execution_trace`] aggregates one `ExecutionTrace` +//! (returned by [`NodeApi::trace_execution_tx`](crate::NodeApi)) into +//! per-opcode weight buckets + the unattributed-weight residual. + +use std::cmp::Ordering; +use std::collections::{BTreeMap, HashMap}; + +use alloy::primitives::TxHash; +use revive_dt_common::subscriptions::StepPath; + +use crate::revive_metadata::runtime_types::pallet_revive::evm::api::debug_rpc_types::{ + ExecutionStepKind, ExecutionTrace, +}; + +/// `byte → {name, category}` catalogs for EVM opcodes and PVM syscalls. +/// Names come from upstream (revm + pallet-revive); categories are the +/// editorial taxonomy in [`categorize`]. Embedded in the report so HTML +/// consumers don't ship their own opcode tables. +#[derive(Debug, Clone, Default, PartialEq, Eq)] +pub struct OpcodeCatalog { + pub evm: BTreeMap, + pub pvm: BTreeMap, + pub category_order: &'static [&'static str], +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct OpcodeEntry { + pub name: String, + pub category: &'static str, +} + +pub const CATEGORY_ORDER: &[&str] = &[ + "Storage", + "Calls", + "Returns", + "Memory", + "Stack", + "Arithmetic & Logic", + "Control flow", + "Context", + "Calldata / Returndata", + "Code", + "Logs", + "Crypto", + "Immutables", + "VM overhead", + "Other", +]; + +/// Unknown names fall into `"Other"` — surfaces in the UI so a new opcode +/// upstream is visible until categorized here. +fn categorize(name: &str) -> &'static str { + match name { + "STOP" | "RETURN" | "REVERT" | "INVALID" | "SELFDESTRUCT" => "Returns", + "ADD" | "MUL" | "SUB" | "DIV" | "SDIV" | "MOD" | "SMOD" | "ADDMOD" | "MULMOD" | "EXP" + | "SIGNEXTEND" | "LT" | "GT" | "SLT" | "SGT" | "EQ" | "ISZERO" | "AND" | "OR" | "XOR" + | "NOT" | "BYTE" | "SHL" | "SHR" | "SAR" => "Arithmetic & Logic", + "KECCAK256" => "Crypto", + "ADDRESS" | "BALANCE" | "ORIGIN" | "CALLER" | "CALLVALUE" | "GASPRICE" | "BLOCKHASH" + | "COINBASE" | "TIMESTAMP" | "NUMBER" | "DIFFICULTY" | "GASLIMIT" | "CHAINID" + | "SELFBALANCE" | "BASEFEE" | "BLOBHASH" | "BLOBBASEFEE" => "Context", + "CALLDATALOAD" | "CALLDATASIZE" | "CALLDATACOPY" | "RETURNDATASIZE" | "RETURNDATACOPY" => { + "Calldata / Returndata" + } + "CODESIZE" | "CODECOPY" | "EXTCODESIZE" | "EXTCODECOPY" | "EXTCODEHASH" => "Code", + "POP" => "Stack", + "MLOAD" | "MSTORE" | "MSTORE8" | "MSIZE" | "MCOPY" => "Memory", + "SLOAD" | "SSTORE" | "TLOAD" | "TSTORE" => "Storage", + "JUMP" | "JUMPI" | "PC" | "GAS" | "JUMPDEST" => "Control flow", + "CREATE" | "CALL" | "CALLCODE" | "DELEGATECALL" | "CREATE2" | "STATICCALL" => "Calls", + n if n.starts_with("PUSH") || n.starts_with("DUP") || n.starts_with("SWAP") => "Stack", + n if n.starts_with("LOG") => "Logs", + + "set_storage" | "set_storage_or_clear" | "get_storage" | "get_storage_or_zero" => "Storage", + "call" | "call_evm" | "delegate_call" | "delegate_call_evm" | "instantiate" => "Calls", + "seal_return" | "terminate" | "consume_all_gas" => "Returns", + "caller" | "origin" | "address" | "balance" | "balance_of" | "chain_id" | "gas_limit" + | "value_transferred" | "gas_price" | "base_fee" | "now" | "block_number" + | "block_hash" | "block_author" => "Context", + "call_data_size" | "call_data_copy" | "call_data_load" | "return_data_size" + | "return_data_copy" => "Calldata / Returndata", + "code_hash" | "code_size" => "Code", + "deposit_event" => "Logs", + "hash_keccak_256" | "ecdsa_to_eth_address" | "sr25519_verify" => "Crypto", + "get_immutable_data" | "set_immutable_data" => "Immutables", + "noop" | "pvm_fuel" | "ref_time_left" => "VM overhead", + + _ => "Other", + } +} + +impl OpcodeCatalog { + pub fn current() -> Self { + let evm = (0..=u8::MAX) + .filter_map(|byte| { + let name = revm_bytecode::opcode::OpCode::name_by_op(byte); + (name != "Unknown").then(|| { + ( + byte, + OpcodeEntry { + name: name.to_string(), + category: categorize(name), + }, + ) + }) + }) + .collect(); + + let mut pvm = BTreeMap::new(); + for byte in 0..=u8::MAX { + let kind = pallet_revive::evm::ExecutionStepKind::PVMSyscall { + op: byte, + args: Vec::new(), + returned: None, + }; + let Ok(value) = serde_json::to_value(&kind) else { + break; + }; + let Some(name) = value.get("op").and_then(|v| v.as_str()) else { + break; + }; + pvm.insert( + byte, + OpcodeEntry { + name: name.to_string(), + category: categorize(name), + }, + ); + } + + Self { + evm, + pvm, + category_order: CATEGORY_ORDER, + } + } +} + +/// Identifier of one opcode kind, distinguishing EVM opcodes from PVM +/// syscalls. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum OpKey { + EvmOpcode(u8), + PvmSyscall(u8), +} + +impl OpKey { + /// Stable display form: `"EVMOpcode:0x52"`, `"PVMSyscall:0x03"`. + pub fn as_string(&self) -> String { + match self { + OpKey::EvmOpcode(b) => format!("EVMOpcode:0x{:02x}", b), + OpKey::PvmSyscall(b) => format!("PVMSyscall:0x{:02x}", b), + } + } +} + +impl Ord for OpKey { + fn cmp(&self, other: &Self) -> Ordering { + match (self, other) { + (OpKey::EvmOpcode(a), OpKey::EvmOpcode(b)) => a.cmp(b), + (OpKey::PvmSyscall(a), OpKey::PvmSyscall(b)) => a.cmp(b), + (OpKey::EvmOpcode(_), OpKey::PvmSyscall(_)) => Ordering::Less, + (OpKey::PvmSyscall(_), OpKey::EvmOpcode(_)) => Ordering::Greater, + } + } +} + +impl PartialOrd for OpKey { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +/// Per-opcode aggregate across one transaction's `struct_logs`. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct OpcodeProfile { + pub op_key: OpKey, + pub count: u64, + pub total_ref_time: u128, + pub total_proof_size: u128, +} + +/// Profile of one watched transaction. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct TxProfile { + pub tx_hash: TxHash, + pub step_path: StepPath, + pub failed: bool, + pub gas_used: u64, + pub weight_consumed_ref_time: u64, + pub weight_consumed_proof_size: u64, + pub base_call_weight_ref_time: u64, + pub base_call_weight_proof_size: u64, + pub opcodes: Vec, + pub unattributed_ref_time: i128, + pub unattributed_proof_size: i128, +} + +impl TxProfile { + /// Pure transform from `ExecutionTrace` to `TxProfile`. + pub fn from_execution_trace( + tx_hash: TxHash, + step_path: StepPath, + trace: &ExecutionTrace, + ) -> Self { + let mut by_op: HashMap = HashMap::new(); + let mut step_total_ref_time: u128 = 0; + let mut step_total_proof_size: u128 = 0; + + for step in &trace.struct_logs { + let key = match step.kind { + ExecutionStepKind::EVMOpcode { op, .. } => OpKey::EvmOpcode(op), + ExecutionStepKind::PVMSyscall { op, .. } => OpKey::PvmSyscall(op), + }; + let entry = by_op.entry(key).or_insert((0, 0, 0)); + entry.0 += 1; + entry.1 += step.weight_cost.ref_time as u128; + entry.2 += step.weight_cost.proof_size as u128; + step_total_ref_time += step.weight_cost.ref_time as u128; + step_total_proof_size += step.weight_cost.proof_size as u128; + } + + let mut opcodes: Vec = by_op + .into_iter() + .map( + |(op_key, (count, total_ref_time, total_proof_size))| OpcodeProfile { + op_key, + count, + total_ref_time, + total_proof_size, + }, + ) + .collect(); + opcodes.sort_by(|a, b| { + b.total_ref_time + .cmp(&a.total_ref_time) + .then_with(|| a.op_key.cmp(&b.op_key)) + }); + + let unattributed_ref_time = + i128::from(trace.weight_consumed.ref_time) - step_total_ref_time as i128; + let unattributed_proof_size = + i128::from(trace.weight_consumed.proof_size) - step_total_proof_size as i128; + + Self { + tx_hash, + step_path, + failed: trace.failed, + gas_used: trace.gas, + weight_consumed_ref_time: trace.weight_consumed.ref_time, + weight_consumed_proof_size: trace.weight_consumed.proof_size, + base_call_weight_ref_time: trace.base_call_weight.ref_time, + base_call_weight_proof_size: trace.base_call_weight.proof_size, + opcodes, + unattributed_ref_time, + unattributed_proof_size, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn opcode_catalog_resolves_names_and_categories() { + let catalog = OpcodeCatalog::current(); + // EVM names from revm-bytecode + categories from `categorize`. + let mstore = catalog.evm.get(&0x52).expect("0x52 in EVM"); + assert_eq!(mstore.name, "MSTORE"); + assert_eq!(mstore.category, "Memory"); + let call = catalog.evm.get(&0xf1).expect("0xf1 in EVM"); + assert_eq!(call.name, "CALL"); + assert_eq!(call.category, "Calls"); + let push7 = catalog.evm.get(&0x66).expect("0x66 in EVM"); + assert_eq!(push7.name, "PUSH7"); + assert_eq!(push7.category, "Stack"); + assert!( + catalog.evm.get(&0x0c).is_none(), + "0x0c is unassigned in EVM" + ); + // PVM names from pallet-revive's serde adapter. + let set_storage = catalog.pvm.get(&0x01).expect("0x01 in PVM"); + assert_eq!(set_storage.name, "set_storage"); + assert_eq!(set_storage.category, "Storage"); + let pvm_fuel = catalog.pvm.get(&0x29).expect("0x29 in PVM"); + assert_eq!(pvm_fuel.name, "pvm_fuel"); + assert_eq!(pvm_fuel.category, "VM overhead"); + assert!( + catalog.pvm.get(&0x2a).is_none(), + "past end of list_trace_ops" + ); + assert!(catalog.pvm.len() >= 42); + // Display order ends with the catch-all bucket. + assert_eq!(catalog.category_order.last().copied(), Some("Other")); + } + + use crate::revive_metadata::runtime_types::pallet_revive::evm::api::byte::Bytes; + use crate::revive_metadata::runtime_types::pallet_revive::evm::api::debug_rpc_types::{ + ExecutionStep, ExecutionStepKind, ExecutionTrace, + }; + use crate::revive_metadata::runtime_types::sp_weights::weight_v2::Weight; + + fn weight(ref_time: u64, proof_size: u64) -> Weight { + Weight { + ref_time, + proof_size, + } + } + + fn evm_step(op: u8, ref_time: u64, proof_size: u64) -> ExecutionStep { + ExecutionStep { + gas: 0, + gas_cost: 0, + weight_cost: weight(ref_time, proof_size), + depth: 1, + return_data: Bytes(Vec::new()), + error: None, + kind: ExecutionStepKind::EVMOpcode { + pc: 0, + op, + stack: Vec::new(), + memory: Vec::new(), + storage: None, + }, + } + } + + fn pvm_step(op: u8, ref_time: u64, proof_size: u64) -> ExecutionStep { + ExecutionStep { + gas: 0, + gas_cost: 0, + weight_cost: weight(ref_time, proof_size), + depth: 1, + return_data: Bytes(Vec::new()), + error: None, + kind: ExecutionStepKind::PVMSyscall { + op, + args: Vec::new(), + returned: None, + }, + } + } + + fn trace( + base: Weight, + consumed: Weight, + failed: bool, + steps: Vec, + ) -> ExecutionTrace { + ExecutionTrace { + gas: 0, + weight_consumed: consumed, + base_call_weight: base, + failed, + return_value: Bytes(Vec::new()), + struct_logs: steps, + } + } + + #[test] + fn repeated_opcode_accumulates_count_and_weight() { + let t = trace( + weight(0, 0), + weight(300, 30), + false, + vec![ + evm_step(0x01, 100, 10), // ADD + evm_step(0x01, 100, 10), + evm_step(0x01, 100, 10), + ], + ); + let p = TxProfile::from_execution_trace(TxHash::ZERO, StepPath::new(vec![]), &t); + assert_eq!(p.opcodes.len(), 1); + assert_eq!(p.opcodes[0].count, 3); + assert_eq!(p.opcodes[0].total_ref_time, 300); + assert_eq!(p.opcodes[0].total_proof_size, 30); + } + + #[test] + fn evm_and_pvm_kept_separate_sorted_by_ref_time() { + let t = trace( + weight(0, 0), + weight(0, 0), // not relevant — we test ordering, not residual + false, + vec![ + evm_step(0x01, 50, 0), // ADD: small + pvm_step(0x03, 500, 0), // big PVM syscall + evm_step(0x52, 200, 0), // MSTORE: medium + pvm_step(0x03, 500, 0), // same PVM syscall again → 1000 total + ], + ); + let p = TxProfile::from_execution_trace(TxHash::ZERO, StepPath::new(vec![]), &t); + assert_eq!(p.opcodes.len(), 3); + assert_eq!(p.opcodes[0].op_key, OpKey::PvmSyscall(0x03)); + assert_eq!(p.opcodes[0].count, 2); + assert_eq!(p.opcodes[0].total_ref_time, 1000); + assert_eq!(p.opcodes[1].op_key, OpKey::EvmOpcode(0x52)); + assert_eq!(p.opcodes[2].op_key, OpKey::EvmOpcode(0x01)); + } + + #[test] + fn unattributed_residual_captures_overhead() { + // weight_consumed = 2000, sum of step weights = 1500. + // Residual = 2000 - 1500 = 500. + let t = trace( + weight(100, 5), + weight(2000, 50), + false, + vec![evm_step(0x01, 1000, 20), evm_step(0x52, 500, 15)], + ); + let p = TxProfile::from_execution_trace(TxHash::ZERO, StepPath::new(vec![]), &t); + assert_eq!(p.unattributed_ref_time, 500); + assert_eq!(p.unattributed_proof_size, 15); + } + + #[test] + fn op_key_display_format() { + assert_eq!(OpKey::EvmOpcode(0x52).as_string(), "EVMOpcode:0x52"); + assert_eq!(OpKey::PvmSyscall(0x03).as_string(), "PVMSyscall:0x03"); + } +} diff --git a/crates/report/src/aggregator.rs b/crates/report/src/aggregator.rs index fd3eec83..5dd53b45 100644 --- a/crates/report/src/aggregator.rs +++ b/crates/report/src/aggregator.rs @@ -110,6 +110,9 @@ impl ReportAggregator { self.handle_contract_information(*event); } RunnerEvent::BlockMined(event) => self.handle_block_mined(*event), + RunnerEvent::OpcodeProfileCompleted(event) => { + self.handle_opcode_profile_completed(*event) + } } } self.handle_completion(CompletionEvent {}); @@ -515,6 +518,11 @@ impl ReportAggregator { .push(event.mined_block_information); } + fn handle_opcode_profile_completed(&mut self, event: OpcodeProfileCompletedEvent) { + self.execution_information(&event.execution_specifier) + .opcode_profile = Some(event.summary); + } + fn test_case_report(&mut self, specifier: &TestSpecifier) -> &mut ExecutionReport { self.report .execution_information @@ -738,6 +746,9 @@ pub struct ExecutionInformation { /// Information on the deployed libraries. #[serde(default, skip_serializing_if = "Option::is_none")] pub deployed_libraries: Option>, + /// Opcode-level execution profile for sampled watched transactions. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub opcode_profile: Option, } /// The post-link-only compilation report. diff --git a/crates/report/src/lib.rs b/crates/report/src/lib.rs index 566a7b4b..6e017a55 100644 --- a/crates/report/src/lib.rs +++ b/crates/report/src/lib.rs @@ -2,12 +2,14 @@ mod aggregator; mod common; +mod opcode_profile; mod reporter_event; mod runner_event; pub mod prelude { pub use crate::aggregator::*; pub use crate::common::*; + pub use crate::opcode_profile::*; pub use crate::reporter_event::*; pub use crate::runner_event::*; } @@ -47,5 +49,6 @@ pub(crate) mod internal_prelude { pub use aggregator::*; pub use common::*; +pub use opcode_profile::*; pub use reporter_event::*; pub use runner_event::*; diff --git a/crates/report/src/opcode_profile.rs b/crates/report/src/opcode_profile.rs new file mode 100644 index 00000000..a46035d4 --- /dev/null +++ b/crates/report/src/opcode_profile.rs @@ -0,0 +1,69 @@ +//! Serializable opcode-profile types. Decoupled from `revive_dt_node_interaction`'s +//! `TxProfile` (which embeds subxt-generated, non-`Serialize` types). + +use std::collections::BTreeMap; + +use crate::internal_prelude::*; + +#[derive(Clone, Debug, Default, Serialize, Deserialize)] +pub struct OpcodeProfileSummary { + pub sampled_tx_count: usize, + pub block_count: u32, + /// Sampled txs whose tracer reported `failed = true`. Reverted txs are + /// still aggregated below — they spent metered weight worth profiling. + pub failed_count: usize, + /// Sorted descending by `total_ref_time`; trailing rows beyond top-N + /// collapse into a single `"Other"` entry. + pub opcodes: Vec, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub tx_profiles: Vec, + #[serde(default, skip_serializing_if = "OpcodeCatalogWire::is_empty")] + pub opcode_catalog: OpcodeCatalogWire, +} + +/// Wire view of `revive_dt_node_interaction::OpcodeCatalog`. Byte keys are +/// JSON strings (`"0".."255"`). +#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] +pub struct OpcodeCatalogWire { + #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] + pub evm: BTreeMap, + #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] + pub pvm: BTreeMap, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub category_order: Vec, +} + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct OpcodeEntryWire { + pub name: String, + pub category: String, +} + +impl OpcodeCatalogWire { + pub fn is_empty(&self) -> bool { + self.evm.is_empty() && self.pvm.is_empty() && self.category_order.is_empty() + } +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct AggregatedOpcode { + pub op_key: String, + pub sample_count: u64, + pub total_ref_time: u128, + pub total_proof_size: u128, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct TxProfileWire { + pub tx_hash: TxHash, + pub step_path: StepPath, + pub failed: bool, + pub gas_used: u64, + pub weight_consumed_ref_time: u64, + pub weight_consumed_proof_size: u64, + pub base_call_weight_ref_time: u64, + pub base_call_weight_proof_size: u64, + pub unattributed_ref_time: i128, + pub unattributed_proof_size: i128, + pub opcodes: Vec, +} diff --git a/crates/report/src/runner_event.rs b/crates/report/src/runner_event.rs index 72813fab..807d1654 100644 --- a/crates/report/src/runner_event.rs +++ b/crates/report/src/runner_event.rs @@ -157,6 +157,12 @@ revive_dt_proc_macros::define_runner_event! { /// Information on the mined block. mined_block_information: MinedBlockInformation, }, + /// An event emitted when the opcode profiler has finished for this + /// (test_specifier, platform_identifier). + OpcodeProfileCompleted { + /// The aggregated profile data for the workload run. + summary: OpcodeProfileSummary, + }, }, // Events on PostLinkCompilationSpecificReporter —