@btc-vision/btc-runtime
Version:
Bitcoin L1 Smart Contract Runtime for OP_NET. Build decentralized applications on Bitcoin using AssemblyScript and WebAssembly. Fully audited.
715 lines (575 loc) • 18.8 kB
Markdown
# Stored Arrays
Stored arrays persist ordered collections of values on-chain. They support push, pop, get, set, and length operations with automatic bounds checking.
## Overview
```typescript
import {
StoredU256Array,
StoredU128Array,
StoredU64Array,
StoredAddressArray,
StoredBooleanArray,
Blockchain,
} from '@btc-vision/btc-runtime/runtime';
import { u256 } from '@btc-vision/as-bignum/assembly';
// Allocate storage pointer
private holdersPointer: u16 = Blockchain.nextPointer;
// Create stored array with subPointer
private holders: StoredAddressArray;
constructor() {
super();
this.holders = new StoredAddressArray(this.holdersPointer, EMPTY_POINTER);
}
// Operations
this.holders.push(newAddress);
const first = this.holders.get(0);
const length = this.holders.getLength();
this.holders.deleteLast(); // removes last element
this.holders.save(); // commit changes to storage
```
## Available Types
| Type | Element Type | Description |
|------|-------------|-------------|
| `StoredU256Array` | `u256` | Array of 256-bit unsigned |
| `StoredU128Array` | `u128` | Array of 128-bit unsigned |
| `StoredU64Array` | `u64` | Array of 64-bit unsigned |
| `StoredU32Array` | `u32` | Array of 32-bit unsigned |
| `StoredU16Array` | `u16` | Array of 16-bit unsigned |
| `StoredU8Array` | `u8` | Array of bytes |
| `StoredAddressArray` | `Address` | Array of addresses |
| `StoredBooleanArray` | `bool` | Array of booleans |
## Storage Structure
Arrays use multiple storage slots with sequential subPointers for each element:
```mermaid
flowchart LR
subgraph instance["StoredArray Instance"]
A["pointer: u16<br/>e.g., 0x0005"]
end
subgraph layout["Storage Layout"]
B["Length Slot<br/>pointer base"]
C["Element 0<br/>subPointer = 0"]
D["Element 1<br/>subPointer = 1"]
E["Element 2<br/>subPointer = 2"]
F["Element 3<br/>subPointer = 3"]
G["..."]
end
subgraph keys["Storage Keys (SHA256)"]
H["pointer + 0<br/>-> length: u64"]
I["pointer + 1<br/>-> element 0"]
J["pointer + 2<br/>-> element 1"]
K["pointer + 3<br/>-> element 2"]
end
A --> B
B --> H
C --> I
D --> J
E --> K
H --> L[("Blockchain Storage")]
I --> L
J --> L
K --> L
```
### Size Limits
Maximum array length: **4,294,967,294 elements** (u32.MAX_VALUE - 1), though practical limits depend on gas costs.
```typescript
// Check before adding
if (this.holders.getLength() >= MAX_ALLOWED) {
throw new Revert('Array full');
}
this.holders.push(newHolder);
this.holders.save(); // Don't forget to save!
```
## Operations
### Push
Add element to end. The push operation follows this flow:
```mermaid
---
config:
theme: dark
---
flowchart LR
A["array.push(value)"] --> B["Read length"]
B --> C{"length < 65535?"}
C -->|"No"| D["Throw error"]
C -->|"Yes"| E["Calculate key"]
E --> F["setStorageAt()"]
F --> G["Increment length"]
G --> H["Save length"]
```
```typescript
// Add new element
this.holders.push(newHolder);
this.holders.save(); // Commit changes
// Length increases by 1
const newLength = this.holders.getLength();
```
### deleteLast / shift
Remove elements from the array. `deleteLast()` removes from the end, `shift()` removes from the beginning.
```mermaid
---
config:
theme: dark
---
flowchart LR
A["array.deleteLast()"] --> B["Read length"]
B --> C{"length > 0?"}
C -->|"No"| D["Throw error"]
C -->|"Yes"| E["Zero last element"]
E --> F["Decrement length"]
F --> G["Mark as changed"]
```
```typescript
// Remove last element
this.holders.deleteLast();
this.holders.save(); // Commit changes
// Remove first element and return it
const removed: Address = this.holders.shift();
this.holders.save(); // Commit changes
// Reverts if array is empty
```
### Get
Read element at index:
```typescript
// Get element at index
const holder: Address = this.holders.get(index);
// Reverts if index >= length
```
### Set
Write element at index:
```typescript
// Set element at index
this.holders.set(index, newValue);
this.holders.save(); // Commit changes
// Reverts if index >= MAX_LENGTH
// Can set beyond current length (use push for proper length tracking)
```
### Length
Get current array length:
```typescript
// Get length
const count: u32 = this.holders.getLength();
// Check if empty
if (this.holders.getLength() === 0) {
throw new Revert('No holders');
}
```
## Solidity vs OP_NET Comparison
### Quick Reference Table
| Solidity Array Type | OP_NET Equivalent | Elements per Slot | Default Max |
|---------------------|------------------|-------------------|-------------|
| `uint256[]` | `StoredU256Array` | 1 | u32.MAX_VALUE - 1 |
| `uint128[]` | `StoredU128Array` | 2 | u32.MAX_VALUE - 1 |
| `uint64[]` | `StoredU64Array` | 4 | u32.MAX_VALUE - 1 |
| `uint32[]` | `StoredU32Array` | 8 | u32.MAX_VALUE - 1 |
| `uint16[]` | `StoredU16Array` | 16 | u32.MAX_VALUE - 1 |
| `uint8[]` / `bytes` | `StoredU8Array` | 32 | u32.MAX_VALUE - 1 |
| `address[]` | `StoredAddressArray` | 1 | u32.MAX_VALUE - 1 |
| `bool[]` | `StoredBooleanArray` | 256 (bit-packed) | u32.MAX_VALUE - 1 |
### Operations Comparison
| Operation | Solidity | OP_NET |
|-----------|----------|-------|
| Declare array | `address[] public holders;` | `private holders: StoredAddressArray;` |
| Initialize | Automatic | `this.holders = new StoredAddressArray(this.holdersPointer, EMPTY_POINTER);` |
| Push element | `holders.push(addr);` | `holders.push(addr); holders.save();` |
| Pop element | `holders.pop();` | `holders.deleteLast(); holders.save();` |
| Shift element | N/A | `holders.shift(); holders.save();` |
| Get element | `holders[i]` | `holders.get(i)` |
| Set element | `holders[i] = addr;` | `holders.set(i, addr); holders.save();` |
| Get length | `holders.length` | `holders.getLength()` |
| Delete at index | `delete holders[i];` | `holders.delete(i); holders.save();` |
| Check bounds | Runtime revert | Runtime revert |
| Clear array | `delete holders;` | `holders.deleteAll();` |
| Reset array | N/A | `holders.reset();` |
### Common Patterns
| Pattern | Solidity | OP_NET |
|---------|----------|-------|
| Loop through array | `for (uint i = 0; i < arr.length; i++)` | `for (let i: u32 = 0; i < arr.getLength(); i++)` |
| Remove at index (swap) | `arr[i] = arr[arr.length-1]; arr.pop();` | `arr.set(i, arr.get(arr.getLength()-1)); arr.deleteLast(); arr.save();` |
| Check if empty | `arr.length == 0` | `arr.getLength() === 0` |
| Get last element | `arr[arr.length - 1]` | `arr.get(arr.getLength() - 1)` |
| Initialize with values | `arr = [1, 2, 3];` | Multiple `arr.push()` calls in `onDeployment`, then `arr.save()` |
For complete contract examples using stored arrays, see the [Examples](../examples/) section.
## Side-by-Side Code Examples
### Simple Address List
**Solidity:**
```solidity
contract AddressList {
address[] public addresses;
function add(address addr) external {
addresses.push(addr);
}
function remove(uint256 index) external {
require(index < addresses.length, "Out of bounds");
addresses[index] = addresses[addresses.length - 1];
addresses.pop();
}
function get(uint256 index) external view returns (address) {
return addresses[index];
}
function count() external view returns (uint256) {
return addresses.length;
}
function contains(address addr) external view returns (bool) {
for (uint i = 0; i < addresses.length; i++) {
if (addresses[i] == addr) return true;
}
return false;
}
}
```
**OP_NET:**
```typescript
export class AddressList extends OP_NET {
private addressesPointer: u16 = Blockchain.nextPointer;
private addresses: StoredAddressArray;
constructor() {
super();
this.addresses = new StoredAddressArray(this.addressesPointer, EMPTY_POINTER);
}
public add(calldata: Calldata): BytesWriter {
const addr = calldata.readAddress();
this.addresses.push(addr);
this.addresses.save();
return new BytesWriter(0);
}
public remove(calldata: Calldata): BytesWriter {
const index = calldata.readU32();
const length = this.addresses.getLength();
if (index >= length) {
throw new Revert('Out of bounds');
}
if (index < length - 1) {
this.addresses.set(index, this.addresses.get(length - 1));
}
this.addresses.deleteLast();
this.addresses.save();
return new BytesWriter(0);
}
public get(calldata: Calldata): BytesWriter {
const index = calldata.readU32();
const writer = new BytesWriter(32);
writer.writeAddress(this.addresses.get(index));
return writer;
}
public count(_calldata: Calldata): BytesWriter {
const writer = new BytesWriter(4);
writer.writeU32(this.addresses.getLength());
return writer;
}
public contains(calldata: Calldata): BytesWriter {
const addr = calldata.readAddress();
let found = false;
const length = this.addresses.getLength();
for (let i: u32 = 0; i < length; i++) {
if (this.addresses.get(i).equals(addr)) {
found = true;
break;
}
}
const writer = new BytesWriter(1);
writer.writeBoolean(found);
return writer;
}
}
```
### Value Queue (FIFO-like with array)
**Solidity:**
```solidity
contract ValueQueue {
uint256[] public values;
function enqueue(uint256 value) external {
values.push(value);
}
// Note: This is O(n) - not efficient for large queues
function dequeue() external returns (uint256) {
require(values.length > 0, "Empty queue");
uint256 first = values[0];
for (uint i = 0; i < values.length - 1; i++) {
values[i] = values[i + 1];
}
values.pop();
return first;
}
function peek() external view returns (uint256) {
require(values.length > 0, "Empty queue");
return values[0];
}
function size() external view returns (uint256) {
return values.length;
}
}
```
**OP_NET:**
```typescript
export class ValueQueue extends OP_NET {
private valuesPointer: u16 = Blockchain.nextPointer;
private values: StoredU256Array;
constructor() {
super();
this.values = new StoredU256Array(this.valuesPointer, EMPTY_POINTER);
}
public enqueue(calldata: Calldata): BytesWriter {
const value = calldata.readU256();
this.values.push(value);
this.values.save();
return new BytesWriter(0);
}
// Note: Use shift() for O(1) dequeue - it uses circular buffer with startIndex
public dequeue(_calldata: Calldata): BytesWriter {
const length = this.values.getLength();
if (length === 0) {
throw new Revert('Empty queue');
}
const first = this.values.shift(); // O(1) operation
this.values.save();
const writer = new BytesWriter(32);
writer.writeU256(first);
return writer;
}
public peek(_calldata: Calldata): BytesWriter {
if (this.values.getLength() === 0) {
throw new Revert('Empty queue');
}
const writer = new BytesWriter(32);
writer.writeU256(this.values.get(0));
return writer;
}
public size(_calldata: Calldata): BytesWriter {
const writer = new BytesWriter(4);
writer.writeU32(this.values.getLength());
return writer;
}
}
```
## Common Patterns
### Iterating
```typescript
// Forward iteration
const length = this.holders.getLength();
for (let i: u32 = 0; i < length; i++) {
const holder = this.holders.get(i);
// Process holder...
}
// Batch retrieval for efficiency
const batchSize: u32 = 100;
const allValues = this.values.getAll(0, batchSize);
for (let i = 0; i < allValues.length; i++) {
// Process allValues[i]...
}
```
### Finding Elements
```typescript
// Find index of element
private indexOf(array: StoredAddressArray, target: Address): i32 {
const length = array.getLength();
for (let i: u32 = 0; i < length; i++) {
if (array.get(i).equals(target)) {
return i32(i);
}
}
return -1; // Not found
}
// Check if element exists
private contains(array: StoredAddressArray, target: Address): bool {
return this.indexOf(array, target) >= 0;
}
```
### Removing Elements
```typescript
// Remove at index (swap with last, then deleteLast)
private removeAt(array: StoredAddressArray, index: u32): void {
const length = array.getLength();
if (index >= length) {
throw new Revert('Index out of bounds');
}
// If not last element, swap with last
if (index < length - 1) {
const last = array.get(length - 1);
array.set(index, last);
}
// Remove last element
array.deleteLast();
array.save();
}
// Remove by value
private removeValue(array: StoredAddressArray, value: Address): bool {
const idx = this.indexOf(array, value);
if (idx < 0) {
return false;
}
this.removeAt(array, u32(idx));
return true;
}
// Alternative: Use delete(index) to remove at specific index
// This sets the element to zero but doesn't shift other elements
```
### Unique Elements Set
```typescript
// Add only if not present
private addUnique(array: StoredAddressArray, value: Address): bool {
if (this.contains(array, value)) {
return false; // Already exists
}
array.push(value);
array.save();
return true;
}
```
## Use Cases
### Token Holder Tracking
```typescript
export class Token extends OP20 {
private holdersPointer: u16 = Blockchain.nextPointer;
private holders: StoredAddressArray;
constructor() {
super();
this.holders = new StoredAddressArray(this.holdersPointer, EMPTY_POINTER);
}
public override _transfer(from: Address, to: Address, amount: u256): void {
// Track new holders
if (this.balanceOf(to).isZero() && !amount.isZero()) {
this.holders.push(to);
this.holders.save();
}
super._transfer(from, to, amount);
// Note: Removing holders when balance becomes zero
// requires additional logic (holder index mapping)
}
public getHolderCount(_calldata: Calldata): BytesWriter {
const writer = new BytesWriter(4);
writer.writeU32(this.holders.getLength());
return writer;
}
}
```
### Order Queue
```typescript
export class OrderBook extends OP_NET {
private ordersPointer: u16 = Blockchain.nextPointer;
private orders: StoredU256Array;
constructor() {
super();
this.orders = new StoredU256Array(this.ordersPointer, EMPTY_POINTER);
}
public addOrder(calldata: Calldata): BytesWriter {
const orderId = calldata.readU256();
this.orders.push(orderId);
this.orders.save();
return new BytesWriter(0);
}
public processNextOrder(_calldata: Calldata): BytesWriter {
if (this.orders.getLength() === 0) {
throw new Revert('No orders');
}
// FIFO: Use shift() to get first order (O(1) with circular buffer)
const orderId = this.orders.shift();
this.orders.save();
// Process order...
const writer = new BytesWriter(32);
writer.writeU256(orderId);
return writer;
}
}
```
### Whitelist Management
```typescript
export class Whitelist extends OP_NET {
private addressesPointer: u16 = Blockchain.nextPointer;
private addresses: StoredAddressArray;
constructor() {
super();
this.addresses = new StoredAddressArray(this.addressesPointer, EMPTY_POINTER);
}
public add(calldata: Calldata): BytesWriter {
this.onlyDeployer(Blockchain.tx.sender);
const addr = calldata.readAddress();
// Check not already in list
const length = this.addresses.getLength();
for (let i: u32 = 0; i < length; i++) {
if (this.addresses.get(i).equals(addr)) {
throw new Revert('Already whitelisted');
}
}
this.addresses.push(addr);
this.addresses.save();
return new BytesWriter(0);
}
public isWhitelisted(calldata: Calldata): BytesWriter {
const addr = calldata.readAddress();
let found = false;
const length = this.addresses.getLength();
for (let i: u32 = 0; i < length; i++) {
if (this.addresses.get(i).equals(addr)) {
found = true;
break;
}
}
const writer = new BytesWriter(1);
writer.writeBoolean(found);
return writer;
}
}
```
## Best Practices
### 1. Limit Array Size
```typescript
const MAX_ARRAY_SIZE: u32 = 1000;
public addItem(calldata: Calldata): BytesWriter {
if (this.items.getLength() >= MAX_ARRAY_SIZE) {
throw new Revert('Array size limit reached');
}
this.items.push(calldata.readU256());
this.items.save();
return new BytesWriter(0);
}
```
### 2. Cache Length in Loops
```typescript
// Good: Cache length
const length = this.items.getLength();
for (let i: u32 = 0; i < length; i++) {
// ...
}
// Even better: Use getAll() for batch operations
const items = this.items.getAll(0, 100);
for (let i = 0; i < items.length; i++) {
// Process items[i]
}
```
### 3. Use Maps for Lookup-Heavy Cases
If you frequently check "is X in array?", consider using a map alongside the array:
```typescript
private itemsPointer: u16 = Blockchain.nextPointer;
private itemExistsPointer: u16 = Blockchain.nextPointer;
private items: StoredU256Array;
private itemExists: StoredMapU256;
constructor() {
super();
this.items = new StoredU256Array(this.itemsPointer, EMPTY_POINTER);
this.itemExists = new StoredMapU256(this.itemExistsPointer);
}
public addItem(item: u256): void {
if (!this.itemExists.get(item).isZero()) {
throw new Revert('Already exists');
}
this.items.push(item);
this.items.save();
this.itemExists.set(item, u256.One);
}
```
### 4. Always Call save() After Modifications
```typescript
// Multiple modifications, single save
this.items.push(value1);
this.items.push(value2);
this.items.set(0, value3);
this.items.save(); // Commit all changes at once
```
---
**Navigation:**
- Previous: [Stored Primitives](./stored-primitives.md)
- Next: [Stored Maps](./stored-maps.md)