Skip to content

Commit

Permalink
feat(gas): add real gas metering
Browse files Browse the repository at this point in the history
  • Loading branch information
FranklinWaller committed Nov 28, 2024
1 parent 9c5a319 commit 9dafd13
Show file tree
Hide file tree
Showing 14 changed files with 269 additions and 77 deletions.
14 changes: 7 additions & 7 deletions libtallyvm/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,7 @@ pub unsafe extern "C" fn execute_tally_vm(
}
}

const DEFAULT_GAS_LIMIT_ENV_VAR: &str = "DR_GAS_LIMIT";
const DEFAULT_GAS_LIMIT_ENV_VAR: &str = "DR_TALLY_GAS_LIMIT";

fn _execute_tally_vm(
sedad_home: &Path,
Expand Down Expand Up @@ -234,15 +234,15 @@ fn _execute_tally_vm(
mod test {
use std::collections::BTreeMap;

use crate::_execute_tally_vm;
use crate::{_execute_tally_vm, DEFAULT_GAS_LIMIT_ENV_VAR};

#[test]
fn execute_tally_vm() {
let wasm_bytes = include_bytes!("../../integration-test.wasm");
let mut envs: BTreeMap<String, String> = BTreeMap::new();
// VM_MODE dr to force the http_fetch path
envs.insert("VM_MODE".to_string(), "dr".to_string());
envs.insert("DR_GAS_LIMIT".to_string(), "2000000".to_string());
envs.insert(DEFAULT_GAS_LIMIT_ENV_VAR.to_string(), "300000000000000".to_string());

let tempdir = std::env::temp_dir();
let result = _execute_tally_vm(
Expand All @@ -266,7 +266,7 @@ mod test {
let wasm_bytes = include_bytes!("../../integration-test.wasm");
let mut envs: BTreeMap<String, String> = BTreeMap::new();
envs.insert("VM_MODE".to_string(), "dr".to_string());
envs.insert("DR_GAS_LIMIT".to_string(), "2000000".to_string());
envs.insert(DEFAULT_GAS_LIMIT_ENV_VAR.to_string(), "300000000000000".to_string());

let tempdir = std::env::temp_dir();
let result = _execute_tally_vm(
Expand All @@ -289,7 +289,7 @@ mod test {
fn execute_tally_vm_no_args() {
let wasm_bytes = include_bytes!("../../tally.wasm");
let mut envs: BTreeMap<String, String> = BTreeMap::new();
envs.insert("DR_GAS_LIMIT".to_string(), "2000000".to_string());
envs.insert(DEFAULT_GAS_LIMIT_ENV_VAR.to_string(), "300000000000000".to_string());

let tempdir = std::env::temp_dir();
let result = _execute_tally_vm(&tempdir, wasm_bytes.to_vec(), vec![], envs).unwrap();
Expand All @@ -302,7 +302,7 @@ mod test {
let wasm_bytes = include_bytes!("../../integration-test.wasm");
let mut envs: BTreeMap<String, String> = BTreeMap::new();
envs.insert("VM_MODE".to_string(), "dr".to_string());
envs.insert("DR_GAS_LIMIT".to_string(), "1000".to_string());
envs.insert(DEFAULT_GAS_LIMIT_ENV_VAR.to_string(), "1000".to_string());

let tempdir = std::env::temp_dir();
let result = _execute_tally_vm(
Expand All @@ -322,7 +322,7 @@ mod test {
let wasm_bytes = include_bytes!("../../integration-test.wasm");
let mut envs: BTreeMap<String, String> = BTreeMap::new();
envs.insert("VM_MODE".to_string(), "dr".to_string());
envs.insert("DR_GAS_LIMIT".to_string(), "2000000".to_string());
envs.insert(DEFAULT_GAS_LIMIT_ENV_VAR.to_string(), "300000000000000".to_string());

let tempdir = std::env::temp_dir();
let result =
Expand Down
19 changes: 14 additions & 5 deletions runtime/core/src/context.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
use std::sync::Arc;

use parking_lot::{Mutex, RwLock};
use wasmer::{AsStoreRef, FunctionEnv, Memory, MemoryView, Store};
use seda_runtime_sdk::VmCallData;
use wasmer::{AsStoreRef, FunctionEnv, Instance, Memory, MemoryView, Store};
use wasmer_wasix::WasiEnv;

#[derive(Clone)]
pub struct VmContext {
pub result: Arc<Mutex<Vec<u8>>>,
pub memory: Option<Memory>,
pub wasi_env: FunctionEnv<WasiEnv>,
pub call_data: VmCallData,
pub result: Arc<Mutex<Vec<u8>>>,
pub memory: Option<Memory>,
pub wasi_env: FunctionEnv<WasiEnv>,

/// Used for internal use only
/// This is used to temp store a result of an action
Expand All @@ -19,18 +21,25 @@ pub struct VmContext {
/// use these 3 calls in sequental we are fine, but it could crash if the
/// order changes.
pub call_result_value: Arc<RwLock<Vec<u8>>>,
pub instance: Option<Instance>,
}

impl VmContext {
#[allow(clippy::too_many_arguments)]
pub fn create_vm_context(store: &mut Store, wasi_env: FunctionEnv<WasiEnv>) -> FunctionEnv<VmContext> {
pub fn create_vm_context(
store: &mut Store,
wasi_env: FunctionEnv<WasiEnv>,
call_data: VmCallData,
) -> FunctionEnv<VmContext> {
FunctionEnv::new(
store,
VmContext {
result: Arc::new(Mutex::new(Vec::new())),
memory: None,
wasi_env,
call_result_value: Arc::new(RwLock::new(Vec::new())),
instance: None,
call_data,
},
)
}
Expand Down
13 changes: 11 additions & 2 deletions runtime/core/src/core_vm_imports/execution_result.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,18 @@
use wasmer::{Function, FunctionEnv, FunctionEnvMut, Store, WasmPtr};

use crate::{context::VmContext, errors::Result};
use crate::{context::VmContext, errors::Result, metering::apply_gas_cost};

pub fn execution_result_import_obj(store: &mut Store, vm_context: &FunctionEnv<VmContext>) -> Function {
fn execution_result(env: FunctionEnvMut<'_, VmContext>, result_ptr: WasmPtr<u8>, result_length: i32) -> Result<()> {
fn execution_result(
mut env: FunctionEnvMut<'_, VmContext>,
result_ptr: WasmPtr<u8>,
result_length: i32,
) -> Result<()> {
apply_gas_cost(
crate::metering::ExternalCallType::ExecutionResult(result_length as u64),
&mut env,
)?;

let ctx = env.data();
let memory = ctx.memory_view(&env);

Expand Down
9 changes: 7 additions & 2 deletions runtime/core/src/core_vm_imports/keccak256.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
use sha3::{Digest, Keccak256};
use wasmer::{Function, FunctionEnv, FunctionEnvMut, Store, WasmPtr};

use crate::{errors::Result, VmContext};
use crate::{errors::Result, metering::apply_gas_cost, VmContext};

pub fn keccak256_import_obj(store: &mut Store, vm_context: &FunctionEnv<VmContext>) -> Function {
fn keccak256(env: FunctionEnvMut<'_, VmContext>, message_ptr: WasmPtr<u8>, message_length: u32) -> Result<u32> {
fn keccak256(mut env: FunctionEnvMut<'_, VmContext>, message_ptr: WasmPtr<u8>, message_length: u32) -> Result<u32> {
apply_gas_cost(
crate::metering::ExternalCallType::Keccak256(message_length as u64),
&mut env,
)?;

let ctx = env.data();
let memory = ctx.memory_view(&env);

Expand Down
9 changes: 7 additions & 2 deletions runtime/core/src/core_vm_imports/secp256_k1.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use k256::ecdsa::{signature::hazmat::PrehashVerifier, Signature, VerifyingKey};
use sha3::{Digest, Keccak256};
use wasmer::{Function, FunctionEnv, FunctionEnvMut, Store, WasmPtr};

use crate::{context::VmContext, errors::Result};
use crate::{context::VmContext, errors::Result, metering::apply_gas_cost};

/// Verifies a `Secp256k1` ECDSA signature.
///
Expand All @@ -15,14 +15,19 @@ use crate::{context::VmContext, errors::Result};
/// - u8 (boolean, 1 for true)
pub fn secp256k1_verify_import_obj(store: &mut Store, vm_context: &FunctionEnv<VmContext>) -> Function {
fn secp256k1_verify(
env: FunctionEnvMut<'_, VmContext>,
mut env: FunctionEnvMut<'_, VmContext>,
message: WasmPtr<u8>,
message_length: i64,
signature: WasmPtr<u8>,
signature_length: i32,
public_key: WasmPtr<u8>,
public_key_length: i32,
) -> Result<u8> {
apply_gas_cost(
crate::metering::ExternalCallType::Secp256k1Verify(message_length as u64),
&mut env,
)?;

let ctx = env.data();
let memory = ctx.memory_view(&env);

Expand Down
3 changes: 3 additions & 0 deletions runtime/core/src/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,9 @@ pub enum RuntimeError {

#[error(transparent)]
Ecdsa(#[from] k256::ecdsa::Error),

#[error("Out of gas")]
OutOfGas,
}

impl From<InstantiationError> for RuntimeError {
Expand Down
3 changes: 2 additions & 1 deletion runtime/core/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
mod context;
mod core_vm_imports;
mod errors;
mod metering;

pub mod metering;
mod resources_dir;
mod runtime;
mod runtime_context;
Expand Down
127 changes: 123 additions & 4 deletions runtime/core/src/metering.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,126 @@
use wasmer::wasmparser::Operator;
use wasmer::{wasmparser::Operator, FunctionEnvMut, WASM_PAGE_SIZE};
use wasmer_middlewares::metering::{get_remaining_points, set_remaining_points, MeteringPoints};

use crate::{errors::Result, RuntimeError, VmContext};

pub fn is_accounting(operator: &Operator) -> bool {
matches!(
operator,
Operator::Loop { .. }
| Operator::End
| Operator::If { .. }
| Operator::Else
| Operator::Br { .. }
| Operator::BrTable { .. }
| Operator::BrIf { .. }
| Operator::Call { .. }
| Operator::CallIndirect { .. }
| Operator::Return
| Operator::Throw { .. }
| Operator::ThrowRef
| Operator::Rethrow { .. }
| Operator::Delegate { .. }
| Operator::Catch { .. }
| Operator::ReturnCall { .. }
| Operator::ReturnCallIndirect { .. }
| Operator::BrOnCast { .. }
| Operator::BrOnCastFail { .. }
| Operator::CallRef { .. }
| Operator::ReturnCallRef { .. }
| Operator::BrOnNull { .. }
| Operator::BrOnNonNull { .. }
)
}

const GAS_PER_OPERATION: u64 = 115;
const GAS_ACCOUNTING_MULTIPLIER: u64 = 14;
const GAS_MEMORY_GROW_BASE: u64 = 1_000_000;

// Gas for reading and writing bytes
pub const GAS_PER_BYTE: u64 = 10_000;
const GAS_PER_BYTE_EXECUTION_RESULT: u64 = 10_000_000;

const GAS_HTTP_FETCH_BASE: u64 = 1_000_000_000;
const GAS_BN254_VERIFY_BASE: u64 = 10_000_000;
const GAS_PROXY_HTTP_FETCH_BASE: u64 = 5_000_000_000;
const GAS_SECP256K1_BASE: u64 = 10_000_000;
const GAS_KECCAK256_BASE: u64 = 10_000_000;
pub const GAS_STARTUP: u64 = 5_000_000_000_000;

/// Gas cost for each operator
/// TODO: For now we give everything an equal gas cost, we should expand this
pub fn get_wasm_operation_cost(_operator: &Operator) -> u64 {
1
pub fn get_wasm_operation_gas_cost(operator: &Operator) -> u64 {
if is_accounting(operator) {
return GAS_PER_OPERATION * GAS_ACCOUNTING_MULTIPLIER;
}

match operator {
Operator::MemoryGrow { mem, mem_byte: _ } => {
GAS_MEMORY_GROW_BASE + ((WASM_PAGE_SIZE as u64 * *mem as u64) * GAS_PER_BYTE)
}
_ => GAS_PER_OPERATION,
}
}

#[derive(Debug)]
pub enum ExternalCallType {
/// Takes as argument the bytes length
ExecutionResult(u64),
/// Takes as argument the bytes length
HttpFetchRequest(u64),
/// Takes as argument the bytes length
HttpFetchResponse(u64),
/// Takes as argument the length of the message
Bn254Verify(u64),
/// Takes as argument the bytes length
ProxyHttpFetchRequest(u64),
/// Takes as argument the length of the message
Secp256k1Verify(u64),
/// Takes as argument the length of the message
Keccak256(u64),
}

pub fn check_enough_gas(gas_cost: u64, remaining_gas: u64, gas_limit: u64) -> Result<u64> {
let gas_used = gas_limit - remaining_gas;

if (gas_cost + gas_used) > gas_limit {
return Err(RuntimeError::OutOfGas);
}

Ok(remaining_gas - gas_cost)
}

pub fn apply_gas_cost(external_call_type: ExternalCallType, env: &mut FunctionEnvMut<'_, VmContext>) -> Result<()> {
let context: &VmContext = env.data();
let instance = match &context.instance {
None => Err(RuntimeError::VmHostError(
"Instance on VmContext was not set".to_string(),
)),
Some(v) => Ok(v.clone()),
}?;

if let Some(gas_limit) = context.call_data.gas_limit {
let remaining_gas = match get_remaining_points(env, &instance) {
MeteringPoints::Exhausted => 0,
MeteringPoints::Remaining(remaining_gas) => remaining_gas,
};

let gas_cost = match external_call_type {
ExternalCallType::ExecutionResult(bytes_length) => GAS_PER_BYTE_EXECUTION_RESULT * bytes_length,
ExternalCallType::HttpFetchRequest(bytes_length) => GAS_HTTP_FETCH_BASE + (GAS_PER_BYTE * bytes_length),
ExternalCallType::HttpFetchResponse(bytes_length) => GAS_PER_BYTE * bytes_length,
ExternalCallType::Bn254Verify(bytes_length) => GAS_BN254_VERIFY_BASE + (GAS_PER_BYTE * bytes_length),
ExternalCallType::ProxyHttpFetchRequest(bytes_length) => {
GAS_PROXY_HTTP_FETCH_BASE + (GAS_PER_BYTE * bytes_length)
}
ExternalCallType::Secp256k1Verify(bytes_length) => {
GAS_SECP256K1_BASE + GAS_KECCAK256_BASE + (GAS_PER_BYTE * bytes_length)
}
ExternalCallType::Keccak256(bytes_length) => GAS_KECCAK256_BASE + (GAS_PER_BYTE * bytes_length),
};

let gas_left = check_enough_gas(gas_cost, remaining_gas, gas_limit)?;
set_remaining_points(env, &instance, gas_left);
}

Ok(())
}
30 changes: 23 additions & 7 deletions runtime/core/src/runtime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,15 @@ use std::io::Read;

use seda_runtime_sdk::{ExecutionResult, ExitInfo, VmCallData, VmResult, VmResultStatus};
use wasmer::Instance;
use wasmer_middlewares::metering::{get_remaining_points, MeteringPoints};
use wasmer_middlewares::metering::{get_remaining_points, set_remaining_points, MeteringPoints};
use wasmer_wasix::{Pipe, WasiEnv, WasiRuntimeError};

use crate::{context::VmContext, runtime_context::RuntimeContext, vm_imports::create_wasm_imports};

const MAX_DR_GAS_LIMIT: u64 = 5_000_000_000;
use crate::{
context::VmContext,
metering::{GAS_PER_BYTE, GAS_STARTUP},
runtime_context::RuntimeContext,
vm_imports::create_wasm_imports,
};

fn internal_run_vm(
call_data: VmCallData,
Expand Down Expand Up @@ -38,7 +41,7 @@ fn internal_run_vm(
.finalize(&mut context.wasm_store)
.map_err(|_| VmResultStatus::WasiEnvInitializeFailure)?;

let vm_context = VmContext::create_vm_context(&mut context.wasm_store, wasi_env.env.clone());
let vm_context = VmContext::create_vm_context(&mut context.wasm_store, wasi_env.env.clone(), call_data.clone());

let imports = create_wasm_imports(
&mut context.wasm_store,
Expand All @@ -52,6 +55,8 @@ fn internal_run_vm(
let wasmer_instance = Instance::new(&mut context.wasm_store, &context.wasm_module, &imports)
.map_err(|e| VmResultStatus::FailedToCreateWasmerInstance(e.to_string()))?;

vm_context.as_mut(&mut context.wasm_store).instance = Some(wasmer_instance.clone());

let env_mut = vm_context.as_mut(&mut context.wasm_store);
env_mut.memory = Some(
wasmer_instance
Expand All @@ -71,6 +76,19 @@ fn internal_run_vm(
.get_function(&function_name)
.map_err(|_| VmResultStatus::FailedToGetWASMFn)?;

// Apply arguments gas cost
if let Some(gas_limit) = call_data.gas_limit {
let args_bytes_total = call_data.args.iter().fold(0, |acc, v| acc + v.len());
// Gas startup costs (for spinning up the VM)
let gas_cost = (GAS_PER_BYTE * args_bytes_total as u64) + GAS_STARTUP;

if gas_cost < gas_limit {
set_remaining_points(&mut context.wasm_store, &wasmer_instance, gas_limit - gas_cost);
} else {
set_remaining_points(&mut context.wasm_store, &wasmer_instance, 0);
}
}

let runtime_result = main_func.call(&mut context.wasm_store, &[]);

wasi_env.on_exit(&mut context.wasm_store, None);
Expand All @@ -90,8 +108,6 @@ fn internal_run_vm(
}

let gas_used: u64 = if let Some(gas_limit) = call_data.gas_limit {
let gas_limit = gas_limit.clamp(0, MAX_DR_GAS_LIMIT);

match get_remaining_points(&mut context.wasm_store, &wasmer_instance) {
MeteringPoints::Exhausted => {
stderr.push("Ran out of gas".to_string());
Expand Down
Loading

0 comments on commit 9dafd13

Please sign in to comment.