@btc-vision/btc-runtime
Version:
Bitcoin L1 Smart Contract Runtime for OP_NET. Build decentralized applications on Bitcoin using AssemblyScript and WebAssembly. Fully audited.
745 lines (599 loc) • 19.7 kB
Markdown
# Calldata
The `Calldata` type handles parsing of input parameters passed to contract methods. It provides methods to read various data types in sequence.
## Overview
```typescript
import { Calldata, Address, BytesWriter } from '@btc-vision/btc-runtime/runtime';
import { u256 } from '@btc-vision/as-bignum/assembly';
public transfer(calldata: Calldata): BytesWriter {
// Read parameters in order
const to: Address = calldata.readAddress();
const amount: u256 = calldata.readU256();
// ... process transfer
return new BytesWriter(0);
}
```
### Calldata Architecture
```mermaid
classDiagram
class Calldata {
<<extends BytesReader>>
+readU8() u8
+readU16(be) u16
+readU32(be) u32
+readU64(be) u64
+readU128(be) u128
+readU256(be) u256
+readI8() i8
+readI16() i16
+readI32() i32
+readI64(be) i64
+readI128(be) i128
+readAddress() Address
+readString(length) string
+readStringWithLength(be) string
+readBytes(length) Uint8Array
+readBytesWithLength(be) Uint8Array
+readBoolean() bool
+readSelector() Selector
+readAddressArray(be) Address[]
+readU256Array(be) u256[]
+readAddressMapU256(be) AddressMap~u256~
+getOffset() i32
+setOffset(offset) void
+byteLength i32
}
class BytesReader {
-DataView buffer
-i32 currentOffset
+readU8() u8
+readU32() u32
+readU256() u256
}
class Contract {
+transfer(calldata: Calldata) BytesWriter
+approve(calldata: Calldata) BytesWriter
+balanceOf(calldata: Calldata) BytesWriter
}
BytesReader <|-- Calldata : extends
Contract --> Calldata : receives
note for Calldata "Sequential parameter reading\nfrom encoded transaction data"
note for Contract "All public methods\nreceive Calldata"
```
## Reading Data
### Primitive Types
```typescript
// Boolean (1 byte)
const flag: bool = calldata.readBoolean();
// Unsigned integers
const u8val: u8 = calldata.readU8();
const u16val: u16 = calldata.readU16();
const u32val: u32 = calldata.readU32();
const u64val: u64 = calldata.readU64();
const u128val: u128 = calldata.readU128();
const u256val: u256 = calldata.readU256();
// Signed integers
const i8val: i8 = calldata.readI8();
const i16val: i16 = calldata.readI16();
const i32val: i32 = calldata.readI32();
const i64val: i64 = calldata.readI64();
```
### Complex Types
```typescript
// Address (32 bytes)
const addr: Address = calldata.readAddress();
// String with u32 length prefix
const name: string = calldata.readStringWithLength();
// String with known length (stops at null byte)
const fixedName: string = calldata.readString(32);
// Bytes with u32 length prefix
const data: Uint8Array = calldata.readBytesWithLength();
// Bytes with known length
const fixedData: Uint8Array = calldata.readBytes(64);
// Selector (4 bytes, big-endian)
const selector: Selector = calldata.readSelector();
```
### Arrays
All array methods expect a u16 length prefix (max 65535 elements):
```typescript
// Array of addresses
const addresses: Address[] = calldata.readAddressArray();
// Numeric arrays
const u8Values: u8[] = calldata.readU8Array();
const u16Values: u16[] = calldata.readU16Array();
const u32Values: u32[] = calldata.readU32Array();
const u64Values: u64[] = calldata.readU64Array();
const u128Values: u128[] = calldata.readU128Array();
const u256Values: u256[] = calldata.readU256Array();
// Array of variable-length buffers
const buffers: Uint8Array[] = calldata.readArrayOfBuffer();
```
### Maps
```typescript
// Address -> u256 mapping
const map: AddressMap<u256> = calldata.readAddressMapU256();
// Usage
const keys = map.keys();
for (let i = 0; i < keys.length; i++) {
const address = keys[i];
const value = map.get(address);
// Process...
}
```
## Read Order
**IMPORTANT:** Data must be read in the exact order it was written.
```typescript
// Correct order
public myMethod(calldata: Calldata): BytesWriter {
const address = calldata.readAddress(); // First
const amount = calldata.readU256(); // Second
const flag = calldata.readBoolean(); // Third
// ...
}
// Wrong order will read garbage!
public myMethod(calldata: Calldata): BytesWriter {
const amount = calldata.readU256(); // WRONG!
const address = calldata.readAddress(); // WRONG!
const flag = calldata.readBoolean();
}
```
### Sequential Read Flow
```mermaid
---
config:
theme: dark
---
flowchart LR
A["Calldata Buffer"] --> B["readAddress: 32 bytes"]
B --> C["readU256: 32 bytes"]
C --> D["readBoolean: 1 byte"]
D --> E{"hasMoreData?"}
E -->|"Yes"| F["Continue"]
E -->|"No"| G["Complete"]
B -.->|"Wrong Order"| H["Garbage Data/Revert"]
C -.->|"Wrong Order"| H
```
## Data Encoding
### Encoding Format
```
| Field 1 | Field 2 | Field 3 | ... |
|---------|---------|---------|-----|
```
Each field is encoded according to its type:
| Type | Encoding |
|------|----------|
| `bool` | 1 byte (0 or 1) |
| `u8` | 1 byte |
| `u16` | 2 bytes (big-endian, default) |
| `u32` | 4 bytes (big-endian, default) |
| `u64` | 8 bytes (big-endian, default) |
| `u128` | 16 bytes (big-endian, default) |
| `u256` | 32 bytes (big-endian, default) |
| `Address` | 32 bytes |
| `Selector` | 4 bytes (big-endian u32) |
| `string` | 4-byte length (u32 BE) + UTF-8 bytes |
| `bytes` | 4-byte length (u32 BE) + raw bytes |
| `arrays` | 2-byte length (u16 BE) + elements |
### String Encoding (with length prefix)
```
| Length (4 bytes, BE) | UTF-8 Content |
|----------------------|---------------|
| 0x00 0x00 0x00 0x0B | "Hello World" |
```
### Array Encoding
Arrays use a u16 length prefix (max 65535 elements):
```
| Length (2 bytes, BE) | Element 1 | Element 2 | ... |
|----------------------|-----------|-----------|-----|
```
## Solidity vs OP_NET Comparison
### Calldata Decoding Comparison Table
| Feature | Solidity | OP_NET |
|---------|----------|-------|
| **Parameter access** | Automatic (named parameters) | Manual sequential reading |
| **Decode function** | `abi.decode(data, (T1, T2))` | `calldata.readT1(); calldata.readT2();` |
| **Type safety** | Compile-time | Runtime |
| **Read order** | Any order (named) | Must match encoding order |
| **Error on insufficient data** | Reverts | Reverts |
| **Dynamic types** | ABI-encoded with offset | Length-prefixed inline |
| **Memory location** | `calldata`, `memory` keywords | Always sequential buffer |
### Type-by-Type Decoding Comparison
| Solidity Decoding | OP_NET Decoding |
|-------------------|----------------|
| `abi.decode(data, (uint256))` | `calldata.readU256()` |
| `abi.decode(data, (uint128))` | `calldata.readU128()` |
| `abi.decode(data, (uint64))` | `calldata.readU64()` |
| `abi.decode(data, (uint32))` | `calldata.readU32()` |
| `abi.decode(data, (uint16))` | `calldata.readU16()` |
| `abi.decode(data, (uint8))` | `calldata.readU8()` |
| `abi.decode(data, (int256))` | N/A (use u256 with sign handling) |
| `abi.decode(data, (bool))` | `calldata.readBoolean()` |
| `abi.decode(data, (address))` | `calldata.readAddress()` |
| `abi.decode(data, (bytes32))` | `calldata.readBytes()` (length-prefixed) |
| `abi.decode(data, (string))` | `calldata.readString()` |
| `abi.decode(data, (bytes))` | `calldata.readBytes()` |
| `abi.decode(data, (address[]))` | `calldata.readAddressArray()` |
| `abi.decode(data, (uint256[]))` | `calldata.readU256Array()` |
### Side-by-Side Code Examples
#### Simple Function Parameters
```solidity
// Solidity - Parameters decoded automatically
function transfer(address to, uint256 amount) public returns (bool) {
// 'to' and 'amount' are immediately available
_transfer(msg.sender, to, amount);
return true;
}
```
```typescript
// OP_NET - Parameters read sequentially
public transfer(calldata: Calldata): BytesWriter {
// Must read in exact order they were encoded
const to: Address = calldata.readAddress();
const amount: u256 = calldata.readU256();
this._transfer(Blockchain.tx.sender, to, amount);
const writer = new BytesWriter(1);
writer.writeBoolean(true);
return writer;
}
```
#### Multiple Parameters
```solidity
// Solidity
function transferFrom(
address from,
address to,
uint256 amount
) public returns (bool) {
_spendAllowance(from, msg.sender, amount);
_transfer(from, to, amount);
return true;
}
```
```typescript
// OP_NET
public transferFrom(calldata: Calldata): BytesWriter {
const from: Address = calldata.readAddress();
const to: Address = calldata.readAddress();
const amount: u256 = calldata.readU256();
this._spendAllowance(from, Blockchain.tx.sender, amount);
this._transfer(from, to, amount);
const writer = new BytesWriter(1);
writer.writeBoolean(true);
return writer;
}
```
#### Decoding Complex Types
```solidity
// Solidity - Decoding from raw bytes
function decodeTransfer(bytes calldata data) public pure returns (address, uint256) {
(address to, uint256 amount) = abi.decode(data, (address, uint256));
return (to, amount);
}
// Or with explicit offset
function decodeWithOffset(bytes calldata data) public pure {
address to = abi.decode(data[0:32], (address));
uint256 amount = abi.decode(data[32:64], (uint256));
}
```
```typescript
// OP_NET - Sequential reading handles offset automatically
public decodeTransfer(calldata: Calldata): BytesWriter {
const to: Address = calldata.readAddress(); // Reads bytes 0-31
const amount: u256 = calldata.readU256(); // Reads bytes 32-63
const writer = new BytesWriter(64);
writer.writeAddress(to);
writer.writeU256(amount);
return writer;
}
```
#### String and Bytes Handling
```solidity
// Solidity
function setName(string calldata name) public {
require(bytes(name).length > 0, "Empty name");
_name = name;
}
function processData(bytes calldata data) public {
require(data.length >= 4, "Too short");
// Process data...
}
```
```typescript
// OP_NET
public setName(calldata: Calldata): BytesWriter {
const name: string = calldata.readString(); // Length-prefixed
if (name.length == 0) {
throw new Revert('Empty name');
}
this._name.value = name;
return new BytesWriter(0);
}
public processData(calldata: Calldata): BytesWriter {
const data: Uint8Array = calldata.readBytes(); // Length-prefixed
if (data.length < 4) {
throw new Revert('Too short');
}
// Process data...
return new BytesWriter(0);
}
```
#### Array Parameters
```solidity
// Solidity
function batchTransfer(
address[] calldata recipients,
uint256[] calldata amounts
) public {
require(recipients.length == amounts.length, "Length mismatch");
for (uint i = 0; i < recipients.length; i++) {
_transfer(msg.sender, recipients[i], amounts[i]);
}
}
```
```typescript
// OP_NET
public batchTransfer(calldata: Calldata): BytesWriter {
const recipients: Address[] = calldata.readAddressArray();
const amounts: u256[] = calldata.readU256Array();
if (recipients.length != amounts.length) {
throw new Revert('Length mismatch');
}
for (let i = 0; i < recipients.length; i++) {
this._transfer(Blockchain.tx.sender, recipients[i], amounts[i]);
}
return new BytesWriter(0);
}
```
#### Optional/Variable Parameters
```solidity
// Solidity - Using bytes for optional data
function safeTransferFrom(
address from,
address to,
uint256 tokenId,
bytes calldata data
) public {
_transfer(from, to, tokenId);
if (data.length > 0) {
// Call onERC721Received
}
}
```
```typescript
// OP_NET - Check for remaining data
public safeTransferFrom(calldata: Calldata): BytesWriter {
const from: Address = calldata.readAddress();
const to: Address = calldata.readAddress();
const tokenId: u256 = calldata.readU256();
// Check if optional data is present by comparing offset to total length
let data: Uint8Array = new Uint8Array(0);
if (calldata.getOffset() < calldata.byteLength) {
data = calldata.readBytesWithLength(); // Read length-prefixed bytes
}
this._transfer(from, to, tokenId);
if (data.length > 0) {
// Handle callback
}
return new BytesWriter(0);
}
```
### Encoding Format Differences
| Aspect | Solidity ABI | OP_NET |
|--------|--------------|-------|
| **Byte order** | Big-endian | Big-endian (default) |
| **Address padding** | Left-padded to 32 bytes | 32 bytes (native size) |
| **Dynamic offset** | Pointer + data section | Inline length prefix |
| **String encoding** | Offset + length + data | 4-byte u32 length + UTF-8 |
| **Array encoding** | Offset + length + elements | 2-byte u16 length + elements |
| **Boolean** | 32 bytes (padded) | 1 byte |
| **uint8-uint248** | 32 bytes (padded) | Native size |
### Encoding Size Comparison
| Type | Solidity ABI Size | OP_NET Size |
|------|-------------------|------------|
| `bool` | 32 bytes | 1 byte |
| `uint8` | 32 bytes | 1 byte |
| `uint16` | 32 bytes | 2 bytes |
| `uint32` | 32 bytes | 4 bytes |
| `uint64` | 32 bytes | 8 bytes |
| `uint128` | 32 bytes | 16 bytes |
| `uint256` | 32 bytes | 32 bytes |
| `address` | 32 bytes | 32 bytes |
| `string "Hello"` | 96 bytes (offset+len+data) | 9 bytes (len+data) |
### Key Differences Summary
| Solidity | OP_NET |
|----------|-------|
| Named parameters in function signature | Single `Calldata` parameter |
| Automatic ABI decoding | Manual `read*()` methods |
| Can access parameters in any order | Must read in sequential order |
| Type info in function signature | Type determined by read method |
| `calldata` keyword optimization | All calldata is read-only by default |
| `msg.data` for raw bytes | Calldata object wraps the buffer |
## Common Patterns
### Method Call Flow
```mermaid
sequenceDiagram
participant U as 👤 User/Client
participant BC as Blockchain
participant C as Contract Method
participant CD as Calldata
participant W as BytesWriter
U->>BC: Transaction with encoded params
Note over BC: Selector + Parameters
BC->>C: Invoke method(calldata)
C->>CD: readAddress()
CD-->>C: Address value
C->>CD: readU256()
CD-->>C: u256 value
C->>C: Execute business logic
Note over C: Process transfer,<br/>update balances, etc.
C->>W: new BytesWriter(size)
C->>W: Write return values
W-->>C: Encoded response
C-->>BC: Return BytesWriter
BC-->>U: Transaction result
Note over CD: All parameters read<br/>sequentially in order
```
### Single Value Methods
```typescript
// balanceOf(address)
public balanceOf(calldata: Calldata): BytesWriter {
const account = calldata.readAddress();
const balance = this.balances.get(account);
const writer = new BytesWriter(32);
writer.writeU256(balance);
return writer;
}
```
### Multi-Parameter Methods
```typescript
// transferFrom(from, to, amount)
public transferFrom(calldata: Calldata): BytesWriter {
const from = calldata.readAddress();
const to = calldata.readAddress();
const amount = calldata.readU256();
this._transfer(from, to, amount);
return new BytesWriter(0);
}
```
### Optional Parameters
```typescript
// Method with optional data field
public safeTransfer(calldata: Calldata): BytesWriter {
const to = calldata.readAddress();
const tokenId = calldata.readU256();
// Check if there's more data by comparing offset to total length
let data: Uint8Array = new Uint8Array(0);
if (calldata.getOffset() < calldata.byteLength) {
data = calldata.readBytesWithLength(); // Read length-prefixed bytes
}
this._safeTransfer(Blockchain.tx.sender, to, tokenId, data);
return new BytesWriter(0);
}
```
### Batch Operations
```typescript
// Airdrop to multiple addresses
public airdrop(calldata: Calldata): BytesWriter {
const recipients = calldata.readAddressMapU256();
const addresses = recipients.keys();
for (let i = 0; i < addresses.length; i++) {
const addr = addresses[i];
const amount = recipients.get(addr);
this._mint(addr, amount);
}
return new BytesWriter(0);
}
```
## Error Handling
### Insufficient Data
```typescript
// If calldata doesn't have enough bytes, read will fail
public myMethod(calldata: Calldata): BytesWriter {
// If only 32 bytes provided...
const addr = calldata.readAddress(); // OK (32 bytes)
const amount = calldata.readU256(); // FAILS! No more data
}
```
### Validation
```typescript
public myMethod(calldata: Calldata): BytesWriter {
const to = calldata.readAddress();
const amount = calldata.readU256();
// Validate after reading
if (to.equals(Address.zero())) {
throw new Revert('Invalid recipient');
}
if (amount.isZero()) {
throw new Revert('Amount is zero');
}
// ... proceed
}
```
## Deployment Calldata
The `onDeployment` method receives initialization parameters:
```typescript
public override onDeployment(calldata: Calldata): void {
// Read deployment parameters
const name = calldata.readString();
const symbol = calldata.readString();
const maxSupply = calldata.readU256();
const decimals = calldata.readU8();
// Initialize contract
this._name.value = name;
this._symbol.value = symbol;
this._maxSupply.value = maxSupply;
this._decimals.value = decimals;
}
```
### Deployment Parameter Flow
```mermaid
---
config:
theme: dark
---
flowchart LR
A["Deploy Transaction"] --> B["Calldata Buffer"]
B --> C["name: string"]
C --> D["symbol: string"]
D --> E["maxSupply: u256"]
E --> F["decimals: u8"]
F --> G["onDeployment Method"]
G --> H["Initialize State"]
H --> I["Contract Ready"]
```
## Best Practices
### 1. Document Parameter Order
```typescript
/**
* Transfer tokens to recipient.
* @param calldata Contains:
* - to: Address (32 bytes) - Recipient address
* - amount: u256 (32 bytes) - Amount to transfer
*/
public transfer(calldata: Calldata): BytesWriter {
const to = calldata.readAddress();
const amount = calldata.readU256();
// ...
}
```
### 2. Validate Early
```typescript
public mint(calldata: Calldata): BytesWriter {
// Read all parameters first
const to = calldata.readAddress();
const amount = calldata.readU256();
// Then validate
this.onlyDeployer(Blockchain.tx.sender);
if (to.equals(Address.zero())) {
throw new Revert('Cannot mint to zero address');
}
if (amount.isZero()) {
throw new Revert('Amount must be positive');
}
// Then execute
this._mint(to, amount);
return new BytesWriter(0);
}
```
### 3. Handle Arrays Carefully
```typescript
public batchTransfer(calldata: Calldata): BytesWriter {
const recipients = calldata.readAddressArray();
const amounts = calldata.readU256Array();
// Validate array lengths match
if (recipients.length !== amounts.length) {
throw new Revert('Array length mismatch');
}
// Limit array size to prevent DoS attacks
if (recipients.length > 100) {
throw new Revert('Too many recipients');
}
for (let i = 0; i < recipients.length; i++) {
this._transfer(Blockchain.tx.sender, recipients[i], amounts[i]);
}
return new BytesWriter(0);
}
```
---
**Navigation:**
- Previous: [SafeMath](./safe-math.md)
- Next: [BytesWriter/Reader](./bytes-writer-reader.md)