diff --git a/Cargo.lock b/Cargo.lock index 60eab74a2ee..d7bae924962 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6809,6 +6809,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..1cf77ff1923 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,62 @@ 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_sections(sections: &[(&str, &[u8])]) -> Vec { + let mut module = Module::new(); + // Add a minimal type section so it's a valid module + let mut types = TypeSection::new(); + types.ty().function(vec![], vec![]); + module.section(&types); + // 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_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_sections(&[("other", b"data")]); + let result = get_custom_section_data(&wasm, "sails:idl"); + assert_eq!(result.unwrap(), None); + } + + #[test] + fn first_match_returned() { + 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"); + } + + #[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..9df27b72c2f 100644 --- a/core/src/code/utils.rs +++ b/core/src/code/utils.rs @@ -537,3 +537,23 @@ 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. +/// The error preserves the underlying parser diagnostic (offset + reason). +pub fn get_custom_section_data<'a>( + wasm: &'a [u8], + section_name: &str, +) -> Result, wasmparser::BinaryReaderError> { + 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())); + } + } + + Ok(None) +} diff --git a/gsdk/src/rpc.rs b/gsdk/src/rpc.rs index 7680e159227..aed07a99dde 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. 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 at specified block. + #[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..2d519604f93 100644 --- a/gsdk/tests/rpc.rs +++ b/gsdk/tests/rpc.rs @@ -99,6 +99,39 @@ async fn test_calculate_create_gas() -> Result<()> { Ok(()) } +#[tokio::test] +async fn test_read_wasm_custom_section() -> Result<()> { + let (_node, api) = dev_node().await; + + 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(); + + api.upload_code(wasm.clone()).await?; + let code_id = CodeId::generate(&wasm); + + let present = api.read_wasm_custom_section(code_id, "sails:idl").await?; + assert_eq!(present.as_deref(), Some(b"hello idl".as_ref())); + + let missing_section = api + .read_wasm_custom_section(code_id, "no:such:section") + .await?; + assert!(missing_section.is_none()); + + let unknown_code = api + .read_wasm_custom_section(CodeId::from([0u8; 32]), "sails:idl") + .await?; + assert!(unknown_code.is_none()); + + Ok(()) +} + #[tokio::test] async fn test_calculate_handle_gas() -> Result<()> { let (_node, api) = dev_node().await; diff --git a/node/service/src/rpc/mod.rs b/node/service/src/rpc/mod.rs index fe7bdc73a9d..6b2e42c48ea 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 { @@ -144,6 +145,7 @@ where 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(()); @@ -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..3e6c7893c11 --- /dev/null +++ b/node/service/src/rpc/wasm_section.rs @@ -0,0 +1,116 @@ +// 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 gear_core::code::get_custom_section_data; +use jsonrpsee::{core::RpcResult, proc_macros::rpc, types::ErrorObjectOwned}; +use parity_scale_codec::{Compact, Decode}; +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}; + +const ERROR_CODE: i32 = 9000; + +#[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, + _marker: PhantomData<(Block, Backend)>, +} + +impl WasmSectionApi { + pub(crate) fn new(client: Arc) -> Self { + Self { + client, + _marker: 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); + + // 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(|e| rpc_err("WASM section read error", Some(e.to_string())))?; + + let Some(wasm_data) = wasm_data else { + return Ok(None); + }; + + 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) { + 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 rpc_err(message: &str, data: Option) -> ErrorObjectOwned { + use jsonrpsee::types::error::ErrorObject; + + ErrorObject::owned(ERROR_CODE, message, data) +}