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.

774 lines (600 loc) 20.4 kB
# Address Type The `Address` type represents a 32-byte Bitcoin/OP_NET address. It provides methods for creating, comparing, and serializing addresses. ## Overview ```typescript import { Address, Blockchain } from '@btc-vision/btc-runtime/runtime'; // Get current sender const sender: Address = Blockchain.tx.sender; // Create zero address const zero: Address = Address.zero(); // Compare addresses if (sender.equals(zero)) { throw new Revert('Invalid sender'); } ``` ### Address Type Architecture ```mermaid classDiagram class Address { +Uint8Array[32] bytes -bool isDefined -Uint8Array _mldsaPublicKey +zero() Address$ +fromString(hex) Address$ +fromUint8Array(bytes) Address$ +equals(other) bool +notEquals(other) bool +lessThan(other) bool +greaterThan(other) bool +lessThanOrEqual(other) bool +greaterThanOrEqual(other) bool +isZero() bool +clone() Address +toHex() string +toString() string +mldsaPublicKey Uint8Array } class ExtendedAddress { +Uint8Array[32] tweakedPublicKey +fromStringPair(schnorr, mldsa) ExtendedAddress +p2tr() string +isDead() bool +downCast() Address +dead() ExtendedAddress } class Uint8Array { <<built-in>> +length: 32 } Uint8Array <|-- Address : extends Address <|-- ExtendedAddress : extends note for Address "32-byte ML-DSA\npublic key hash" note for ExtendedAddress "Dual-key support:\nSchnorr + ML-DSA" ``` ## Creating Addresses ### Address Creation Flow ```mermaid --- config: theme: dark --- flowchart LR A["Address Creation"] --> B{"Source?"} B -->|"Runtime"| C["Blockchain.tx.sender/origin"] B -->|"Factory"| D["Address.zero()<br/>ExtendedAddress.dead()"] B -->|"Parsing"| E["fromString/fromBytes"] B -->|"Calldata"| F["calldata.readAddress"] C --> G["32-byte Address"] D --> G E --> G F --> G G --> H{"Valid?"} H -->|"Yes"| I["Address Instance"] H -->|"No"| J["Revert"] ``` ### From Runtime ```typescript // Current transaction sender const sender: Address = Blockchain.tx.sender; // Original transaction signer const origin: Address = Blockchain.tx.origin; // This contract's address const self: Address = Blockchain.contract.address; // Contract deployer const deployer: Address = Blockchain.contract.deployer; ``` ### Special Addresses ```typescript // Zero address (all zeros) const zero: Address = Address.zero(); // Equivalent to address(0) in Solidity // Note: For dead/burn addresses, use ExtendedAddress.dead() // See ExtendedAddress section below ``` ### From Bytes ```typescript // From Uint8Array (32 bytes) - uses efficient memory copy const bytes = new Uint8Array(32); bytes[31] = 0x01; const addr = Address.fromUint8Array(bytes); // From u8[] array (32 bytes) const byteArray: u8[] = new Array<u8>(32); byteArray[31] = 0x01; const addr2 = new Address(byteArray); // From hex string const addr3 = Address.fromString('0x' + '00'.repeat(32)); ``` ### From Calldata ```typescript public myMethod(calldata: Calldata): BytesWriter { // Read address from calldata (32 bytes) const recipient: Address = calldata.readAddress(); // ... } ``` ## Comparing Addresses ### Address Comparison Methods ```mermaid --- config: theme: dark --- flowchart LR A["Address A"] --> C{"Comparison Method"} B["Address B"] --> C C -->|"equals()"| D["Byte-by-byte compare"] C -->|"lessThan/greaterThan"| E["Integer compare"] D --> F{"Result"} E --> F F --> G["true/false"] ``` ### Equality ```typescript const addr1 = Blockchain.tx.sender; const addr2 = Blockchain.tx.origin; // Using equals() if (addr1.equals(addr2)) { // Same address } // Using == operator if (addr1 == addr2) { // Same address } // Not equal if (!addr1.equals(Address.zero())) { // Not zero address } ``` ### Common Checks ```typescript // Check for zero address private validateAddress(addr: Address): void { if (addr.equals(Address.zero())) { throw new Revert('Cannot be zero address'); } } // Check if sender is deployer private onlyDeployer(): void { if (!Blockchain.tx.sender.equals(Blockchain.contract.deployer)) { throw new Revert('Not deployer'); } } // Check if two addresses are the same private preventSelfTransfer(from: Address, to: Address): void { if (from.equals(to)) { throw new Revert('Cannot transfer to self'); } } ``` ## Serialization ### To Bytes ```typescript const addr: Address = Blockchain.tx.sender; // Address extends Uint8Array, so it can be used directly as bytes // Access the underlying buffer const bytes: ArrayBuffer = addr.buffer; // Get hex string representation const hexString: string = addr.toHex(); // Clone the address const cloned: Address = addr.clone(); ``` ### With BytesWriter ```typescript const writer = new BytesWriter(64); // Write address (32 bytes) writer.writeAddress(sender); writer.writeAddress(recipient); ``` ### From BytesReader ```typescript const reader = new BytesReader(data); // Read address (32 bytes) const sender: Address = reader.readAddress(); ``` ## Address Size OP_NET addresses are **32 bytes**, compared to Ethereum's 20 bytes: | Platform | Address Size | Format | |----------|-------------|--------| | Ethereum | 20 bytes | 0x + 40 hex chars | | OP_NET | 32 bytes | 64 hex chars | ```typescript // Full 32-byte address (Address extends Uint8Array) const addr: Address = Blockchain.tx.sender; assert(addr.length === 32); ``` ## Storage with Addresses ### Storing Addresses ```typescript import { StoredAddress } from '@btc-vision/btc-runtime/runtime'; // Store a single address private ownerPointer: u16 = Blockchain.nextPointer; private _owner: StoredAddress; constructor() { super(); this._owner = new StoredAddress(this.ownerPointer); } // Set/get this._owner.value = newOwner; const owner: Address = this._owner.value; ``` ### Address Mappings ```typescript import { AddressMemoryMap } from '@btc-vision/btc-runtime/runtime'; // mapping(address => uint256) private balancesPointer: u16 = Blockchain.nextPointer; private balances: AddressMemoryMap; constructor() { super(); this.balances = new AddressMemoryMap(this.balancesPointer); } // Usage const balance: u256 = this.balances.get(userAddress); this.balances.set(userAddress, newBalance); ``` ## ML-DSA Public Key Access Every `Address` in OP_NET can access its ML-DSA (quantum-resistant) public key directly: ```typescript const sender: Address = Blockchain.tx.sender; // Get the ML-DSA public key (1312 bytes for ML-DSA-44) const mldsaKey: Uint8Array = sender.mldsaPublicKey; // Key is lazily loaded and cached const sameKey: Uint8Array = sender.mldsaPublicKey; // Returns cached key ``` ### ML-DSA Key Loading Sequence ```mermaid sequenceDiagram participant C as Contract participant A as Address participant L as Lazy Loader participant S as Storage C->>A: address.mldsaPublicKey A->>A: Check cache (_mldsaPublicKey) alt Cache Hit A-->>C: Return cached key (1312 bytes) else Cache Miss A->>L: loadMLDSAPublicKey(address, Level2) L->>S: Retrieve from blockchain storage S-->>L: ML-DSA-44 public key L-->>A: 1312-byte key A->>A: Cache key (_mldsaPublicKey = key) A-->>C: Return key (1312 bytes) end Note over A,S: Address = SHA256(ML-DSA Public Key) Note over L: Security Level 2 (ML-DSA-44) ``` **Key points:** - The address itself is the SHA256 hash of the ML-DSA public key - The full public key is loaded on-demand from the blockchain - No custom storage needed - the runtime handles this See [Quantum Resistance](../advanced/quantum-resistance.md) for signature verification details. ## Extended Address `ExtendedAddress` supports dual-key addresses (Schnorr + ML-DSA) for the quantum transition: ```typescript import { ExtendedAddress } from '@btc-vision/btc-runtime/runtime'; // Create from both key components const extAddr = ExtendedAddress.fromStringPair( '0x' + 'aa'.repeat(32), // Tweaked Schnorr key (for taproot) '0x' + 'bb'.repeat(32) // ML-DSA key hash ); // Access Schnorr tweaked key (32 bytes) const schnorrKey: Uint8Array = extAddr.tweakedPublicKey; // Access ML-DSA public key (inherited from Address) const mldsaKey: Uint8Array = extAddr.mldsaPublicKey; // Generate Bitcoin P2TR address const p2trAddress: string = extAddr.p2tr(); // "bc1p..." or "tb1p..." // Downcast to base Address const addr: Address = extAddr.downCast(); ``` ### ExtendedAddress Memory Layout ```mermaid graph LR subgraph ExtendedAddress["ExtendedAddress (64 bytes total)"] direction TB subgraph Inherited["Inherited from Address (32 bytes)"] A1["Byte 0-31: ML-DSA Public Key Hash"] A2["SHA256 of full ML-DSA key"] end subgraph Extended["Extended Fields (32 bytes)"] B1["Byte 0-31: Tweaked Schnorr Public Key"] B2["Used for P2TR address generation"] end subgraph Cached["Lazily Loaded"] C1["ML-DSA Full Key: 1312 bytes"] C2["Loaded on first access"] end end A1 --> A2 B1 --> B2 A2 -.->|"derives"| C1 C1 --> C2 ``` ### Dead Address The dead address is derived from the Bitcoin genesis block (block 0) public key: - **Genesis block public key**: `04678afdb0fe5548271967f1a67130b7105cd6a828e03909a67962e0ea1f61deb649f6bc3f4cef38c4f35504e51ec112de5c384df7ba0b8d578a4c702b6bf11d5f` - **Resulting hash**: `284ae4acdb32a99ba3ebfa66a91ddb41a7b7a1d2fef415399922cd8a04485c02` This address is commonly used as a burn address or null recipient in contracts. ```typescript // Dead address for burns (derived from Bitcoin block 0 pubkey) const dead: ExtendedAddress = ExtendedAddress.dead(); // Check if address is dead if (extAddr.isDead()) { // Funds are being burned } // Also available via Blockchain singleton const deadAddr: ExtendedAddress = Blockchain.DEAD_ADDRESS; ``` **Note:** The `Address` base class does NOT have a `dead()` method. Only `ExtendedAddress` provides the dead address functionality. See [Quantum Resistance](../advanced/quantum-resistance.md) for details. ## Solidity vs OP_NET Comparison ### Address Type Comparison Table | Feature | Solidity | OP_NET | |---------|----------|-------| | **Type name** | `address` | `Address` | | **Size** | 20 bytes (160 bits) | 32 bytes (256 bits) | | **Format** | `0x` + 40 hex chars | 64 hex chars | | **Zero address** | `address(0)` | `Address.zero()` | | **Dead/burn address** | `0x000...dEaD` | `ExtendedAddress.dead()` (Bitcoin block 0 pubkey hash) | | **Payable variant** | `address payable` | N/A (different model) | | **Current sender** | `msg.sender` | `Blockchain.tx.sender` | | **Original signer** | `tx.origin` | `Blockchain.tx.origin` | | **Contract address** | `address(this)` | `Blockchain.contract.address` | | **Deployer** | N/A (use constructor arg) | `Blockchain.contract.deployer` | | **Equality check** | `addr1 == addr2` | `addr1.equals(addr2)` | | **Zero check** | `addr == address(0)` | `addr.isZero()` or `addr.equals(Address.zero())` | | **From bytes** | `address(bytes20(data))` | `Address.fromUint8Array(data)` | | **To bytes** | `abi.encodePacked(addr)` | `addr` (extends Uint8Array) | | **Checksum** | EIP-55 mixed-case | N/A | | **Quantum-resistant key** | N/A | `addr.mldsaPublicKey` (1312 bytes) | ### Side-by-Side Code Examples #### Getting Addresses ```solidity // Solidity address sender = msg.sender; address origin = tx.origin; address self = address(this); ``` ```typescript // OP_NET const sender: Address = Blockchain.tx.sender; const origin: Address = Blockchain.tx.origin; const self: Address = Blockchain.contract.address; ``` #### Zero Address Checks ```solidity // Solidity require(to != address(0), "Cannot send to zero address"); ``` ```typescript // OP_NET if (to.equals(Address.zero())) { throw new Revert('Cannot send to zero address'); } // Or using isZero() if (to.isZero()) { throw new Revert('Cannot send to zero address'); } ``` #### Address Comparison ```solidity // Solidity require(msg.sender == owner, "Not owner"); require(from != to, "Cannot transfer to self"); ``` ```typescript // OP_NET if (!Blockchain.tx.sender.equals(owner)) { throw new Revert('Not owner'); } if (from.equals(to)) { throw new Revert('Cannot transfer to self'); } ``` #### Storing Owner Address ```solidity // Solidity address public owner; constructor() { owner = msg.sender; } modifier onlyOwner() { require(msg.sender == owner, "Not owner"); _; } function transferOwnership(address newOwner) public onlyOwner { require(newOwner != address(0), "Invalid address"); owner = newOwner; } ``` ```typescript // OP_NET private ownerPointer: u16 = Blockchain.nextPointer; private _owner: StoredAddress; constructor() { super(); this._owner = new StoredAddress(this.ownerPointer); } 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); } ``` #### Balance Mappings ```solidity // Solidity mapping(address => uint256) public balances; function balanceOf(address account) public view returns (uint256) { return balances[account]; } ``` ```typescript // OP_NET private balancesPointer: u16 = Blockchain.nextPointer; private balances: AddressMemoryMap; constructor() { super(); this.balances = new AddressMemoryMap(this.balancesPointer); } public balanceOf(calldata: Calldata): BytesWriter { const account = calldata.readAddress(); const balance: u256 = this.balances.get(account); const writer = new BytesWriter(32); writer.writeU256(balance); return writer; } ``` #### Sending Value (Key Difference) ```solidity // Solidity - Native ETH transfers address payable recipient = payable(to); recipient.transfer(amount); // Reverts on failure bool success = recipient.send(amount); // Returns false on failure (bool ok, ) = recipient.call{value: amount}(""); // Low-level call ``` ```typescript // OP_NET - No native value transfers on addresses // Bitcoin UTXO model is fundamentally different // Token transfers are done via contract calls instead: this._transfer(from, to, amount); // Internal token transfer logic ``` ### Key Differences Explained | Aspect | Solidity/Ethereum | OP_NET/Bitcoin | |--------|-------------------|---------------| | **Address derivation** | Keccak256 hash of public key (last 20 bytes) | SHA256 of ML-DSA public key (32 bytes) | | **Native currency** | ETH handled via `payable` | Bitcoin UTXOs handled separately | | **Transfer mechanism** | `addr.transfer()`, `addr.send()`, `addr.call()` | Contract method calls only | | **Balance query** | `addr.balance` (native ETH) | N/A for native; use contract mappings for tokens | | **Code size** | `addr.code.length` | N/A | | **Code hash** | `addr.codehash` | N/A | | **Quantum resistance** | None | ML-DSA public key accessible via `mldsaPublicKey` | ## Common Patterns ### Transfer Validation ```typescript public transfer(calldata: Calldata): BytesWriter { const to = calldata.readAddress(); const amount = calldata.readU256(); // Validate recipient if (to.equals(Address.zero())) { throw new Revert('Cannot transfer to zero address'); } // Prevent self-transfer (optional) if (to.equals(Blockchain.tx.sender)) { throw new Revert('Cannot transfer to self'); } // ... execute transfer } ``` ### Access Control ```typescript // Store owner/admin private ownerPointer: u16 = Blockchain.nextPointer; private _owner: StoredAddress; constructor() { super(); this._owner = new StoredAddress(this.ownerPointer); } 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('New owner is zero address'); } this._owner.value = newOwner; return new BytesWriter(0); } ``` ### Address as Map Key ```typescript // Using address as map key private userDataPointer: u16 = Blockchain.nextPointer; private userData: AddressMemoryMap; constructor() { super(); this.userData = new AddressMemoryMap(this.userDataPointer); } // Store data by address public setUserData(calldata: Calldata): BytesWriter { const data = calldata.readU256(); this.userData.set(Blockchain.tx.sender, data); return new BytesWriter(0); } // Get data by address public getUserData(calldata: Calldata): BytesWriter { const user = calldata.readAddress(); const data: u256 = this.userData.get(user); const writer = new BytesWriter(32); writer.writeU256(data); return writer; } ``` ## API Reference ### Static Methods | Method | Returns | Description | |--------|---------|-------------| | `Address.zero()` | `Address` | All-zero address (cloned from constant) | | `Address.fromString(hex)` | `Address` | Create from hex string (with or without 0x prefix) | | `Address.fromUint8Array(bytes)` | `Address` | Create from Uint8Array using direct memory copy | ### Instance Methods | Method | Returns | Description | |--------|---------|-------------| | `equals(other)` | `bool` | Compare addresses (operator `==`) | | `notEquals(other)` | `bool` | Check inequality (operator `!=`) | | `lessThan(other)` | `bool` | Compare as big-endian integers (operator `<`) | | `greaterThan(other)` | `bool` | Compare as big-endian integers (operator `>`) | | `lessThanOrEqual(other)` | `bool` | Compare addresses (operator `<=`) | | `greaterThanOrEqual(other)` | `bool` | Compare addresses (operator `>=`) | | `isZero()` | `bool` | Check if zero address | | `clone()` | `Address` | Create a deep copy | | `toHex()` | `string` | Convert to hex string (no 0x prefix) | | `toString()` | `string` | Same as toHex() | ### Instance Properties | Property | Type | Description | |----------|------|-------------| | `mldsaPublicKey` | `Uint8Array` | ML-DSA public key (lazily loaded, 1312 bytes) | ### ExtendedAddress Static Methods | Method | Returns | Description | |--------|---------|-------------| | `ExtendedAddress.dead()` | `ExtendedAddress` | Bitcoin block 0 pubkey-derived burn address | | `ExtendedAddress.zero()` | `ExtendedAddress` | All-zero address (cloned from constant) | | `ExtendedAddress.fromStringPair(schnorr, mldsa)` | `ExtendedAddress` | Create from two hex strings | | `ExtendedAddress.fromUint8Array(bytes)` | `ExtendedAddress` | Create from 64-byte array | | `ExtendedAddress.toCSV(address, blocks)` | `string` | Generate CSV timelocked P2WSH address | | `ExtendedAddress.p2wpkh(address)` | `string` | Generate P2WPKH address | ### ExtendedAddress Instance Methods | Method | Returns | Description | |--------|---------|-------------| | `p2tr()` | `string` | Generate P2TR (taproot) address | | `isDead()` | `bool` | Check if this is the dead address | | `isZero()` | `bool` | Check if ML-DSA key hash is all zeros | | `downCast()` | `Address` | Cast to base Address type | | `clone()` | `ExtendedAddress` | Create a deep copy | | `toString()` | `string` | Returns P2TR address (overrides base) | ### ExtendedAddress Properties | Property | Type | Description | |----------|------|-------------| | `tweakedPublicKey` | `Uint8Array` | Schnorr tweaked key (32 bytes) | | `mldsaPublicKey` | `Uint8Array` | ML-DSA public key (inherited, 1312 bytes) | --- **Navigation:** - Previous: [ReentrancyGuard](../contracts/reentrancy-guard.md) - Next: [SafeMath](./safe-math.md)