@btc-vision/btc-runtime
Version:
Bitcoin L1 Smart Contract Runtime for OP_NET. Build decentralized applications on Bitcoin using AssemblyScript and WebAssembly. Fully audited.
392 lines (293 loc) • 10.4 kB
Markdown
# Pointers
Pointers are the foundation of OP_NET's storage system. Understanding how they work is essential for building efficient and secure smart contracts.
## What Are Pointers?
A pointer is a `u16` value (0-65535) that identifies a storage slot in your contract. Combined with an optional `u256` sub-pointer, it creates a unique storage key through SHA256 hashing.
```
Storage Key = SHA256(pointer || subPointer)
```
### Pointer Structure Visualization
```mermaid
flowchart LR
subgraph Construction["32-Byte Storage Key Construction"]
A["Pointer<br/>(u16, 2 bytes)<br/>0x0003"]
B["SubPointer<br/>(u256, 30 bytes)<br/>0x00...ABC123"]
Concat["Concatenation (||)"]
Hash["SHA256 Hash"]
Key["Storage Key<br/>(32 bytes)<br/>0x7F3E...9A2D"]
A --> Concat
B --> Concat
Concat --> Hash
Hash --> Key
end
```
### Pointer Byte Layout
```mermaid
flowchart LR
subgraph Input["Input to SHA256 (32 bytes total)"]
P["Pointer<br/>2 bytes<br/>[0-1]"]
S["SubPointer<br/>30 bytes<br/>[2-31]"]
P --> S
end
subgraph Process["Hashing Process"]
Arrow["SHA256 Hash"]
end
subgraph Result["Output"]
Output["Storage Key<br/>32 bytes"]
end
S --> Arrow
Arrow --> Output
```
## Why Pointers?
OP_NET's pointer system provides:
| Benefit | Description |
|---------|-------------|
| **Determinism** | Same inputs always produce same storage keys |
| **Collision Resistance** | SHA256 ensures unique keys |
| **Verifiability** | Storage proofs can be validated off-chain |
| **Explicitness** | Storage layout is explicit and auditable |
## Solidity Comparison
In Solidity, storage slots are assigned implicitly by the compiler. In OP_NET, you explicitly allocate pointers at runtime:
```solidity
// Solidity - Implicit slot assignment
contract Token {
uint256 public totalSupply; // slot 0 (assigned by compiler)
string public name; // slot 1 (assigned by compiler)
mapping(address => uint256) balances; // slot 2 (assigned by compiler)
}
```
```typescript
// OP_NET - Explicit pointer allocation
export class Token extends OP_NET {
private totalSupplyPointer: u16 = Blockchain.nextPointer; // ~0 (allocated at runtime)
private namePointer: u16 = Blockchain.nextPointer; // ~1 (allocated at runtime)
private balancesPointer: u16 = Blockchain.nextPointer; // ~2 (allocated at runtime)
}
```
### Storage Key Hashing Comparison
| Solidity | OP_NET |
|----------|-------|
| Automatic slot assignment | Explicit pointer allocation |
| Slot 0, 1, 2, ... | `Blockchain.nextPointer` |
| `keccak256(key . slot)` | `SHA256(pointer \|\| subPointer)` |
## Allocating Pointers
### Basic Allocation
Use `Blockchain.nextPointer` to get unique pointers:
```typescript
import { Blockchain } from '@btc-vision/btc-runtime/runtime';
@final
export class MyContract extends OP_NET {
// Each call returns a unique, sequential u16
private counterPointer: u16 = Blockchain.nextPointer; // e.g., 0
private ownerPointer: u16 = Blockchain.nextPointer; // e.g., 1
private balancesPointer: u16 = Blockchain.nextPointer; // e.g., 2
}
```
### Pointer Range
```
u16 range: 0 to 65,535
Your contract can have up to 65,536 unique pointer slots.
```
### Important Rules
1. **Call `nextPointer` once per storage slot** - Don't reuse pointers
2. **Allocate at class level** - Pointers should be class properties
3. **Order matters** - Pointers are assigned sequentially
4. **Never hardcode** - Always use `Blockchain.nextPointer`
## Pointer vs Sub-Pointer
### Primary Pointer (u16)
The primary pointer identifies the **type** of storage:
```typescript
private balancesPointer: u16 = Blockchain.nextPointer; // "balances mapping"
private allowancesPointer: u16 = Blockchain.nextPointer; // "allowances mapping"
```
### Sub-Pointer (u256)
The sub-pointer identifies a **specific entry** within that storage type:
```typescript
// balances[address] = amount
// pointer = balancesPointer
// subPointer = address (converted to u256)
const key = SHA256(balancesPointer || addressAsU256);
```
### Visual Example
```
Contract Storage
|
+-- Pointer 0 (totalSupply)
| +-- SubPointer 0 -> u256 value
|
+-- Pointer 1 (name)
| +-- SubPointer 0 -> string value
|
+-- Pointer 2 (balances mapping)
| +-- SubPointer 0xAAA... -> balance of 0xAAA
| +-- SubPointer 0xBBB... -> balance of 0xBBB
| +-- SubPointer 0xCCC... -> balance of 0xCCC
|
+-- Pointer 3 (allowances nested mapping)
+-- SubPointer hash(0xAAA, 0xBBB) -> allowance[AAA][BBB]
+-- SubPointer hash(0xAAA, 0xCCC) -> allowance[AAA][CCC]
```
### Multiple Storage Variables Example
```mermaid
flowchart LR
subgraph Contract["Token Contract"]
Root["Contract Root"]
end
subgraph SimpleValues["Simple Storage (Single Values)"]
P0["Pointer 0: totalSupply<br/>StoredU256"]
P1["Pointer 1: name<br/>StoredString"]
P2["Pointer 2: symbol<br/>StoredString"]
K0["Key: SHA256(0 || 0)<br/>Value: u256"]
K1["Key: SHA256(1 || 0)<br/>Value: 'MyToken'"]
K2["Key: SHA256(2 || 0)<br/>Value: 'MTK'"]
P0 --> K0
P1 --> K1
P2 --> K2
end
subgraph Mappings["Mappings (Multiple Values)"]
P3["Pointer 3: balances<br/>AddressMemoryMap"]
P4["Pointer 4: allowances<br/>MapOfMap"]
K3A["Key: SHA256(3 || 0xAAA...)<br/>Value: 1000"]
K3B["Key: SHA256(3 || 0xBBB...)<br/>Value: 500"]
K3C["Key: SHA256(3 || 0xCCC...)<br/>Value: 250"]
K4A["Key: SHA256(4 || hash(owner,spender1))<br/>Value: 100"]
K4B["Key: SHA256(4 || hash(owner,spender2))<br/>Value: 200"]
P3 --> K3A
P3 --> K3B
P3 --> K3C
P4 --> K4A
P4 --> K4B
end
Root --> P0
Root --> P1
Root --> P2
Root --> P3
Root --> P4
```
## Pointer Patterns
### Simple Value
```typescript
import { EMPTY_POINTER } from '@btc-vision/btc-runtime/runtime';
// Single value storage
private totalSupplyPointer: u16 = Blockchain.nextPointer;
private _totalSupply: StoredU256 = new StoredU256(
this.totalSupplyPointer,
EMPTY_POINTER
);
// Storage key: SHA256(totalSupplyPointer || 0)
```
### Mapping
```typescript
// mapping(address => uint256)
private balancesPointer: u16 = Blockchain.nextPointer;
private balances: AddressMemoryMap = new AddressMemoryMap(this.balancesPointer);
// For balances[0xABC]:
// Storage key: SHA256(balancesPointer || 0xABC)
```
### Nested Mapping
```typescript
// mapping(address => mapping(address => uint256))
private allowancesPointer: u16 = Blockchain.nextPointer;
// For allowances[owner][spender]:
// SubPointer = SHA256(owner || spender)
// Storage key: SHA256(allowancesPointer || SubPointer)
```
### Array
```typescript
// uint256[] holders
private holdersPointer: u16 = Blockchain.nextPointer;
private holders: StoredU256Array = new StoredU256Array(this.holdersPointer);
// For holders[i]:
// Storage key: SHA256(holdersPointer || i)
// For holders.length:
// Storage key: SHA256(holdersPointer || MAX_U256) // Special length slot
```
## Advanced: Manual Key Calculation
For advanced use cases, you can calculate storage keys manually:
```typescript
import { Blockchain, encodePointer } from '@btc-vision/btc-runtime/runtime';
// Generate storage key (32-byte hash)
const pointerHash = encodePointer(pointer, subPointer);
// Direct storage access
const stored = Blockchain.getStorageAt(pointerHash);
const value = u256.fromUint8ArrayBE(stored);
// Write to storage
Blockchain.setStorageAt(pointerHash, value.toUint8Array(true));
```
### encodePointer() Function Flow
```mermaid
flowchart LR
A["pointer: u16<br/>subPointer: u256"] --> B["32-byte buffer<br/>[0-1] = pointer<br/>[2-31] = subPointer"]
B --> C["SHA256"]
C --> D["Storage Key<br/>(32 bytes)"]
```
## Collision Prevention
The SHA256 hashing ensures collision resistance:
```typescript
// Different pointers = different keys
SHA256(0 || 0xABC) != SHA256(1 || 0xABC)
// Different sub-pointers = different keys
SHA256(0 || 0xABC) != SHA256(0 || 0xDEF)
// Even with same total bits
SHA256(0x0001 || 0x00...00) != SHA256(0x0000 || 0x00...01)
```
## Best Practices
### 1. Document Your Pointer Layout
```typescript
/**
* Storage Layout:
* Pointer 0: totalSupply (u256)
* Pointer 1: name (string)
* Pointer 2: symbol (string)
* Pointer 3: decimals (u8)
* Pointer 4: balances (address => u256)
* Pointer 5: allowances (address => address => u256)
* Pointer 6: paused (bool)
*/
export class MyToken extends OP20 {
// ... pointers allocated in this order
}
```
### 2. Group Related Pointers
```typescript
// Token metadata pointers
private namePointer: u16 = Blockchain.nextPointer;
private symbolPointer: u16 = Blockchain.nextPointer;
private decimalsPointer: u16 = Blockchain.nextPointer;
// Balance pointers
private balancesPointer: u16 = Blockchain.nextPointer;
private totalSupplyPointer: u16 = Blockchain.nextPointer;
// Approval pointers
private allowancesPointer: u16 = Blockchain.nextPointer;
```
### 3. Never Reuse Pointers
```typescript
// WRONG: Reusing pointer for different data
private myPointer: u16 = Blockchain.nextPointer;
private valueA: StoredU256 = new StoredU256(this.myPointer, EMPTY_POINTER);
private valueB: StoredU256 = new StoredU256(this.myPointer, EMPTY_POINTER); // BUG!
// CORRECT: Unique pointer for each
private pointerA: u16 = Blockchain.nextPointer;
private pointerB: u16 = Blockchain.nextPointer;
private valueA: StoredU256 = new StoredU256(this.pointerA, EMPTY_POINTER);
private valueB: StoredU256 = new StoredU256(this.pointerB, EMPTY_POINTER);
```
### 4. Understand Inheritance
When extending contracts, parent pointers are allocated first:
```typescript
// OP20 allocates pointers 0-6 internally
export class MyToken extends OP20 {
// Your pointers start after OP20's
private customPointer: u16 = Blockchain.nextPointer; // ~7
}
```
## Debugging Pointers
To debug storage issues:
```typescript
// Log pointer values during development
console.log(`Balance pointer: ${this.balancesPointer}`);
console.log(`Expected key: ${encodePointer(this.balancesPointer, addressAsU256)}`);
```
---
**Navigation:**
- Previous: [Storage System](./storage-system.md)
- Next: [Events](./events.md)