Skip to content
Open
Show file tree
Hide file tree
Changes from 10 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:?}"
);
}
}
17 changes: 14 additions & 3 deletions ethexe/common/src/db.rs
Original file line number Diff line number Diff line change
Expand Up @@ -76,8 +76,13 @@ 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, runtime_id: u32, version: u32, code_id: CodeId) -> bool;
fn instrumented_code(
&self,
runtime_id: u32,
version: u32,
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What the reason of adding second version? As soon as version can be changed only when runtime is changed, then we can use only one here, runtime version or instrumentation version

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 +92,13 @@ 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,
runtime_id: u32,
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
12 changes: 11 additions & 1 deletion ethexe/common/src/mock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,16 @@ use crate::{
gear::{BatchCommitment, ChainCommitment, CodeCommitment, Message, StateTransition},
injected::{AddressedInjectedTransaction, InjectedTransaction},
};

/// Mock equivalent of `ethexe_runtime_common::RUNTIME_ID`.
///
/// `ethexe-runtime-common` depends on `ethexe-common`, so we can't pull the
/// real constant in here without a dep cycle. `ethexe-runtime-common` has a
/// matching-constants test that fails loudly if this drifts.
pub const MOCK_RUNTIME_ID: u32 = 1;

/// Mock equivalent of `ethexe_runtime_common::VERSION`. See [`MOCK_RUNTIME_ID`].
pub const MOCK_VERSION: u32 = 2;
use alloc::{collections::BTreeMap, vec};
use gear_core::{
code::{CodeMetadata, InstrumentedCode},
Expand Down Expand Up @@ -725,7 +735,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_RUNTIME_ID, 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
9 changes: 7 additions & 2 deletions ethexe/compute/src/codes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -68,8 +68,11 @@ impl<P: ProcessorExt> CodesSubService<P> {
"Code {code_id:?} must exist in database"
);
debug_assert!(
self.db
.instrumented_code_exists(ethexe_runtime_common::VERSION, code_id),
self.db.instrumented_code_exists(
ethexe_runtime_common::RUNTIME_ID,
ethexe_runtime_common::VERSION,
code_id,
),
"Instrumented code {code_id:?} must exist in database"
);
}
Expand All @@ -90,6 +93,7 @@ impl<P: ProcessorExt> CodesSubService<P> {
{
db.set_original_code(&code);
db.set_instrumented_code(
ethexe_runtime_common::RUNTIME_ID,
ethexe_runtime_common::VERSION,
code_id,
instrumented_code,
Expand Down Expand Up @@ -159,6 +163,7 @@ mod tests {
db.set_code_valid(code_id, true);
db.set_original_code(code_and_id.code());
db.set_instrumented_code(
ethexe_runtime_common::RUNTIME_ID,
ethexe_runtime_common::VERSION,
code_id,
InstrumentedCode::new(
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::{RUNTIME_ID, 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(RUNTIME_ID, VERSION, code_id, instrumented_code);
db.set_code_metadata(code_id, code_metadata);
db.set_code_valid(code_id, true);

Expand Down
1 change: 1 addition & 0 deletions ethexe/compute/src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -476,6 +476,7 @@ async fn process_code_for_already_processed_valid_code_emits_code_processed() ->
let code_id = db.set_original_code(&code);

db.set_instrumented_code(
ethexe_runtime_common::RUNTIME_ID,
ethexe_runtime_common::VERSION,
code_id,
InstrumentedCode::new(vec![0], InstantiatedSectionSizes::new(0, 0, 0, 0, 0, 0)),
Expand Down
75 changes: 59 additions & 16 deletions ethexe/db/src/database.rs
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,8 @@ enum Key {
AnnounceMeta(HashOf<Announce>) = 6,

ProgramToCodeId(ActorId) = 7,
InstrumentedCode(u32, CodeId) = 8,
/// `(runtime_id, instrumentation_version, code_id)`.
InstrumentedCode(u32, u32, CodeId) = 8,
CodeMetadata(CodeId) = 9,
CodeUploadInfo(CodeId) = 10,
CodeValid(CodeId) = 11,
Expand All @@ -93,8 +94,10 @@ impl Key {
}

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

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

Self::InstrumentedCode(runtime_id, code_id) => {
Self::InstrumentedCode(runtime_id, version, code_id) => {
bytes.extend(runtime_id.to_le_bytes());
bytes.extend(version.to_le_bytes());
bytes.extend(code_id.as_ref());
}
Self::Globals | Self::Config => {
Expand All @@ -135,7 +139,7 @@ impl Key {
"Key must be longer than H256, to avoid collision with CAS keys"
);
debug_assert!(
bytes.len() <= 2 * size_of::<H256>() + size_of::<u32>(),
bytes.len() <= 2 * size_of::<H256>() + 2 * size_of::<u32>(),
"Key must not be longer than maximum possible length"
);

Expand Down Expand Up @@ -609,14 +613,19 @@ impl CodesStorageRO for RawDatabase {
})
}

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

fn instrumented_code(&self, runtime_id: u32, code_id: CodeId) -> Option<InstrumentedCode> {
fn instrumented_code(
&self,
runtime_id: u32,
version: u32,
code_id: CodeId,
) -> Option<InstrumentedCode> {
self.kv
.get(&Key::InstrumentedCode(runtime_id, code_id).to_bytes())
.get(&Key::InstrumentedCode(runtime_id, 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 +687,21 @@ impl CodesStorageRW for RawDatabase {
);
}

fn set_instrumented_code(&self, runtime_id: u32, code_id: CodeId, code: InstrumentedCode) {
fn set_instrumented_code(
&self,
runtime_id: u32,
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(runtime_id, version, code_id).to_bytes(),
code.encode(),
);
}
Expand Down Expand Up @@ -960,8 +976,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, runtime_id: u32, version: u32, code_id: CodeId) -> bool;
fn instrumented_code(&self, runtime_id: u32, 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 +988,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, runtime_id: u32, 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 +1060,19 @@ mod tests {
limited::LimitedVec,
};

/// `migrations::v5` hardcodes the `InstrumentedCode` discriminant (= 8) to
/// drop legacy entries under the old 2-tuple key layout. If the `Key` enum
/// gets reordered, this test fails loudly so the migration can be updated.
#[test]
fn instrumented_code_key_discriminant_is_stable() {
let bytes = Key::InstrumentedCode(0, 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 @@ -1171,16 +1200,30 @@ mod tests {
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(runtime_id, version, code_id, instrumented_code.clone());
assert_eq!(
db.instrumented_code(runtime_id, code_id)
db.instrumented_code(runtime_id, version, code_id)
.as_ref()
.map(|c| c.bytes()),
Some(instrumented_code.bytes())
);

// Different version under same runtime_id is a distinct namespace.
assert!(
db.instrumented_code(runtime_id, version + 1, code_id)
.is_none(),
"bumping version must invalidate prior entries"
);
// Different runtime_id under same version is also distinct.
assert!(
db.instrumented_code(runtime_id + 1, version, code_id)
.is_none(),
"changing runtime_id must invalidate prior entries"
);
}

#[test]
Expand Down
Loading
Loading