From cfd529d03f57ca8129307f8a3ec46262175022b4 Mon Sep 17 00:00:00 2001 From: Simonas Kazlauskas Date: Tue, 11 Feb 2025 17:48:30 +0200 Subject: [PATCH] start implementing a custom Externals The MockExternal is unfortunately to rudimentary for what is necessary out of the debugger here. In particular one thing is that we want to be able to manage `Storage` separately from the lifetime of externals, and another thing is that its `generate_data_id` for example is too simplistic and does not necessarily reflect as closely what one would expect from a real system. --- index.html | 2 +- loader.js | 14 ++- src/near_vm_runner/mod.rs | 259 +++++++++++++++++++++++++++++++++++--- 3 files changed, 253 insertions(+), 22 deletions(-) diff --git a/index.html b/index.html index 7bb4a0c..c4abc22 100644 --- a/index.html +++ b/index.html @@ -23,7 +23,7 @@

The debugging experience will differ between browsers. For instance Chromium supports DWARF debug info. Contracts built from Rust source will embed such debug info into the - .wasm file, for example, so long as debug info is enabled. This would then allow + .wasm file, as long as debug info is enabled. This would then allow debugging Rust code and not the underlying WebAssembly!

diff --git a/loader.js b/loader.js index 0925ca2..7529a73 100644 --- a/loader.js +++ b/loader.js @@ -1,4 +1,4 @@ -import init, { b64encode, list_methods, prepare_contract, Logic, Context, init_panic_hook } from "./pkg/neardebug.js"; +import init, { b64encode, list_methods, prepare_contract, Logic, Context, Store, init_panic_hook } from "./pkg/neardebug.js"; (function(window, document) { async function run(method_name) { @@ -6,7 +6,7 @@ import init, { b64encode, list_methods, prepare_contract, Logic, Context, init_p const memory = new WebAssembly.Memory({ initial: 1024, maximum: 2048 }); contract.memory = memory; const context = new Context().input_str(document.querySelector("#input").value); - const logic = new Logic(context, memory); + const logic = new Logic(context, memory, contract.store); contract.logic = logic; const import_object = { env: {} }; @@ -100,7 +100,7 @@ import init, { b64encode, list_methods, prepare_contract, Logic, Context, init_p bls12381_pairing_check: (value_len, value_ptr) /* -> [u64] */ => { console.log("TODO bls12381_pairing_check"); }, bls12381_p1_decompress: (value_len, value_ptr, register_id) /* -> [u64] */ => { console.log("TODO bls(12381_p1_decompress"); }, bls12381_p2_decompress: (value_len, value_ptr, register_id) /* -> [u64] */ => { console.log("TODO bls12381_p2_decompress"); }, - sandbox_debug_log: (len, ptr) /* -> [] */ => { console.log("TODO samdbox_debug_log"); }, + sandbox_debug_log: (len, ptr) /* -> [] */ => { console.log("TODO sandbox_debug_log"); }, sleep_nanos: (duration) /* -> [] */ => { console.log("TODO sleep_nanos"); }, }; window.contract.instance = await WebAssembly.instantiate(window.contract.module, import_object); @@ -109,6 +109,8 @@ import init, { b64encode, list_methods, prepare_contract, Logic, Context, init_p async function load(contract_data) { delete contract.instance; + delete contract.memory; + delete contract.logic; if (contract_data === undefined) { delete contract.module; return; @@ -142,9 +144,9 @@ import init, { b64encode, list_methods, prepare_contract, Logic, Context, init_p async function on_load() { await init(); init_panic_hook(); - window.contract = {}; - window.contract.registers = []; - window.contract.storage = {}; + window.contract = { + store: new Store(), + }; const form = document.querySelector('#contract_form'); form.addEventListener('submit', async (e) => { e.preventDefault(); diff --git a/src/near_vm_runner/mod.rs b/src/near_vm_runner/mod.rs index 50acef8..d2cf4f9 100644 --- a/src/near_vm_runner/mod.rs +++ b/src/near_vm_runner/mod.rs @@ -3,18 +3,254 @@ pub mod logic; pub mod profile; use std::str::FromStr as _; +use std::sync::MutexGuard; use js_sys::ArrayBuffer; +use logic::mocks::mock_external::MockedValuePtr; pub use logic::with_ext_cost_counter; -use logic::{gas_counter, ExecutionResultState, External, GasCounter, MemSlice, VMContext}; +use logic::{ + gas_counter, ExecutionResultState, External, GasCounter, MemSlice, VMContext, VMLogicError, + ValuePtr, +}; use logic::{mocks::mock_external, types::PromiseIndex}; use near_parameters::vm::Config; pub use near_primitives_core::code::ContractCode; -use near_primitives_core::types::{AccountId, EpochHeight, Gas, StorageUsage}; +use near_primitives_core::types::{AccountId, Balance, EpochHeight, Gas, StorageUsage}; pub use profile::ProfileDataV3; use serde::Serialize as _; +use std::result::Result as SResult; use wasm_bindgen::prelude::*; +fn js_serializer() -> serde_wasm_bindgen::Serializer { + serde_wasm_bindgen::Serializer::new() + .serialize_missing_as_null(true) + .serialize_large_number_types_as_bigints(true) + .serialize_bytes_as_arrays(false) +} + +#[wasm_bindgen] +#[derive(Clone)] +pub struct Store(std::sync::Arc, Vec>>>); + +#[wasm_bindgen] +impl Store { + #[wasm_bindgen(constructor)] + pub fn new() -> Self { + Self(Default::default()) + } + + fn guard(&self) -> MutexGuard, Vec>> { + self.0.lock().unwrap_or_else(|e| e.into_inner()) + } + + pub fn to_value(&self) -> Result { + self.guard().serialize(&js_serializer()).map_err(Into::into) + } + + pub fn set(&self, key: &[u8], value: &[u8]) { + self.guard().insert(key.to_vec(), value.to_vec()); + } + + pub fn get(&self, key: &[u8]) -> Option> { + self.guard().get(key).cloned() + } + + pub fn remove(&self, key: &[u8]) { + self.guard().remove(key); + } + + pub fn remove_subtree(&self, prefix: &[u8]) { + self.guard().retain(|key, _| !key.starts_with(prefix)); + } + + pub fn has_key(&self, key: &[u8]) -> bool { + self.guard().contains_key(key) + } +} + +#[wasm_bindgen] +pub struct DebugExternal { + store: Store, +} + +#[wasm_bindgen] +impl DebugExternal { + #[wasm_bindgen(constructor)] + pub fn new(store: &Store) -> Self { + Self { + store: store.clone(), + } + } +} + +impl External for DebugExternal { + fn storage_set(&mut self, key: &[u8], value: &[u8]) -> SResult<(), VMLogicError> { + self.store.set(key, value); + Ok(()) + } + + fn storage_get<'a>( + &'a self, + key: &[u8], + _: near_parameters::vm::StorageGetMode, + ) -> SResult>, VMLogicError> { + let v = self.store.get(key); + Ok(v.map(|v| Box::new(MockedValuePtr::new(&v)) as Box<_>)) + } + + fn storage_remove(&mut self, key: &[u8]) -> SResult<(), VMLogicError> { + self.store.remove(key); + Ok(()) + } + + fn storage_remove_subtree(&mut self, prefix: &[u8]) -> SResult<(), VMLogicError> { + self.store.remove_subtree(prefix); + Ok(()) + } + + fn storage_has_key( + &mut self, + key: &[u8], + _: near_parameters::vm::StorageGetMode, + ) -> SResult { + Ok(self.store.has_key(key)) + } + + fn generate_data_id(&mut self) -> near_primitives_core::hash::CryptoHash { + todo!() + } + + fn get_trie_nodes_count(&self) -> logic::TrieNodesCount { + logic::TrieNodesCount { db_reads: 0, mem_reads: 0 } + } + + fn get_recorded_storage_size(&self) -> usize { + 0 + } + + fn validator_stake(&self, account_id: &AccountId) -> SResult, VMLogicError> { + todo!() + } + + fn validator_total_stake(&self) -> SResult { + todo!() + } + + fn create_action_receipt( + &mut self, + receipt_indices: Vec, + receiver_id: AccountId, + ) -> SResult { + todo!() + } + + fn create_promise_yield_receipt( + &mut self, + receiver_id: AccountId, + ) -> SResult< + ( + logic::types::ReceiptIndex, + near_primitives_core::hash::CryptoHash, + ), + logic::VMLogicError, + > { + todo!() + } + + fn submit_promise_resume_data( + &mut self, + data_id: near_primitives_core::hash::CryptoHash, + data: Vec, + ) -> SResult { + todo!() + } + + fn append_action_create_account( + &mut self, + receipt_index: logic::types::ReceiptIndex, + ) -> SResult<(), logic::VMLogicError> { + todo!() + } + + fn append_action_deploy_contract( + &mut self, + receipt_index: logic::types::ReceiptIndex, + code: Vec, + ) -> SResult<(), logic::VMLogicError> { + todo!() + } + + fn append_action_function_call_weight( + &mut self, + receipt_index: logic::types::ReceiptIndex, + method_name: Vec, + args: Vec, + attached_deposit: Balance, + prepaid_gas: Gas, + gas_weight: near_primitives_core::types::GasWeight, + ) -> SResult<(), logic::VMLogicError> { + todo!() + } + + fn append_action_transfer( + &mut self, + receipt_index: logic::types::ReceiptIndex, + deposit: Balance, + ) -> SResult<(), logic::VMLogicError> { + todo!() + } + + fn append_action_stake( + &mut self, + receipt_index: logic::types::ReceiptIndex, + stake: Balance, + public_key: near_crypto::PublicKey, + ) { + todo!() + } + + fn append_action_add_key_with_full_access( + &mut self, + receipt_index: logic::types::ReceiptIndex, + public_key: near_crypto::PublicKey, + nonce: near_primitives_core::types::Nonce, + ) { + todo!() + } + + fn append_action_add_key_with_function_call( + &mut self, + receipt_index: logic::types::ReceiptIndex, + public_key: near_crypto::PublicKey, + nonce: near_primitives_core::types::Nonce, + allowance: Option, + receiver_id: AccountId, + method_names: Vec>, + ) -> SResult<(), logic::VMLogicError> { + todo!() + } + + fn append_action_delete_key( + &mut self, + receipt_index: logic::types::ReceiptIndex, + public_key: near_crypto::PublicKey, + ) { + todo!() + } + + fn append_action_delete_account( + &mut self, + receipt_index: logic::types::ReceiptIndex, + beneficiary_id: AccountId, + ) -> SResult<(), logic::VMLogicError> { + todo!() + } + + fn get_receipt_receiver(&self, receipt_index: logic::types::ReceiptIndex) -> &AccountId { + todo!() + } +} + #[wasm_bindgen] pub struct Context(VMContext); @@ -60,7 +296,7 @@ type Result = std::result::Result; #[wasm_bindgen] impl Logic { #[wasm_bindgen(constructor)] - pub fn new(context: Context, memory: js_sys::WebAssembly::Memory) -> Self { + pub fn new(context: Context, memory: js_sys::WebAssembly::Memory, store: &Store) -> Self { let max_gas_burnt = u64::max_value(); let prepaid_gas = u64::max_value(); let is_view = false; @@ -75,10 +311,10 @@ impl Logic { ); let result_state = ExecutionResultState::new(&context.0, gas_counter, config.wasm_config.clone()); - let mock_ext = Box::new(mock_external::MockedExternal::new()); + let ext = Box::new(DebugExternal::new(store)); Self { logic: logic::VMLogic::new( - mock_ext, + ext, context.0, config.fees.clone(), result_state, @@ -87,17 +323,10 @@ impl Logic { } } - fn js_serializer(&self) -> serde_wasm_bindgen::Serializer { - serde_wasm_bindgen::Serializer::new() - .serialize_missing_as_null(true) - .serialize_large_number_types_as_bigints(true) - .serialize_bytes_as_arrays(false) - } - pub fn context(&self) -> Result { self.logic .context - .serialize(&self.js_serializer()) + .serialize(&js_serializer()) .map_err(Into::into) } @@ -106,12 +335,12 @@ impl Logic { .result_state .clone() .compute_outcome() - .serialize(&self.js_serializer()) + .serialize(&js_serializer()) .map_err(Into::into) } pub fn registers(&mut self) -> Result { - let s = self.js_serializer(); + let s = js_serializer(); self.logic.registers().serialize(&s).map_err(Into::into) }