@btc-vision/btc-runtime
Version:
Bitcoin L1 Smart Contract Runtime for OP_NET. Build decentralized applications on Bitcoin using AssemblyScript and WebAssembly. Fully audited.
609 lines (478 loc) • 16.3 kB
Markdown
# Stored Primitives
Stored primitives are typed wrappers for single values that persist on-chain. They handle storage reading, writing, and caching automatically.
## Overview
```typescript
import {
StoredU256,
StoredU64,
StoredU32,
StoredBoolean,
StoredString,
StoredAddress,
Blockchain,
EMPTY_POINTER,
} from '@btc-vision/btc-runtime/runtime';
import { u256 } from '@btc-vision/as-bignum/assembly';
// Allocate storage pointer
private counterPointer: u16 = Blockchain.nextPointer;
// Create stored value with default
private counter: StoredU256 = new StoredU256(this.counterPointer, EMPTY_POINTER);
// Read and write
const current = this.counter.value; // Read
this.counter.value = newValue; // Write
```
## Class Hierarchy
The stored primitives are standalone final classes (not inheriting from a common base):
```mermaid
classDiagram
class StoredU256 {
-pointer: u16
-subPointer: Uint8Array
-_value: u256
+value: u256 getter/setter
+add(value: u256) this
+sub(value: u256) this
+mul(value: u256) this
+set(value: u256) this
+toString() string
}
class StoredU64 {
-pointer: u16
-subPointer: Uint8Array
-_values: u64[4]
+get(index: u8) u64
+set(index: u8, value: u64) void
+save() void
+getAll() u64[]
}
class StoredU32 {
-pointer: u16
-subPointer: Uint8Array
-_values: u32[8]
+get(index: u8) u32
+set(index: u8, value: u32) void
+save() void
+getAll() u32[]
}
class StoredBoolean {
-pointer: u16
-_value: Uint8Array
+value: bool getter/setter
+toUint8Array() Uint8Array
}
class StoredString {
-pointer: u16
-index: u64
+value: string getter/setter
}
class StoredAddress {
-pointer: u16
-_value: Address
+value: Address getter/setter
+isDead() bool
}
```
## Available Types
| Type | Value Type | Size | Description |
|------|------------|------|-------------|
| `StoredU256` | `u256` | 32 bytes | 256-bit unsigned integer |
| `StoredU64` | `u64[4]` | 32 bytes | Stores 4 u64 values in one slot |
| `StoredU32` | `u32[8]` | 32 bytes | Stores 8 u32 values in one slot |
| `StoredBoolean` | `bool` | 32 bytes | Boolean value |
| `StoredString` | `string` | Variable | UTF-8 string (max 65,535 bytes) |
| `StoredAddress` | `Address` | 32 bytes | Address value |
> **Note:** `StoredU64` and `StoredU32` are packed storage types that store multiple values in a single 256-bit storage slot. Use `get(index)` and `set(index, value)` to access individual values, then call `save()` to persist changes.
## Storage Key Generation
Each stored primitive computes its storage key using `SHA256(pointer || subPointer)`. See [Pointers](../core-concepts/pointers.md#encodepointer-function-flow) for the detailed flow diagram.
## Usage
### StoredU256
```typescript
// Declaration
private balancePointer: u16 = Blockchain.nextPointer;
private _balance: StoredU256 = new StoredU256(this.balancePointer, EMPTY_POINTER);
// Read
const balance: u256 = this._balance.value;
// Write
this._balance.value = newBalance;
// Arithmetic
this._balance.value = SafeMath.add(this._balance.value, amount);
```
### StoredBoolean
```typescript
// Declaration
private pausedPointer: u16 = Blockchain.nextPointer;
private _paused: StoredBoolean = new StoredBoolean(this.pausedPointer, false);
// Read
if (this._paused.value) {
throw new Revert('Contract is paused');
}
// Write
this._paused.value = true;
```
### StoredString
```typescript
// Declaration
private namePointer: u16 = Blockchain.nextPointer;
private _name: StoredString = new StoredString(this.namePointer, 0);
// Write (typically in onDeployment)
this._name.value = 'My Token';
// Read
const name: string = this._name.value;
```
### StoredAddress
```typescript
// Declaration - takes only pointer (default value is Address.zero())
private ownerPointer: u16 = Blockchain.nextPointer;
private _owner: StoredAddress = new StoredAddress(this.ownerPointer);
// Write
this._owner.value = Blockchain.tx.origin;
// Read
const owner: Address = this._owner.value;
// Check if address is zero (Note: isDead() in StoredAddress actually checks for zero address)
if (this._owner.isDead()) {
throw new Revert('Owner not set');
}
// Alternative: use isZero() on the Address instance directly
if (this._owner.value.isZero()) {
throw new Revert('Owner not set');
}
// Compare
if (!Blockchain.tx.sender.equals(this._owner.value)) {
throw new Revert('Not owner');
}
```
## Storage Behavior
### Lazy Loading (Value Read Flow)
Values are loaded from storage on first access. The read flow follows this pattern:
```mermaid
---
config:
theme: dark
---
flowchart LR
A["Access .value"] --> B{"Cached?"}
B -->|"Yes"| C["Return cached"]
B -->|"No"| D["ensureValue()"]
D --> E["encodePointer()"]
E --> F["getStorageAt()"]
F --> G["decode()"]
G --> H["Cache & Return"]
```
```typescript
// First access triggers storage read
const balance = this._balance.value; // Reads from storage
// Subsequent accesses use cached value
const balance2 = this._balance.value; // Uses cache (no storage read)
```
### Automatic Commit (Value Write Flow)
Changes are committed to storage automatically following this flow:
```mermaid
---
config:
theme: dark
---
flowchart LR
A["Set .value"] --> B["encode()"]
B --> C["Update cache"]
C --> D["encodePointer()"]
D --> E["setStorageAt()"]
E --> F["Committed"]
```
```typescript
// Write value
this._balance.value = newBalance; // Marks as dirty
// Value is committed at transaction end
// (or immediately in some implementations)
```
### Manual Commit Control
For advanced use cases:
```typescript
// Some stored types support NoCommit for read-only access
const value = this._balance.valueNoCommit; // Read without triggering commit
// Useful for view functions that shouldn't modify storage
```
## Initialization
### Default Values
Always provide a meaningful default:
```typescript
// Good: Zero/empty defaults
private counter: StoredU256 = new StoredU256(ptr, EMPTY_POINTER);
private name: StoredString = new StoredString(ptr, 0);
private paused: StoredBoolean = new StoredBoolean(ptr, false);
private owner: StoredAddress = new StoredAddress(ptr); // Default is Address.zero()
// The default is returned when storage slot is empty (never written)
```
### Setting Initial Values
Set values in `onDeployment`:
```typescript
public override onDeployment(calldata: Calldata): void {
// Set initial values
this._name.value = calldata.readString();
this._symbol.value = calldata.readString();
this._totalSupply.value = calldata.readU256();
this._owner.value = Blockchain.tx.origin;
}
```
## Solidity vs OP_NET Comparison
### Quick Reference Table
| Solidity | OP_NET | Default Value |
|----------|-------|---------------|
| `uint256 public value;` | `StoredU256` | `u256.Zero` |
| `uint64[4] packed;` | `StoredU64` | `[0, 0, 0, 0]` |
| `uint32[8] packed;` | `StoredU32` | `[0, 0, 0, 0, 0, 0, 0, 0]` |
| `string public name;` | `StoredString` | `""` |
| `bool public paused;` | `StoredBoolean` | `false` |
| `address public owner;` | `StoredAddress` | `Address.zero()` |
> **Note:** `StoredU64` and `StoredU32` pack multiple values into a single storage slot for efficiency. For single-value storage, use `StoredU256` with appropriate conversions.
### Operations Comparison
| Operation | Solidity | OP_NET |
|-----------|----------|-------|
| Declare state variable | `uint256 public value;` | `private _value: StoredU256 = new StoredU256(ptr, EMPTY_POINTER);` |
| Read value | `value` or `this.value` | `this._value.value` |
| Write value | `value = newValue;` | `this._value.value = newValue;` |
| Increment | `value++;` | `this._value.value = SafeMath.add(this._value.value, u256.One);` |
| Decrement | `value--;` | `this._value.value = SafeMath.sub(this._value.value, u256.One);` |
| Add amount | `value += amount;` | `this._value.value = SafeMath.add(this._value.value, amount);` |
| Check zero | `value == 0` | `this._value.value.isZero()` |
| Compare | `value > other` | `this._value.value > other` |
| Set in constructor | `value = initial;` | Use `onDeployment()` |
| Public getter | Automatic | Must define manually |
### Declaration Patterns
| Solidity Pattern | OP_NET Equivalent |
|------------------|------------------|
| `uint256 public totalSupply;` | `private totalSupplyPtr: u16 = Blockchain.nextPointer;`<br>`private _totalSupply: StoredU256 = new StoredU256(this.totalSupplyPtr, EMPTY_POINTER);` |
| `string public name = "Token";` | `private namePtr: u16 = Blockchain.nextPointer;`<br>`private _name: StoredString = new StoredString(this.namePtr, 0);`<br>Then in `onDeployment`: `this._name.value = "Token";` |
| `bool public paused = false;` | `private pausedPtr: u16 = Blockchain.nextPointer;`<br>`private _paused: StoredBoolean = new StoredBoolean(this.pausedPtr, false);` |
| `address public owner;` | `private ownerPtr: u16 = Blockchain.nextPointer;`<br>`private _owner: StoredAddress = new StoredAddress(this.ownerPtr);` |
For complete token examples using stored primitives, see [Basic Token Example](../examples/basic-token.md).
## Side-by-Side Code Examples
### Counter Contract
**Solidity:**
```solidity
contract Counter {
uint256 public count;
function increment() external {
count++;
}
function decrement() external {
require(count > 0, "Cannot go below zero");
count--;
}
function add(uint256 amount) external {
count += amount;
}
function reset() external {
count = 0;
}
}
```
**OP_NET:**
```typescript
export class Counter extends OP_NET {
private countPointer: u16 = Blockchain.nextPointer;
private _count: StoredU256 = new StoredU256(this.countPointer, EMPTY_POINTER);
public increment(_calldata: Calldata): BytesWriter {
this._count.value = SafeMath.add(this._count.value, u256.One);
return new BytesWriter(0);
}
public decrement(_calldata: Calldata): BytesWriter {
if (this._count.value.isZero()) {
throw new Revert('Cannot go below zero');
}
this._count.value = SafeMath.sub(this._count.value, u256.One);
return new BytesWriter(0);
}
public add(calldata: Calldata): BytesWriter {
const amount = calldata.readU256();
this._count.value = SafeMath.add(this._count.value, amount);
return new BytesWriter(0);
}
public reset(_calldata: Calldata): BytesWriter {
this._count.value = u256.Zero;
return new BytesWriter(0);
}
public count(_calldata: Calldata): BytesWriter {
const writer = new BytesWriter(32);
writer.writeU256(this._count.value);
return writer;
}
}
```
### Ownable Contract
**Solidity:**
```solidity
contract Ownable {
address public owner;
bool public paused;
modifier onlyOwner() {
require(msg.sender == owner, "Not owner");
_;
}
constructor() {
owner = msg.sender;
}
function transferOwnership(address newOwner) external onlyOwner {
require(newOwner != address(0), "Invalid address");
owner = newOwner;
}
function pause() external onlyOwner {
paused = true;
}
function unpause() external onlyOwner {
paused = false;
}
}
```
**OP_NET:**
```typescript
export class Ownable extends OP_NET {
private ownerPointer: u16 = Blockchain.nextPointer;
private pausedPointer: u16 = Blockchain.nextPointer;
private _owner: StoredAddress = new StoredAddress(this.ownerPointer);
private _paused: StoredBoolean = new StoredBoolean(this.pausedPointer, false);
public override onDeployment(_calldata: Calldata): void {
this._owner.value = Blockchain.tx.origin;
}
private onlyOwner(): void {
if (!Blockchain.tx.sender.equals(this._owner.value)) {
throw new Revert('Not owner');
}
}
public transferOwnership(calldata: Calldata): BytesWriter {
this.onlyOwner();
const newOwner = calldata.readAddress();
if (newOwner.equals(Address.zero())) {
throw new Revert('Invalid address');
}
this._owner.value = newOwner;
return new BytesWriter(0);
}
public pause(_calldata: Calldata): BytesWriter {
this.onlyOwner();
this._paused.value = true;
return new BytesWriter(0);
}
public unpause(_calldata: Calldata): BytesWriter {
this.onlyOwner();
this._paused.value = false;
return new BytesWriter(0);
}
public owner(_calldata: Calldata): BytesWriter {
const writer = new BytesWriter(32);
writer.writeAddress(this._owner.value);
return writer;
}
public paused(_calldata: Calldata): BytesWriter {
const writer = new BytesWriter(1);
writer.writeBoolean(this._paused.value);
return writer;
}
}
```
## Patterns
### Read-Modify-Write
```typescript
// Increment counter
public increment(_calldata: Calldata): BytesWriter {
const current = this._counter.value;
this._counter.value = SafeMath.add(current, u256.One);
return new BytesWriter(0);
}
// Toggle boolean
public togglePause(_calldata: Calldata): BytesWriter {
this.onlyDeployer(Blockchain.tx.sender);
this._paused.value = !this._paused.value;
return new BytesWriter(0);
}
```
### Conditional Updates
```typescript
public setOwner(calldata: Calldata): BytesWriter {
this.onlyDeployer(Blockchain.tx.sender);
const newOwner = calldata.readAddress();
// Validate before writing
if (newOwner.equals(Address.zero())) {
throw new Revert('Invalid owner');
}
// Only write if different
if (!newOwner.equals(this._owner.value)) {
this._owner.value = newOwner;
this.emitEvent(new OwnershipTransferred(this._owner.value, newOwner));
}
return new BytesWriter(0);
}
```
### View Functions
```typescript
// Return stored value
public totalSupply(_calldata: Calldata): BytesWriter {
const writer = new BytesWriter(32);
writer.writeU256(this._totalSupply.value);
return writer;
}
// Return multiple values
public getInfo(_calldata: Calldata): BytesWriter {
const writer = new BytesWriter(256);
writer.writeString(this._name.value);
writer.writeString(this._symbol.value);
writer.writeU256(this._totalSupply.value);
writer.writeU8(this._decimals.value);
return writer;
}
```
## Best Practices
### 1. Initialize All Storage
```typescript
// Always set initial values in onDeployment
public override onDeployment(calldata: Calldata): void {
this._name.value = 'Token';
this._symbol.value = 'TKN';
this._decimals.value = 18;
this._owner.value = Blockchain.tx.origin;
}
```
### 2. Use Meaningful Defaults
```typescript
// Good: EMPTY_POINTER for uninitialized u256 values
private counter: StoredU256 = new StoredU256(ptr, EMPTY_POINTER);
// Note: Set initial values in onDeployment if needed
// this._counter.value = u256.fromU64(100);
```
### 3. Validate Before Writing
```typescript
public setLimit(calldata: Calldata): BytesWriter {
const newLimit = calldata.readU256();
// Validate
if (newLimit.isZero()) {
throw new Revert('Limit cannot be zero');
}
if (newLimit > u256.fromU64(1000000)) {
throw new Revert('Limit too high');
}
// Then write
this._limit.value = newLimit;
return new BytesWriter(0);
}
```
### 4. Cache Reads in Loops
```typescript
// Bad: Multiple storage reads
for (let i = 0; i < count; i++) {
if (amount > this._balance.value) { // Storage read each iteration
// ...
}
}
// Good: Cache the value
const balance = this._balance.value; // One storage read
for (let i = 0; i < count; i++) {
if (amount > balance) {
// ...
}
}
```
---
**Navigation:**
- Previous: [BytesWriter/Reader](../types/bytes-writer-reader.md)
- Next: [Stored Arrays](./stored-arrays.md)