UNPKG

@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
# 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)