From 2826bda6ea220c067d9c285559be5c2f7b7281a9 Mon Sep 17 00:00:00 2001 From: Vadim Smirnov Date: Thu, 2 Apr 2026 16:34:33 +0700 Subject: [PATCH 1/5] feat(node): add `gear_readWasmCustomSection` RPC endpoint Add a node-level RPC that reads custom sections from program WASM original code stored on-chain. This enables clients to retrieve Sails IDL v2 (or any named custom section) without original build artifacts. Co-Authored-By: Claude Opus 4.6 (1M context) --- Cargo.lock | 1 + core/Cargo.toml | 1 + core/src/code/mod.rs | 69 ++++++++++++++- core/src/code/utils.rs | 29 +++++++ node/service/src/rpc/mod.rs | 3 + node/service/src/rpc/wasm_section.rs | 121 +++++++++++++++++++++++++++ 6 files changed, 223 insertions(+), 1 deletion(-) create mode 100644 node/service/src/rpc/wasm_section.rs diff --git a/Cargo.lock b/Cargo.lock index 5454a14e555..bedd0aeefe8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6770,6 +6770,7 @@ dependencies = [ "scale-info", "serde", "tracing-subscriber", + "wasm-encoder 0.230.0 (registry+https://github.com/rust-lang/crates.io-index)", "wasmparser 0.230.0 (registry+https://github.com/rust-lang/crates.io-index)", "wat", ] diff --git a/core/Cargo.toml b/core/Cargo.toml index 15095073df7..d3f75280256 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -44,6 +44,7 @@ gear-workspace-hack.workspace = true [dev-dependencies] tracing-subscriber.workspace = true wat.workspace = true +wasm-encoder.workspace = true proptest.workspace = true rand = { workspace = true, features = ["std", "std_rng"] } numerated = { workspace = true, features = ["mock"] } diff --git a/core/src/code/mod.rs b/core/src/code/mod.rs index 88aac3a6639..02238a7d01e 100644 --- a/core/src/code/mod.rs +++ b/core/src/code/mod.rs @@ -33,7 +33,9 @@ mod utils; pub use errors::*; pub use instrumented::*; pub use metadata::*; -pub use utils::{ALLOWED_EXPORTS, MAX_WASM_PAGES_AMOUNT, REQUIRED_EXPORTS}; +pub use utils::{ + ALLOWED_EXPORTS, MAX_WASM_PAGES_AMOUNT, REQUIRED_EXPORTS, get_custom_section_data, +}; use utils::CodeTypeSectionSizes; @@ -1245,4 +1247,69 @@ mod tests { )) )); } + + mod custom_section_tests { + use alloc::{vec, vec::Vec}; + use crate::code::get_custom_section_data; + + fn make_wasm_with_custom_section(name: &str, data: &[u8]) -> Vec { + let mut module = wasm_encoder::Module::new(); + // Add a minimal type section so it's a valid module + let mut types = wasm_encoder::TypeSection::new(); + types.ty().function(vec![], vec![]); + module.section(&types); + // Add the custom section + module.section(&wasm_encoder::CustomSection { + name: name.into(), + data: data.into(), + }); + module.finish() + } + + #[test] + fn section_found() { + let wasm = make_wasm_with_custom_section("sails:idl", b"hello idl"); + let result = get_custom_section_data(&wasm, "sails:idl"); + assert_eq!(result.unwrap().unwrap(), b"hello idl"); + } + + #[test] + fn section_not_found() { + let wasm = make_wasm_with_custom_section("other", b"data"); + let result = get_custom_section_data(&wasm, "sails:idl"); + assert_eq!(result.unwrap(), None); + } + + #[test] + fn first_match_returned() { + let mut module = wasm_encoder::Module::new(); + let mut types = wasm_encoder::TypeSection::new(); + types.ty().function(vec![], vec![]); + module.section(&types); + module.section(&wasm_encoder::CustomSection { + name: "sails:idl".into(), + data: b"first".into(), + }); + module.section(&wasm_encoder::CustomSection { + name: "sails:idl".into(), + data: b"second".into(), + }); + let wasm = module.finish(); + + let result = get_custom_section_data(&wasm, "sails:idl"); + assert_eq!(result.unwrap().unwrap(), b"first"); + } + + #[test] + fn invalid_wasm() { + let result = get_custom_section_data(b"not wasm at all", "sails:idl"); + assert!(result.is_err()); + } + + #[test] + fn empty_input() { + let result = get_custom_section_data(b"", "sails:idl"); + assert!(result.is_err()); + } + } } diff --git a/core/src/code/utils.rs b/core/src/code/utils.rs index 465e51ef280..fe498c23516 100644 --- a/core/src/code/utils.rs +++ b/core/src/code/utils.rs @@ -537,3 +537,32 @@ pub fn get_code_type_sections_sizes( type_section: type_section_size, }) } + +/// Extract data of a custom section by name from raw WASM bytes. +/// +/// Returns `Ok(Some(data))` if the section is found, `Ok(None)` if the WASM +/// is valid but the section is not present, and `Err` if the WASM is malformed. +pub fn get_custom_section_data<'a>( + wasm: &'a [u8], + section_name: &str, +) -> Result, &'static str> { + let parser = wasmparser::Parser::new(0); + let mut saw_version = false; + + for payload in parser.parse_all(wasm) { + let payload = payload.map_err(|_| "invalid wasm binary")?; + match payload { + Payload::Version { .. } => saw_version = true, + Payload::CustomSection(section) if section.name() == section_name => { + return Ok(Some(section.data())); + } + _ => {} + } + } + + if !saw_version { + return Err("invalid wasm binary"); + } + + Ok(None) +} diff --git a/node/service/src/rpc/mod.rs b/node/service/src/rpc/mod.rs index fe7bdc73a9d..02736d16ec7 100644 --- a/node/service/src/rpc/mod.rs +++ b/node/service/src/rpc/mod.rs @@ -42,6 +42,7 @@ use sp_keystore::KeystorePtr; mod gear_events; mod runtime_info; +mod wasm_section; /// Extra dependencies for BABE. pub struct BabeDeps { @@ -138,6 +139,7 @@ where use pallet_gear_staking_rewards_rpc::{GearStakingRewards, GearStakingRewardsApiServer}; use pallet_transaction_payment_rpc::{TransactionPayment, TransactionPaymentApiServer}; use runtime_info::{RuntimeInfoApi, RuntimeInfoServer}; + use wasm_section::{WasmSectionApi, WasmSectionServer}; use sc_consensus_babe_rpc::{Babe, BabeApiServer}; use sc_consensus_grandpa_rpc::{Grandpa, GrandpaApiServer}; use sc_rpc::dev::{Dev, DevApiServer}; @@ -224,6 +226,7 @@ where } io.merge(RuntimeInfoApi::::new(client.clone()).into_rpc())?; + io.merge(WasmSectionApi::::new(client.clone()).into_rpc())?; io.merge(GearStakingRewards::new(client.clone()).into_rpc())?; diff --git a/node/service/src/rpc/wasm_section.rs b/node/service/src/rpc/wasm_section.rs new file mode 100644 index 00000000000..8a6d28c09a2 --- /dev/null +++ b/node/service/src/rpc/wasm_section.rs @@ -0,0 +1,121 @@ +// This file is part of Gear. + +// Copyright (C) 2021-2025 Gear Technologies Inc. +// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +//! RPC endpoint for reading custom sections from program WASM original code. + +use jsonrpsee::{core::RpcResult, proc_macros::rpc, types::ErrorObjectOwned}; +use sc_client_api::StorageProvider; +use sp_blockchain::HeaderBackend; +use sp_core::{Bytes, H256, twox_128}; +use sp_runtime::traits::Block as BlockT; +use sp_storage::StorageKey; +use std::{marker::PhantomData, sync::Arc}; + +#[rpc(server)] +pub(crate) trait WasmSection { + /// Read a custom section from the original WASM code stored on-chain. + /// + /// This is commonly used to retrieve the Sails IDL embedded in the + /// `sails:idl` custom section. The returned bytes are raw section data; + /// for `sails:idl`, clients must parse the envelope (version + flags) + /// and decompress the payload. + /// + /// Returns `null` if the code is not found or the section does not exist. + #[method(name = "gear_readWasmCustomSection")] + fn read_wasm_custom_section( + &self, + code_id: H256, + section_name: String, + at: Option, + ) -> RpcResult>; +} + +pub(crate) struct WasmSectionApi { + client: Arc, + original_code_prefix: Vec, + _marker1: PhantomData, + _marker2: PhantomData, +} + +impl WasmSectionApi { + pub(crate) fn new(client: Arc) -> Self { + let mut original_code_prefix = twox_128(b"GearProgram").to_vec(); + original_code_prefix.extend_from_slice(&twox_128(b"OriginalCodeStorage")); + + Self { + client, + original_code_prefix, + _marker1: PhantomData, + _marker2: PhantomData, + } + } +} + +impl WasmSectionServer<::Hash> + for WasmSectionApi +where + C: HeaderBackend + StorageProvider + Send + Sync + 'static, + Block: BlockT, + Backend: sc_client_api::Backend + Send + Sync + 'static, +{ + fn read_wasm_custom_section( + &self, + code_id: H256, + section_name: String, + at: Option, + ) -> RpcResult> { + let at = at.unwrap_or_else(|| self.client.info().best_hash); + + // Construct storage key: prefix ++ Identity(code_id) + let mut storage_key = self.original_code_prefix.clone(); + storage_key.extend_from_slice(code_id.as_bytes()); + + let wasm_data = self + .client + .storage(at, &StorageKey(storage_key)) + .map_err(map_err_into_rpc_err)?; + + let Some(wasm_data) = wasm_data else { + return Ok(None); + }; + + // The storage value is SCALE-encoded Vec, so we need to decode it. + let wasm_bytes: Vec = + parity_scale_codec::Decode::decode(&mut wasm_data.0.as_slice()) + .map_err(|e| rpc_err("Failed to decode stored WASM", Some(format!("{e:?}"))))?; + + match gear_core::code::get_custom_section_data(&wasm_bytes, §ion_name) { + Ok(Some(data)) => Ok(Some(Bytes(data.to_vec()))), + Ok(None) => Ok(None), + Err(e) => Err(rpc_err( + "Failed to parse stored WASM", + Some(e.to_string()), + )), + } + } +} + +fn map_err_into_rpc_err(err: impl std::fmt::Debug) -> ErrorObjectOwned { + rpc_err("WASM section read error", Some(format!("{err:?}"))) +} + +fn rpc_err(message: &str, data: Option) -> ErrorObjectOwned { + use jsonrpsee::types::error::ErrorObject; + + ErrorObject::owned(9000, message, data) +} From fba43fe905741419867885d8b5f5d89ef0ea6659 Mon Sep 17 00:00:00 2001 From: Vadim Smirnov Date: Thu, 2 Apr 2026 19:21:14 +0700 Subject: [PATCH 2/5] fix(node): address review comments for wasm_section RPC Move inline crate paths to proper `use` imports for `gear_core::code::get_custom_section_data` and `parity_scale_codec::Decode`. Co-Authored-By: Claude Opus 4.6 (1M context) --- node/service/src/rpc/wasm_section.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/node/service/src/rpc/wasm_section.rs b/node/service/src/rpc/wasm_section.rs index 8a6d28c09a2..ac152a7aed8 100644 --- a/node/service/src/rpc/wasm_section.rs +++ b/node/service/src/rpc/wasm_section.rs @@ -18,7 +18,9 @@ //! RPC endpoint for reading custom sections from program WASM original code. +use gear_core::code::get_custom_section_data; use jsonrpsee::{core::RpcResult, proc_macros::rpc, types::ErrorObjectOwned}; +use parity_scale_codec::Decode; use sc_client_api::StorageProvider; use sp_blockchain::HeaderBackend; use sp_core::{Bytes, H256, twox_128}; @@ -96,10 +98,10 @@ where // The storage value is SCALE-encoded Vec, so we need to decode it. let wasm_bytes: Vec = - parity_scale_codec::Decode::decode(&mut wasm_data.0.as_slice()) + Decode::decode(&mut wasm_data.0.as_slice()) .map_err(|e| rpc_err("Failed to decode stored WASM", Some(format!("{e:?}"))))?; - match gear_core::code::get_custom_section_data(&wasm_bytes, §ion_name) { + match get_custom_section_data(&wasm_bytes, §ion_name) { Ok(Some(data)) => Ok(Some(Bytes(data.to_vec()))), Ok(None) => Ok(None), Err(e) => Err(rpc_err( From 0d152b607dc561baba3e3ed6704ceee1a61f8841 Mon Sep 17 00:00:00 2001 From: Vadim Smirnov Date: Thu, 2 Apr 2026 19:22:52 +0700 Subject: [PATCH 3/5] style(node): apply rustfmt formatting Co-Authored-By: Claude Opus 4.6 (1M context) --- core/src/code/mod.rs | 2 +- node/service/src/rpc/mod.rs | 2 +- node/service/src/rpc/wasm_section.rs | 10 +++------- 3 files changed, 5 insertions(+), 9 deletions(-) diff --git a/core/src/code/mod.rs b/core/src/code/mod.rs index 02238a7d01e..12d2281b91e 100644 --- a/core/src/code/mod.rs +++ b/core/src/code/mod.rs @@ -1249,8 +1249,8 @@ mod tests { } mod custom_section_tests { - use alloc::{vec, vec::Vec}; use crate::code::get_custom_section_data; + use alloc::{vec, vec::Vec}; fn make_wasm_with_custom_section(name: &str, data: &[u8]) -> Vec { let mut module = wasm_encoder::Module::new(); diff --git a/node/service/src/rpc/mod.rs b/node/service/src/rpc/mod.rs index 02736d16ec7..6b2e42c48ea 100644 --- a/node/service/src/rpc/mod.rs +++ b/node/service/src/rpc/mod.rs @@ -139,13 +139,13 @@ where use pallet_gear_staking_rewards_rpc::{GearStakingRewards, GearStakingRewardsApiServer}; use pallet_transaction_payment_rpc::{TransactionPayment, TransactionPaymentApiServer}; use runtime_info::{RuntimeInfoApi, RuntimeInfoServer}; - use wasm_section::{WasmSectionApi, WasmSectionServer}; use sc_consensus_babe_rpc::{Babe, BabeApiServer}; use sc_consensus_grandpa_rpc::{Grandpa, GrandpaApiServer}; use sc_rpc::dev::{Dev, DevApiServer}; use sc_sync_state_rpc::{SyncState, SyncStateApiServer}; use substrate_frame_rpc_system::{System, SystemApiServer}; use substrate_state_trie_migration_rpc::{StateMigration, StateMigrationApiServer}; + use wasm_section::{WasmSectionApi, WasmSectionServer}; let mut io = RpcModule::new(()); diff --git a/node/service/src/rpc/wasm_section.rs b/node/service/src/rpc/wasm_section.rs index ac152a7aed8..46dcf918c57 100644 --- a/node/service/src/rpc/wasm_section.rs +++ b/node/service/src/rpc/wasm_section.rs @@ -97,17 +97,13 @@ where }; // The storage value is SCALE-encoded Vec, so we need to decode it. - let wasm_bytes: Vec = - Decode::decode(&mut wasm_data.0.as_slice()) - .map_err(|e| rpc_err("Failed to decode stored WASM", Some(format!("{e:?}"))))?; + let wasm_bytes: Vec = Decode::decode(&mut wasm_data.0.as_slice()) + .map_err(|e| rpc_err("Failed to decode stored WASM", Some(format!("{e:?}"))))?; match get_custom_section_data(&wasm_bytes, §ion_name) { Ok(Some(data)) => Ok(Some(Bytes(data.to_vec()))), Ok(None) => Ok(None), - Err(e) => Err(rpc_err( - "Failed to parse stored WASM", - Some(e.to_string()), - )), + Err(e) => Err(rpc_err("Failed to parse stored WASM", Some(e.to_string()))), } } } From 015943ff6fa012a228bd73b71a94f22972eb5472 Mon Sep 17 00:00:00 2001 From: Vadim Smirnov Date: Thu, 23 Apr 2026 20:44:49 +0400 Subject: [PATCH 4/5] fix(core): address review feedback on wasm custom section helper MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Drop the `saw_version` flag in `get_custom_section_data` — `parse_all` already rejects input without a valid preamble, so the flag was redundant. Return `wasmparser::BinaryReaderError` instead of `&'static str` so parser offset + reason propagate through the RPC. - Clean `wasm_encoder::*` prefixes out of the test module by importing `Module`, `TypeSection`, `CustomSection` at the top. - Consolidate the test helper into `make_wasm_with_custom_sections(&[(&str, &[u8])])` so `first_match_returned` shares it instead of inlining the module body. - Add `Api::read_wasm_custom_section[_at]` in gsdk and a gsdk integration test that round-trips a `@custom "sails:idl"` WAT via `upload_code` + the RPC, covering the present, missing-section, and unknown-`code_id` cases. This exercises the full storage-key path (`twox_128("GearProgram") ++ twox_128("OriginalCodeStorage") ++ code_id`) and the SCALE envelope decode. Co-Authored-By: Claude Opus 4.7 (1M context) --- core/src/code/mod.rs | 41 +++++++++++++++--------------------- core/src/code/utils.rs | 26 +++++++++-------------- gsdk/src/rpc.rs | 27 ++++++++++++++++++++++++ gsdk/tests/rpc.rs | 48 ++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 102 insertions(+), 40 deletions(-) diff --git a/core/src/code/mod.rs b/core/src/code/mod.rs index 12d2281b91e..1cf77ff1923 100644 --- a/core/src/code/mod.rs +++ b/core/src/code/mod.rs @@ -1251,51 +1251,44 @@ mod tests { mod custom_section_tests { use crate::code::get_custom_section_data; use alloc::{vec, vec::Vec}; + use wasm_encoder::{CustomSection, Module, TypeSection}; - fn make_wasm_with_custom_section(name: &str, data: &[u8]) -> Vec { - let mut module = wasm_encoder::Module::new(); + fn make_wasm_with_custom_sections(sections: &[(&str, &[u8])]) -> Vec { + let mut module = Module::new(); // Add a minimal type section so it's a valid module - let mut types = wasm_encoder::TypeSection::new(); + let mut types = TypeSection::new(); types.ty().function(vec![], vec![]); module.section(&types); - // Add the custom section - module.section(&wasm_encoder::CustomSection { - name: name.into(), - data: data.into(), - }); + // Append each custom section in order + for (name, data) in sections { + module.section(&CustomSection { + name: (*name).into(), + data: (*data).into(), + }); + } module.finish() } #[test] fn section_found() { - let wasm = make_wasm_with_custom_section("sails:idl", b"hello idl"); + let wasm = make_wasm_with_custom_sections(&[("sails:idl", b"hello idl")]); let result = get_custom_section_data(&wasm, "sails:idl"); assert_eq!(result.unwrap().unwrap(), b"hello idl"); } #[test] fn section_not_found() { - let wasm = make_wasm_with_custom_section("other", b"data"); + let wasm = make_wasm_with_custom_sections(&[("other", b"data")]); let result = get_custom_section_data(&wasm, "sails:idl"); assert_eq!(result.unwrap(), None); } #[test] fn first_match_returned() { - let mut module = wasm_encoder::Module::new(); - let mut types = wasm_encoder::TypeSection::new(); - types.ty().function(vec![], vec![]); - module.section(&types); - module.section(&wasm_encoder::CustomSection { - name: "sails:idl".into(), - data: b"first".into(), - }); - module.section(&wasm_encoder::CustomSection { - name: "sails:idl".into(), - data: b"second".into(), - }); - let wasm = module.finish(); - + let wasm = make_wasm_with_custom_sections(&[ + ("sails:idl", b"first"), + ("sails:idl", b"second"), + ]); let result = get_custom_section_data(&wasm, "sails:idl"); assert_eq!(result.unwrap().unwrap(), b"first"); } diff --git a/core/src/code/utils.rs b/core/src/code/utils.rs index fe498c23516..4f10c2bb693 100644 --- a/core/src/code/utils.rs +++ b/core/src/code/utils.rs @@ -542,27 +542,21 @@ pub fn get_code_type_sections_sizes( /// /// Returns `Ok(Some(data))` if the section is found, `Ok(None)` if the WASM /// is valid but the section is not present, and `Err` if the WASM is malformed. +/// The error preserves the underlying parser diagnostic (offset + reason). pub fn get_custom_section_data<'a>( wasm: &'a [u8], section_name: &str, -) -> Result, &'static str> { - let parser = wasmparser::Parser::new(0); - let mut saw_version = false; - - for payload in parser.parse_all(wasm) { - let payload = payload.map_err(|_| "invalid wasm binary")?; - match payload { - Payload::Version { .. } => saw_version = true, - Payload::CustomSection(section) if section.name() == section_name => { - return Ok(Some(section.data())); - } - _ => {} +) -> Result, wasmparser::BinaryReaderError> { + // `parse_all` rejects input without a valid magic + version preamble on + // the first iteration, so there's no need for a separate "was a version + // header observed?" flag. + for payload in wasmparser::Parser::new(0).parse_all(wasm) { + if let Payload::CustomSection(section) = payload? + && section.name() == section_name + { + return Ok(Some(section.data())); } } - if !saw_version { - return Err("invalid wasm binary"); - } - Ok(None) } diff --git a/gsdk/src/rpc.rs b/gsdk/src/rpc.rs index 7680e159227..3221e9567c6 100644 --- a/gsdk/src/rpc.rs +++ b/gsdk/src/rpc.rs @@ -245,6 +245,33 @@ impl Api { Ok(T::decode(&mut bytes.as_slice())?) } + /// Reads a named custom section from the original WASM code stored + /// on-chain at specified block. + /// + /// Actually calls `gear_readWasmCustomSection` RPC method. Returns + /// `None` if the code is not found or the section does not exist. + /// Primary use case: retrieving a Sails IDL embedded in the + /// `sails:idl` custom section. + #[at_block] + pub async fn read_wasm_custom_section_at( + &self, + code_id: CodeId, + section_name: impl AsRef, + block_hash: Option, + ) -> Result> { + self.rpc() + .request( + "gear_readWasmCustomSection", + rpc_params![ + H256(code_id.into()), + section_name.as_ref().to_string(), + block_hash + ], + ) + .await + .map_err(Into::into) + } + /// Calls `runtime_wasmBlobVersion` RPC method at specified block. #[at_block] pub async fn runtime_wasm_blob_version_at(&self, block_hash: Option) -> Result { diff --git a/gsdk/tests/rpc.rs b/gsdk/tests/rpc.rs index 9ee89a95f1f..37dae9dfcca 100644 --- a/gsdk/tests/rpc.rs +++ b/gsdk/tests/rpc.rs @@ -99,6 +99,54 @@ async fn test_calculate_create_gas() -> Result<()> { Ok(()) } +#[tokio::test] +async fn test_read_wasm_custom_section() -> Result<()> { + let (_node, api) = dev_node().await; + + // Minimal valid Gear program (exports `init` + imports `env.memory`) + // augmented with a named custom section. The `@custom` wat annotation + // emits a raw custom section at module scope. + let wat_code = r#" + (module + (import "env" "memory" (memory 0)) + (export "init" (func $init)) + (func $init) + (@custom "sails:idl" "hello idl") + ) + "#; + let wasm = wat::parse_str(wat_code).unwrap(); + + // Upload the code so `OriginalCodeStorage(code_id)` is populated. + api.upload_code(wasm.clone()).await?; + let code_id = CodeId::generate(&wasm); + + // Positive case — present section round-trips through the RPC. This + // exercises the full storage-key path + // (`twox_128("GearProgram") ++ twox_128("OriginalCodeStorage") ++ code_id`), + // the SCALE envelope decode, and `get_custom_section_data`. + let present = api.read_wasm_custom_section(code_id, "sails:idl").await?; + assert_eq!(present.as_deref(), Some(b"hello idl".as_ref())); + + // Missing section on a known code → `Ok(None)` / null. + let missing_section = api + .read_wasm_custom_section(code_id, "no:such:section") + .await?; + assert!(missing_section.is_none()); + + // Unknown code_id → `Ok(None)` / null. + let unknown_code = api + .read_wasm_custom_section(CodeId::from([0u8; 32]), "sails:idl") + .await?; + assert!(unknown_code.is_none()); + + // `at: Some(best_hash)` takes the same storage-read path as `at: None`, + // so exercising `None` here is sufficient for this happy-path test. + // Historical queries against a block preceding code upload would need + // deterministic multi-block state and are out of scope here. + + Ok(()) +} + #[tokio::test] async fn test_calculate_handle_gas() -> Result<()> { let (_node, api) = dev_node().await; From 9cb6f35134fb6a08018659b0b46b94c44ad5f49f Mon Sep 17 00:00:00 2001 From: Vadim Smirnov Date: Mon, 27 Apr 2026 00:06:21 +0400 Subject: [PATCH 5/5] refactor: simplify wasm_section RPC and trim narration comments Apply review feedback from /simplify: - node/service/src/rpc/wasm_section.rs: - Collapse PhantomData + PhantomData into a tuple field - Drop cached `original_code_prefix` field; compute inline with a `keep in sync with pallet-gear-program::OriginalCodeStorage` note - Inline single-use `map_err_into_rpc_err` helper; promote 9000 to `ERROR_CODE` const - Standardize error formatting on `Display` (was a mix of `{:?}` and `to_string()`) so RPC errors don't leak struct internals - Avoid SCALE double-allocation by decoding the `Compact` length prefix and slicing into the storage buffer directly - Delete WHAT-narration comments - core/src/code/utils.rs: drop multi-line comment defending an implementation detail already covered by the doc comment - gsdk/tests/rpc.rs: drop WHAT-narration comments in test_read_wasm_custom_section - gsdk/src/rpc.rs: clarify in the doc that `block_hash: None` falls back to the best block (preserving the trailing `at specified block` required by the `#[at_block]` proc macro) Co-Authored-By: Claude Opus 4.7 (1M context) --- core/src/code/utils.rs | 3 -- gsdk/src/rpc.rs | 4 +-- gsdk/tests/rpc.rs | 15 ---------- node/service/src/rpc/wasm_section.rs | 41 +++++++++++++--------------- 4 files changed, 21 insertions(+), 42 deletions(-) diff --git a/core/src/code/utils.rs b/core/src/code/utils.rs index 4f10c2bb693..9df27b72c2f 100644 --- a/core/src/code/utils.rs +++ b/core/src/code/utils.rs @@ -547,9 +547,6 @@ pub fn get_custom_section_data<'a>( wasm: &'a [u8], section_name: &str, ) -> Result, wasmparser::BinaryReaderError> { - // `parse_all` rejects input without a valid magic + version preamble on - // the first iteration, so there's no need for a separate "was a version - // header observed?" flag. for payload in wasmparser::Parser::new(0).parse_all(wasm) { if let Payload::CustomSection(section) = payload? && section.name() == section_name diff --git a/gsdk/src/rpc.rs b/gsdk/src/rpc.rs index 3221e9567c6..aed07a99dde 100644 --- a/gsdk/src/rpc.rs +++ b/gsdk/src/rpc.rs @@ -246,12 +246,12 @@ impl Api { } /// Reads a named custom section from the original WASM code stored - /// on-chain at specified block. + /// on-chain. When `block_hash` is `None`, the best block is used. /// /// Actually calls `gear_readWasmCustomSection` RPC method. Returns /// `None` if the code is not found or the section does not exist. /// Primary use case: retrieving a Sails IDL embedded in the - /// `sails:idl` custom section. + /// `sails:idl` custom section at specified block. #[at_block] pub async fn read_wasm_custom_section_at( &self, diff --git a/gsdk/tests/rpc.rs b/gsdk/tests/rpc.rs index 37dae9dfcca..2d519604f93 100644 --- a/gsdk/tests/rpc.rs +++ b/gsdk/tests/rpc.rs @@ -103,9 +103,6 @@ async fn test_calculate_create_gas() -> Result<()> { async fn test_read_wasm_custom_section() -> Result<()> { let (_node, api) = dev_node().await; - // Minimal valid Gear program (exports `init` + imports `env.memory`) - // augmented with a named custom section. The `@custom` wat annotation - // emits a raw custom section at module scope. let wat_code = r#" (module (import "env" "memory" (memory 0)) @@ -116,34 +113,22 @@ async fn test_read_wasm_custom_section() -> Result<()> { "#; let wasm = wat::parse_str(wat_code).unwrap(); - // Upload the code so `OriginalCodeStorage(code_id)` is populated. api.upload_code(wasm.clone()).await?; let code_id = CodeId::generate(&wasm); - // Positive case — present section round-trips through the RPC. This - // exercises the full storage-key path - // (`twox_128("GearProgram") ++ twox_128("OriginalCodeStorage") ++ code_id`), - // the SCALE envelope decode, and `get_custom_section_data`. let present = api.read_wasm_custom_section(code_id, "sails:idl").await?; assert_eq!(present.as_deref(), Some(b"hello idl".as_ref())); - // Missing section on a known code → `Ok(None)` / null. let missing_section = api .read_wasm_custom_section(code_id, "no:such:section") .await?; assert!(missing_section.is_none()); - // Unknown code_id → `Ok(None)` / null. let unknown_code = api .read_wasm_custom_section(CodeId::from([0u8; 32]), "sails:idl") .await?; assert!(unknown_code.is_none()); - // `at: Some(best_hash)` takes the same storage-read path as `at: None`, - // so exercising `None` here is sufficient for this happy-path test. - // Historical queries against a block preceding code upload would need - // deterministic multi-block state and are out of scope here. - Ok(()) } diff --git a/node/service/src/rpc/wasm_section.rs b/node/service/src/rpc/wasm_section.rs index 46dcf918c57..3e6c7893c11 100644 --- a/node/service/src/rpc/wasm_section.rs +++ b/node/service/src/rpc/wasm_section.rs @@ -20,7 +20,7 @@ use gear_core::code::get_custom_section_data; use jsonrpsee::{core::RpcResult, proc_macros::rpc, types::ErrorObjectOwned}; -use parity_scale_codec::Decode; +use parity_scale_codec::{Compact, Decode}; use sc_client_api::StorageProvider; use sp_blockchain::HeaderBackend; use sp_core::{Bytes, H256, twox_128}; @@ -28,6 +28,8 @@ use sp_runtime::traits::Block as BlockT; use sp_storage::StorageKey; use std::{marker::PhantomData, sync::Arc}; +const ERROR_CODE: i32 = 9000; + #[rpc(server)] pub(crate) trait WasmSection { /// Read a custom section from the original WASM code stored on-chain. @@ -49,21 +51,14 @@ pub(crate) trait WasmSection { pub(crate) struct WasmSectionApi { client: Arc, - original_code_prefix: Vec, - _marker1: PhantomData, - _marker2: PhantomData, + _marker: PhantomData<(Block, Backend)>, } impl WasmSectionApi { pub(crate) fn new(client: Arc) -> Self { - let mut original_code_prefix = twox_128(b"GearProgram").to_vec(); - original_code_prefix.extend_from_slice(&twox_128(b"OriginalCodeStorage")); - Self { client, - original_code_prefix, - _marker1: PhantomData, - _marker2: PhantomData, + _marker: PhantomData, } } } @@ -83,24 +78,30 @@ where ) -> RpcResult> { let at = at.unwrap_or_else(|| self.client.info().best_hash); - // Construct storage key: prefix ++ Identity(code_id) - let mut storage_key = self.original_code_prefix.clone(); + // Keep in sync with `pallet-gear-program::OriginalCodeStorage` (StorageMap, Identity hasher). + let mut storage_key = twox_128(b"GearProgram").to_vec(); + storage_key.extend_from_slice(&twox_128(b"OriginalCodeStorage")); storage_key.extend_from_slice(code_id.as_bytes()); let wasm_data = self .client .storage(at, &StorageKey(storage_key)) - .map_err(map_err_into_rpc_err)?; + .map_err(|e| rpc_err("WASM section read error", Some(e.to_string())))?; let Some(wasm_data) = wasm_data else { return Ok(None); }; - // The storage value is SCALE-encoded Vec, so we need to decode it. - let wasm_bytes: Vec = Decode::decode(&mut wasm_data.0.as_slice()) - .map_err(|e| rpc_err("Failed to decode stored WASM", Some(format!("{e:?}"))))?; + let mut input = wasm_data.0.as_slice(); + let len = >::decode(&mut input) + .map_err(|e| rpc_err("Failed to decode stored WASM length", Some(e.to_string())))? + .0 as usize; + if input.len() < len { + return Err(rpc_err("Truncated stored WASM blob", None)); + } + let wasm_bytes = &input[..len]; - match get_custom_section_data(&wasm_bytes, §ion_name) { + match get_custom_section_data(wasm_bytes, §ion_name) { Ok(Some(data)) => Ok(Some(Bytes(data.to_vec()))), Ok(None) => Ok(None), Err(e) => Err(rpc_err("Failed to parse stored WASM", Some(e.to_string()))), @@ -108,12 +109,8 @@ where } } -fn map_err_into_rpc_err(err: impl std::fmt::Debug) -> ErrorObjectOwned { - rpc_err("WASM section read error", Some(format!("{err:?}"))) -} - fn rpc_err(message: &str, data: Option) -> ErrorObjectOwned { use jsonrpsee::types::error::ErrorObject; - ErrorObject::owned(9000, message, data) + ErrorObject::owned(ERROR_CODE, message, data) }