UNPKG

@btc-vision/btc-runtime

Version:

Bitcoin Smart Contract Runtime

886 lines (806 loc) 28.5 kB
import { u256 } from '@btc-vision/as-bignum/assembly'; import { BytesReader } from '../buffer/BytesReader'; import { BytesWriter } from '../buffer/BytesWriter'; import { OP_NET } from '../contracts/OP_NET'; import { NetEvent } from '../events/NetEvent'; import { Potential } from '../lang/Definitions'; import { Address } from '../types/Address'; import { ADDRESS_BYTE_LENGTH } from '../utils'; import { Block } from './classes/Block'; import { Transaction } from './classes/Transaction'; import { callContract, deployFromAddress, emit, env_exit, getAccountType, getBlockHash, getCallResult, loadPointer, log, sha256, storePointer, tLoadPointer, tStorePointer, validateBitcoinAddress, verifySchnorrSignature, } from './global'; import { eqUint, MapUint8Array } from '../generic/MapUint8Array'; import { EMPTY_BUFFER } from '../math/bytes'; import { Plugin } from '../plugins/Plugin'; import { Calldata } from '../types'; import { Revert } from '../types/Revert'; import { Selector } from '../math/abi'; import { Network, Networks } from '../script/Networks'; export * from '../env/global'; /** * CallResult encapsulates the outcome of a cross-contract call. * Contains both a success flag and the response data, enabling try-catch patterns. */ @final export class CallResult { constructor( public readonly success: boolean, public readonly data: BytesReader, ) {} } /** * BlockchainEnvironment - Core Runtime Environment for OP_NET Smart Contracts * * Provides the interface between smart contracts and the blockchain runtime, * managing storage, cross-contract calls, cryptographic operations, and execution context. * * @module BlockchainEnvironment */ @final export class BlockchainEnvironment { /** * Standard dead address for burn operations. * Assets sent here are permanently unrecoverable. */ public readonly DEAD_ADDRESS: Address = Address.dead(); private storage: MapUint8Array = new MapUint8Array(); private transientStorage: MapUint8Array = new MapUint8Array(); private _selfContract: Potential<OP_NET> = null; private _plugins: Plugin[] = []; private _network: Networks = Networks.Unknown; /** * Returns the current blockchain network identifier. * * @returns The network enum value (Mainnet, Testnet, etc.) * @throws {Revert} When network is not initialized * * @remarks * Determines address validation rules and network-specific constants. */ @inline public get network(): Networks { if (this._network === Networks.Unknown) { throw new Revert('Network is required'); } return this._network as Networks; } private _block: Potential<Block> = null; /** * Provides access to current block information. * * @returns Block object containing hash, number, and median time * @throws {Revert} When block context is not initialized * * @warning Block timestamps can vary ±900 seconds. Use median time for reliability. * Never use block properties for randomness generation. * * @example * ```typescript * const currentBlock = Blockchain.block; * const blockNumber = currentBlock.number; * const blockTime = currentBlock.medianTime; * ``` */ @inline public get block(): Block { if (!this._block) { throw new Revert('Block is required'); } return this._block as Block; } private _tx: Potential<Transaction> = null; /** * Provides access to current transaction information. * * @returns Transaction object with caller, origin, and tx identifiers * @throws {Revert} When transaction context is not initialized * * @warning tx.caller = immediate calling contract (changes in call chain) * tx.origin = original transaction initiator (stays constant) * Using tx.origin for authentication is usually wrong. * * @example * ```typescript * const tx = Blockchain.tx; * const directCaller = tx.caller; // Who called this function * const txInitiator = tx.origin; // Who started the transaction * ``` */ @inline public get tx(): Transaction { if (!this._tx) { throw new Revert('Transaction is required'); } return this._tx as Transaction; } private _contract: Potential<() => OP_NET> = null; /** * Returns the current contract instance. * * @returns The initialized OP_NET contract */ public get contract(): OP_NET { return this._selfContract as OP_NET; } /** * Sets the contract factory function for lazy initialization. * * @param contract - Factory function that creates the contract instance */ public set contract(contract: () => OP_NET) { this._contract = contract; this.createContractIfNotExists(); } private _nextPointer: u16 = 0; /** * Generates the next available storage pointer. * * @returns Unique pointer value for storage allocation * @throws {Revert} When pointer space is exhausted (after 65,535 allocations) * * @warning Limited to 65,535 storage slots per contract. * Use mappings for dynamic data to avoid exhaustion. * * @example * ```typescript * const ptr = Blockchain.nextPointer; // Gets next unique pointer * ``` */ public get nextPointer(): u16 { if (this._nextPointer === u16.MAX_VALUE) { throw new Revert(`Out of storage pointer.`); } this._nextPointer += 1; return this._nextPointer; } public _contractDeployer: Potential<Address> = null; /** * Returns the address that deployed this contract. * * @returns Deployer's address * @throws {Revert} When deployer is not set * * @remarks * Immutable after deployment. Often used for admin privileges. */ public get contractDeployer(): Address { if (!this._contractDeployer) { throw new Revert('Deployer is required'); } return this._contractDeployer as Address; } public _contractAddress: Potential<Address> = null; /** * Returns this contract's own address. * * @returns Current contract address * @throws {Revert} When address is not initialized * * @example * ```typescript * const selfAddress = Blockchain.contractAddress; * if (caller.equals(selfAddress)) { * // Recursive call detected * } * ``` */ public get contractAddress(): Address { if (!this._contractAddress) { throw new Revert('Contract address is required'); } return this._contractAddress as Address; } public _chainId: Potential<Uint8Array> = null; /** * Returns the blockchain's unique chain identifier. * * @returns 32-byte chain ID * @throws {Revert} When chain ID is not set * * @remarks * Used for replay protection and cross-chain message verification. */ public get chainId(): Uint8Array { if (!this._chainId) { throw new Revert('Chain id is required'); } return this._chainId as Uint8Array; } public _protocolId: Potential<Uint8Array> = null; /** * Returns the protocol version identifier. * * @returns 32-byte protocol ID * @throws {Revert} When protocol ID is not set */ public get protocolId(): Uint8Array { if (!this._protocolId) { throw new Revert('Protocol id is required'); } return this._protocolId as Uint8Array; } /** * Registers a plugin to extend contract functionality. * * @param plugin - Plugin instance to register * * @remarks * Plugins execute in registration order and have full access to contract state. */ public registerPlugin(plugin: Plugin): void { this._plugins.push(plugin); } /** * Handles contract deployment initialization. * * @param calldata - Deployment parameters * * @remarks * Called once during deployment. State changes here are permanent. */ public onDeployment(calldata: Calldata): void { for (let i: i32 = 0; i < this._plugins.length; i++) { const plugin = this._plugins[i]; plugin.onDeployment(calldata); } this.contract.onDeployment(calldata); } /** * Pre-execution hook called before method execution. * * @param selector - Method selector being called * @param calldata - Method parameters * * @remarks * Used for access control, reentrancy guards, and validation. */ public onExecutionStarted(selector: Selector, calldata: Calldata): void { for (let i: i32 = 0; i < this._plugins.length; i++) { const plugin = this._plugins[i]; plugin.onExecutionStarted(selector, calldata); } this.contract.onExecutionStarted(selector, calldata); } /** * Post-execution hook called after successful method execution. * * @param selector - Method selector that was called * @param calldata - Method parameters that were passed * * @remarks * Only called on successful execution. Used for cleanup and events. */ public onExecutionCompleted(selector: Selector, calldata: Calldata): void { for (let i: i32 = 0; i < this._plugins.length; i++) { const plugin = this._plugins[i]; plugin.onExecutionCompleted(selector, calldata); } this.contract.onExecutionCompleted(selector, calldata); } /** * Initializes the blockchain environment with runtime parameters. * * @param data - Encoded environment data from the runtime * * @remarks * Called automatically by the runtime to set up execution context. */ public setEnvironmentVariables(data: Uint8Array): void { const reader: BytesReader = new BytesReader(data); const blockHash = reader.readBytes(32); const blockNumber = reader.readU64(); const blockMedianTime = reader.readU64(); const txId = reader.readBytes(32); const txHash = reader.readBytes(32); const contractAddress = reader.readAddress(); const contractDeployer = reader.readAddress(); const caller = reader.readAddress(); const origin = reader.readAddress(); const chainId = reader.readBytes(32); const protocolId = reader.readBytes(32); this._tx = new Transaction(caller, origin, txId, txHash); this._contractDeployer = contractDeployer; this._contractAddress = contractAddress; this._chainId = chainId; this._protocolId = protocolId; this._network = Network.fromChainId(this.chainId); this._block = new Block(blockHash, blockNumber, blockMedianTime); this.createContractIfNotExists(); } /** * Executes a call to another contract with configurable failure handling. * * @param destinationContract - Target contract address * @param calldata - Encoded function call data * @param stopExecutionOnFailure - Whether to revert on call failure (default: true) * @returns CallResult with success flag and response data * * @example * ```typescript * // Traditional call - reverts on failure * const result = Blockchain.call(tokenAddress, calldata); * const balance = result.data.readU256(); * * // Try-catch pattern - handles failure gracefully * const result = Blockchain.call(unknownContract, calldata, false); * if (result.success) { * const data = result.data; * // Process successful response * } else { * // Handle failure without reverting * this.handleCallFailure(); * } * ``` * * @warning Follow checks-effects-interactions pattern to prevent reentrancy: * 1. Check conditions * 2. Update state * 3. Make external call * * @remarks * The stopExecutionOnFailure parameter enables try-catch style error handling. * When false, failed calls return success=false instead of reverting. */ public call( destinationContract: Address, calldata: BytesWriter, stopExecutionOnFailure: boolean = true, ): CallResult { if (!destinationContract) { throw new Revert('Destination contract is required'); } const resultLengthBuffer = new ArrayBuffer(32); const status = callContract( destinationContract.buffer, calldata.getBuffer().buffer, calldata.bufferLength(), resultLengthBuffer, ); const reader = new BytesReader(Uint8Array.wrap(resultLengthBuffer)); const resultLength = reader.readU32(true); const resultBuffer = new ArrayBuffer(resultLength); getCallResult(0, resultLength, resultBuffer); if (status !== 0 && stopExecutionOnFailure) { env_exit(status, resultBuffer, resultLength); } return new CallResult(status === 0, new BytesReader(Uint8Array.wrap(resultBuffer))); } /** * Emits a log message for debugging. * * @param data - String message to log * * @warning ONLY AVAILABLE IN UNIT TESTING FRAMEWORK. * NOT available in production or testnet environments. * Will fail if called outside of testing context. * * @example * ```typescript * // Only in unit tests: * Blockchain.log("Debug: Transfer initiated"); * Blockchain.log(`Amount: ${amount.toString()}`); * ``` */ public log(data: string): void { const writer = new BytesWriter(String.UTF8.byteLength(data)); writer.writeString(data); const buffer = writer.getBuffer(); log(buffer.buffer, buffer.length); } /** * Emits a structured event for off-chain monitoring. * * @param event - NetEvent instance containing event data * * @remarks * Events are the primary mechanism for dApps to track state changes. * Events are not accessible within contracts. * * @example * ```typescript * class TransferredEvent extends NetEvent { * constructor(operator: Address, from: Address, to: Address, amount: u256) { * const data: BytesWriter = new BytesWriter(ADDRESS_BYTE_LENGTH * 3 + U256_BYTE_LENGTH); * data.writeAddress(operator); * data.writeAddress(from); * data.writeAddress(to); * data.writeU256(amount); * super('Transferred', data); * } * } * Blockchain.emit(new TransferredEvent(operator, sender, recipient, value)); * ``` */ public emit(event: NetEvent): void { const data = event.getEventData(); const writer = new BytesWriter( String.UTF8.byteLength(event.eventType) + 8 + data.byteLength, ); writer.writeStringWithLength(event.eventType); writer.writeBytesWithLength(data); emit(writer.getBuffer().buffer, writer.bufferLength()); } /** * Validates a Bitcoin address format for the current network. * * @param address - Bitcoin address string to validate * @returns true if valid for current network, false otherwise * * @warning Validation rules are network-specific: * - Mainnet: bc1, 1, 3 prefixes * - Testnet: tb1, m, n, 2 prefixes * * @example * ```typescript * if (!Blockchain.validateBitcoinAddress(userAddress)) { * throw new Revert("Invalid Bitcoin address"); * } * ``` */ public validateBitcoinAddress(address: string): bool { const writer = new BytesWriter(String.UTF8.byteLength(address)); writer.writeString(address); const result = validateBitcoinAddress(writer.getBuffer().buffer, address.length); return result === 1; } /** * Deploys a new contract using an existing contract as template. * * @param existingAddress - Template contract address * @param salt - Unique salt for deterministic addressing * @param calldata - Constructor parameters * @returns Address of newly deployed contract * @throws {Revert} When deployment fails * * @warning CREATE2 style deployment: * Same salt + template = same address. * Salt collision will cause deployment to fail. * * @example * ```typescript * const salt = u256.fromBytes(sha256("unique-id")); * const newToken = Blockchain.deployContractFromExisting( * templateAddress, * salt, * constructorData * ); * ``` */ public deployContractFromExisting( existingAddress: Address, salt: u256, calldata: BytesWriter, ): Address { const resultAddressBuffer = new ArrayBuffer(ADDRESS_BYTE_LENGTH); const callDataBuffer = calldata.getBuffer().buffer; const status = deployFromAddress( existingAddress.buffer, salt.toUint8Array(true).buffer, callDataBuffer, callDataBuffer.byteLength, resultAddressBuffer, ); if (status !== 0) { throw new Revert('Failed to deploy contract'); } const contractAddressReader = new BytesReader(Uint8Array.wrap(resultAddressBuffer)); return contractAddressReader.readAddress(); } /** * Reads a value from persistent storage. * * @param pointerHash - 32-byte storage key * @returns 32-byte stored value (zeros if unset) * * @warning Cannot distinguish between unset and explicitly set to zero. * Use hasStorageAt() to check existence. * * @example * ```typescript * const key = sha256("balance:" + address); * const balance = Blockchain.getStorageAt(key); * ``` */ public getStorageAt(pointerHash: Uint8Array): Uint8Array { this.hasPointerStorageHash(pointerHash); if (this.storage.has(pointerHash)) { return this.storage.get(pointerHash); } return new Uint8Array(32); } /** * Reads a value from transient storage (cleared after transaction). * * @param pointerHash - 32-byte storage key * @returns 32-byte stored value (zeros if unset) * * @warning NOT CURRENTLY ENABLED IN PRODUCTION. * Transient storage functionality is experimental and only available in testing. * Will fail if called in production or testnet environments. * Storage is cleared after each transaction when enabled. * * @example * ```typescript * // Reentrancy guard pattern (when enabled) * const GUARD_KEY = sha256("reentrancy"); * if (Blockchain.hasTransientStorageAt(GUARD_KEY)) { * throw new Revert("Reentrancy detected"); * } * Blockchain.setTransientStorageAt(GUARD_KEY, u256.One.toBytes()); * ``` */ public getTransientStorageAt(pointerHash: Uint8Array): Uint8Array { if (this.hasPointerTransientStorageHash(pointerHash)) { return this.transientStorage.get(pointerHash); } return new Uint8Array(32); } /** * Computes SHA-256 hash of input data. * * @param buffer - Data to hash * @returns 32-byte hash result * * @example * ```typescript * const hash = Blockchain.sha256(data); * ``` */ @inline public sha256(buffer: Uint8Array): Uint8Array { return sha256(buffer); } /** * Computes double SHA-256 (Bitcoin's hash256). * * @param buffer - Data to hash * @returns 32-byte double hash result * * @remarks * Standard Bitcoin hash function used for transaction IDs and block hashes. * * @example * ```typescript * const txHash = Blockchain.hash256(transactionData); * ``` */ @inline public hash256(buffer: Uint8Array): Uint8Array { return sha256(sha256(buffer)); } /** * Verifies a Schnorr signature (Bitcoin Taproot). * * @param publicKey - 32-byte public key * @param signature - 64-byte Schnorr signature * @param hash - 32-byte message hash * @returns true if signature is valid * * @warning Schnorr signatures differ from ECDSA: * - Linear aggregation properties * - Used in Taproot (post-2021) * * @example * ```typescript * const isValid = Blockchain.verifySchnorrSignature( * signer, * signature, * messageHash * ); * if (!isValid) throw new Revert("Invalid signature"); * ``` */ public verifySchnorrSignature( publicKey: Address, signature: Uint8Array, hash: Uint8Array, ): boolean { const result: u32 = verifySchnorrSignature(publicKey.buffer, signature.buffer, hash.buffer); return result === 1; } /** * Checks if an address is a contract (not an EOA). * * @param address - Address to check * @returns true if contract, false if EOA or uninitialized * * @warning Cannot distinguish between EOA with no transactions * and uninitialized address. * * @example * ```typescript * if (!Blockchain.isContract(targetAddress)) { * throw new Revert("Must be a contract"); * } * ``` */ @inline public isContract(address: Address): boolean { return getAccountType(address.buffer) !== 0; } /** * Checks if persistent storage slot has a non-zero value. * * @param pointerHash - 32-byte storage key * @returns true if slot contains non-zero value * * @warning Cannot distinguish between never written and explicitly set to zero. * * @example * ```typescript * const EXISTS_KEY = sha256("user:exists:" + address); * if (!Blockchain.hasStorageAt(EXISTS_KEY)) { * // First time user * } * ``` */ public hasStorageAt(pointerHash: Uint8Array): bool { const val: Uint8Array = this.getStorageAt(pointerHash); return !eqUint(val, EMPTY_BUFFER); } /** * Checks if transient storage slot has a non-zero value. * * @param pointerHash - 32-byte storage key * @returns true if slot contains non-zero value * * @warning NOT CURRENTLY ENABLED IN PRODUCTION. * Transient storage functionality is experimental and only available in testing. * Will fail if called in production or testnet environments. * * @remarks * Only reliable within single transaction context when enabled. */ public hasTransientStorageAt(pointerHash: Uint8Array): bool { const val: Uint8Array = this.getTransientStorageAt(pointerHash); return !eqUint(val, EMPTY_BUFFER); } /** * Writes a value to persistent storage. * * @param pointerHash - 32-byte storage key * @param value - Value to store (will be padded/truncated to 32 bytes) * * @warning Storage writes cost significant gas (~20,000). * Batch related updates when possible. * * @example * ```typescript * const key = sha256("balance:" + address); * Blockchain.setStorageAt(key, balance.toBytes()); * ``` */ @inline public setStorageAt(pointerHash: Uint8Array, value: Uint8Array): void { this._internalSetStorageAt(pointerHash, value); } /** * Writes a value to transient storage. * * @param pointerHash - 32-byte storage key * @param value - 32-byte value to store * @throws {Revert} If key or value is not exactly 32 bytes * * @warning NOT CURRENTLY ENABLED IN PRODUCTION. * Transient storage functionality is experimental and only available in testing. * Will fail if called in production or testnet environments. * Value must be exactly 32 bytes (no auto-padding). * Storage is cleared after transaction completes when enabled. * * @example * ```typescript * // Reentrancy lock (when enabled) * Blockchain.setTransientStorageAt(LOCK_KEY, u256.One.toBytes()); * // Lock automatically clears after transaction * ``` */ @inline public setTransientStorageAt(pointerHash: Uint8Array, value: Uint8Array): void { this._internalSetTransientStorageAt(pointerHash, value); } /** * Gets the account type identifier for an address. * * @param address - Address to query * @returns Account type code (0 = EOA/uninitialized, >0 = contract type) * * @remarks * Different contract types may have different codes. */ public getAccountType(address: Address): u32 { return getAccountType(address.buffer); } /** * Retrieves a historical block hash. * * @param blockNumber - Block number to query * @returns 32-byte block hash * * @warning Only recent blocks available (~256 blocks). * Older blocks return zero hash. * Do not use for randomness generation. * * @example * ```typescript * const oldBlock = Blockchain.block.number - 10; * const hash = Blockchain.getBlockHash(oldBlock); * ``` */ public getBlockHash(blockNumber: u64): Uint8Array { const hash = new ArrayBuffer(32); getBlockHash(blockNumber, hash); return Uint8Array.wrap(hash); } private createContractIfNotExists(): void { if (!this._contract) { throw new Revert('Contract is required'); } if (!this._selfContract) { this._selfContract = this._contract(); } } private _internalSetStorageAt(pointerHash: Uint8Array, value: Uint8Array): void { if (pointerHash.buffer.byteLength !== 32) { throw new Revert('Pointer must be 32 bytes long'); } let finalValue: Uint8Array = value; if (value.buffer.byteLength !== 32) { // Auto-pad values shorter than 32 bytes finalValue = new Uint8Array(32); for (let i = 0; i < value.buffer.byteLength && i < 32; i++) { finalValue[i] = value[i]; } } this.storage.set(pointerHash, finalValue); storePointer(pointerHash.buffer, finalValue.buffer); } private _internalSetTransientStorageAt(pointerHash: Uint8Array, value: Uint8Array): void { this.transientStorage.set(pointerHash, value); if (pointerHash.buffer.byteLength !== 32 || value.buffer.byteLength !== 32) { throw new Revert('Transient pointer and value must be 32 bytes long'); } tStorePointer(pointerHash.buffer, value.buffer); } private hasPointerStorageHash(pointer: Uint8Array): bool { if (pointer.buffer.byteLength !== 32) { throw new Revert('Pointer must be 32 bytes long'); } if (this.storage.has(pointer)) { return true; } const resultBuffer = new ArrayBuffer(32); loadPointer(pointer.buffer, resultBuffer); const value: Uint8Array = Uint8Array.wrap(resultBuffer); this.storage.set(pointer, value); // Cache for future reads return !eqUint(value, EMPTY_BUFFER); } private hasPointerTransientStorageHash(pointer: Uint8Array): bool { if (pointer.buffer.byteLength !== 32) { throw new Revert('Transient pointer must be 32 bytes long'); } if (this.transientStorage.has(pointer)) { return true; } const resultBuffer = new ArrayBuffer(32); tLoadPointer(pointer.buffer, resultBuffer); const value: Uint8Array = Uint8Array.wrap(resultBuffer); this.transientStorage.set(pointer, value); // Cache for future reads return !eqUint(value, EMPTY_BUFFER); } }