Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -263,7 +263,7 @@ KVDatabase: get(key) → Vec<u8>, put(key, data) (metadata)
Key prefixes (enum):
BlockSmallData(H256), BlockEvents(H256),
AnnounceProgramStates(HashOf<Announce>), AnnounceSchedule(HashOf<Announce>),
ProgramToCodeId(ActorId), InstrumentedCode(u32, CodeId),
ProgramToCodeId(ActorId), InstrumentedCode(runtime_id: u32, version: u32, CodeId),
CodeMetadata(CodeId), CodeValid(CodeId),
InjectedTransaction(HashOf<Tx>),
Config, Globals, LatestEraValidatorsCommitted(H256)
Expand Down
82 changes: 81 additions & 1 deletion core/src/code/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,8 @@ impl Code {
let table_section_size = utils::get_instantiated_table_section_size(&module);
let element_section_size = utils::get_instantiated_element_section_size(&module)?;

module.strip_custom_sections();

let code = module.serialize()?;

// Use instrumented code to get section sizes.
Expand Down Expand Up @@ -458,7 +460,7 @@ mod tests {
},
gas_metering::CustomConstantCostRules,
};
use alloc::{format, vec::Vec};
use alloc::{format, string::String, vec::Vec};
use gear_wasm_instrument::{InstrumentationError, ModuleError, STACK_END_EXPORT_NAME};

fn wat2wasm_with_validate(s: &str, validate: bool) -> Vec<u8> {
Expand Down Expand Up @@ -1245,4 +1247,82 @@ mod tests {
))
));
}

/// Walks a WASM binary and returns `true` if it contains a custom section
/// with the given name.
fn has_custom_section(wasm: &[u8], name: &str) -> bool {
wasmparser::Parser::new(0)
.parse_all(wasm)
.filter_map(|p| p.ok())
.any(|payload| match payload {
wasmparser::Payload::CustomSection(reader) => reader.name() == name,
_ => false,
})
}

