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.

1,387 lines (1,262 loc) 50.2 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, updateFromAddress, validateBitcoinAddress, verifySignature, } from './global'; import { eqUint, MapUint8Array } from '../generic/MapUint8Array'; import { EMPTY_BUFFER } from '../math/bytes'; import { Calldata } from '../types'; import { Revert } from '../types/Revert'; import { Selector } from '../math/abi'; import { Network, Networks } from '../script/Networks'; import { ExtendedAddress } from '../types/ExtendedAddress'; import { ECDSAKeyFormat, ECDSASubType, SignaturesMethods } from './consensus/Signatures'; import { MLDSAMetadata, MLDSASecurityLevel } from './consensus/MLDSAMetadata'; 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, ) {} } const SCRATCH_SIZE: i32 = 256; const SCRATCH_BUF: ArrayBuffer = new ArrayBuffer(SCRATCH_SIZE); const SCRATCH_VIEW: Uint8Array = Uint8Array.wrap(SCRATCH_BUF); const FOUR_BYTES_UINT8ARRAY_MEMORY_CACHE = new Uint8Array(4); /** * 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: ExtendedAddress = ExtendedAddress.dead(); private storage: MapUint8Array = new MapUint8Array(); private transientStorage: MapUint8Array = new MapUint8Array(); private _selfContract: Potential<OP_NET> = null; 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; } /** * Handles contract deployment initialization. * * @param calldata - Deployment parameters * * @remarks * Called once during deployment. Delegates to the contract's onDeployment * which handles plugin notification. */ public onDeployment(calldata: Calldata): void { this.contract.onDeployment(calldata); } /** * Handles contract bytecode update. * * @param calldata - Update parameters passed to updateContractFromExisting * * @remarks * Called when the contract's bytecode is updated. Delegates to the contract's * onUpdate which handles plugin notification and migration logic. */ public onUpdate(calldata: Calldata): void { this.contract.onUpdate(calldata); } /** * Pre-execution hook called before method execution. * * @param selector - Method selector being called * @param calldata - Method parameters * * @remarks * Delegates to the contract's onExecutionStarted which handles plugin notification. */ public onExecutionStarted(selector: Selector, calldata: Calldata): void { 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 * Delegates to the contract's onExecutionCompleted which handles plugin notification. */ public onExecutionCompleted(selector: Selector, calldata: Calldata): void { 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 { // BytesReader is unavoidable for parsing complex external struct 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.readBytesArray(32); const chainId = reader.readBytes(32); const protocolId = reader.readBytes(32); const tweakedPublicKey = reader.readBytesArray(ADDRESS_BYTE_LENGTH); const consensusFlags = reader.readU64(); const originAddress = new ExtendedAddress(tweakedPublicKey, origin); this._tx = new Transaction(caller, originAddress, txId, txHash, consensusFlags); 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'); } // This creates the underlying ArrayBuffer AND gives us a 'dataStart' pointer for free. const status = callContract( destinationContract.buffer, calldata.getBuffer().buffer, calldata.bufferLength(), FOUR_BYTES_UINT8ARRAY_MEMORY_CACHE.buffer, // Pass the underlying ArrayBuffer to the host ); // OPTIMIZATION: Read raw memory directly using load<u32> // We use .dataStart to get the raw pointer to the payload. const resultLength = bswap<u32>(load<u32>(FOUR_BYTES_UINT8ARRAY_MEMORY_CACHE.dataStart)); 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 eventType = event.eventType; const typeLen = String.UTF8.byteLength(eventType); // Structure: [4 bytes type len] + [type bytes] + [4 bytes data len] + [data bytes] const totalLen = 8 + typeLen + data.length; const writer = new Uint8Array(totalLen); const ptr = writer.dataStart; // Write type length (BE) store<u32>(ptr, bswap<u32>(typeLen)); // Write type string String.UTF8.encodeUnsafe(changetype<usize>(eventType), eventType.length, ptr + 4); // Write data length (BE) const offset = 4 + typeLen; store<u32>(ptr + offset, bswap<u32>(data.length)); // Write data bytes (Safe memory copy) memory.copy(ptr + offset + 4, data.dataStart, data.length); emit(writer.buffer, totalLen); } /** * 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 len = String.UTF8.byteLength(address); if (len <= SCRATCH_SIZE) { String.UTF8.encodeUnsafe( changetype<usize>(address), address.length, SCRATCH_VIEW.dataStart, ); return validateBitcoinAddress(SCRATCH_BUF, len) === 1; } else { const writer = new BytesWriter(len); writer.writeString(address); return validateBitcoinAddress(writer.getBuffer().buffer, len) === 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(); } /** * Updates this contract's bytecode from an existing deployed contract. * * This method triggers a bytecode replacement where the calling contract's execution * logic is replaced with the bytecode from the source contract. The new bytecode * takes effect at the next block. * * @param sourceAddress - Address of the contract containing the new bytecode * @param [calldata] - Optional parameters passed to the new bytecode's onUpdate method * @throws {Revert} When the source address is invalid or the update fails * * @warning This is a privileged operation with significant implications: * - Storage layout compatibility is entirely the developer's responsibility * - The contract address and all storage slots persist unchanged * - Only the execution logic changes * - The source contract must be an already-deployed contract * * @remarks * Contracts should implement their own permission checks and optional timelock * patterns before calling this method. A recommended pattern is: * * 1. `submitUpdate(address)` - Logs source address and block number, validates * that the address is an existing deployed contract * 2. `applyUpdate(address)` - Can only be called after a configured delay, * verifies address matches the submitted one, then calls this method * * This pattern gives users time to assess pending changes and exit if needed. * * @example * ```typescript * // Simple immediate update (not recommended for production) * public update(calldata: Calldata): BytesWriter { * this.onlyDeployer(Blockchain.tx.sender); * const newBytecodeAddress = calldata.readAddress(); * Blockchain.updateContractFromExisting(newBytecodeAddress); * return new BytesWriter(0); * } * ``` * * @example * ```typescript * // Timelock pattern (recommended) * private pendingUpdatePointer: u16 = Blockchain.nextPointer; * private pendingUpdate: StoredAddress = new StoredAddress(this.pendingUpdatePointer); * * private pendingUpdateBlockPointer: u16 = Blockchain.nextPointer; * private pendingUpdateBlock: StoredU64 = new StoredU64(this.pendingUpdateBlockPointer); * * private readonly UPDATE_DELAY: u64 = 144; // ~1 day in blocks * * public submitUpdate(calldata: Calldata): BytesWriter { * this.onlyDeployer(Blockchain.tx.sender); * const sourceAddress = calldata.readAddress(); * * // Validate source is an existing contract * if (!Blockchain.isContract(sourceAddress)) { * throw new Revert('Source must be a deployed contract'); * } * * this.pendingUpdate.value = sourceAddress; * this.pendingUpdateBlock.value = Blockchain.block.number; * return new BytesWriter(0); * } * * public applyUpdate(calldata: Calldata): BytesWriter { * this.onlyDeployer(Blockchain.tx.sender); * const sourceAddress = calldata.readAddress(); * * // Verify address matches pending update * if (!sourceAddress.equals(this.pendingUpdate.value)) { * throw new Revert('Address does not match pending update'); * } * * // Verify delay has passed * const submitBlock = this.pendingUpdateBlock.value; * if (Blockchain.block.number < submitBlock + this.UPDATE_DELAY) { * throw new Revert('Update delay not elapsed'); * } * * Blockchain.updateContractFromExisting(sourceAddress); * return new BytesWriter(0); * } * ``` */ public updateContractFromExisting( sourceAddress: Address, calldata: BytesWriter | null = null, ): void { if (!sourceAddress) { throw new Revert('Source address is required'); } if (!this.isContract(sourceAddress)) { throw new Revert('Source address must be a deployed contract'); } const calldataBuffer = calldata ? calldata.getBuffer().buffer : new ArrayBuffer(0); const status = updateFromAddress( sourceAddress.buffer, calldataBuffer, calldataBuffer.byteLength, ); if (status !== 0) { throw new Revert('Failed to update contract bytecode'); } } /** * 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) * * @deprecated Use Blockchain.verifySignature() instead for automatic consensus migration. * verifySignature() supports both Schnorr and ML-DSA signatures with proper * consensus flag handling for quantum resistance transitions. * * @example * ```typescript * const isValid = Blockchain.verifySchnorrSignature( * signer, * signature, * messageHash * ); * if (!isValid) throw new Revert("Invalid signature"); * ``` */ public verifySchnorrSignature( publicKey: ExtendedAddress, signature: Uint8Array, hash: Uint8Array, ): boolean { WARNING( 'verifySchnorrSignature is deprecated. Use verifySignature() for consensus-aware signature verification and quantum resistance support.', ); return this.internalVerifySchnorr(publicKey, signature, hash); } /** * Verifies an ML-DSA signature (quantum-resistant). * * @param level - Security level (MLDSASecurityLevel.Level2: ML-DSA-44, MLDSASecurityLevel.Level3: ML-DSA-65, MLDSASecurityLevel.Level5: ML-DSA-87) * @param publicKey - ML-DSA public key (1312/1952/2592 bytes based on level) * @param signature - ML-DSA signature (2420/3309/4627 bytes based on level) * @param hash - 32-byte message hash * @returns true if signature is valid * * @warning ML-DSA provides quantum resistance: * - NIST standardized lattice-based signatures * - Larger keys/signatures than classical algorithms * - Security levels: 2 (ML-DSA-44), 3 (ML-DSA-65), 5 (ML-DSA-87) * * @throws {Revert} If public key length doesn't match level * @throws {Revert} If signature length doesn't match level * @throws {Revert} If hash is not 32 bytes * * @example * ```typescript * // ML-DSA-44 (security level 2) * const isValid = Blockchain.verifyMLDSASignature( * MLDSASecurityLevel.Level2, // level 0 = ML-DSA-44 * mldsaPublicKey, // 1312 bytes * mldsaSignature, // 2420 bytes * messageHash // 32 bytes * ); * if (!isValid) throw new Revert("Invalid ML-DSA signature"); * ``` * * @example * ```typescript * // ML-DSA-87 (highest security level 5) * const isValid = Blockchain.verifyMLDSASignature( * MLDSASecurityLevel.Level5, // level 2 = ML-DSA-87 * mldsaPublicKey, // 2592 bytes * mldsaSignature, // 4627 bytes * messageHash // 32 bytes * ); * ``` */ public verifyMLDSASignature( level: MLDSASecurityLevel, publicKey: Uint8Array, signature: Uint8Array, hash: Uint8Array, ): boolean { const publicKeyLength = MLDSAMetadata.fromLevel(level); if (publicKey.length !== (publicKeyLength as i32)) { throw new Revert(`Invalid ML-DSA public key length.`); } if (signature.length !== MLDSAMetadata.signatureLen(publicKeyLength)) { throw new Revert(`Invalid ML-DSA signature length.`); } if (hash.length !== 32) { throw new Revert(`Invalid hash length.`); } const bufferLen = 2 + publicKey.length; const writer = new Uint8Array(bufferLen); const ptr = writer.dataStart; // Single bytes - Endianness irrelevant store<u8>(ptr, <u8>SignaturesMethods.MLDSA); store<u8>(ptr + 1, <u8>level); // Byte array copy memory.copy(ptr + 2, publicKey.dataStart, publicKey.length); const result: u32 = verifySignature(writer.buffer, signature.buffer, hash.buffer); return result === 1; } /** * Verifies a signature based on current consensus rules. * * This method automatically selects the appropriate signature verification algorithm * based on the current consensus state: * * - When `unsafeSignaturesAllowed()` returns `true`: Uses Schnorr signatures (quantum-vulnerable) * - When `unsafeSignaturesAllowed()` returns `false`: Uses ML-DSA signatures (quantum-resistant) * * The `unsafeSignaturesAllowed()` flag indicates whether the network is still accepting * legacy Schnorr signatures. This flag will be `true` during the transition period to * maintain backwards compatibility with existing infrastructure. Once the network completes * its quantum-resistant update, this flag will permanently become `false`, enforcing * ML-DSA signatures exclusively. * * @param address - The address containing the public key(s) to verify against. * For Schnorr, uses the taproot tweaked public key. * For ML-DSA, uses the ML-DSA public key component. * @param signature - The signature bytes to verify. Format depends on algorithm: * - Schnorr: 64-byte signature * - ML-DSA-44 (Level 2): 2420 bytes * - ML-DSA-65 (Level 3): 3309 bytes * - ML-DSA-87 (Level 5): 4627 bytes * @param hash - The 32-byte message hash that was signed. Usually a SHA256 hash * of the transaction data or message being verified. * * @param signatureType - Optional parameter to explicitly specify the signature type for verification. * * @returns `true` if the signature is valid for the given address and hash, * `false` if verification fails or if the signature format is invalid * * @throws May throw if the signature or hash have invalid lengths for the selected * algorithm, though implementations should generally return false instead * * @remarks * The consensus rules determine which signature scheme is active: * - Pre-quantum era: Schnorr signatures (Bitcoin taproot compatible) * - Post-quantum era: ML-DSA Level 2 (NIST standardized, 128-bit quantum security) * * ML-DSA Level 2 (ML-DSA-44) corresponds to NIST security category 2, providing security * equivalent to AES-128 against both classical and quantum attacks. Its security is based on * the hardness of underlying lattice problems and is designed to resist attacks from quantum * computers, including both Shor's algorithm (which breaks RSA/ECC) and Grover's algorithm * (which reduces symmetric key security). The security levels (2, 3, 5) correspond to the * number of quantum gates required for Grover's algorithm to break them, equivalent to * AES-128, AES-192, and AES-256 respectively. * * @example * ```typescript * const isValid = contract.verifySignature( * senderAddress, * signatureBytes, * transactionHash * ); * if (!isValid) { * throw new Error("Invalid signature"); * } * ``` */ public verifySignature( address: ExtendedAddress, signature: Uint8Array, hash: Uint8Array, signatureType: SignaturesMethods = SignaturesMethods.Schnorr, ): boolean { if ( this.tx.consensus.unsafeSignaturesAllowed() && signatureType !== SignaturesMethods.MLDSA ) { if (signatureType === SignaturesMethods.Schnorr) { return this.internalVerifySchnorr(address, signature, hash); } else if (signatureType === SignaturesMethods.ECDSA) { throw new Revert( 'ECDSA verification is not supported by verifySignature(). Use verifyECDSASignature() or verifyBitcoinECDSASignature() instead.', ); } } else { return this.verifyMLDSASignature( MLDSASecurityLevel.Level2, address.mldsaPublicKey, signature, hash, ); } throw new Revert( 'Invalid signature type or signatures schema not allowed under current consensus rules', ); } /** * 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); } /** * Verifies an ECDSA signature using the Ethereum ecrecover model (secp256k1). * * Recovers the signer public key from (hash, r, s, v) and compares against * the provided public key material. Signature must be 65 bytes in Ethereum * wire format: r (32) || s (32) || v (1). * * Accepted public key formats: * 33 bytes -> compressed SEC1 (0x02/0x03 prefix) * 64 bytes -> raw uncompressed (x || y, no prefix) * 65 bytes -> uncompressed SEC1 (0x04 prefix) or hybrid SEC1 (0x06/0x07 prefix) * * @param publicKey - The public key to verify against * @param signature - 65-byte ECDSA signature: r (32) || s (32) || v (1) * @param hash - 32-byte message hash (typically keccak256 of EIP-191/EIP-712 payload) * @returns true if ecrecover(hash, v, r, s) produces a key matching publicKey * * @throws {Revert} If signature is not exactly 65 bytes * @throws {Revert} If hash is not exactly 32 bytes * @throws {Revert} If publicKey length/prefix is invalid * * @deprecated Use Blockchain.verifySignature() for consensus-aware verification. */ public verifyECDSASignature( publicKey: Uint8Array, signature: Uint8Array, hash: Uint8Array, ): boolean { if (!this.tx.consensus.unsafeSignaturesAllowed()) { throw new Revert( 'ECDSA signatures are not allowed under current consensus rules. Use verifySignature() for consensus-aware verification.', ); } WARNING( 'verifyECDSASignature is deprecated. Use verifySignature() for consensus-aware signature verification and quantum resistance support.', ); return this.internalVerifyECDSA(publicKey, signature, hash, ECDSASubType.Ethereum); } /** * Verifies an ECDSA signature using the Bitcoin direct verification model (secp256k1). * * Verifies the signature directly against the provided public key without * key recovery. The host enforces BIP-0062 low-S normalization to reject * signature malleability. Signature must be 64 bytes compact: r (32) || s (32). * * Accepted public key formats: * 33 bytes -> compressed SEC1 (0x02/0x03 prefix) * 64 bytes -> raw uncompressed (x || y, no prefix) * 65 bytes -> uncompressed SEC1 (0x04 prefix) or hybrid SEC1 (0x06/0x07 prefix) * * @param publicKey - The public key to verify against * @param signature - 64-byte compact ECDSA signature: r (32) || s (32) * @param hash - 32-byte message hash (typically SHA-256 double hash for Bitcoin) * @returns true if the signature is valid for the given key and hash * * @throws {Revert} If signature is not exactly 64 bytes * @throws {Revert} If hash is not exactly 32 bytes * @throws {Revert} If publicKey length/prefix is invalid * * @deprecated Use Blockchain.verifySignature() for consensus-aware verification. */ public verifyBitcoinECDSASignature( publicKey: Uint8Array, signature: Uint8Array, hash: Uint8Array, ): boolean { if (!this.tx.consensus.unsafeSignaturesAllowed()) { throw new Revert( 'ECDSA signatures are not allowed under current consensus rules. Use verifySignature() for consensus-aware verification.', ); } WARNING( 'verifyBitcoinECDSASignature is deprecated. Use verifySignature() for consensus-aware signature verification and quantum resistance support.', ); return this.internalVerifyECDSA(publicKey, signature, hash, ECDSASubType.Bitcoin); } /** * Internal ECDSA verification that dispatches to the host with the correct * sub-type byte so the host knows which verification model to use. * * Host buffer layout: [type(1), subtype(1), format(1), ...pubkey_material] * type = SignaturesMethods.ECDSA (0x00) * subtype = ECDSASubType.Ethereum (0x00) or ECDSASubType.Bitcoin (0x01) * format = ECDSAKeyFormat tag describing the public key encoding * pubkey = raw SEC1 bytes (33, 64, or 65 bytes depending on format) */ private internalVerifyECDSA( pubKey: Uint8Array, signature: Uint8Array, hash: Uint8Array, subType: ECDSASubType, ): boolean { if (subType === ECDSASubType.Ethereum) { if (signature.length !== 65) throw new Revert( 'Invalid ECDSA signature length. Ethereum format requires exactly 65 bytes (r32 || s32 || v1).', ); } else if (subType === ECDSASubType.Bitcoin) { if (signature.length !== 64) throw new Revert( 'Invalid ECDSA signature length. Bitcoin format requires exactly 64 bytes (r32 || s32).', ); } else { throw new Revert('Unsupported ECDSA sub-type.'); } if (hash.length !== 32) throw new Revert('Invalid hash length.'); this.validateSecp256k1PublicKey(pubKey); const keyLen: i32 = pubKey.length; let formatTag: ECDSAKeyFormat; if (keyLen === 33) { formatTag = ECDSAKeyFormat.Compressed; } else if (keyLen === 64) { formatTag = ECDSAKeyFormat.Raw; } else if (pubKey[0] === 0x06 || pubKey[0] === 0x07) { formatTag = ECDSAKeyFormat.Hybrid; } else { formatTag = ECDSAKeyFormat.Uncompressed; } const bufferLen: i32 = 3 + keyLen; const buffer = new Uint8Array(bufferLen); const ptr = buffer.dataStart; store<u8>(ptr, <u8>SignaturesMethods.ECDSA); store<u8>(ptr + 1, <u8>subType); store<u8>(ptr + 2, <u8>formatTag); memory.copy(ptr + 3, pubKey.dataStart, keyLen); return verifySignature(buffer.buffer, signature.buffer, hash.buffer) === 1; } /** * Validates that a byte array is a well-formed secp256k1 public key. * * Accepted formats: * 33 bytes with 0x02 or 0x03 prefix -> compressed SEC1 * 64 bytes (any first byte) -> raw uncompressed (x || y, no prefix) * 65 bytes with 0x04 prefix -> uncompressed SEC1 * 65 bytes with 0x06 or 0x07 prefix -> hybrid SEC1 * * @param key - The raw public key bytes to validate * @throws {Revert} If the key length or prefix is not a recognized format */ private validateSecp256k1PublicKey(key: Uint8Array): void { const keyLen: i32 = key.length; if (keyLen === 33) { const prefix: u8 = key[0]; if (prefix !== 0x02 && prefix !== 0x03) { throw new Revert('Invalid compressed public key prefix. Expected 0x02 or 0x03.'); } } else if (keyLen === 64) { // Raw uncompressed: 32-byte X || 32-byte Y, no prefix byte. // No prefix validation needed since the first byte is part of the X coordinate. } else if (keyLen === 65) { const prefix: u8 = key[0]; if (prefix !== 0x04 && prefix !== 0x06 && prefix !== 0x07) { throw new Revert( 'Invalid uncompressed public key prefix. Expected 0x04, 0x06, or 0x07.', ); } } else { throw new Revert( 'Invalid ECDSA public key length. Accepted: 33 (compressed), 64 (raw), or 65 (uncompressed/hybrid).', ); } } private internalVerifySchnorr( publicKey: ExtendedAddress, signature: Uint8Array, hash: Uint8Array, ): boolean { if (signature.length !== 64) throw new Revert(`Invalid signature length.`); if (hash.length !== 32) throw new Revert(`Invalid hash length.`); // 1 byte prefix + 32 bytes address const totalLen = 1 + ADDRESS_BYTE_LENGTH; const buffer = new Uint8Array(totalLen); const ptr = buffer.dataStart; store<u8>(ptr, <u8>SignaturesMethods.Schnorr); memory.copy(ptr + 1, publicKey.tweakedPublicKey.dataStart, ADDRESS_BYTE_LENGTH); return verifySignature(buffer.buffer, signature.buffer, hash.buffer) === 1; } 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.length !== 32) { throw new Revert('Pointer must be 32 bytes long'); } let finalValue: Uint8Array = value; if (value.length !== 32) { // Optimization: Pad manually using memory.copy to avoid loop finalValue = new Uint8Array(32); const len = value.length < 32 ? value.length : 32; memory.copy(finalValue.dataStart, value.dataStart, len); } 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);