diff --git a/.cargo/audit.toml b/.cargo/audit.toml index 06b356b6b..178a113ba 100644 --- a/.cargo/audit.toml +++ b/.cargo/audit.toml @@ -2,4 +2,7 @@ ignore = [ "RUSTSEC-2020-0071", # `chrono` is a transient dependency used by `near-sandbox-utils` and other dependencies "RUSTSEC-2022-0093", # security audit error in ed25519-dalek which is not relevant for near-sdk-rs + "RUSTSEC-2024-0344", # security audit error in ed25519-dalek which is not relevant for near-sdk-rs + "RUSTSEC-2021-0145", # this needs to be investigated, `atty` has not been updated for 4 years + "RUSTSEC-2022-0054", # unmaintained, perhaps needs to be replaced ] diff --git a/examples/adder/Cargo.toml b/examples/adder/Cargo.toml index 2de440f46..2e65c9995 100644 --- a/examples/adder/Cargo.toml +++ b/examples/adder/Cargo.toml @@ -11,7 +11,7 @@ crate-type = ["cdylib"] near-sdk = { path = "../../near-sdk" } [dev-dependencies] -near-workspaces = { version = "0.9.0", default-features = false, features = ["install"] } +near-workspaces = "0.10.1" tokio = { version = "1.14", features = ["full"] } anyhow = "1.0" near-abi = "0.4.0" diff --git a/examples/callback-results/Cargo.toml b/examples/callback-results/Cargo.toml index a10388d80..dda4f26d6 100644 --- a/examples/callback-results/Cargo.toml +++ b/examples/callback-results/Cargo.toml @@ -11,7 +11,7 @@ crate-type = ["cdylib"] near-sdk = { path = "../../near-sdk" } [dev-dependencies] -near-workspaces = { version = "0.9.0", default-features = false, features = ["install"] } +near-workspaces = "0.10.1" tokio = { version = "1.14", features = ["full"] } anyhow = "1.0" diff --git a/examples/cross-contract-calls/Cargo.toml b/examples/cross-contract-calls/Cargo.toml index 55f749977..6272c3d21 100644 --- a/examples/cross-contract-calls/Cargo.toml +++ b/examples/cross-contract-calls/Cargo.toml @@ -9,7 +9,7 @@ anyhow = "1.0" near-sdk = { path = "../../near-sdk" } test-case = "2.0" tokio = { version = "1.14", features = ["full"] } -near-workspaces = { version = "0.9.0", default-features = false, features = ["install"] } +near-workspaces = "0.10.1" cross-contract-high-level = { path = "./high-level" } cross-contract-low-level = { path = "./low-level" } diff --git a/examples/factory-contract/Cargo.toml b/examples/factory-contract/Cargo.toml index d5090a8c1..8b7693e13 100644 --- a/examples/factory-contract/Cargo.toml +++ b/examples/factory-contract/Cargo.toml @@ -8,7 +8,7 @@ edition = "2021" anyhow = "1.0" test-case = "2.0" tokio = { version = "1.14", features = ["full"] } -near-workspaces = { version = "0.9.0", default-features = false, features = ["install"] } +near-workspaces = "0.10.1" [profile.release] codegen-units = 1 diff --git a/examples/fungible-token/Cargo.toml b/examples/fungible-token/Cargo.toml index 33bf5162b..e1fbc0ced 100644 --- a/examples/fungible-token/Cargo.toml +++ b/examples/fungible-token/Cargo.toml @@ -8,7 +8,7 @@ edition = "2021" anyhow = "1.0" near-sdk = { path = "../../near-sdk", features = ["unit-testing"] } tokio = { version = "1.14", features = ["full"] } -near-workspaces = { version = "0.9.0", default-features = false, features = ["install"] } +near-workspaces = "0.10.1" [profile.release] codegen-units = 1 diff --git a/examples/lockable-fungible-token/Cargo.toml b/examples/lockable-fungible-token/Cargo.toml index 5b455b985..fda831735 100644 --- a/examples/lockable-fungible-token/Cargo.toml +++ b/examples/lockable-fungible-token/Cargo.toml @@ -14,7 +14,7 @@ near-sdk = { path = "../../near-sdk", features = ["legacy"] } anyhow = "1.0" tokio = { version = "1.14", features = ["full"] } near-sdk = { path = "../../near-sdk", features = ["unit-testing"] } -near-workspaces = { version = "0.9.0", default-features = false, features = ["install"] } +near-workspaces = "0.10.1" [profile.release] codegen-units = 1 diff --git a/examples/non-fungible-token/Cargo.toml b/examples/non-fungible-token/Cargo.toml index 7458178ae..cb9ead1f4 100644 --- a/examples/non-fungible-token/Cargo.toml +++ b/examples/non-fungible-token/Cargo.toml @@ -9,7 +9,7 @@ anyhow = "1.0" near-contract-standards = { path = "../../near-contract-standards" } near-sdk = { path = "../../near-sdk" } tokio = { version = "1.14", features = ["full"] } -near-workspaces = { version = "0.9.0", default-features = false, features = ["install"] } +near-workspaces = "0.10.1" [profile.release] codegen-units = 1 diff --git a/near-sdk/Cargo.toml b/near-sdk/Cargo.toml index 9d08b5d4c..203449124 100644 --- a/near-sdk/Cargo.toml +++ b/near-sdk/Cargo.toml @@ -63,6 +63,11 @@ rand_chacha = "0.3.1" near-rng = "0.1.1" near-abi = { version = "0.4.0", features = ["__chunked-entries"] } symbolic-debuginfo = "12" +near-workspaces = { version = "0.10.1", features = ["unstable"] } +anyhow = "1.0" +tokio = { version = "1", features = ["full"] } +strum = "0.25.0" +strum_macros = "0.25.3" [features] default = ["wee_alloc"] diff --git a/near-sdk/tests/store_performance_tests.rs b/near-sdk/tests/store_performance_tests.rs new file mode 100644 index 000000000..194b4ef05 --- /dev/null +++ b/near-sdk/tests/store_performance_tests.rs @@ -0,0 +1,414 @@ +// As wasm VM performance is tested, there is no need to test this on other types of OS. +// This test runs only on Linux, as it's much slower on OS X due to an interpreted VM. +#![cfg(target_os = "linux")] + +use near_account_id::AccountId; +use near_gas::NearGas; +use near_workspaces::network::Sandbox; +use near_workspaces::types::{KeyType, SecretKey}; +use near_workspaces::{Account, Worker}; +use rand::Rng; +use serde::{Deserialize, Serialize}; +use std::sync::Arc; +use strum_macros::Display; + +const DEFAULT_INDEX_OFFSET: usize = 0; + +#[derive(Serialize, Deserialize, Display, Copy, Clone, PartialEq, Eq, Hash)] +#[serde(crate = "near_sdk::serde")] +pub enum Collection { + IterableSet, + IterableMap, + UnorderedSet, + UnorderedMap, + LookupMap, + LookupSet, + TreeMap, + Vector, +} + +fn random_account_id(collection: Collection, seed: &str) -> AccountId { + let mut rng = rand::thread_rng(); + let random_num = rng.gen_range(10000000000000usize..99999999999999); + let account_id = format!( + "dev-{}-{}-{}-{}", + random_num, + seed, + random_num, + collection.to_string().to_lowercase() + ); + let account_id: AccountId = + account_id.try_into().expect("could not convert dev account into AccountId"); + + account_id +} +async fn dev_generate( + worker: Arc>, + collection: Collection, + seed: String, +) -> anyhow::Result<(Account, Collection)> { + let id = random_account_id(collection, &seed); + let sk = SecretKey::from_seed(KeyType::ED25519, &seed); + let account = worker.create_tla(id.clone(), sk).await?; + Ok((account.into_result()?, collection)) +} + +async fn setup_worker() -> anyhow::Result<(Arc>, AccountId)> { + let worker = Arc::new(near_workspaces::sandbox().await?); + let wasm = near_workspaces::compile_project("./tests/test-contracts/store").await?; + let contract = worker.dev_deploy(&wasm).await?; + let res = contract.call("new").max_gas().transact().await?; + assert!(res.is_success()); + Ok((worker, contract.id().clone())) +} + +fn perform_asserts(total_gas: u64, col: &Collection) { + // Constraints a bit relaxed to account for binary differences due to on-demand compilation. + assert!( + total_gas < NearGas::from_tgas(110).as_gas(), + "performance regression {}: {}", + col, + NearGas::from_gas(total_gas) + ); + assert!( + total_gas > NearGas::from_tgas(90).as_gas(), + "not enough gas consumed {}: {}, adjust the number of iterations to spot regressions", + col, + NearGas::from_gas(total_gas) + ); +} + +#[allow(unused)] +async fn setup_several(num: usize) -> anyhow::Result<(Vec, AccountId)> { + let (worker, contract_id) = setup_worker().await?; + let mut accounts = Vec::new(); + + for acc_seed in 0..num { + let (account, _) = + dev_generate(worker.clone(), Collection::IterableSet, acc_seed.to_string()).await?; + accounts.push(account); + } + + Ok((accounts, contract_id)) +} + +async fn setup() -> anyhow::Result<(Account, AccountId)> { + let (worker, contract_id) = setup_worker().await?; + + let (account, _) = + dev_generate(worker.clone(), Collection::IterableSet, "seed".to_string()).await?; + + Ok((account, contract_id)) +} + +#[tokio::test] +async fn insert_and_remove() -> anyhow::Result<()> { + let collection_types = &[ + Collection::TreeMap, + Collection::IterableSet, + Collection::IterableMap, + Collection::UnorderedSet, + Collection::UnorderedMap, + Collection::LookupMap, + Collection::LookupSet, + Collection::Vector, + ]; + + let (account, contract_id) = setup().await?; + // insert test, max_iterations here is the number of elements to insert. It's used to measure + // relative performance. + for (col, max_iterations) in collection_types.map(|col| match col { + Collection::TreeMap => (col, 340), + Collection::IterableSet => (col, 340), + Collection::IterableMap => (col, 350), + Collection::UnorderedSet => (col, 340), + Collection::UnorderedMap => (col, 350), + Collection::LookupMap => (col, 600), + Collection::LookupSet => (col, 970), + Collection::Vector => (col, 1000), + }) { + let total_gas = account + .call(&contract_id, "insert") + .args_json((col, DEFAULT_INDEX_OFFSET, max_iterations)) + .max_gas() + .transact() + .await? + .unwrap() + .total_gas_burnt + .as_gas(); + + perform_asserts(total_gas, &col); + } + + // remove test, max_iterations here is the number of elements to remove. It's used to measure + // relative performance. + for (col, max_iterations) in collection_types.map(|col| match col { + Collection::TreeMap => (col, 220), + Collection::IterableSet => (col, 120), + Collection::IterableMap => (col, 115), + Collection::UnorderedSet => (col, 220), + Collection::UnorderedMap => (col, 220), + Collection::LookupMap => (col, 480), + Collection::LookupSet => (col, 970), + Collection::Vector => (col, 500), + }) { + let total_gas = account + .call(&contract_id, "remove") + .args_json((col, max_iterations)) + .max_gas() + .transact() + .await? + .unwrap() + .total_gas_burnt + .as_gas(); + + perform_asserts(total_gas, &col); + } + + Ok(()) +} + +#[tokio::test] +async fn iter() -> anyhow::Result<()> { + // LookupMap and LookupSet are not iterable. + let collection_types = &[ + Collection::TreeMap, + Collection::IterableSet, + Collection::IterableMap, + Collection::UnorderedSet, + Collection::UnorderedMap, + Collection::Vector, + ]; + + let element_number = 100; + let (account, contract_id) = setup().await?; + + // pre-populate + for col in collection_types { + account + .call(&contract_id, "insert") + .args_json((col, DEFAULT_INDEX_OFFSET, element_number)) + .max_gas() + .transact() + .await? + .unwrap(); + } + + // iter, repeat here is the number that reflects how many times the iterator is consumed fully. + // It's used to measure relative performance. + for (col, repeat) in collection_types.map(|col| match col { + Collection::TreeMap => (col, 5), + Collection::IterableSet => (col, 20), + Collection::IterableMap => (col, 9), + Collection::UnorderedSet => (col, 18), + Collection::UnorderedMap => (col, 8), + Collection::Vector => (col, 19), + _ => (col, 0), + }) { + let total_gas = account + .call(&contract_id.clone(), "iter") + .args_json((col, repeat, element_number)) + .max_gas() + .transact() + .await? + .unwrap() + .total_gas_burnt + .as_gas(); + + perform_asserts(total_gas, &col); + } + + Ok(()) +} + +#[tokio::test] +async fn random_access() -> anyhow::Result<()> { + // LookupMap and LookupSet are not iterable. + let collection_types = &[ + Collection::TreeMap, + Collection::IterableSet, + Collection::IterableMap, + Collection::UnorderedSet, + Collection::UnorderedMap, + Collection::Vector, + ]; + let element_number = 100; + let (account, contract_id) = setup().await?; + + // pre-populate + for col in collection_types { + account + .call(&contract_id, "insert") + .args_json((col, DEFAULT_INDEX_OFFSET, element_number)) + .max_gas() + .transact() + .await? + .unwrap(); + } + + // iter, repeat here is the number that reflects how many times we retrieve a random element. + // It's used to measure relative performance. + for (col, repeat) in collection_types.map(|col| match col { + Collection::TreeMap => (col, 14), + Collection::IterableSet => (col, 1600), + Collection::IterableMap => (col, 720), + Collection::UnorderedSet => (col, 37), + Collection::UnorderedMap => (col, 33), + Collection::Vector => (col, 1600), + _ => (col, 0), + }) { + let total_gas = account + .call(&contract_id.clone(), "nth") + .args_json((col, repeat, element_number)) + .max_gas() + .transact() + .await? + .unwrap() + .total_gas_burnt + .as_gas(); + + perform_asserts(total_gas, &col); + } + + Ok(()) +} + +#[tokio::test] +async fn contains() -> anyhow::Result<()> { + // Vector does not implement contains. + let collection_types = &[ + Collection::TreeMap, + Collection::IterableSet, + Collection::IterableMap, + Collection::UnorderedSet, + Collection::UnorderedMap, + Collection::LookupMap, + Collection::LookupSet, + ]; + // Each collection gets the same number of elements. + let element_number = 100; + let (account, contract_id) = setup().await?; + + // prepopulate + for col in collection_types { + account + .call(&contract_id, "insert") + .args_json((col, DEFAULT_INDEX_OFFSET, element_number)) + .max_gas() + .transact() + .await? + .unwrap(); + } + + // contains test, repeat here is the number of times we check all the elements in each collection. + // It's used to measure relative performance. + for (col, repeat) in collection_types.map(|col| match col { + Collection::TreeMap => (col, 12), + Collection::IterableSet => (col, 11), + Collection::IterableMap => (col, 12), + Collection::UnorderedSet => (col, 11), + Collection::UnorderedMap => (col, 12), + Collection::LookupMap => (col, 16), + Collection::LookupSet => (col, 14), + _ => (col, 0), + }) { + let total_gas = account + .call(&contract_id.clone(), "contains") + .args_json((col, repeat, element_number)) + .max_gas() + .transact() + .await? + .unwrap() + .total_gas_burnt + .as_gas(); + + perform_asserts(total_gas, &col); + } + + Ok(()) +} + +// This test demonstrates the difference in gas consumption between iterable and unordered collections, +// when most of the elements have been deleted. +#[tokio::test] +async fn iterable_vs_unordered() -> anyhow::Result<()> { + let element_number = 300; + let deleted_element_number = 299; + let (account, contract_id) = setup().await?; + + // We only care about Unordered* and Iterable* collections. + let collection_types = &[ + Collection::UnorderedSet, + Collection::UnorderedMap, + Collection::IterableMap, + Collection::IterableSet, + ]; + + // insert `element_number` elements. + for col in collection_types { + account + .call(&contract_id, "insert") + .args_json((col, DEFAULT_INDEX_OFFSET, element_number)) + .max_gas() + .transact() + .await? + .unwrap(); + } + + // remove `deleted_element_number` elements. This leaves only one element in each collection. + for (col, max_iterations) in &collection_types.map(|col| (col, deleted_element_number)) { + account + .call(&contract_id, "remove") + .args_json((col, max_iterations)) + .max_gas() + .transact() + .await? + .unwrap(); + } + + // iter, repeat here is the number of times we iterate through the whole collection. It's used to + // measure relative performance. + for (col, repeat) in collection_types.map(|col| match col { + Collection::IterableSet => (col, 240000), + Collection::IterableMap => (col, 130000), + Collection::UnorderedSet => (col, 260), + Collection::UnorderedMap => (col, 260), + _ => (col, 0), + }) { + let total_gas = account + .call(&contract_id.clone(), "iter") + .args_json((col, repeat, element_number - deleted_element_number)) + .max_gas() + .transact() + .await? + .unwrap() + .total_gas_burnt + .as_gas(); + + perform_asserts(total_gas, &col); + } + + // random access, repeat here is the number of times we try to access an element in the + // collection. It's used to measure relative performance. + for (col, repeat) in &collection_types.map(|col| match col { + Collection::IterableSet => (col, 540000), + Collection::IterableMap => (col, 260000), + Collection::UnorderedSet => (col, 255), + Collection::UnorderedMap => (col, 255), + _ => (col, 0), + }) { + let total_gas = account + .call(&contract_id.clone(), "nth") + .args_json((col, repeat, element_number - deleted_element_number)) + .max_gas() + .transact() + .await? + .unwrap() + .total_gas_burnt + .as_gas(); + + perform_asserts(total_gas, col); + } + + Ok(()) +} diff --git a/near-sdk/tests/test-contracts/store/Cargo.toml b/near-sdk/tests/test-contracts/store/Cargo.toml new file mode 100644 index 000000000..cfb1c1934 --- /dev/null +++ b/near-sdk/tests/test-contracts/store/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "store" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +near-sdk = { path = "../../../../near-sdk", features = ["default", "unstable"]} + +[workspace] diff --git a/near-sdk/tests/test-contracts/store/src/lib.rs b/near-sdk/tests/test-contracts/store/src/lib.rs new file mode 100644 index 000000000..d3d7f172d --- /dev/null +++ b/near-sdk/tests/test-contracts/store/src/lib.rs @@ -0,0 +1,265 @@ +#![allow(deprecated)] + +use near_sdk::borsh::{BorshDeserialize, BorshSerialize}; +use near_sdk::serde::{Deserialize, Serialize}; +use near_sdk::{near, store, PanicOnDefault}; +use Collection::*; + +#[derive(BorshSerialize, BorshDeserialize, Ord, PartialOrd, Eq, PartialEq, Clone)] +#[borsh(crate = "near_sdk::borsh")] +pub struct Insertable { + pub index: u32, + pub data: String, + pub is_valid: bool, +} + +#[near(contract_state)] +#[derive(PanicOnDefault)] +// This contract is designed for testing all of the `store` collections. +pub struct StoreContract { + pub iterable_set: store::IterableSet, + pub iterable_map: store::IterableMap, + pub unordered_set: store::UnorderedSet, + pub unordered_map: store::UnorderedMap, + pub tree_map: store::TreeMap, + pub lookup_map: store::LookupMap, + pub lookup_set: store::LookupSet, + pub vec: store::Vector, +} + +#[derive(Serialize, Deserialize)] +#[serde(crate = "near_sdk::serde")] +pub enum Collection { + IterableSet, + IterableMap, + UnorderedSet, + UnorderedMap, + LookupMap, + LookupSet, + TreeMap, + Vector, +} + +#[near] +impl StoreContract { + #[init] + pub fn new() -> Self { + let vec = store::Vector::new(b"1"); + let iterable_set = store::IterableSet::new(b"2"); + let iterable_map = store::IterableMap::new(b"3"); + let unordered_set = store::UnorderedSet::new(b"4"); + let unordered_map = store::UnorderedMap::new(b"5"); + let tree_map = store::TreeMap::new(b"6"); + let lookup_map = store::LookupMap::new(b"7"); + let lookup_set = store::LookupSet::new(b"8"); + + Self { + vec, + iterable_set, + iterable_map, + unordered_set, + unordered_map, + tree_map, + lookup_map, + lookup_set, + } + } + + fn insertable(&self) -> Insertable { + Insertable { index: 0, data: "scatter cinnamon wheel useless please rough situate iron eager noise try evolve runway neglect onion".to_string(), is_valid: true } + } + + #[payable] + pub fn insert(&mut self, col: Collection, index_offset: usize, iterations: usize) { + let mut insertable = self.insertable(); + for iter in 0..=iterations { + insertable.index = iter as u32; + insertable.index += index_offset as u32; + self.insert_op(&col, insertable.clone()) + } + } + + #[payable] + pub fn remove(&mut self, col: Collection, iterations: usize) { + let mut insertable = self.insertable(); + for iter in 0..=iterations { + insertable.index = iter as u32; + self.remove_op(&col, &insertable) + } + } + + #[payable] + pub fn contains(&mut self, col: Collection, repeat: usize, iterations: usize) { + let mut insertable = self.insertable(); + for iter in 0..=iterations { + insertable.index = iter as u32; + for _ in 0..repeat { + self.contains_op(&col, &insertable) + } + } + } + + #[payable] + pub fn iter(&mut self, col: Collection, repeat: usize, iterations: usize) { + for _ in 0..=iterations { + for _ in 0..repeat { + self.iter_op(&col, iterations) + } + } + } + + #[payable] + pub fn nth(&mut self, col: Collection, repeat: usize, iterations: usize) { + for iter in 0..=iterations { + for _ in 0..repeat { + self.nth_op(&col, iter) + } + } + } + + fn insert_op(&mut self, col: &Collection, val: Insertable) { + match col { + IterableSet => { + self.iterable_set.insert(val); + } + IterableMap => { + self.iterable_map.insert(val.index, val); + } + UnorderedMap => { + self.unordered_map.insert(val.index, val); + } + UnorderedSet => { + self.unordered_set.insert(val); + } + LookupMap => { + self.lookup_map.insert(val.index, val); + } + LookupSet => { + self.lookup_set.insert(val); + } + TreeMap => { + self.tree_map.insert(val.index, val); + } + Vector => { + self.vec.push(val); + } + }; + } + + fn remove_op(&mut self, col: &Collection, val: &Insertable) { + match col { + IterableSet => { + self.iterable_set.remove(&val); + } + IterableMap => { + self.iterable_map.remove(&val.index); + } + UnorderedMap => { + self.unordered_map.remove(&val.index); + } + UnorderedSet => { + self.unordered_set.remove(&val); + } + LookupMap => { + self.lookup_map.remove(&val.index); + } + LookupSet => { + self.lookup_set.remove(&val); + } + TreeMap => { + self.tree_map.remove(&val.index); + } + Vector => { + if self.vec.is_empty() { + return; + } + // Take the opportunity to take swap and pop. + self.vec.swap_remove(self.vec.len() - 1); + } + }; + } + + fn contains_op(&mut self, col: &Collection, val: &Insertable) { + match col { + IterableSet => self.iterable_set.contains(val), + IterableMap => self.iterable_map.contains_key(&val.index), + UnorderedMap => self.unordered_map.contains_key(&val.index), + UnorderedSet => self.unordered_set.contains(val), + LookupMap => self.lookup_map.contains_key(&val.index), + LookupSet => self.lookup_set.contains(val), + TreeMap => self.tree_map.contains_key(&val.index), + // no contains method + Vector => unimplemented!(), + }; + } + + fn iter_op(&mut self, col: &Collection, take: usize) { + match col { + IterableSet => { + let mut iter = self.iterable_set.iter(); + for _ in 0..take { + iter.next(); + } + } + IterableMap => { + let mut iter = self.iterable_map.iter(); + for _ in 0..take { + iter.next(); + } + } + UnorderedMap => { + let mut iter = self.unordered_map.iter(); + for _ in 0..take { + iter.next(); + } + } + UnorderedSet => { + let mut iter = self.unordered_set.iter(); + for _ in 0..take { + iter.next(); + } + } + TreeMap => { + let mut iter = self.tree_map.iter(); + for _ in 0..take { + iter.next(); + } + } + Vector => { + let mut iter = self.vec.iter(); + for _ in 0..take { + iter.next(); + } + } + // Lookup* collections are not iterable. + LookupMap => unimplemented!(), + LookupSet => unimplemented!(), + }; + } + + fn nth_op(&mut self, col: &Collection, element_idx: usize) { + match col { + IterableSet => { + self.iterable_set.iter().nth(element_idx); + } + IterableMap => { + self.iterable_map.iter().nth(element_idx); + } + UnorderedMap => { + self.unordered_map.iter().nth(element_idx); + } + UnorderedSet => { + self.unordered_set.iter().nth(element_idx); + } + TreeMap => { + self.tree_map.iter().nth(element_idx); + } + Vector => { + self.vec.iter().nth(element_idx); + } + // Lookup* collections are not iterable. + LookupMap => unimplemented!(), + LookupSet => unimplemented!(), + }; + } +}