Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
124 changes: 116 additions & 8 deletions framework/base/src/storage/mappers/address_to_id_mapper.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,123 @@ use crate::{
types::{ManagedAddress, ManagedType},
};

static ID_SUFFIX: &[u8] = b"addrId";
static ADDRESS_SUFFIX: &[u8] = b"addr";
static LAST_ID_SUFFIX: &[u8] = b"lastId";
const ID_SUFFIX: &str = "addrId";
const ADDRESS_SUFFIX: &str = "addr";
const LAST_ID_SUFFIX: &str = "lastId";

static UNKNOWN_ADDR_ERR_MSG: &[u8] = b"Unknown address";
const UNKNOWN_ADDR_ERR_MSG: &str = "Unknown address";

pub type AddressId = u64;
pub const NULL_ID: AddressId = 0;

/// A specialized bidirectional mapper between addresses and auto-incrementing numeric IDs.
///
/// # Storage Layout
///
/// The `AddressToIdMapper` maintains bidirectional mappings with sequential ID assignment:
///
/// 1. **Address to ID mapping**:
/// - `base_key + "addr" + encoded_address` → assigned ID (u64)
///
/// 2. **ID to address mapping**:
/// - `base_key + "addrId" + id` → address
///
/// 3. **ID counter**:
/// - `base_key + "lastId"` → highest assigned ID (for generating new IDs)
///
/// # ID Assignment
///
/// - IDs start from 1 and increment sequentially
/// - `NULL_ID` (0) represents "no ID assigned" or "not found"
/// - Once an ID is assigned, it remains associated with that address until explicitly removed
/// - Removed IDs are not reused (IDs only increment, never decrement)
///
/// # Main Operations
///
/// - **Auto-insert**: `get_id_or_insert(address)` - Gets ID or assigns new one if not exists. O(1).
/// - **Strict insert**: `insert_new(address)` - Assigns ID only if address is new, errors if exists. O(1).
/// - **Lookup ID**: `get_id(address)` - Returns ID for address (0 if not found). O(1).
/// - **Lookup Address**: `get_address(id)` - Returns address for ID. O(1).
/// - **Contains**: `contains_id(id)` - Checks if ID is assigned. O(1).
/// - **Remove by ID**: `remove_by_id(id)` - Removes mapping by ID, returns address. O(1).
/// - **Remove by Address**: `remove_by_address(address)` - Removes mapping by address, returns ID. O(1).
/// - **Counter**: `get_last_id()` - Returns the highest assigned ID.
///
/// # Trade-offs
///
/// - **Pros**: Sequential IDs are compact and predictable; auto-increment simplifies ID management;
/// O(1) bidirectional lookup; no duplicate addresses.
/// - **Cons**: IDs are never reused (gaps after removal); ID overflow at u64::MAX (unlikely but possible);
/// slightly less flexible than generic `BiDiMapper`.
///
/// # Comparison with BiDiMapper
///
/// - **AddressToIdMapper**: Specialized for addresses; auto-incrementing IDs; single type for IDs (u64)
/// - **BiDiMapper**: Generic; manual ID/value assignment; supports any types for both sides
///
/// # Use Cases
///
/// - User registration systems (address → user ID)
/// - Whitelist/participant management with sequential numbering
/// - Address indexing for efficient iteration
/// - Mapping external addresses to internal compact IDs
/// - Any scenario where addresses need numeric identifiers
///
/// # Example
///
/// ```rust
/// # use multiversx_sc::storage::mappers::{StorageMapper, AddressToIdMapper};
/// # use multiversx_sc::types::ManagedAddress;
/// # fn example<SA: multiversx_sc::api::StorageMapperApi>(
/// # addr1: ManagedAddress<SA>,
/// # addr2: ManagedAddress<SA>,
/// # addr3: ManagedAddress<SA>
/// # ) {
/// # let mapper = AddressToIdMapper::<SA>::new(
/// # multiversx_sc::storage::StorageKey::new(&b"users"[..])
/// # );
/// // Auto-assign IDs (get or create)
/// let id1 = mapper.get_id_or_insert(&addr1); // Returns 1
/// let id2 = mapper.get_id_or_insert(&addr2); // Returns 2
/// let id1_again = mapper.get_id_or_insert(&addr1); // Returns 1 (existing)
///
/// assert_eq!(id1, 1);
/// assert_eq!(id2, 2);
/// assert_eq!(id1_again, 1);
/// assert_eq!(mapper.get_last_id(), 2);
///
/// // Strict insert (errors if address already exists)
/// let id3 = mapper.insert_new(&addr3); // Returns 3
/// assert_eq!(id3, 3);
/// // mapper.insert_new(&addr1); // Would error: "Address already registered"
///
/// // Bidirectional lookup
/// assert_eq!(mapper.get_id(&addr1), 1);
/// assert_eq!(mapper.get_address(1), Some(addr1.clone()));
///
/// // Check existence
/// assert!(mapper.contains_id(2));
/// assert_eq!(mapper.get_id(&addr2), 2); // Returns 2
///
/// // Non-zero lookup (errors if not found)
/// let id = mapper.get_id_non_zero(&addr1); // Returns 1
/// assert_eq!(id, 1);
/// // mapper.get_id_non_zero(&unknown_addr); // Would error: "Unknown address"
///
/// // Remove by address
/// let removed_id = mapper.remove_by_address(&addr2);
/// assert_eq!(removed_id, 2);
/// assert_eq!(mapper.get_id(&addr2), 0); // Now returns NULL_ID
/// assert!(!mapper.contains_id(2));
///
/// // Remove by ID
/// let removed_addr = mapper.remove_by_id(1);
/// assert_eq!(removed_addr, Some(addr1.clone()));
/// assert_eq!(mapper.get_id(&addr1), 0);
///
/// // Note: next inserted address gets ID 4, not 2 (IDs never reused)
/// # }
/// ```
pub struct AddressToIdMapper<SA, A = CurrentStorage>
where
SA: StorageMapperApi,
Expand Down Expand Up @@ -73,7 +181,7 @@ where
pub fn get_id_non_zero(&self, address: &ManagedAddress<SA>) -> AddressId {
let id = self.get_id(address);
if id == NULL_ID {
SA::error_api_impl().signal_error(UNKNOWN_ADDR_ERR_MSG);
SA::error_api_impl().signal_error(UNKNOWN_ADDR_ERR_MSG.as_bytes());
}

id
Expand All @@ -91,23 +199,23 @@ where

fn id_to_address_key(&self, id: AddressId) -> StorageKey<SA> {
let mut item_key = self.base_key.clone();
item_key.append_bytes(ID_SUFFIX);
item_key.append_bytes(ID_SUFFIX.as_bytes());
item_key.append_item(&id);

item_key
}

fn address_to_id_key(&self, address: &ManagedAddress<SA>) -> StorageKey<SA> {
let mut item_key = self.base_key.clone();
item_key.append_bytes(ADDRESS_SUFFIX);
item_key.append_bytes(ADDRESS_SUFFIX.as_bytes());
item_key.append_item(address);

item_key
}

fn last_id_key(&self) -> StorageKey<SA> {
let mut item_key = self.base_key.clone();
item_key.append_bytes(LAST_ID_SUFFIX);
item_key.append_bytes(LAST_ID_SUFFIX.as_bytes());

item_key
}
Expand Down
110 changes: 108 additions & 2 deletions framework/base/src/storage/mappers/bi_di_mapper.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,114 @@ const ID_TO_VALUE_SUFFIX: &[u8] = b"_id_to_value";

type Keys<'a, SA, T, A> = unordered_set_mapper::Iter<'a, SA, T, A>;

/// A bi-directional map, from values to ids and viceversa.
/// The mapper is based on UnorderedSetMapper, reason why the remove is done by swap_remove
/// A storage mapper implementing a bidirectional map with one-to-one correspondence between IDs and values.
///
/// # Storage Layout
///
/// The `BiDiMapper` uses two `UnorderedSetMapper` instances and maintains bidirectional lookups:
///
/// 1. **ID set** (via `UnorderedSetMapper`):
/// - `base_key + "_id.len"` → count of IDs
/// - `base_key + "_id.item" + index` → ID at index (1-based)
/// - `base_key + "_id.index" + encoded_id` → index of ID
///
/// 2. **Value set** (via `UnorderedSetMapper`):
/// - `base_key + "_value.len"` → count of values
/// - `base_key + "_value.item" + index` → value at index (1-based)
/// - `base_key + "_value.index" + encoded_value` → index of value
///
/// 3. **Bidirectional mappings**:
/// - `base_key + "_value_to_id" + encoded_value` → corresponding ID
/// - `base_key + "_id_to_value" + encoded_id` → corresponding value
///
/// # Main Operations
///
/// - **Insert**: `insert(id, value)` - Adds bidirectional mapping. O(1). Fails if ID or value already exists.
/// - **Lookup ID**: `get_id(value)` - Retrieves ID for a value. O(1) with one storage read.
/// - **Lookup Value**: `get_value(id)` - Retrieves value for an ID. O(1) with one storage read.
/// - **Contains**: `contains_id(id)` / `contains_value(value)` - Checks existence. O(1).
/// - **Remove by ID**: `remove_by_id(id)` - Removes by ID, clears both directions. O(1).
/// - **Remove by Value**: `remove_by_value(value)` - Removes by value, clears both directions. O(1).
/// - **Batch Remove**: `remove_all_by_ids(iter)` / `remove_all_by_values(iter)` - Removes multiple entries.
/// - **Iteration**: `iter()` - Iterates over (ID, value) pairs; `get_all_ids()` / `get_all_values()` - specific direction.
///
/// # Uniqueness Guarantee
///
/// Both IDs and values must be unique:
/// - Each ID maps to exactly one value
/// - Each value maps to exactly one ID
/// - Inserting a duplicate ID or value fails and returns `false`
///
/// # Trade-offs
///
/// - **Pros**: O(1) bidirectional lookup; enforces one-to-one correspondence; efficient for reverse lookups.
/// - **Cons**: Double storage overhead (two sets + two mappings); removal changes element positions (swap_remove);
/// cannot have duplicate IDs or values; more complex than unidirectional maps.
///
/// # Use Cases
///
/// - Token ID ↔ Token name mappings
/// - User address ↔ Username associations
/// - NFT ID ↔ Metadata hash relationships
/// - Any scenario requiring efficient lookup in both directions
/// - Implementing invertible mappings
///
/// # Example
///
/// ```rust
/// # use multiversx_sc::storage::mappers::{StorageMapper, BiDiMapper};
/// # fn example<SA: multiversx_sc::api::StorageMapperApi>() {
/// # let mut mapper = BiDiMapper::<SA, u32, u64>::new(
/// # multiversx_sc::storage::StorageKey::new(&b"token_mapping"[..])
/// # );
/// // Insert bidirectional mappings
/// assert!(mapper.insert(1, 100));
/// assert!(mapper.insert(2, 200));
/// assert!(mapper.insert(3, 300));
///
/// // Cannot insert duplicate ID or value
/// assert!(!mapper.insert(1, 400)); // ID 1 already exists
/// assert!(!mapper.insert(4, 100)); // Value 100 already exists
///
/// assert_eq!(mapper.len(), 3);
///
/// // Bidirectional lookup
/// assert_eq!(mapper.get_value(&2), 200);
/// assert_eq!(mapper.get_id(&200), 2);
///
/// // Check existence in both directions
/// assert!(mapper.contains_id(&1));
/// assert!(mapper.contains_value(&300));
///
/// // Remove by ID
/// assert!(mapper.remove_by_id(&2));
/// assert!(!mapper.contains_id(&2));
/// assert!(!mapper.contains_value(&200)); // Both directions removed
/// assert_eq!(mapper.len(), 2);
///
/// // Remove by value
/// assert!(mapper.remove_by_value(&100));
/// assert!(!mapper.contains_id(&1));
/// assert!(!mapper.contains_value(&100));
///
/// // Iterate over all mappings
/// for (id, value) in mapper.iter() {
/// // Process each bidirectional pair
/// }
///
/// // Iterate only IDs or values
/// for id in mapper.get_all_ids() {
/// // Process IDs
/// }
/// for value in mapper.get_all_values() {
/// // Process values
/// }
///
/// // Batch removal
/// mapper.remove_all_by_ids(vec![3]);
/// assert!(mapper.is_empty());
/// # }
/// ```
pub struct BiDiMapper<SA, K, V, A = CurrentStorage>
where
SA: StorageMapperApi,
Expand Down
79 changes: 79 additions & 0 deletions framework/base/src/storage/mappers/linked_list_mapper.rs
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,85 @@ impl LinkedListInfo {
}
}

