@btc-vision/btc-runtime
Version:
Bitcoin L1 Smart Contract Runtime for OP_NET. Build decentralized applications on Bitcoin using AssemblyScript and WebAssembly. Fully audited.
687 lines (542 loc) • 19.9 kB
Markdown
# Stored Maps
Stored maps provide key-value storage on-chain, similar to Solidity's mappings. They support various key and value types with efficient storage access.
## Overview
```typescript
import {
StoredMapU256,
Blockchain,
} from '@btc-vision/btc-runtime/runtime';
import { u256 } from '@btc-vision/as-bignum/assembly';
// Allocate storage pointer
private dataPointer: u16 = Blockchain.nextPointer;
// Create stored map
private data: StoredMapU256;
constructor() {
super();
this.data = new StoredMapU256(this.dataPointer);
}
// Operations
this.data.set(key, value);
const value = this.data.get(key);
const exists = this.data.has(key);
```
## CRITICAL: Map Implementation Warning
> **DO NOT USE AssemblyScript's Built-in Map**
>
> When creating custom map implementations or extending map functionality, you **MUST** use the Map class from `@btc-vision/btc-runtime/runtime`, NOT the built-in AssemblyScript Map.
>
> **Why the AssemblyScript Map is broken for blockchain:**
> - NOT optimized for blockchain storage patterns
> - Does NOT handle Uint8Array buffers as keys correctly
> - Does NOT work properly with Address key comparisons
> - Will cause silent data corruption or key collisions
>
> **CORRECT:**
> ```typescript
> import { Map } from '@btc-vision/btc-runtime/runtime';
>
> export class MyCustomMap<V> extends Map<Address, V> {
> // Your implementation
> }
> ```
>
> **WRONG:**
> ```typescript
> // DO NOT DO THIS - will break!
> const map = new Map<Uint8Array, u256>(); // AssemblyScript Map
> ```
>
> The btc-runtime Map is specifically designed to:
> - Handle Address and Uint8Array key comparisons correctly
> - Optimize for blockchain storage access patterns
> - Support proper serialization for persistent storage
> - Prevent key collisions with custom equality logic
## StoredMapU256
Basic key-value map with `u256` keys and values. Each key-value pair is stored at a unique storage slot computed via SHA256:
```mermaid
---
config:
theme: dark
---
flowchart LR
subgraph instance["StoredMapU256 Instance"]
A["pointer: u16"]
B["Storage Operations"]
end
subgraph kvstore["Key-Value Storage"]
C["key1: u256"] --> D["value1: u256"]
E["key2: u256"] --> F["value2: u256"]
G["key3: u256"] --> H["value3: u256"]
end
subgraph keycalc["Storage Key Calculation (SHA256)"]
I["map.pointer"]
J["user key (u256)"]
K["SHA256(pointer + key)"]
L["32-byte storage key"]
end
A --> I
B --> K
J --> K
K --> L
L --> M[("Blockchain Storage")]
C -.stored at.- M
E -.stored at.- M
G -.stored at.- M
```
```typescript
private balancesPointer: u16 = Blockchain.nextPointer;
private balances: StoredMapU256;
constructor() {
super();
this.balances = new StoredMapU256(this.balancesPointer);
}
// Set value
this.balances.set(userId, balance);
// Get value (returns u256.Zero if not set)
const balance: u256 = this.balances.get(userId);
// Check existence (StoredMapU256 doesn't have has() - compare with zero)
const exists: bool = !this.balances.get(userId).isZero();
// Delete (set to zero)
this.balances.set(userId, u256.Zero);
```
### Storage Layout
```
Map Pointer + SHA256(key) -> value
For key=5, pointer=3:
Storage key = SHA256(3 || 5)
```
## Using Address Keys
For address-keyed mappings, convert address to `u256`:
```typescript
// Address to u256 conversion
private addressToKey(addr: Address): u256 {
const bytes = addr.toBytes();
return u256.fromBytes(bytes);
}
// Get balance for address
public getBalance(addr: Address): u256 {
const key = this.addressToKey(addr);
return this.balances.get(key);
}
// Set balance for address
public setBalance(addr: Address, amount: u256): void {
const key = this.addressToKey(addr);
this.balances.set(key, amount);
}
```
## Nested Maps
For `mapping(key1 => mapping(key2 => value))`:
```typescript
private allowancesPointer: u16 = Blockchain.nextPointer;
private allowances: StoredMapU256;
constructor() {
super();
this.allowances = new StoredMapU256(this.allowancesPointer);
}
// Create composite key
private allowanceKey(owner: Address, spender: Address): u256 {
const ownerBytes = owner.toBytes();
const spenderBytes = spender.toBytes();
// Combine and hash
const combined = new Uint8Array(64);
combined.set(ownerBytes, 0);
combined.set(spenderBytes, 32);
return u256.fromBytes(Blockchain.sha256(combined));
}
// Get allowance
public getAllowance(owner: Address, spender: Address): u256 {
const key = this.allowanceKey(owner, spender);
return this.allowances.get(key);
}
// Set allowance
public setAllowance(owner: Address, spender: Address, amount: u256): void {
const key = this.allowanceKey(owner, spender);
this.allowances.set(key, amount);
}
```
## MapOfMap
For complex nested mappings, use `MapOfMap`. This provides a two-level structure:
```mermaid
---
config:
theme: dark
---
flowchart LR
A["MapOfMap instance<br/>pointer: u16"] --> B["owner1"]
A --> C["owner2"]
A --> D["owner3"]
B --> E1["Nested<u256>"]
C --> E2["Nested<u256>"]
D --> E3["Nested<u256>"]
E1 --> F1["spender1 -> u256"]
E1 --> F2["spender2 -> u256"]
E2 --> F3["spender1 -> u256"]
E3 --> F4["spender3 -> u256"]
```
```typescript
import { MapOfMap, Nested } from '@btc-vision/btc-runtime/runtime';
// mapping(address => mapping(address => uint256))
private allowancesPointer: u16 = Blockchain.nextPointer;
private allowances: MapOfMap<u256>;
constructor() {
super();
this.allowances = new MapOfMap<u256>(this.allowancesPointer);
}
// Get nested value - two-step process
public getAllowance(owner: Address, spender: Address): u256 {
const ownerMap = this.allowances.get(owner); // Returns Nested<u256>
return ownerMap.get(spender); // Returns u256
}
// Set nested value - get, modify, commit back
public setAllowance(owner: Address, spender: Address, amount: u256): void {
const ownerMap = this.allowances.get(owner); // Get the nested map
ownerMap.set(spender, amount); // Modify it
this.allowances.set(owner, ownerMap); // Commit back
}
```
### MapOfMap Get/Set Pattern
> **Important:** `MapOfMap.get(key)` returns a `Nested<T>` object, not the final value. You must call `.get()` on the nested object to retrieve the actual value. Similarly, when setting values, you must retrieve the nested map, modify it, then commit it back to the parent map.
```mermaid
---
config:
theme: dark
---
flowchart LR
A["allowances.get(owner)"] --> B["Returns Nested<u256>"]
B --> C["nested.get(spender)"]
C --> D["Returns u256 value"]
E["allowances.get(owner)"] --> F["nested.set(spender, amount)"]
F --> G["allowances.set(owner, nested)"]
G --> H["Commit to storage"]
```
## Solidity vs OP_NET Comparison
### Quick Reference Table
| Solidity Mapping Type | OP_NET Equivalent | Notes |
|-----------------------|------------------|-------|
| `mapping(uint256 => uint256)` | `StoredMapU256` | u256 keys and values |
| `mapping(address => uint256)` | `AddressMemoryMap` | Recommended for address keys |
| `mapping(address => uint256)` | `StoredMapU256` with `addressToKey()` | Alternative approach |
| `mapping(K => mapping(K2 => V))` | `MapOfMap<V>` | Two-level nesting |
| `mapping(K => mapping(K2 => V))` | `StoredMapU256` with composite key | Hash-based approach |
### Operations Comparison
| Operation | Solidity | OP_NET (StoredMapU256) |
|-----------|----------|----------------------|
| Declare | `mapping(uint256 => uint256) data;` | `private data: StoredMapU256;` |
| Initialize | Automatic | `this.data = new StoredMapU256(this.dataPointer);` |
| Read value | `data[key]` | `data.get(key)` |
| Write value | `data[key] = value;` | `data.set(key, value)` |
| Check exists | `data[key] != 0` | `!data.get(key).isZero()` |
| Delete entry | `delete data[key];` | `data.set(key, u256.Zero)` |
| Default value | `0` | `u256.Zero` |
### Nested Mapping Comparison
| Operation | Solidity | OP_NET (MapOfMap) |
|-----------|----------|------------------|
| Declare | `mapping(address => mapping(address => uint256)) allowances;` | `private allowances: MapOfMap<u256>;` |
| Read nested | `allowances[owner][spender]` | `allowances.get(owner).get(spender)` |
| Write nested | `allowances[owner][spender] = amount;` | `const m = allowances.get(owner); m.set(spender, amount); allowances.set(owner, m);` |
### Address Key Patterns
| Solidity Pattern | OP_NET Equivalent |
|------------------|------------------|
| `mapping(address => uint256) balances;` | `private balances: AddressMemoryMap;` (preferred) |
| `balances[msg.sender]` | `balances.get(Blockchain.tx.sender)` |
| `balances[addr] = x;` | `balances.set(addr, x)` |
| `balances[addr] += amount;` | `balances.set(addr, SafeMath.add(balances.get(addr), amount))` |
### Common Use Cases
| Use Case | Solidity | OP_NET |
|----------|----------|-------|
| Token balances | `mapping(address => uint256) balances;` | `AddressMemoryMap` |
| Approvals | `mapping(address => mapping(address => uint256))` | `MapOfMap<u256>` |
| Nonces | `mapping(address => uint256) nonces;` | `AddressMemoryMap` or `StoredMapU256` |
| Roles/permissions | `mapping(bytes32 => mapping(address => bool))` | `MapOfMap<u256>` with role hash |
| Token metadata | `mapping(uint256 => string)` | `StoredMapU256` with encoded strings |
| Checkpoints | `mapping(address => mapping(uint256 => uint256))` | `MapOfMap<u256>` or composite keys |
For a complete token implementation using these map types, see [Basic Token Example](../examples/basic-token.md).
## Side-by-Side Code Examples
### Simple Key-Value Store
**Solidity:**
```solidity
contract KeyValueStore {
mapping(uint256 => uint256) public data;
function set(uint256 key, uint256 value) external {
data[key] = value;
}
function get(uint256 key) external view returns (uint256) {
return data[key];
}
function remove(uint256 key) external {
delete data[key];
}
function increment(uint256 key) external {
data[key]++;
}
}
```
**OP_NET:**
```typescript
@final
export class KeyValueStore extends OP_NET {
private dataPointer: u16 = Blockchain.nextPointer;
private data: StoredMapU256;
constructor() {
super();
this.data = new StoredMapU256(this.dataPointer);
}
public set(calldata: Calldata): BytesWriter {
const key = calldata.readU256();
const value = calldata.readU256();
this.data.set(key, value);
return new BytesWriter(0);
}
public get(calldata: Calldata): BytesWriter {
const key = calldata.readU256();
const writer = new BytesWriter(32);
writer.writeU256(this.data.get(key));
return writer;
}
public remove(calldata: Calldata): BytesWriter {
const key = calldata.readU256();
this.data.set(key, u256.Zero);
return new BytesWriter(0);
}
public increment(calldata: Calldata): BytesWriter {
const key = calldata.readU256();
this.data.set(key, SafeMath.add(this.data.get(key), u256.One));
return new BytesWriter(0);
}
}
```
### Approval System with Nested Mapping
**Solidity:**
```solidity
contract ApprovalSystem {
mapping(address => mapping(address => uint256)) public allowances;
function approve(address spender, uint256 amount) external {
allowances[msg.sender][spender] = amount;
}
function allowance(address owner, address spender) external view returns (uint256) {
return allowances[owner][spender];
}
function increaseAllowance(address spender, uint256 addedValue) external {
allowances[msg.sender][spender] += addedValue;
}
function decreaseAllowance(address spender, uint256 subtractedValue) external {
uint256 currentAllowance = allowances[msg.sender][spender];
require(currentAllowance >= subtractedValue, "Decreased below zero");
allowances[msg.sender][spender] = currentAllowance - subtractedValue;
}
function spend(address owner, uint256 amount) external {
uint256 currentAllowance = allowances[owner][msg.sender];
require(currentAllowance >= amount, "Insufficient allowance");
allowances[owner][msg.sender] = currentAllowance - amount;
}
}
```
**OP_NET:**
```typescript
@final
export class ApprovalSystem extends OP_NET {
private allowancesPointer: u16 = Blockchain.nextPointer;
private allowances: MapOfMap<u256>;
constructor() {
super();
this.allowances = new MapOfMap<u256>(this.allowancesPointer);
}
public approve(calldata: Calldata): BytesWriter {
const spender = calldata.readAddress();
const amount = calldata.readU256();
const sender = Blockchain.tx.sender;
const senderAllowances = this.allowances.get(sender);
senderAllowances.set(spender, amount);
this.allowances.set(sender, senderAllowances);
return new BytesWriter(0);
}
public allowance(calldata: Calldata): BytesWriter {
const owner = calldata.readAddress();
const spender = calldata.readAddress();
const ownerAllowances = this.allowances.get(owner);
const writer = new BytesWriter(32);
writer.writeU256(ownerAllowances.get(spender));
return writer;
}
public increaseAllowance(calldata: Calldata): BytesWriter {
const spender = calldata.readAddress();
const addedValue = calldata.readU256();
const sender = Blockchain.tx.sender;
const senderAllowances = this.allowances.get(sender);
const current = senderAllowances.get(spender);
senderAllowances.set(spender, SafeMath.add(current, addedValue));
this.allowances.set(sender, senderAllowances);
return new BytesWriter(0);
}
public decreaseAllowance(calldata: Calldata): BytesWriter {
const spender = calldata.readAddress();
const subtractedValue = calldata.readU256();
const sender = Blockchain.tx.sender;
const senderAllowances = this.allowances.get(sender);
const currentAllowance = senderAllowances.get(spender);
if (currentAllowance < subtractedValue) {
throw new Revert('Decreased below zero');
}
senderAllowances.set(spender, SafeMath.sub(currentAllowance, subtractedValue));
this.allowances.set(sender, senderAllowances);
return new BytesWriter(0);
}
public spend(calldata: Calldata): BytesWriter {
const owner = calldata.readAddress();
const amount = calldata.readU256();
const sender = Blockchain.tx.sender;
const ownerAllowances = this.allowances.get(owner);
const currentAllowance = ownerAllowances.get(sender);
if (currentAllowance < amount) {
throw new Revert('Insufficient allowance');
}
ownerAllowances.set(sender, SafeMath.sub(currentAllowance, amount));
this.allowances.set(owner, ownerAllowances);
return new BytesWriter(0);
}
}
```
## Common Patterns
### Counter/Nonce Tracking
```typescript
private noncesPointer: u16 = Blockchain.nextPointer;
private nonces: StoredMapU256;
constructor() {
super();
this.nonces = new StoredMapU256(this.noncesPointer);
}
public getNonce(addr: Address): u256 {
return this.nonces.get(this.addressKey(addr));
}
public incrementNonce(addr: Address): u256 {
const key = this.addressKey(addr);
const current = this.nonces.get(key);
const next = SafeMath.add(current, u256.One);
this.nonces.set(key, next);
return current; // Return old nonce
}
private addressKey(addr: Address): u256 {
return u256.fromBytes(addr.toBytes());
}
```
### Role Management
```typescript
private rolesPointer: u16 = Blockchain.nextPointer;
private roles: StoredMapU256;
constructor() {
super();
this.roles = new StoredMapU256(this.rolesPointer);
}
private readonly ADMIN_ROLE: u256 = u256.One;
private readonly MINTER_ROLE: u256 = u256.fromU64(2);
public hasRole(addr: Address, role: u256): bool {
const key = this.roleKey(addr, role);
return !this.roles.get(key).isZero();
}
public grantRole(calldata: Calldata): BytesWriter {
this.onlyDeployer(Blockchain.tx.sender);
const addr = calldata.readAddress();
const role = calldata.readU256();
const key = this.roleKey(addr, role);
this.roles.set(key, u256.One);
return new BytesWriter(0);
}
private roleKey(addr: Address, role: u256): u256 {
// Combine address and role into unique key
const bytes = new Uint8Array(64);
bytes.set(addr.toBytes(), 0);
bytes.set(role.toBytes(), 32);
return u256.fromBytes(Blockchain.sha256(bytes));
}
```
### Token Metadata Storage
```typescript
// Store arbitrary metadata per token ID
private metadataPointer: u16 = Blockchain.nextPointer;
private metadata: StoredMapU256;
constructor() {
super();
this.metadata = new StoredMapU256(this.metadataPointer);
}
// Each token can have multiple metadata fields
// Use composite keys: tokenId + fieldId
private readonly FIELD_NAME: u256 = u256.One;
private readonly FIELD_LEVEL: u256 = u256.fromU64(2);
private readonly FIELD_RARITY: u256 = u256.fromU64(3);
public getMetadata(tokenId: u256, field: u256): u256 {
const key = this.metadataKey(tokenId, field);
return this.metadata.get(key);
}
public setMetadata(tokenId: u256, field: u256, value: u256): void {
const key = this.metadataKey(tokenId, field);
this.metadata.set(key, value);
}
private metadataKey(tokenId: u256, field: u256): u256 {
const bytes = new Uint8Array(64);
bytes.set(tokenId.toBytes(), 0);
bytes.set(field.toBytes(), 32);
return u256.fromBytes(Blockchain.sha256(bytes));
}
```
### Snapshot/Checkpoint Pattern
```typescript
// Store values at specific block numbers
private checkpointsPointer: u16 = Blockchain.nextPointer;
private checkpoints: StoredMapU256;
constructor() {
super();
this.checkpoints = new StoredMapU256(this.checkpointsPointer);
}
public checkpoint(addr: Address, value: u256): void {
const blockNumber = Blockchain.block.number;
const key = this.checkpointKey(addr, blockNumber);
this.checkpoints.set(key, value);
}
public getCheckpoint(addr: Address, blockNumber: u64): u256 {
const key = this.checkpointKey(addr, blockNumber);
return this.checkpoints.get(key);
}
private checkpointKey(addr: Address, blockNumber: u64): u256 {
const bytes = new Uint8Array(40);
bytes.set(addr.toBytes(), 0);
// Encode block number in remaining bytes
const blockBytes = new Uint8Array(8);
// ... encode blockNumber
bytes.set(blockBytes, 32);
return u256.fromBytes(Blockchain.sha256(bytes));
}
```
## Best Practices
### 1. Use Consistent Key Functions
```typescript
// Define key functions once
private addressKey(addr: Address): u256 {
return u256.fromBytes(addr.toBytes());
}
// Use consistently throughout contract
const key = this.addressKey(user);
```
### 2. Handle Default Values
```typescript
// Map returns u256.Zero for unset keys
const balance = this.balances.get(key);
// Check if actually set vs zero balance
// Option 1: Use separate "exists" tracking
// Option 2: Use non-zero sentinel for "set"
// Option 3: Accept that zero and unset are equivalent
```
### 3. Document Key Structures
```typescript
/**
* Storage layout:
* - balances: address -> u256
* Key: SHA256(balancesPointer || SHA256(address))
*
* - allowances: (owner, spender) -> u256
* Key: SHA256(allowancesPointer || SHA256(owner || spender))
*/
```
---
**Navigation:**
- Previous: [Stored Arrays](./stored-arrays.md)
- Next: [Memory Maps](./memory-maps.md)