@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
text/typescript
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);