/// A storage mapper implementing a doubly-linked list with efficient insertion and removal at any position.
///
/// # Storage Layout
///
/// The `LinkedListMapper` stores metadata and individual nodes separately:
///
/// 1. **Metadata**:
/// - `base_key + ".info"` → `LinkedListInfo` struct containing:
/// - `len`: number of elements
/// - `front`: node ID of the first element
/// - `back`: node ID of the last element
/// - `new`: counter for generating unique node IDs
///
/// 2. **Nodes**:
/// - `base_key + ".node" + node_id` → `LinkedListNode<T>` containing:
/// - `value`: the stored item
/// - `node_id`: this node's unique ID
/// - `next_id`: ID of the next node (0 if none)
/// - `prev_id`: ID of the previous node (0 if none)
///
/// # Main Operations
///
/// - **Append**: `push_back(item)` - Adds to the end. O(1) with constant storage writes.
/// - **Prepend**: `push_front(item)` - Adds to the beginning. O(1) with constant storage writes.
/// - **Insert**: `push_after(node, item)` / `push_before(node, item)` - Inserts at specific position. O(1).
/// - **Remove**: `pop_front()` / `pop_back()` / `remove_node(node)` - Removes from any position. O(1).
/// - **Access**: `front()` / `back()` / `get_node_by_id(id)` - Retrieves nodes. O(1).
/// - **Iteration**: `iter()` - Traverses from front to back; `iter_from_node_id(id)` - starts from specific node.
///
/// # Trade-offs
///
/// - **Pros**: O(1) insertion/removal at any position; maintains insertion order; efficient for queue/deque patterns.
/// - **Cons**: No random access by index; higher storage overhead per element (stores prev/next pointers);
/// iteration requires following links; removed nodes leave "gaps" in node ID space.
///
/// # Use Cases
///
/// - Task queues where items can be added/removed at any position
/// - Priority management where items move positions frequently
/// - Scenarios requiring both ordered iteration and arbitrary insertion/removal
///
/// # Example
///
/// ```rust
/// # use multiversx_sc::storage::mappers::{StorageMapper, LinkedListMapper};
/// # fn example<SA: multiversx_sc::api::StorageMapperApi>() {
/// # let mut mapper = LinkedListMapper::<SA, u64>::new(
/// # multiversx_sc::storage::StorageKey::new(&b"tasks"[..])
/// # );
/// // Add elements
/// let node1 = mapper.push_back(100);
/// let node2 = mapper.push_back(200);
/// let node3 = mapper.push_back(300);
///
/// assert_eq!(mapper.len(), 3);
/// assert_eq!(mapper.front().unwrap().into_value(), 100);
/// assert_eq!(mapper.back().unwrap().into_value(), 300);
///
/// // Insert in the middle
/// let mut node2_mut = node2.clone();
/// mapper.push_after(&mut node2_mut, 250);
/// assert_eq!(mapper.len(), 4);
///
/// // Remove from front
/// let removed = mapper.pop_front().unwrap();
/// assert_eq!(removed.into_value(), 100);
/// assert_eq!(mapper.len(), 3);
///
/// // Remove specific node
/// mapper.remove_node(&node2);
/// assert_eq!(mapper.len(), 2);
///
/// // Iterate through remaining elements
/// for node in mapper.iter() {
/// let value = node.into_value();
/// // Process value
/// }
/// # }
/// ```
pub struct LinkedListMapper<SA, T, A = CurrentStorage>
where
SA: StorageMapperApi,
Expand Down
Loading