-
Notifications
You must be signed in to change notification settings - Fork 384
Make gas limit immutable in cosmwasm_vm::instance::Instance
#136
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -88,28 +88,33 @@ where | |
} | ||
|
||
/// get instance returns a wasmer Instance tied to a previously saved wasm | ||
pub fn get_instance(&mut self, id: &[u8], deps: Extern<S, A>) -> Result<Instance<S, A>, Error> { | ||
pub fn get_instance( | ||
&mut self, | ||
id: &[u8], | ||
deps: Extern<S, A>, | ||
gas_limit: u64, | ||
) -> Result<Instance<S, A>, Error> { | ||
let hash = WasmHash::generate(&id); | ||
|
||
// pop from lru cache if present | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The issue with this PR, is that if I pull an Instance off the lru cache, it will have the same gas limit as the first time. Try this, by running some code (with lru), get_instance the first time with a too low gas, and it fails. Then put it back on cache. Then try There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I am working on a dedicated test for the recovering after out of gas now. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done in 201829c |
||
if let Some(cache) = &mut self.instances { | ||
if let Some(cached_instance) = cache.pop(&hash) { | ||
self.stats.hits_instance += 1; | ||
return Ok(Instance::from_wasmer(cached_instance, deps)); | ||
return Ok(Instance::from_wasmer(cached_instance, deps, gas_limit)); | ||
} | ||
} | ||
|
||
// try from the module cache | ||
let res = self.modules.load_with_backend(hash, backend()); | ||
if let Ok(module) = res { | ||
self.stats.hits_module += 1; | ||
return Instance::from_module(&module, deps); | ||
return Instance::from_module(&module, deps, gas_limit); | ||
} | ||
|
||
// fall back to wasm cache (and re-compiling) - this is for backends that don't support serialization | ||
let wasm = self.load_wasm(id)?; | ||
self.stats.misses += 1; | ||
Instance::from_code(&wasm, deps) | ||
Instance::from_code(&wasm, deps, gas_limit) | ||
} | ||
|
||
pub fn store_instance(&mut self, id: &[u8], instance: Instance<S, A>) -> Option<Extern<S, A>> { | ||
|
@@ -135,6 +140,7 @@ mod test { | |
use cosmwasm::mock::{dependencies, mock_env, MockApi, MockStorage}; | ||
use cosmwasm::types::coin; | ||
|
||
static TESTING_GAS_LIMIT: u64 = 400_000; | ||
static CONTRACT_0_7: &[u8] = include_bytes!("../testdata/contract_0.7.wasm"); | ||
|
||
#[test] | ||
|
@@ -170,7 +176,7 @@ mod test { | |
let mut cache = unsafe { CosmCache::new(tmp_dir.path(), 10).unwrap() }; | ||
let id = cache.save_wasm(CONTRACT_0_7).unwrap(); | ||
let deps = dependencies(20); | ||
let _instance = cache.get_instance(&id, deps).unwrap(); | ||
let _instance = cache.get_instance(&id, deps, TESTING_GAS_LIMIT).unwrap(); | ||
assert_eq!(cache.stats.hits_instance, 0); | ||
assert_eq!(cache.stats.hits_module, 1); | ||
assert_eq!(cache.stats.misses, 0); | ||
|
@@ -184,11 +190,11 @@ mod test { | |
let deps1 = dependencies(20); | ||
let deps2 = dependencies(20); | ||
let deps3 = dependencies(20); | ||
let instance1 = cache.get_instance(&id, deps1).unwrap(); | ||
let instance1 = cache.get_instance(&id, deps1, TESTING_GAS_LIMIT).unwrap(); | ||
cache.store_instance(&id, instance1); | ||
let instance2 = cache.get_instance(&id, deps2).unwrap(); | ||
let instance2 = cache.get_instance(&id, deps2, TESTING_GAS_LIMIT).unwrap(); | ||
cache.store_instance(&id, instance2); | ||
let instance3 = cache.get_instance(&id, deps3).unwrap(); | ||
let instance3 = cache.get_instance(&id, deps3, TESTING_GAS_LIMIT).unwrap(); | ||
cache.store_instance(&id, instance3); | ||
assert_eq!(cache.stats.hits_instance, 2); | ||
assert_eq!(cache.stats.hits_module, 1); | ||
|
@@ -201,7 +207,7 @@ mod test { | |
let mut cache = unsafe { CosmCache::new(tmp_dir.path(), 10).unwrap() }; | ||
let id = cache.save_wasm(CONTRACT_0_7).unwrap(); | ||
let deps = dependencies(20); | ||
let mut instance = cache.get_instance(&id, deps).unwrap(); | ||
let mut instance = cache.get_instance(&id, deps, TESTING_GAS_LIMIT).unwrap(); | ||
|
||
// run contract | ||
let env = mock_env(&instance.api, "creator", &coin("1000", "earth"), &[]); | ||
|
@@ -219,7 +225,7 @@ mod test { | |
let mut cache = unsafe { CosmCache::new(tmp_dir.path(), 10).unwrap() }; | ||
let id = cache.save_wasm(CONTRACT_0_7).unwrap(); | ||
let deps = dependencies(20); | ||
let mut instance = cache.get_instance(&id, deps).unwrap(); | ||
let mut instance = cache.get_instance(&id, deps, TESTING_GAS_LIMIT).unwrap(); | ||
|
||
// init contract | ||
let env = mock_env(&instance.api, "creator", &coin("1000", "earth"), &[]); | ||
|
@@ -252,7 +258,7 @@ mod test { | |
let deps2 = dependencies(20); | ||
|
||
// init instance 1 | ||
let mut instance = cache.get_instance(&id, deps1).unwrap(); | ||
let mut instance = cache.get_instance(&id, deps1, TESTING_GAS_LIMIT).unwrap(); | ||
let env = mock_env(&instance.api, "owner1", &coin("1000", "earth"), &[]); | ||
let msg = r#"{"verifier": "sue", "beneficiary": "mary"}"#.as_bytes(); | ||
let res = call_init(&mut instance, &env, msg).unwrap(); | ||
|
@@ -261,7 +267,7 @@ mod test { | |
let deps1 = cache.store_instance(&id, instance).unwrap(); | ||
|
||
// init instance 2 | ||
let mut instance = cache.get_instance(&id, deps2).unwrap(); | ||
let mut instance = cache.get_instance(&id, deps2, TESTING_GAS_LIMIT).unwrap(); | ||
let env = mock_env(&instance.api, "owner2", &coin("500", "earth"), &[]); | ||
let msg = r#"{"verifier": "bob", "beneficiary": "john"}"#.as_bytes(); | ||
let res = call_init(&mut instance, &env, msg).unwrap(); | ||
|
@@ -270,7 +276,7 @@ mod test { | |
let deps2 = cache.store_instance(&id, instance).unwrap(); | ||
|
||
// run contract 2 - just sanity check - results validate in contract unit tests | ||
let mut instance = cache.get_instance(&id, deps2).unwrap(); | ||
let mut instance = cache.get_instance(&id, deps2, TESTING_GAS_LIMIT).unwrap(); | ||
let env = mock_env( | ||
&instance.api, | ||
"bob", | ||
|
@@ -284,7 +290,7 @@ mod test { | |
let _ = cache.store_instance(&id, instance).unwrap(); | ||
|
||
// run contract 1 - just sanity check - results validate in contract unit tests | ||
let mut instance = cache.get_instance(&id, deps1).unwrap(); | ||
let mut instance = cache.get_instance(&id, deps1, TESTING_GAS_LIMIT).unwrap(); | ||
let env = mock_env( | ||
&instance.api, | ||
"sue", | ||
|
@@ -297,4 +303,76 @@ mod test { | |
assert_eq!(1, msgs.len()); | ||
let _ = cache.store_instance(&id, instance); | ||
} | ||
|
||
#[test] | ||
#[cfg(feature = "default-singlepass")] | ||
fn resets_gas_when_reusing_instance() { | ||
let tmp_dir = TempDir::new().unwrap(); | ||
let mut cache = unsafe { CosmCache::new(tmp_dir.path(), 10).unwrap() }; | ||
let id = cache.save_wasm(CONTRACT_0_7).unwrap(); | ||
|
||
let deps1 = dependencies(20); | ||
let deps2 = dependencies(20); | ||
|
||
// Init from module cache | ||
let mut instance1 = cache.get_instance(&id, deps1, TESTING_GAS_LIMIT).unwrap(); | ||
assert_eq!(cache.stats.hits_module, 1); | ||
assert_eq!(cache.stats.hits_instance, 0); | ||
assert_eq!(cache.stats.misses, 0); | ||
let original_gas = instance1.get_gas(); | ||
|
||
// Consume some gas | ||
let env = mock_env(&instance1.api, "owner1", &coin("1000", "earth"), &[]); | ||
let msg = r#"{"verifier": "sue", "beneficiary": "mary"}"#.as_bytes(); | ||
call_init(&mut instance1, &env, msg).unwrap(); | ||
assert!(instance1.get_gas() < original_gas); | ||
cache.store_instance(&id, instance1).unwrap(); | ||
|
||
// Init from instance cache | ||
let instance2 = cache.get_instance(&id, deps2, TESTING_GAS_LIMIT).unwrap(); | ||
assert_eq!(cache.stats.hits_module, 1); | ||
assert_eq!(cache.stats.hits_instance, 1); | ||
assert_eq!(cache.stats.misses, 0); | ||
assert_eq!(instance2.get_gas(), TESTING_GAS_LIMIT); | ||
} | ||
|
||
#[test] | ||
#[cfg(feature = "default-singlepass")] | ||
fn recovers_from_out_of_gas() { | ||
let tmp_dir = TempDir::new().unwrap(); | ||
let mut cache = unsafe { CosmCache::new(tmp_dir.path(), 10).unwrap() }; | ||
let id = cache.save_wasm(CONTRACT_0_7).unwrap(); | ||
|
||
let deps1 = dependencies(20); | ||
let deps2 = dependencies(20); | ||
|
||
// Init from module cache | ||
let mut instance1 = cache.get_instance(&id, deps1, 10).unwrap(); | ||
assert_eq!(cache.stats.hits_module, 1); | ||
assert_eq!(cache.stats.hits_instance, 0); | ||
assert_eq!(cache.stats.misses, 0); | ||
|
||
// Consume some gas. This fails | ||
let env1 = mock_env(&instance1.api, "owner1", &coin("1000", "earth"), &[]); | ||
let msg1 = r#"{"verifier": "sue", "beneficiary": "mary"}"#.as_bytes(); | ||
match call_init(&mut instance1, &env1, msg1) { | ||
Err(Error::RuntimeErr { .. }) => (), // all good, continue | ||
Err(e) => panic!("unexpected error, {:?}", e), | ||
Ok(_) => panic!("call_init must run out of gas"), | ||
} | ||
assert_eq!(instance1.get_gas(), 0); | ||
cache.store_instance(&id, instance1).unwrap(); | ||
|
||
// Init from instance cache | ||
let mut instance2 = cache.get_instance(&id, deps2, TESTING_GAS_LIMIT).unwrap(); | ||
assert_eq!(cache.stats.hits_module, 1); | ||
assert_eq!(cache.stats.hits_instance, 1); | ||
assert_eq!(cache.stats.misses, 0); | ||
assert_eq!(instance2.get_gas(), TESTING_GAS_LIMIT); | ||
|
||
// Now it works | ||
let env2 = mock_env(&instance2.api, "owner2", &coin("500", "earth"), &[]); | ||
let msg2 = r#"{"verifier": "bob", "beneficiary": "john"}"#.as_bytes(); | ||
call_init(&mut instance2, &env2, msg2).unwrap(); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -31,12 +31,12 @@ where | |
S: Storage + 'static, | ||
A: Api + 'static, | ||
{ | ||
pub fn from_code(code: &[u8], deps: Extern<S, A>) -> Result<Self> { | ||
pub fn from_code(code: &[u8], deps: Extern<S, A>, gas_limit: u64) -> Result<Self> { | ||
let module = compile(code)?; | ||
Instance::from_module(&module, deps) | ||
Instance::from_module(&module, deps, gas_limit) | ||
} | ||
|
||
pub fn from_module(module: &Module, deps: Extern<S, A>) -> Result<Self> { | ||
pub fn from_module(module: &Module, deps: Extern<S, A>, gas_limit: u64) -> Result<Self> { | ||
// copy this so it can be moved into the closures, without pulling in deps | ||
let api = deps.api; | ||
let import_obj = imports! { | ||
|
@@ -72,10 +72,15 @@ where | |
}, | ||
}; | ||
let wasmer_instance = module.instantiate(&import_obj).context(WasmerErr {})?; | ||
Ok(Instance::from_wasmer(wasmer_instance, deps)) | ||
Ok(Instance::from_wasmer(wasmer_instance, deps, gas_limit)) | ||
} | ||
|
||
pub fn from_wasmer(wasmer_instance: wasmer_runtime_core::Instance, deps: Extern<S, A>) -> Self { | ||
pub fn from_wasmer( | ||
mut wasmer_instance: wasmer_runtime_core::Instance, | ||
deps: Extern<S, A>, | ||
gas_limit: u64, | ||
) -> Self { | ||
set_gas(&mut wasmer_instance, gas_limit); | ||
let res = Instance { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes. Looks proper |
||
wasmer_instance: wasmer_instance, | ||
api: deps.api, | ||
|
@@ -95,10 +100,6 @@ where | |
get_gas(&self.wasmer_instance) | ||
} | ||
|
||
pub fn set_gas(&mut self, gas: u64) { | ||
set_gas(&mut self.wasmer_instance, gas) | ||
} | ||
|
||
pub fn with_storage<F: FnMut(&mut S)>(&self, func: F) { | ||
with_storage_from_context(self.wasmer_instance.context(), func) | ||
} | ||
|
@@ -145,32 +146,26 @@ where | |
#[cfg(test)] | ||
mod test { | ||
use crate::calls::{call_handle, call_init, call_query}; | ||
use crate::testing::mock_instance; | ||
use crate::testing::{mock_instance, mock_instance_with_gas_limit}; | ||
use cosmwasm::mock::mock_env; | ||
use cosmwasm::types::coin; | ||
|
||
static CONTRACT_0_7: &[u8] = include_bytes!("../testdata/contract_0.7.wasm"); | ||
|
||
#[test] | ||
#[cfg(feature = "default-cranelift")] | ||
fn get_and_set_gas_cranelift_noop() { | ||
let mut instance = mock_instance(&CONTRACT_0_7); | ||
fn set_get_and_gas_cranelift_noop() { | ||
let instance = mock_instance_with_gas_limit(&CONTRACT_0_7, 123321); | ||
let orig_gas = instance.get_gas(); | ||
assert!(orig_gas > 1000); | ||
// this is a no-op | ||
instance.set_gas(123456); | ||
assert_eq!(orig_gas, instance.get_gas()); | ||
assert_eq!(orig_gas, 1_000_000); | ||
} | ||
|
||
#[test] | ||
#[cfg(feature = "default-singlepass")] | ||
fn get_and_set_gas_singlepass_works() { | ||
let mut instance = mock_instance(&CONTRACT_0_7); | ||
fn set_get_and_gas_singlepass_works() { | ||
let instance = mock_instance_with_gas_limit(&CONTRACT_0_7, 123321); | ||
let orig_gas = instance.get_gas(); | ||
assert!(orig_gas > 1000000); | ||
// it is updated to whatever we set it with | ||
instance.set_gas(123456); | ||
assert_eq!(123456, instance.get_gas()); | ||
assert_eq!(orig_gas, 123321); | ||
} | ||
|
||
#[test] | ||
|
@@ -185,8 +180,7 @@ mod test { | |
#[cfg(feature = "default-singlepass")] | ||
fn contract_deducts_gas() { | ||
let mut instance = mock_instance(&CONTRACT_0_7); | ||
let orig_gas = 200_000; | ||
instance.set_gas(orig_gas); | ||
let orig_gas = instance.get_gas(); | ||
|
||
// init contract | ||
let env = mock_env(&instance.api, "creator", &coin("1000", "earth"), &[]); | ||
|
@@ -200,7 +194,7 @@ mod test { | |
assert_eq!(init_used, 52_468); | ||
|
||
// run contract - just sanity check - results validate in contract unit tests | ||
instance.set_gas(orig_gas); | ||
let gas_before_handle = instance.get_gas(); | ||
let env = mock_env( | ||
&instance.api, | ||
"verifies", | ||
|
@@ -212,17 +206,15 @@ mod test { | |
let msgs = res.unwrap().messages; | ||
assert_eq!(1, msgs.len()); | ||
|
||
let handle_used = orig_gas - instance.get_gas(); | ||
let handle_used = gas_before_handle - instance.get_gas(); | ||
println!("handle used: {}", handle_used); | ||
assert_eq!(handle_used, 86_749); | ||
} | ||
|
||
#[test] | ||
#[cfg(feature = "default-singlepass")] | ||
fn contract_enforces_gas_limit() { | ||
let mut instance = mock_instance(&CONTRACT_0_7); | ||
let orig_gas = 20_000; | ||
instance.set_gas(orig_gas); | ||
let mut instance = mock_instance_with_gas_limit(&CONTRACT_0_7, 20_000); | ||
|
||
// init contract | ||
let env = mock_env(&instance.api, "creator", &coin("1000", "earth"), &[]); | ||
|
@@ -235,23 +227,21 @@ mod test { | |
#[cfg(feature = "default-singlepass")] | ||
fn query_works_with_metering() { | ||
let mut instance = mock_instance(&CONTRACT_0_7); | ||
let orig_gas = 200_000; | ||
instance.set_gas(orig_gas); | ||
|
||
// init contract | ||
let env = mock_env(&instance.api, "creator", &coin("1000", "earth"), &[]); | ||
let msg = r#"{"verifier": "verifies", "beneficiary": "benefits"}"#.as_bytes(); | ||
let _res = call_init(&mut instance, &env, msg).unwrap().unwrap(); | ||
|
||
// run contract - just sanity check - results validate in contract unit tests | ||
instance.set_gas(orig_gas); | ||
let gas_before_query = instance.get_gas(); | ||
// we need to encode the key in base64 | ||
let msg = r#"{"verifier":{}}"#.as_bytes(); | ||
let res = call_query(&mut instance, msg).unwrap(); | ||
let answer = res.unwrap(); | ||
assert_eq!(answer.as_slice(), "verifies".as_bytes()); | ||
|
||
let query_used = orig_gas - instance.get_gas(); | ||
let query_used = gas_before_query - instance.get_gas(); | ||
println!("query used: {}", query_used); | ||
assert_eq!(query_used, 44_919); | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Safer for sure
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Without that, I got overflow crashes
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I did also months ago with huge values, but then just used smaller numbers and forgot about it. Good fix