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.

745 lines (599 loc) 19.7 kB
# 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)