Skip to content

Commit

Permalink
runtime: start lifting contract preparation up through tx runtime lay…
Browse files Browse the repository at this point in the history
…ers (#11810)

Best reviewed commit-by-commit.

This ultimately lifts the contract preparation up through a several
function call layers in the transaction runtime up to the layer where
all the currently necessary data are available.

This PR also establishes in broad strokes where the pipelining decisions
will be made (`ApplyProcessingReceiptState`) and makes some minor
changes to the type to have it contain local receipts (in addition to
the previously contained delayed receipts etc) in a queue of sorts which
would allow the pipelining code to look-ahead of the ongoing processing
work and queue-up preparation of the upcoming contracts there.

This work so far is intended to have no functional changes.

Part of #11319
  • Loading branch information
nagisa authored Jul 23, 2024
1 parent 3eb30ba commit e883ee2
Show file tree
Hide file tree
Showing 30 changed files with 607 additions and 397 deletions.
6 changes: 3 additions & 3 deletions runtime/near-vm-runner/fuzz/fuzz_targets/diffrunner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,19 +21,19 @@ libfuzzer_sys::fuzz_target!(|module: ArbitraryModule| {
fn run_fuzz(code: &ContractCode, vm_kind: VMKind) -> VMOutcome {
let mut fake_external = MockedExternal::with_code(code.clone_for_tests());
let method_name = find_entry_point(code).unwrap_or_else(|| "main".to_string());
let mut context = create_context(&method_name, vec![]);
let mut context = create_context(vec![]);
context.prepaid_gas = 10u64.pow(14);
let config_store = RuntimeConfigStore::new(None);
let config = config_store.get_config(PROTOCOL_VERSION);
let fees = Arc::clone(&config.fees);
let mut wasm_config = near_parameters::vm::Config::clone(&config.wasm_config);
wasm_config.limit_config.contract_prepare_version =
near_vm_runner::logic::ContractPrepareVersion::V2;

let gas_counter = context.make_gas_counter(&wasm_config);
let res = vm_kind
.runtime(wasm_config.into())
.unwrap()
.prepare(&fake_external, &context, None)
.prepare(&fake_external, None, gas_counter, &method_name)
.run(&mut fake_external, &context, fees);

// Remove the VMError message details as they can differ between runtimes
Expand Down
5 changes: 3 additions & 2 deletions runtime/near-vm-runner/fuzz/fuzz_targets/runner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,16 +19,17 @@ libfuzzer_sys::fuzz_target!(|module: ArbitraryModule| {
fn run_fuzz(code: &ContractCode, config: Arc<RuntimeConfig>) -> VMOutcome {
let mut fake_external = MockedExternal::with_code(code.clone_for_tests());
let method_name = find_entry_point(code).unwrap_or_else(|| "main".to_string());
let mut context = create_context(&method_name, vec![]);
let mut context = create_context(vec![]);
context.prepaid_gas = 10u64.pow(14);
let mut wasm_config = near_parameters::vm::Config::clone(&config.wasm_config);
wasm_config.limit_config.wasmer2_stack_limit = i32::MAX; // If we can crash wasmer2 even without the secondary stack limit it's still good to know
let vm_kind = config.wasm_config.vm_kind;
let fees = Arc::clone(&config.fees);
let gas_counter = context.make_gas_counter(&wasm_config);
vm_kind
.runtime(wasm_config.into())
.unwrap()
.prepare(&fake_external, &context, None)
.prepare(&fake_external, None, gas_counter, &method_name)
.run(&mut fake_external, &context, fees)
.unwrap_or_else(|err| panic!("fatal error: {err:?}"))
}
3 changes: 1 addition & 2 deletions runtime/near-vm-runner/fuzz/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,12 @@ pub fn find_entry_point(contract: &ContractCode) -> Option<String> {
None
}

pub fn create_context(method: &str, input: Vec<u8>) -> VMContext {
pub fn create_context(input: Vec<u8>) -> VMContext {
VMContext {
current_account_id: "alice".parse().unwrap(),
signer_account_id: "bob".parse().unwrap(),
signer_account_pk: vec![0, 1, 2, 3, 4],
predecessor_account_id: "carol".parse().unwrap(),
method: method.into(),
input,
promise_results: Vec::new().into(),
block_height: 10,
Expand Down
2 changes: 1 addition & 1 deletion runtime/near-vm-runner/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ pub use code::ContractCode;
#[cfg(feature = "metrics")]
pub use metrics::{report_metrics, reset_metrics};
pub use profile::ProfileDataV3;
pub use runner::{run, PreparedContract, VM};
pub use runner::{prepare, run, Contract, PreparedContract, VM};

/// This is public for internal experimentation use only, and should otherwise be considered an
/// implementation detail of `near-vm-runner`.
Expand Down
19 changes: 17 additions & 2 deletions runtime/near-vm-runner/src/logic/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,6 @@ pub struct VMContext {
/// If this execution is the result of direct execution of transaction then it
/// is equal to `signer_account_id`.
pub predecessor_account_id: AccountId,
/// The name of the method to invoke.
pub method: String,
/// The input to the contract call.
/// Encoded as base64 string to be able to pass input in borsh binary format.
pub input: Vec<u8>,
Expand Down Expand Up @@ -62,4 +60,21 @@ impl VMContext {
pub fn is_view(&self) -> bool {
self.view_config.is_some()
}

/// Make a gas counter based on the configuration in this VMContext.
///
/// Meant for use in tests only.
pub fn make_gas_counter(&self, config: &near_parameters::vm::Config) -> super::GasCounter {
let max_gas_burnt = match self.view_config {
Some(near_primitives_core::config::ViewConfig { max_gas_burnt }) => max_gas_burnt,
None => config.limit_config.max_gas_burnt,
};
crate::logic::GasCounter::new(
config.ext_costs.clone(),
max_gas_burnt,
config.regular_op_cost,
self.prepaid_gas,
self.is_view(),
)
}
}
6 changes: 0 additions & 6 deletions runtime/near-vm-runner/src/logic/dependencies.rs
Original file line number Diff line number Diff line change
Expand Up @@ -488,10 +488,4 @@ pub trait External {
///
/// Panics if `ReceiptIndex` is invalid.
fn get_receipt_receiver(&self, receipt_index: ReceiptIndex) -> &AccountId;

/// Hash of the contract for the current account.
fn code_hash(&self) -> CryptoHash;

/// Get the contract code
fn get_contract(&self) -> Option<std::sync::Arc<crate::ContractCode>>;
}
117 changes: 90 additions & 27 deletions runtime/near-vm-runner/src/logic/gas_counter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ type Result<T> = ::std::result::Result<T, VMLogicError>;
/// instance by intrinsifying host functions responsible for gas metering.
#[repr(C)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FastGasCounter {
pub(crate) struct FastGasCounter {
/// The following three fields must be put next to another to make sure
/// generated gas counting code can use and adjust them.
/// We will share counter to ensure we never miss synchronization.
Expand Down Expand Up @@ -119,7 +119,7 @@ impl GasCounter {
/// Simpler version of `deduct_gas()` for when no promises are involved.
///
/// Return an error if there are arithmetic overflows.
pub fn burn_gas(&mut self, gas_burnt: Gas) -> Result<()> {
pub(crate) fn burn_gas(&mut self, gas_burnt: Gas) -> Result<()> {
let new_burnt_gas =
self.fast_counter.burnt_gas.checked_add(gas_burnt).ok_or(HostError::IntegerOverflow)?;
if new_burnt_gas <= self.fast_counter.gas_limit {
Expand All @@ -138,7 +138,7 @@ impl GasCounter {
}
}

pub fn process_gas_limit(&mut self, new_burnt_gas: Gas, new_used_gas: Gas) -> HostError {
pub(crate) fn process_gas_limit(&mut self, new_burnt_gas: Gas, new_used_gas: Gas) -> HostError {
use std::cmp::min;
// Never burn more gas than what was paid for.
let hard_burnt_limit = min(self.prepaid_gas, self.max_gas_burnt);
Expand Down Expand Up @@ -178,22 +178,90 @@ impl GasCounter {
}

/// Very special function to get the gas counter pointer for generated machine code.
///
/// Please do not use, unless fully understand Rust aliasing and other consequences.
/// Can be used to emit inlined code like `pay_wasm_gas()`, i.e.
/// mov base, gas_counter_raw_ptr
/// mov rax, [base + 0] ; current burnt gas
/// mov rcx, [base + 16] ; opcode cost
/// imul rcx, block_ops_count ; block cost
/// add rax, rcx ; new burnt gas
/// jo emit_integer_overflow
/// cmp rax, [base + 8] ; unsigned compare with burnt limit
/// mov [base + 0], rax
/// ja emit_gas_exceeded
pub fn gas_counter_raw_ptr(&mut self) -> *mut FastGasCounter {
#[cfg(all(any(feature = "wasmer2_vm", feature = "near_vm"), target_arch = "x86_64"))]
pub(crate) fn fast_counter_raw_ptr(&mut self) -> *mut FastGasCounter {
use std::ptr;
ptr::addr_of_mut!(self.fast_counter)
}

/// Add a cost for loading the contract code in the VM.
///
/// This cost does not consider the structure of the contract code, only the
/// size. This is currently the only loading fee. A fee that takes the code
/// structure into consideration could be added. But since that would have
/// to happen after loading, we cannot pre-charge it. This is the main
/// motivation to (only) have this simple fee.
#[cfg(any(
feature = "wasmtime_vm",
all(
target_arch = "x86_64",
any(feature = "wasmer0_vm", feature = "wasmer2_vm", feature = "near_vm")
)
))]
pub(crate) fn add_contract_loading_fee(&mut self, code_len: u64) -> Result<()> {
self.pay_per(ExtCosts::contract_loading_bytes, code_len)?;
self.pay_base(ExtCosts::contract_loading_base)
}

/// VM independent setup before loading the executable.
///
/// Does VM independent checks that happen after the instantiation of
/// VMLogic but before loading the executable. This includes pre-charging gas
/// costs for loading the executable, which depends on the size of the WASM code.
#[cfg(any(
feature = "wasmtime_vm",
all(
target_arch = "x86_64",
any(feature = "wasmer0_vm", feature = "wasmer2_vm", feature = "near_vm")
)
))]
pub(crate) fn before_loading_executable(
&mut self,
config: &near_parameters::vm::Config,
method_name: &str,
wasm_code_bytes: u64,
) -> std::result::Result<(), super::errors::FunctionCallError> {
if method_name.is_empty() {
let error = super::errors::FunctionCallError::MethodResolveError(
super::errors::MethodResolveError::MethodEmptyName,
);
return Err(error);
}
if config.fix_contract_loading_cost {
if self.add_contract_loading_fee(wasm_code_bytes).is_err() {
let error =
super::errors::FunctionCallError::HostError(super::HostError::GasExceeded);
return Err(error);
}
}
Ok(())
}

/// Legacy code to preserve old gas charging behaviour in old protocol versions.
#[cfg(any(
feature = "wasmtime_vm",
all(
target_arch = "x86_64",
any(feature = "wasmer0_vm", feature = "wasmer2_vm", feature = "near_vm")
)
))]
pub(crate) fn after_loading_executable(
&mut self,
config: &near_parameters::vm::Config,
wasm_code_bytes: u64,
) -> std::result::Result<(), super::errors::FunctionCallError> {
if !config.fix_contract_loading_cost {
if self.add_contract_loading_fee(wasm_code_bytes).is_err() {
return Err(super::errors::FunctionCallError::HostError(
super::HostError::GasExceeded,
));
}
}
Ok(())
}

#[inline]
fn inc_ext_costs_counter(&mut self, cost: ExtCosts, value: u64) {
with_ext_cost_counter(|cc| *cc.entry(cost).or_default() += value)
Expand All @@ -210,7 +278,7 @@ impl GasCounter {
}

/// A helper function to pay a multiple of a cost.
pub fn pay_per(&mut self, cost: ExtCosts, num: u64) -> Result<()> {
pub(crate) fn pay_per(&mut self, cost: ExtCosts, num: u64) -> Result<()> {
let use_gas =
num.checked_mul(cost.gas(&self.ext_costs_config)).ok_or(HostError::IntegerOverflow)?;

Expand All @@ -222,7 +290,7 @@ impl GasCounter {
}

/// A helper function to pay base cost gas.
pub fn pay_base(&mut self, cost: ExtCosts) -> Result<()> {
pub(crate) fn pay_base(&mut self, cost: ExtCosts) -> Result<()> {
let base_fee = cost.gas(&self.ext_costs_config);
self.inc_ext_costs_counter(cost, 1);
let old_burnt_gas = self.fast_counter.burnt_gas;
Expand All @@ -236,7 +304,7 @@ impl GasCounter {
/// * `burn_gas`: amount of gas to burn;
/// * `use_gas`: amount of gas to reserve;
/// * `action`: what kind of action is charged for;
pub fn pay_action_accumulated(
pub(crate) fn pay_action_accumulated(
&mut self,
burn_gas: Gas,
use_gas: Gas,
Expand All @@ -251,31 +319,26 @@ impl GasCounter {
deduct_gas_result
}

pub fn add_trie_fees(&mut self, count: &TrieNodesCount) -> Result<()> {
pub(crate) fn add_trie_fees(&mut self, count: &TrieNodesCount) -> Result<()> {
self.pay_per(touching_trie_node, count.db_reads)?;
self.pay_per(read_cached_trie_node, count.mem_reads)?;
Ok(())
}

pub fn prepay_gas(&mut self, use_gas: Gas) -> Result<()> {
pub(crate) fn prepay_gas(&mut self, use_gas: Gas) -> Result<()> {
self.deduct_gas(0, use_gas)
}

pub fn burnt_gas(&self) -> Gas {
pub(crate) fn burnt_gas(&self) -> Gas {
self.fast_counter.burnt_gas
}

/// Amount of gas used through promises and amount burned.
pub fn used_gas(&self) -> Gas {
pub(crate) fn used_gas(&self) -> Gas {
self.promises_gas + self.fast_counter.burnt_gas
}

/// Remaining gas based on the amount of prepaid gas not yet used.
pub fn unused_gas(&self) -> Gas {
self.prepaid_gas - self.used_gas()
}

pub fn profile_data(&self) -> ProfileDataV3 {
pub(crate) fn profile_data(&self) -> ProfileDataV3 {
self.profile.clone()
}
}
Expand Down
Loading

0 comments on commit e883ee2

Please sign in to comment.