#[test]
fn instrumented_code_strips_custom_sections_but_original_keeps_them() {
// Build a valid gear program and inject a `sails:idl` custom section
// into its bytes before instrumentation.
let wat = r#"
(module
(import "env" "memory" (memory 1))
(export "init" (func $init))
(func $init)
)
"#;
let base_bytes = wat2wasm(wat);

// Same mechanism sails tooling uses to embed the IDL.
let idl_payload: Vec<u8> = (0..64u8).collect();
let module = gear_wasm_instrument::Module::new(&base_bytes).unwrap();
let mut builder = gear_wasm_instrument::ModuleBuilder::from_module(module);
builder.push_custom_section("sails:idl", idl_payload.clone());
let original_with_idl = builder.build().serialize().unwrap();

// Sanity: the constructed original actually carries the section.
assert!(
has_custom_section(&original_with_idl, "sails:idl"),
"test fixture must contain the sails:idl custom section before instrumentation"
);

// Run through the full Code pipeline.
let code = Code::try_new_mock_const_or_no_rules(
original_with_idl,
true,
TryNewCodeConfig::default(),
)
.expect("valid gear program must instrument");

// OriginalCode preserves the section — IDL readers (RPC) rely on this.
assert!(
has_custom_section(code.original_code(), "sails:idl"),
"OriginalCode must retain sails:idl custom section"
);

// InstrumentedCode must have the sails:idl section stripped.
assert!(
!has_custom_section(code.instrumented_code().bytes(), "sails:idl"),
"InstrumentedCode must have sails:idl stripped"
);

// Broader check: every custom section other than `name` is gone.
// The WASM binary format stores the name section as a custom section
// named "name"; we intentionally preserve it for readable trap
// backtraces (see `Module::strip_custom_sections`).
let lingering: Vec<String> = wasmparser::Parser::new(0)
.parse_all(code.instrumented_code().bytes())
.filter_map(|p| p.ok())
.filter_map(|payload| match payload {
wasmparser::Payload::CustomSection(reader) if reader.name() != "name" => {
Some(String::from(reader.name()))
}
_ => None,
})
.collect();
assert!(
lingering.is_empty(),
"InstrumentedCode must have no custom sections apart from `name`; found: {lingering:?}"
);
}
}
6 changes: 3 additions & 3 deletions ethexe/common/src/db.rs
Original file line number Diff line number Diff line change
Expand Up @@ -76,8 +76,8 @@ pub trait CodesStorageRO {
fn original_code_exists(&self, code_id: CodeId) -> bool;
fn original_code(&self, code_id: CodeId) -> Option<Vec<u8>>;
fn program_code_id(&self, program_id: ActorId) -> Option<CodeId>;
fn instrumented_code_exists(&self, runtime_id: u32, code_id: CodeId) -> bool;
fn instrumented_code(&self, runtime_id: u32, code_id: CodeId) -> Option<InstrumentedCode>;
fn instrumented_code_exists(&self, version: u32, code_id: CodeId) -> bool;
fn instrumented_code(&self, version: u32, code_id: CodeId) -> Option<InstrumentedCode>;
fn code_metadata(&self, code_id: CodeId) -> Option<CodeMetadata>;
fn code_valid(&self, code_id: CodeId) -> Option<bool>;
fn valid_codes(&self) -> BTreeSet<CodeId>;
Expand All @@ -87,7 +87,7 @@ pub trait CodesStorageRO {
pub trait CodesStorageRW: CodesStorageRO {
fn set_original_code(&self, code: &[u8]) -> CodeId;
fn set_program_code_id(&self, program_id: ActorId, code_id: CodeId);
fn set_instrumented_code(&self, runtime_id: u32, code_id: CodeId, code: InstrumentedCode);
fn set_instrumented_code(&self, version: u32, code_id: CodeId, code: InstrumentedCode);
fn set_code_metadata(&self, code_id: CodeId, code_metadata: CodeMetadata);
fn set_code_valid(&self, code_id: CodeId, valid: bool);
}
Expand Down
7 changes: 6 additions & 1 deletion ethexe/common/src/mock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,11 @@ use crate::{
gear::{BatchCommitment, ChainCommitment, CodeCommitment, Message, StateTransition},
injected::{AddressedInjectedTransaction, InjectedTransaction},
};

/// Mock equivalent of `ethexe_runtime_common::VERSION` (can't import directly:
/// `ethexe-runtime-common` depends on `ethexe-common`). A matching-constants
/// test in `ethexe-runtime-common` fails if this drifts.
pub const MOCK_VERSION: u32 = 2;
use alloc::{collections::BTreeMap, vec};
use gear_core::{
code::{CodeMetadata, InstrumentedCode},
Expand Down Expand Up @@ -725,7 +730,7 @@ impl BlockChain {
db.set_original_code(&original_bytes);

if let Some(InstrumentedCodeData { instrumented, meta }) = instrumented {
db.set_instrumented_code(1, code_id, instrumented);
db.set_instrumented_code(MOCK_VERSION, code_id, instrumented);
db.set_code_metadata(code_id, meta);
db.set_code_blob_info(code_id, blob_info);
db.set_code_valid(code_id, true);
Expand Down
4 changes: 2 additions & 2 deletions ethexe/compute/src/compute.rs
Original file line number Diff line number Diff line change
Expand Up @@ -428,7 +428,7 @@ mod tests {
injected::{InjectedTransaction, SignedInjectedTransaction},
};
use ethexe_processor::ValidCodeInfo;
use ethexe_runtime_common::RUNTIME_ID;
use ethexe_runtime_common::VERSION;
use gear_core::ids::prelude::CodeIdExt;
use gprimitives::{CodeId, MessageId};

Expand All @@ -454,7 +454,7 @@ mod tests {
.expect("code is invalid");

db.set_original_code(&code);
db.set_instrumented_code(RUNTIME_ID, code_id, instrumented_code);
db.set_instrumented_code(VERSION, code_id, instrumented_code);
db.set_code_metadata(code_id, code_metadata);
db.set_code_valid(code_id, true);

Expand Down
52 changes: 36 additions & 16 deletions ethexe/db/src/database.rs
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,8 @@ enum Key {
AnnounceMeta(HashOf<Announce>) = 6,

ProgramToCodeId(ActorId) = 7,
/// `(instrumentation_version, code_id)`. Bumping
/// `ethexe_runtime_common::VERSION` invalidates every prior entry.
InstrumentedCode(u32, CodeId) = 8,
CodeMetadata(CodeId) = 9,
CodeUploadInfo(CodeId) = 10,
Expand All @@ -93,7 +95,9 @@ impl Key {
}

fn to_bytes(&self) -> Vec<u8> {
// Pre-allocate enough space for the largest possible key.
// Pre-allocate enough space for the largest possible key
// (InstrumentedCode carries a u32 prefix in addition to the
// discriminant and CodeId).
let mut bytes = Vec::with_capacity(2 * size_of::<H256>() + size_of::<u32>());
bytes.extend(self.prefix());

Expand All @@ -120,8 +124,8 @@ impl Key {
| Self::CodeUploadInfo(code_id)
| Self::CodeValid(code_id) => bytes.extend(code_id.as_ref()),

Self::InstrumentedCode(runtime_id, code_id) => {
bytes.extend(runtime_id.to_le_bytes());
Self::InstrumentedCode(version, code_id) => {
bytes.extend(version.to_le_bytes());
bytes.extend(code_id.as_ref());
}
Self::Globals | Self::Config => {
Expand Down Expand Up @@ -609,14 +613,14 @@ impl CodesStorageRO for RawDatabase {
})
}

fn instrumented_code_exists(&self, runtime_id: u32, code_id: CodeId) -> bool {
fn instrumented_code_exists(&self, version: u32, code_id: CodeId) -> bool {
self.kv
.contains(&Key::InstrumentedCode(runtime_id, code_id).to_bytes())
.contains(&Key::InstrumentedCode(version, code_id).to_bytes())
}

fn instrumented_code(&self, runtime_id: u32, code_id: CodeId) -> Option<InstrumentedCode> {
fn instrumented_code(&self, version: u32, code_id: CodeId) -> Option<InstrumentedCode> {
self.kv
.get(&Key::InstrumentedCode(runtime_id, code_id).to_bytes())
.get(&Key::InstrumentedCode(version, code_id).to_bytes())
.map(|data| {
Decode::decode(&mut data.as_slice())
.expect("Failed to decode data into `InstrumentedCode`")
Expand Down Expand Up @@ -678,14 +682,14 @@ impl CodesStorageRW for RawDatabase {
);
}

fn set_instrumented_code(&self, runtime_id: u32, code_id: CodeId, code: InstrumentedCode) {
fn set_instrumented_code(&self, version: u32, code_id: CodeId, code: InstrumentedCode) {
tracing::trace!(
code_id = ?code_id,
runtime_id = %runtime_id,
version = %version,
"Set instrumented code"
);
self.kv.put(
&Key::InstrumentedCode(runtime_id, code_id).to_bytes(),
&Key::InstrumentedCode(version, code_id).to_bytes(),
code.encode(),
);
}
Expand Down Expand Up @@ -960,8 +964,8 @@ impl CodesStorageRO for Database {
fn original_code_exists(&self, code_id: CodeId) -> bool;
fn original_code(&self, code_id: CodeId) -> Option<Vec<u8>>;
fn program_code_id(&self, program_id: ActorId) -> Option<CodeId>;
fn instrumented_code_exists(&self, runtime_id: u32, code_id: CodeId) -> bool;
fn instrumented_code(&self, runtime_id: u32, code_id: CodeId) -> Option<InstrumentedCode>;
fn instrumented_code_exists(&self, version: u32, code_id: CodeId) -> bool;
fn instrumented_code(&self, version: u32, code_id: CodeId) -> Option<InstrumentedCode>;
fn code_metadata(&self, code_id: CodeId) -> Option<CodeMetadata>;
fn code_valid(&self, code_id: CodeId) -> Option<bool>;
fn valid_codes(&self) -> BTreeSet<CodeId>;
Expand All @@ -972,7 +976,7 @@ impl CodesStorageRW for Database {
delegate!(to self.raw {
fn set_original_code(&self, code: &[u8]) -> CodeId;
fn set_program_code_id(&self, program_id: ActorId, code_id: CodeId);
fn set_instrumented_code(&self, runtime_id: u32, code_id: CodeId, code: InstrumentedCode);
fn set_instrumented_code(&self, version: u32, code_id: CodeId, code: InstrumentedCode);
fn set_code_metadata(&self, code_id: CodeId, code_metadata: CodeMetadata);
fn set_code_valid(&self, code_id: CodeId, valid: bool);
});
Expand Down Expand Up @@ -1044,6 +1048,17 @@ mod tests {
limited::LimitedVec,
};

/// `migrations::v5` hardcodes discriminant `8`; this test pins it.
#[test]
fn instrumented_code_key_discriminant_is_stable() {
let bytes = Key::InstrumentedCode(0, CodeId::zero()).to_bytes();
assert_eq!(
&bytes[..size_of::<H256>()],
H256::from_low_u64_be(8).as_bytes(),
"Key::InstrumentedCode discriminant drifted; update ethexe/db/src/migrations/v5.rs"
);
}

#[test]
fn test_injected_transaction() {
let db = Database::memory();
Expand Down Expand Up @@ -1170,17 +1185,22 @@ mod tests {
fn test_instrumented_code() {
let db = Database::memory();

let runtime_id = 1;
let version = 2;
let code_id = CodeId::default();
let section_sizes = InstantiatedSectionSizes::new(0, 0, 0, 0, 0, 0);
let instrumented_code = InstrumentedCode::new(vec![1, 2, 3, 4], section_sizes);
db.set_instrumented_code(runtime_id, code_id, instrumented_code.clone());
db.set_instrumented_code(version, code_id, instrumented_code.clone());
assert_eq!(
db.instrumented_code(runtime_id, code_id)
db.instrumented_code(version, code_id)
.as_ref()
.map(|c| c.bytes()),
Some(instrumented_code.bytes())
);

assert!(
db.instrumented_code(version + 1, code_id).is_none(),
"bumping version must invalidate prior entries"
);
}

#[test]
Expand Down
2 changes: 1 addition & 1 deletion ethexe/db/src/iterator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -587,7 +587,7 @@ where

if let Some(instrumented_code) = self
.storage
.instrumented_code(ethexe_runtime_common::RUNTIME_ID, code_id)
.instrumented_code(ethexe_runtime_common::VERSION, code_id)
{
self.push_node(InstrumentedCodeNode {
code_id,
Expand Down
4 changes: 2 additions & 2 deletions ethexe/db/src/migrations/init.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ use ethexe_common::{
gear::{GenesisBlockInfo, Timelines},
};
use ethexe_ethereum::router::RouterQuery;
use ethexe_runtime_common::{RUNTIME_ID, ScheduleRestorer, state::Storage};
use ethexe_runtime_common::{ScheduleRestorer, VERSION, state::Storage};
use futures::{TryStreamExt, stream::FuturesUnordered};
use gprimitives::{CodeId, H256};

Expand Down Expand Up @@ -297,7 +297,7 @@ async fn genesis_data_initialization(
);

db_clone.set_code_metadata(code_id, code_metadata);
db_clone.set_instrumented_code(RUNTIME_ID, code_id, instrumented_code);
db_clone.set_instrumented_code(VERSION, code_id, instrumented_code);
db_clone.set_code_valid(code_id, true);

Ok::<_, anyhow::Error>(())
Expand Down
4 changes: 3 additions & 1 deletion ethexe/db/src/migrations/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,15 +35,17 @@ mod v1;
mod v2;
mod v3;
mod v4;
mod v5;

pub const OLDEST_SUPPORTED_VERSION: u32 = v0::VERSION;
pub const LATEST_VERSION: u32 = v4::VERSION;
pub const LATEST_VERSION: u32 = v5::VERSION;

pub const MIGRATIONS: &[&dyn Migration] = &[
&v1::migration_from_v0,
&v2::migration_from_v1,
&v3::migration_from_v2,
&v4::migration_from_v3,
&v5::migration_from_v4,
];

const _: () = assert!(
Expand Down
2 changes: 1 addition & 1 deletion ethexe/db/src/migrations/v1.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ pub const VERSION: u32 = 1;

const _: () = const {
assert!(
crate::VERSION == super::v4::VERSION,
crate::VERSION == super::v5::VERSION,
"Check migration code for types changing in case of version change: DBConfig, DBGlobals, ProtocolTimelines"
);
};
Expand Down
2 changes: 1 addition & 1 deletion ethexe/db/src/migrations/v2.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ pub const VERSION: u32 = 2;

const _: () = const {
assert!(
crate::VERSION == super::v4::VERSION,
crate::VERSION == super::v5::VERSION,
"Check migration code for types changing in case of version change: DBConfig, DBGlobals, Announce, BlockSmallData. \
Also check AnnounceStorageRW, KVDatabase, dyn KVDatabase implementations"
);
Expand Down
2 changes: 1 addition & 1 deletion ethexe/db/src/migrations/v4.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ pub const VERSION: u32 = 4;

const _: () = const {
assert!(
crate::VERSION == VERSION,
crate::VERSION == super::v5::VERSION,
"Check migration code for types changing in case of version change: DBConfig, DBGlobals, Announce, BlockSmallData. \
Also check AnnounceStorageRW, KVDatabase, dyn KVDatabase implementations"
);
Expand Down
Loading
Loading