@btc-vision/btc-runtime
Version:
Bitcoin L1 Smart Contract Runtime for OP_NET. Build decentralized applications on Bitcoin using AssemblyScript and WebAssembly. Fully audited.
732 lines (573 loc) • 19.8 kB
Markdown
# Storage API Reference
Storage classes provide persistent state management for OP_NET smart contracts.
## Import
```typescript
import {
StoredU256,
StoredU64,
StoredU32,
StoredBoolean,
StoredString,
StoredAddress,
StoredU256Array,
StoredAddressArray,
AddressMemoryMap,
StoredMapU256,
Blockchain,
EMPTY_POINTER,
} from '@btc-vision/btc-runtime/runtime';
```
> **Important:** Do NOT use AssemblyScript's built-in Map for blockchain storage. See [CRITICAL: Map Implementation Warning](../storage/stored-maps.md#critical-map-implementation-warning) for details.
## Storage Type Hierarchy
The following diagram shows the complete hierarchy of storage types available in the runtime:
```mermaid
graph LR
A[Storage Types]
subgraph "Primitive Types"
B1[StoredU256<br/>StoredU64<br/>StoredU32]
B2[StoredBoolean<br/>StoredString<br/>StoredAddress]
end
subgraph "Array Types"
C1[StoredU256Array<br/>StoredU64Array]
C2[StoredU32Array<br/>StoredAddressArray]
C3[StoredBooleanArray]
end
subgraph "Map Types"
D1[AddressMemoryMap<br/>Address -> u256]
D2[StoredMapU256<br/>u256 -> u256]
D3[MapOfMap<br/>Nested maps]
end
subgraph "Backend Storage"
E1[encodePointer<br/>Generate hash]
E2[getStorageAt<br/>Read value]
E3[setStorageAt<br/>Write value]
end
A --> B1 & B2
A --> C1 & C2 & C3
A --> D1 & D2 & D3
B1 & B2 --> E1
C1 & C2 & C3 --> E1
D1 & D2 & D3 --> E1
E1 --> E2 & E3
```
```mermaid
classDiagram
class StoredU256 {
-pointer: u16
-subPointer: Uint8Array
+get value() u256
+set value(v: u256)
}
class StoredString {
-pointer: u16
-index: u64
+get value() string
+set value(v: string)
}
class AddressMemoryMap {
-pointer: u16
+get(key: Address) u256
+set(key: Address, value: u256)
+has(key: Address) bool
+delete(key: Address) bool
}
class StoredU256Array {
-pointer: u16
+getLength() u32
+push(value: u256) u32
+shift() u256
+get(index: u32) u256
+set(index: u32, value: u256)
+save()
}
class MapOfMap {
-pointer: u16
+get(key: Address) Nested~u256~
+set(key: Address, value: Nested~u256~)
+has(key: Address) bool
+delete(key: Address) bool
}
class Blockchain {
+getStorageAt(hash: Uint8Array) Uint8Array
+setStorageAt(hash: Uint8Array, value: Uint8Array)
+encodePointer(pointer: u16, subPointer: Uint8Array) Uint8Array
}
StoredU256 ..> Blockchain
StoredString ..> Blockchain
AddressMemoryMap ..> Blockchain
StoredU256Array ..> Blockchain
MapOfMap ..> Blockchain
note for Blockchain "All storage types use\nBlockchain as backend"
```
## Primitive Storage
### StoredU256
Stores a 256-bit unsigned integer.
```typescript
class StoredU256 {
constructor(pointer: u16, subPointer: Uint8Array)
public get value(): u256
public set value(v: u256)
public get toBytes(): Uint8Array
public toString(): string
public set(value: u256): this
public add(value: u256): this // operator +
public sub(value: u256): this // operator -
public mul(value: u256): this // operator *
public addNoCommit(value: u256): this
public subNoCommit(value: u256): this
public commit(): this
public toUint8Array(): Uint8Array
}
```
```typescript
private balancePointer: u16 = Blockchain.nextPointer;
private _balance: StoredU256 = new StoredU256(this.balancePointer, EMPTY_POINTER);
// Usage
this._balance.value = u256.fromU64(1000);
const balance = this._balance.value;
```
The following sequence diagram shows how storage read and write operations work:
```mermaid
sequenceDiagram
participant Contract
participant Storage as Storage Object
participant Encode as encodePointer
participant BC as Blockchain
Note over Contract,BC: Write Operation
Contract->>Contract: private balance: StoredU256
Contract->>Contract: balance.value = u256.fromU64(1000)
Storage->>Encode: encodePointer(pointer, subPointer)
Encode->>Encode: SHA-256(pointer + subPointer)
Encode->>Storage: Return 32-byte hash
Storage->>Storage: value.toUint8Array(true)
Storage->>BC: setStorageAt(hash, bytes)
BC->>BC: Write to persistent storage
Note over Contract,BC: Read Operation
Contract->>Contract: const amount = balance.value
Storage->>Encode: encodePointer(pointer, subPointer)
Encode->>Storage: Return 32-byte hash
Storage->>BC: getStorageAt(hash)
BC->>Storage: Return 32-byte value
Storage->>Storage: u256.fromUint8ArrayBE(bytes)
Storage->>Contract: Return u256 value
```
### StoredU64
Stores up to four 64-bit unsigned integers within a single u256 storage slot.
```typescript
class StoredU64 {
constructor(pointer: u16, subPointer: Uint8Array)
public get(index: u8): u64 // index 0-3
public set(index: u8, value: u64): void
public save(): void
public getAll(): u64[]
public setMultiple(values: u64[]): void
public reset(): void
public toString(): string
}
```
```typescript
private timestampPointer: u16 = Blockchain.nextPointer;
private _timestamps: StoredU64 = new StoredU64(this.timestampPointer, EMPTY_POINTER);
// Usage - stores up to 4 u64 values in one storage slot
this._timestamps.set(0, Blockchain.block.medianTime); // First u64
this._timestamps.set(1, someOtherTimestamp); // Second u64
this._timestamps.save(); // Commit to storage
const firstTimestamp = this._timestamps.get(0);
```
### StoredU32
Stores up to eight 32-bit unsigned integers within a single u256 storage slot.
```typescript
class StoredU32 {
constructor(pointer: u16, subPointer: Uint8Array)
public get(index: u8): u32 // index 0-7
public set(index: u8, value: u32): void
public save(): void
public getAll(): u32[]
public setMultiple(values: u32[]): void
public reset(): void
public toString(): string
}
```
```typescript
private configPointer: u16 = Blockchain.nextPointer;
private _config: StoredU32 = new StoredU32(this.configPointer, EMPTY_POINTER);
// Usage - stores up to 8 u32 values in one storage slot
this._config.set(0, 100); // First u32
this._config.set(1, 200); // Second u32
this._config.save(); // Commit to storage
const firstValue = this._config.get(0);
```
### StoredBoolean
Stores a boolean value.
```typescript
class StoredBoolean {
constructor(pointer: u16, defaultValue: bool)
public get value(): bool
public set value(v: bool)
public toUint8Array(): Uint8Array
}
```
```typescript
private pausedPointer: u16 = Blockchain.nextPointer;
private _paused: StoredBoolean = new StoredBoolean(this.pausedPointer, false);
// Usage
this._paused.value = true;
if (this._paused.value) {
throw new Revert('Contract is paused');
}
```
### StoredString
Stores a string value.
```typescript
class StoredString {
constructor(pointer: u16, index: u64 = 0)
public get value(): string
public set value(v: string)
}
```
```typescript
private namePointer: u16 = Blockchain.nextPointer;
private _name: StoredString = new StoredString(this.namePointer, 0);
// Usage
this._name.value = 'My Token';
const name = this._name.value;
```
### StoredAddress
Stores an Address value. Default value is Address.zero().
```typescript
class StoredAddress {
constructor(pointer: u16)
public get value(): Address
public set value(v: Address)
public isDead(): bool // Note: checks if address equals Address.zero(), not ExtendedAddress.dead()
}
```
```typescript
private ownerPointer: u16 = Blockchain.nextPointer;
private _owner: StoredAddress = new StoredAddress(this.ownerPointer);
// Usage
this._owner.value = Blockchain.tx.origin;
const owner = this._owner.value;
```
## Array Storage
### StoredU256Array
Dynamic array of u256 values. Elements are packed into 32-byte storage slots.
```typescript
class StoredU256Array {
constructor(pointer: u16, subPointer: Uint8Array, maxLength: u32 = DEFAULT_MAX_LENGTH)
public getLength(): u32
public push(value: u256, isPhysical?: bool): u32
public deleteLast(): void
public delete(index: u32): void
public shift(): u256
public get(index: u32): u256
public set(index: u32, value: u256): void
public getAll(startIndex: u32, count: u32): u256[]
public setMultiple(startIndex: u32, values: u256[]): void
public save(): void
public reset(): void
public deleteAll(): void
public startingIndex(): u32
public setStartingIndex(index: u32): void
}
```
```typescript
private tokenIdsPointer: u16 = Blockchain.nextPointer;
private tokenIds: StoredU256Array = new StoredU256Array(this.tokenIdsPointer, EMPTY_POINTER);
// Usage
this.tokenIds.push(u256.fromU64(1));
this.tokenIds.push(u256.fromU64(2));
this.tokenIds.save(); // Commit changes to storage
const first = this.tokenIds.get(0); // u256.fromU64(1)
const len = this.tokenIds.getLength(); // 2
```
The following diagram shows the array operation flow:
```mermaid
flowchart LR
A[StoredU256Array] --> B{Operation<br/>Type}
subgraph "push Operation"
B -->|push| C[Get current length]
C --> D[Encode pointer<br/>with index]
D --> E[Write value at index]
E --> F[Increment length]
end
subgraph "get Operation"
B -->|get| G[Validate<br/>index < length]
G --> H[Encode pointer<br/>with index]
H --> I[Read value<br/>from storage]
I --> J[Return u256]
end
subgraph "shift Operation"
B -->|shift| K[Validate<br/>length > 0]
K --> L[Read first element]
L --> M[Decrement length]
M --> N[Return value]
end
subgraph "set Operation"
B -->|set| O[Validate<br/>index < length]
O --> P[Encode pointer<br/>with index]
P --> Q[Write new value]
end
```
### StoredAddressArray
Dynamic array of Address values. Each address takes one 32-byte storage slot.
```typescript
class StoredAddressArray {
constructor(pointer: u16, subPointer: Uint8Array, maxLength: u32 = DEFAULT_MAX_LENGTH)
public getLength(): u32
public push(value: Address, isPhysical?: bool): u32
public deleteLast(): void
public delete(index: u32): void
public shift(): Address
public get(index: u32): Address
public set(index: u32, value: Address): void
public getAll(startIndex: u32, count: u32): Address[]
public setMultiple(startIndex: u32, values: Address[]): void
public save(): void
public reset(): void
public deleteAll(): void
public startingIndex(): u32
public setStartingIndex(index: u32): void
}
```
```typescript
private oraclesPointer: u16 = Blockchain.nextPointer;
private oracles: StoredAddressArray = new StoredAddressArray(this.oraclesPointer, EMPTY_POINTER);
// Add oracle
this.oracles.push(oracleAddress);
this.oracles.save(); // Commit changes
// Iterate
for (let i: u32 = 0; i < this.oracles.getLength(); i++) {
const oracle = this.oracles.get(i);
// Process oracle
}
```
### Other Array Types
All array types share the same base API as `StoredU256Array` (extending `StoredPackedArray<T>`) with their respective element types:
- `StoredU128Array` - 2 u128 values per 32-byte slot
- `StoredU64Array` - 4 u64 values per 32-byte slot
- `StoredU32Array` - 8 u32 values per 32-byte slot
- `StoredU16Array` - 16 u16 values per 32-byte slot
- `StoredU8Array` - 32 u8 values per 32-byte slot
- `StoredBooleanArray` - 256 boolean values per 32-byte slot (bit-packed)
> **Note:** These are array types only. There are no standalone `StoredU128`, `StoredU16`, or `StoredU8` primitive classes. For storing single small values, use `StoredU64` (which packs 4 u64 values) or `StoredU32` (which packs 8 u32 values) in a single storage slot.
## Map Storage
### AddressMemoryMap
Maps addresses to u256 values. Always returns u256.Zero for unset addresses.
```typescript
class AddressMemoryMap {
constructor(pointer: u16)
public get(key: Address): u256
public set(key: Address, value: u256): this
public getAsUint8Array(key: Address): Uint8Array
public setAsUint8Array(key: Address, value: Uint8Array): this
public has(key: Address): bool
public delete(key: Address): bool
}
```
The following diagram shows how map keys are converted to storage hashes:
```mermaid
flowchart LR
subgraph "Key to Hash"
A[AddressMemoryMap] --> B[Address Key]
B --> C[encodePointer<br/>pointer, address.toBytes]
C --> D[32-byte storage hash]
end
subgraph "Operations"
D --> E{get or set?}
E -->|get| F[Blockchain.getStorageAt<br/>hash]
F --> G[Convert to u256]
G --> H[Return value<br/>or u256.Zero]
E -->|set| I[value.toUint8Array]
I --> J[Blockchain.setStorageAt<br/>hash, bytes]
end
```
#### Usage Example
```typescript
// mapping(address => uint256)
private balancesPointer: u16 = Blockchain.nextPointer;
private balances: AddressMemoryMap;
constructor() {
super();
this.balances = new AddressMemoryMap(this.balancesPointer);
}
// Usage
const balance = this.balances.get(userAddress); // Returns u256
this.balances.set(userAddress, u256.fromU64(1000));
```
The following sequence diagram shows the complete balance mapping flow:
```mermaid
sequenceDiagram
participant Contract
participant Map as AddressMemoryMap
participant BC as Blockchain
Note over Contract,BC: Balance Mapping Example
Contract->>Contract: balances = new AddressMemoryMap(pointer)
Note over Contract,BC: Set Balance
Contract->>Map: balances.set(userAddress, u256.fromU64(1000))
Map->>Map: hash = encodePointer(pointer, userAddress.toBytes())
Map->>BC: setStorageAt(hash, 1000.toUint8Array())
BC->>Map: Storage updated
Note over Contract,BC: Get Balance
Contract->>Map: balances.get(userAddress)
Map->>Map: hash = encodePointer(pointer, userAddress.toBytes())
Map->>BC: getStorageAt(hash)
BC->>Map: Return bytes
Map->>Map: u256.fromUint8ArrayBE(bytes)
Map->>Contract: Return u256.fromU64(1000)
Note over Contract,BC: Get Non-Existent Balance
Contract->>Map: balances.get(unknownAddress)
Map->>BC: getStorageAt(hash)
BC->>Map: Return zeros
Map->>Contract: Return u256.Zero
```
### StoredMapU256
Maps u256 keys to u256 values.
```typescript
class StoredMapU256 {
constructor(pointer: u16, subPointer: Uint8Array = new Uint8Array(30))
public get(key: u256): u256
public set(key: u256, value: u256): void
public delete(key: u256): void // Sets value to zero
}
```
Note: `StoredMapU256` does not have a `has()` method. To check if a key exists, compare the returned value with `u256.Zero`.
```typescript
private dataPointer: u16 = Blockchain.nextPointer;
private data: StoredMapU256 = new StoredMapU256(this.dataPointer);
// Usage
const key = u256.fromU64(123);
this.data.set(key, u256.fromU64(456));
const value = this.data.get(key);
```
### Nested Maps (MapOfMap)
For allowances pattern (owner => spender => amount):
```typescript
class MapOfMap<T> {
constructor(pointer: u16)
public get(key: Address): Nested<T>
public set(key: Address, value: Nested<T>): this
public has(key: Address): bool
public delete(key: Address): bool
public clear(): void
}
class Nested<T> {
constructor(parent: Uint8Array, pointer: u16)
public get(key: Uint8Array): T
public set(key: Uint8Array, value: T): this
public has(key: Uint8Array): bool
}
```
> **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.
See [Stored Maps - MapOfMap](../storage/stored-maps.md#mapofmap) for detailed usage patterns and diagrams.
## Storage Patterns
### Lazy Initialization
```typescript
private _data: StoredU256 | null = null;
private dataPointer: u16 = Blockchain.nextPointer;
private get data(): StoredU256 {
if (!this._data) {
this._data = new StoredU256(this.dataPointer, EMPTY_POINTER);
}
return this._data;
}
```
### Computed Storage Keys
```typescript
import { encodePointer } from '@btc-vision/btc-runtime/runtime';
private storagePointer: u16 = Blockchain.nextPointer;
private getKey(addr: Address, slot: u256): u256 {
const combined = new Uint8Array(64);
combined.set(addr.toBytes(), 0);
combined.set(slot.toUint8Array(), 32);
return u256.fromBytes(Blockchain.sha256(combined));
}
public getValue(addr: Address, slot: u256): u256 {
const key = this.getKey(addr, slot);
const pointerHash = encodePointer(this.storagePointer, key.toUint8Array(true));
const stored = Blockchain.getStorageAt(pointerHash);
return u256.fromUint8ArrayBE(stored);
}
```
### Counter Pattern
```typescript
private counterPointer: u16 = Blockchain.nextPointer;
private _counter: StoredU256 = new StoredU256(this.counterPointer, EMPTY_POINTER);
public getNextId(): u256 {
const current = this._counter.value;
this._counter.value = SafeMath.add(current, u256.One);
return current;
}
```
## Low-Level Storage
Direct storage access via Blockchain:
```typescript
import { encodePointer } from '@btc-vision/btc-runtime/runtime';
// Generate storage key hash
public encodePointer(pointer: u16, subPointer: Uint8Array): Uint8Array
// Write to storage
public setStorageAt(pointerHash: Uint8Array, value: Uint8Array): void
// Read from storage
public getStorageAt(pointerHash: Uint8Array): Uint8Array
// Check existence
public hasStorageAt(pointerHash: Uint8Array): bool
```
```typescript
// Direct storage example
const pointer: u16 = Blockchain.nextPointer;
const subPointer = u256.fromU64(1).toUint8Array(true);
// Create storage key
const pointerHash = encodePointer(pointer, subPointer);
// Write value
Blockchain.setStorageAt(pointerHash, u256.fromU64(100).toUint8Array(true));
// Read value
const stored = Blockchain.getStorageAt(pointerHash);
const value = u256.fromUint8ArrayBE(stored);
```
## Best Practices
### 1. Declare Pointers First
```typescript
class MyContract extends OP_NET {
// Declare all pointers before any stored values
private ptr1: u16 = Blockchain.nextPointer;
private ptr2: u16 = Blockchain.nextPointer;
private ptr3: u16 = Blockchain.nextPointer;
// Then declare stored values
private _value1: StoredU256;
private _value2: StoredBoolean;
public constructor() {
super();
this._value1 = new StoredU256(this.ptr1, EMPTY_POINTER);
this._value2 = new StoredBoolean(this.ptr2, false);
}
}
```
### 2. Use Appropriate Types
```typescript
// Good - use smallest type that fits
private _count: StoredU32; // If count < 4 billion
// Less efficient
private _count: StoredU256; // Uses more storage
```
### 3. Batch Updates
```typescript
// Multiple related updates in one call
public updateBoth(a: u256, b: u256): void {
this._valueA.value = a;
this._valueB.value = b;
}
```
## Solidity Comparison
| Solidity | OP_NET Storage |
|----------|---------------|
| `uint256 public value` | `StoredU256` |
| `mapping(address => uint256)` | `AddressMemoryMap` |
| `mapping(address => mapping(address => uint256))` | `MapOfMap<u256>` |
| `uint256[] public array` | `StoredU256Array` |
| `bool public paused` | `StoredBoolean` |
| `string public name` | `StoredString` |
| `address public owner` | `StoredAddress` |
---
**Navigation:**
- Previous: [SafeMath API](./safe-math.md)
- Next: [Events API](./events.md)