Skip to content
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

Key- and value-only iteration #1834

Merged
merged 12 commits into from
Aug 28, 2023
2 changes: 1 addition & 1 deletion packages/check/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ use cosmwasm_vm::capabilities_from_csv;
use cosmwasm_vm::internals::{check_wasm, compile, make_compiling_engine};

const DEFAULT_AVAILABLE_CAPABILITIES: &str =
"iterator,staking,stargate,cosmwasm_1_1,cosmwasm_1_2,cosmwasm_1_3";
"iterator,staking,stargate,cosmwasm_1_1,cosmwasm_1_2,cosmwasm_1_3,cosmwasm_1_4";
chipshort marked this conversation as resolved.
Show resolved Hide resolved

pub fn main() {
let matches = Command::new("Contract checking")
Expand Down
4 changes: 4 additions & 0 deletions packages/std/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,10 @@ cosmwasm_1_2 = ["cosmwasm_1_1"]
# This feature makes `BankQuery::DenomMetadata` available for the contract to call, but requires
# the host blockchain to run CosmWasm `1.3.0` or higher.
cosmwasm_1_3 = ["cosmwasm_1_2"]
# Together with the `iterator` feature this enables additional imports for more
# efficient iteration over DB keys or values.
# It requires the host blockchain to run CosmWasm `1.4.0` or higher.
cosmwasm_1_4 = ["cosmwasm_1_3"]

[dependencies]
base64 = "0.21.0"
Expand Down
4 changes: 4 additions & 0 deletions packages/std/src/imports.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ extern "C" {
fn db_scan(start_ptr: u32, end_ptr: u32, order: i32) -> u32;
#[cfg(feature = "iterator")]
fn db_next(iterator_id: u32) -> u32;
#[cfg(all(feature = "iterator", feature = "cosmwasm_1_4"))]
fn db_next_key(iterator_id: u32) -> u32;
#[cfg(all(feature = "iterator", feature = "cosmwasm_1_4"))]
fn db_next_value(iterator_id: u32) -> u32;

fn addr_validate(source_ptr: u32) -> u32;
fn addr_canonicalize(source_ptr: u32, destination_ptr: u32) -> u32;
Expand Down
28 changes: 28 additions & 0 deletions packages/vm/src/backend.rs
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,34 @@ pub trait Storage {
#[cfg(feature = "iterator")]
fn next(&mut self, iterator_id: u32) -> BackendResult<Option<Record>>;

/// Returns the next value of the iterator with the given ID.
chipshort marked this conversation as resolved.
Show resolved Hide resolved
/// Since the iterator is incremented, the corresponding key will never be accessible.
///
/// If the ID is not found, a BackendError::IteratorDoesNotExist is returned.
///
/// The default implementation uses [`Storage::next`] and discards the key.
/// More efficient implementations might be possible depending on the storage.
#[cfg(feature = "iterator")]
fn next_value(&mut self, iterator_id: u32) -> BackendResult<Option<Vec<u8>>> {
let (result, gas_info) = self.next(iterator_id);
let result = result.map(|record| record.map(|(_, v)| v));
(result, gas_info)
}

/// Returns the next key of the iterator with the given ID.
chipshort marked this conversation as resolved.
Show resolved Hide resolved
/// Since the iterator is incremented, the corresponding value will never be accessible.
///
/// If the ID is not found, a BackendError::IteratorDoesNotExist is returned.
///
/// The default implementation uses [`Storage::next`] and discards the value.
/// More efficient implementations might be possible depending on the storage.
#[cfg(feature = "iterator")]
fn next_key(&mut self, iterator_id: u32) -> BackendResult<Option<Vec<u8>>> {
let (result, gas_info) = self.next(iterator_id);
let result = result.map(|record| record.map(|(k, _)| k));
(result, gas_info)
}

fn set(&mut self, key: &[u8], value: &[u8]) -> BackendResult<()>;

/// Removes a database entry at `key`.
Expand Down
4 changes: 4 additions & 0 deletions packages/vm/src/compatibility.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ const SUPPORTED_IMPORTS: &[&str] = &[
"env.db_scan",
#[cfg(feature = "iterator")]
"env.db_next",
#[cfg(feature = "iterator")]
"env.db_next_key",
#[cfg(feature = "iterator")]
"env.db_next_value",
];

/// Lists all entry points we expect to be present when calling a contract.
Expand Down
2 changes: 2 additions & 0 deletions packages/vm/src/environment.rs
Original file line number Diff line number Diff line change
Expand Up @@ -493,6 +493,8 @@ mod tests {
"db_remove" => Function::new_typed(&mut store, |_a: u32| {}),
"db_scan" => Function::new_typed(&mut store, |_a: u32, _b: u32, _c: i32| -> u32 { 0 }),
"db_next" => Function::new_typed(&mut store, |_a: u32| -> u32 { 0 }),
"db_next_key" => Function::new_typed(&mut store, |_a: u32| -> u32 { 0 }),
"db_next_value" => Function::new_typed(&mut store, |_a: u32| -> u32 { 0 }),
"query_chain" => Function::new_typed(&mut store, |_a: u32| -> u32 { 0 }),
"addr_validate" => Function::new_typed(&mut store, |_a: u32| -> u32 { 0 }),
"addr_canonicalize" => Function::new_typed(&mut store, |_a: u32, _b: u32| -> u32 { 0 }),
Expand Down
125 changes: 125 additions & 0 deletions packages/vm/src/imports.rs
Original file line number Diff line number Diff line change
Expand Up @@ -533,6 +533,48 @@ pub fn do_db_next<A: BackendApi + 'static, S: Storage + 'static, Q: Querier + 's
write_to_contract(data, &mut store, &out_data)
}

#[cfg(feature = "iterator")]
pub fn do_db_next_key<A: BackendApi + 'static, S: Storage + 'static, Q: Querier + 'static>(
mut env: FunctionEnvMut<Environment<A, S, Q>>,
iterator_id: u32,
) -> VmResult<u32> {
let (data, mut store) = env.data_and_store_mut();

let (result, gas_info) =
data.with_storage_from_context::<_, _>(|store| Ok(store.next_key(iterator_id)))?;

process_gas_info(data, &mut store, gas_info)?;

// Empty key will later be treated as _no more element_.
let key = result?.unwrap_or_default();

write_to_contract(data, &mut store, &key)
}

#[cfg(feature = "iterator")]
pub fn do_db_next_value<A: BackendApi + 'static, S: Storage + 'static, Q: Querier + 'static>(
mut env: FunctionEnvMut<Environment<A, S, Q>>,
iterator_id: u32,
) -> VmResult<u32> {
let (data, mut store) = env.data_and_store_mut();

let (result, gas_info) =
data.with_storage_from_context::<_, _>(|store| Ok(store.next_value(iterator_id)))?;

process_gas_info(data, &mut store, gas_info)?;

// Empty key will later be treated as _no more element_.
// No need to encode this further, as it is a single value.
let (mut value, has_next) = match result? {
chipshort marked this conversation as resolved.
Show resolved Hide resolved
Some(value) => (value, true),
None => (Vec::<u8>::new(), false),
};

// add has_next flag at the end
value.push(has_next as u8);
write_to_contract(data, &mut store, &value)
}

/// Creates a Region in the contract, writes the given data to it and returns the memory location
fn write_to_contract<A: BackendApi + 'static, S: Storage + 'static, Q: Querier + 'static>(
data: &Environment<A, S, Q>,
Expand Down Expand Up @@ -634,6 +676,8 @@ mod tests {
"db_remove" => Function::new_typed(&mut store, |_a: u32| {}),
"db_scan" => Function::new_typed(&mut store, |_a: u32, _b: u32, _c: i32| -> u32 { 0 }),
"db_next" => Function::new_typed(&mut store, |_a: u32| -> u32 { 0 }),
"db_next_key" => Function::new_typed(&mut store, |_a: u32| -> u32 { 0 }),
"db_next_value" => Function::new_typed(&mut store, |_a: u32| -> u32 { 0 }),
"query_chain" => Function::new_typed(&mut store, |_a: u32| -> u32 { 0 }),
"addr_validate" => Function::new_typed(&mut store, |_a: u32| -> u32 { 0 }),
"addr_canonicalize" => Function::new_typed(&mut store, |_a: u32, _b: u32| -> u32 { 0 }),
Expand Down Expand Up @@ -2173,4 +2217,85 @@ mod tests {
e => panic!("Unexpected error: {e:?}"),
}
}

#[test]
#[cfg(feature = "iterator")]
fn do_db_next_key_works() {
let api = MockApi::default();
let (fe, mut store, _instance) = make_instance(api);
let mut fe_mut = fe.into_mut(&mut store);

leave_default_data(&mut fe_mut);

let id = do_db_scan(fe_mut.as_mut(), 0, 0, Order::Ascending.into()).unwrap();

// Entry 1
let key_region_ptr = do_db_next_key(fe_mut.as_mut(), id).unwrap();
assert_eq!(force_read(&mut fe_mut, key_region_ptr), KEY1);

// Entry 2
let key_region_ptr = do_db_next_key(fe_mut.as_mut(), id).unwrap();
assert_eq!(force_read(&mut fe_mut, key_region_ptr), KEY2);

// End
let key_region_ptr: u32 = do_db_next_key(fe_mut.as_mut(), id).unwrap();
assert_eq!(force_read(&mut fe_mut, key_region_ptr), b"");
}

#[test]
#[cfg(feature = "iterator")]
fn do_db_next_value_works() {
let api = MockApi::default();
let (fe, mut store, _instance) = make_instance(api);
let mut fe_mut = fe.into_mut(&mut store);

leave_default_data(&mut fe_mut);

let id = do_db_scan(fe_mut.as_mut(), 0, 0, Order::Ascending.into()).unwrap();

// Entry 1
let value_region_ptr = do_db_next_value(fe_mut.as_mut(), id).unwrap();
assert_eq!(
force_read(&mut fe_mut, value_region_ptr),
[VALUE1, &[1]].concat()
);

// Entry 2
let value_region_ptr = do_db_next_value(fe_mut.as_mut(), id).unwrap();
assert_eq!(
force_read(&mut fe_mut, value_region_ptr),
[VALUE2, &[1]].concat()
);

// End
let value_region_ptr = do_db_next_value(fe_mut.as_mut(), id).unwrap();
assert_eq!(force_read(&mut fe_mut, value_region_ptr), [0]);
}

#[test]
#[cfg(feature = "iterator")]
fn do_db_next_works_mixed() {
let api = MockApi::default();
let (fe, mut store, _instance) = make_instance(api);
let mut fe_mut = fe.into_mut(&mut store);

leave_default_data(&mut fe_mut);

let id = do_db_scan(fe_mut.as_mut(), 0, 0, Order::Ascending.into()).unwrap();

// Key 1
let key_region_ptr = do_db_next_key(fe_mut.as_mut(), id).unwrap();
assert_eq!(force_read(&mut fe_mut, key_region_ptr), KEY1);

// Value 2
let value_region_ptr = do_db_next_value(fe_mut.as_mut(), id).unwrap();
assert_eq!(
force_read(&mut fe_mut, value_region_ptr),
[VALUE2, &[1]].concat()
);

// End
let kv_region_ptr = do_db_next(fe_mut.as_mut(), id).unwrap();
assert_eq!(force_read(&mut fe_mut, kv_region_ptr), b"\0\0\0\0\0\0\0\0");
}
}
23 changes: 22 additions & 1 deletion packages/vm/src/instance.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ use crate::imports::{
do_secp256k1_recover_pubkey, do_secp256k1_verify,
};
#[cfg(feature = "iterator")]
use crate::imports::{do_db_next, do_db_scan};
use crate::imports::{do_db_next, do_db_next_key, do_db_next_value, do_db_scan};
use crate::memory::{read_region, write_region};
use crate::size::Size;
use crate::wasm_backend::{compile, make_compiling_engine};
Expand Down Expand Up @@ -237,6 +237,27 @@ where
Function::new_typed_with_env(&mut store, &fe, do_db_next),
);

// Get next key of iterator with ID `iterator_id`.
// Creates a region containing the key and returns its address.
// Ownership of the result region is transferred to the contract.
// An empty key means no more elements.
chipshort marked this conversation as resolved.
Show resolved Hide resolved
#[cfg(feature = "iterator")]
env_imports.insert(
"db_next_key",
Function::new_typed_with_env(&mut store, &fe, do_db_next_key),
);

// Get next value of iterator with ID `iterator_id`.
// Creates a region containing the value and returns its address.
// Ownership of the result region is transferred to the contract.
// The region uses the format value || has_next (without length encoding),
// where has_next is a bool (encoded as u8) indicating whether there are more elements.
#[cfg(feature = "iterator")]
env_imports.insert(
"db_next_value",
Function::new_typed_with_env(&mut store, &fe, do_db_next_value),
);

import_obj.register_namespace("env", env_imports);

if let Some(extra_imports) = extra_imports {
Expand Down