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.

819 lines (658 loc) 28.2 kB
# Signature Verification OP_NET supports multiple signature schemes for authentication and authorization. This guide covers Schnorr signatures, ECDSA (secp256k1), quantum-resistant ML-DSA, and common verification patterns. ## Overview ```typescript import { Blockchain, SignaturesMethods } from '@btc-vision/btc-runtime/runtime'; // Consensus-aware signature verification (recommended) // Uses Schnorr during transition period, ML-DSA after quantum deadline const isValid: bool = Blockchain.verifySignature( Blockchain.tx.origin, // ExtendedAddress signature, // Signature bytes messageHash, // 32-byte message hash SignaturesMethods.Schnorr // Signature type (default) ); // Force quantum-resistant verification (always uses ML-DSA) const isValidQuantum: bool = Blockchain.verifySignature( Blockchain.tx.origin, signature, messageHash, SignaturesMethods.MLDSA // Force ML-DSA regardless of consensus flags ); // ECDSA verification (deprecated, Ethereum ecrecover model) const isValidECDSA: bool = Blockchain.verifyECDSASignature( publicKey, // 33, 64, or 65-byte secp256k1 public key signature, // 65-byte signature: r(32) || s(32) || v(1) messageHash, // 32-byte message hash (typically keccak256) ); // ECDSA verification (deprecated, Bitcoin direct verify model) const isValidBTC: bool = Blockchain.verifyBitcoinECDSASignature( publicKey, // 33, 64, or 65-byte secp256k1 public key signature, // 64-byte compact signature: r(32) || s(32) messageHash, // 32-byte message hash (typically SHA-256 double hash) ); ``` ## Signature Scheme Comparison OP_NET supports Schnorr, ECDSA (secp256k1), and quantum-resistant ML-DSA signatures: ```mermaid --- config: theme: dark --- flowchart LR subgraph OP_NET["OP_NET Signature Verification"] subgraph ECDSA["ECDSA - Legacy (Deprecated)"] E1["Public Key: 33/64/65 bytes"] E2["Signature: 64 or 65 bytes"] E3["Security: Classical only"] E4["Status: Deprecated"] end subgraph Schnorr["Schnorr - Traditional"] S1["Public Key: 32 bytes"] S2["Signature: 64 bytes"] S3["Security: Classical only"] S4["Status: Deprecated"] end subgraph MLDSA["ML-DSA - Quantum-Resistant"] M1["Public Key: 1,312 bytes"] M2["Signature: 2,420 bytes"] M3["Security: Post-quantum"] M4["Status: Recommended"] end Q["Quantum Computer Threat"] -.->|"Breaks"| E3 Q -.->|"Breaks"| S3 Q -.->|"Cannot break"| M3 end ``` ## The verifySignature Method The recommended approach for all signature verification: ```typescript import { SignaturesMethods } from '@btc-vision/btc-runtime/runtime'; Blockchain.verifySignature( address: ExtendedAddress, // Signer's address (contains both key references) signature: Uint8Array, // Signature bytes hash: Uint8Array, // 32-byte message hash signatureType: SignaturesMethods = SignaturesMethods.Schnorr // Signature type ): boolean ``` **Important:** The first parameter must be an `ExtendedAddress`, not a plain `Address`. Use `Blockchain.tx.origin` which returns `ExtendedAddress` for verifying the transaction originator's signature. The `ExtendedAddress` type contains both: - `tweakedPublicKey` (32 bytes) - for Schnorr/Taproot signatures - `mldsaPublicKey` (1,312 bytes for Level2) - for quantum-resistant ML-DSA signatures **Behavior:** - `SignaturesMethods.Schnorr` (default): Uses Schnorr verification if `UNSAFE_QUANTUM_SIGNATURES_ALLOWED` consensus flag is set, otherwise falls back to ML-DSA - `SignaturesMethods.MLDSA`: Always uses ML-DSA (quantum-resistant) verification with ML-DSA-44 (Level2) - `SignaturesMethods.ECDSA`: Emits a deprecation error recommending migration to ML-DSA. Use `verifyECDSASignature()` or `verifyBitcoinECDSASignature()` directly instead The method automatically: 1. Loads the appropriate public key from the address 2. Selects the correct verification algorithm based on the `signatureType` parameter and consensus rules 3. Handles all internal key formatting 4. Throws a `Revert` if the signature type is not allowed under current consensus rules ## Schnorr Verification When using Schnorr signatures (during transition period), the verification follows BIP340: ```mermaid --- config: theme: dark --- sequenceDiagram participant Contract as Contract participant Blockchain as OP_NET Runtime participant SchnorrVerifier as Schnorr Verifier participant ExtendedAddress as ExtendedAddress Contract->>Blockchain: verifySignature(address, sig, hash, Schnorr) Note over Blockchain: Check consensus flags Blockchain->>Blockchain: UNSAFE_QUANTUM_SIGNATURES_ALLOWED? Blockchain->>ExtendedAddress: Load tweakedPublicKey ExtendedAddress-->>Blockchain: 32-byte Schnorr key Blockchain->>SchnorrVerifier: verify(pubkey, signature, hash) Note over SchnorrVerifier: BIP340 verification SchnorrVerifier->>SchnorrVerifier: Compute R from signature SchnorrVerifier->>SchnorrVerifier: Compute challenge e SchnorrVerifier->>SchnorrVerifier: Verify s*G = R + e*P SchnorrVerifier-->>Blockchain: valid: bool Blockchain-->>Contract: result: bool ``` ### Low-Level Schnorr Verification (Deprecated) ```typescript // Deprecated - use Blockchain.verifySignature() instead const isValid = Blockchain.verifySchnorrSignature( extendedAddress, // ExtendedAddress (contains tweaked public key) signature, // 64-byte Schnorr signature messageHash // 32-byte message hash ); ``` ## ML-DSA Verification When using quantum-resistant ML-DSA signatures, the verification follows FIPS 204: ```mermaid --- config: theme: dark --- sequenceDiagram participant Contract as Contract participant Blockchain as OP_NET Runtime participant MLDSAVerifier as ML-DSA Verifier participant Address as Address Contract->>Blockchain: verifySignature(address, sig, hash, MLDSA) Note over Blockchain: signatureType = MLDSA Blockchain->>Address: Load mldsaPublicKey Note over Address: SHA256 hash stored in address Address->>Address: Lazy load full public key Address-->>Blockchain: 1,312-byte ML-DSA public key Blockchain->>MLDSAVerifier: verify(Level2, pubkey, sig, hash) Note over MLDSAVerifier: FIPS 204 ML-DSA-44 MLDSAVerifier->>MLDSAVerifier: Decode signature (s1, s2, h) MLDSAVerifier->>MLDSAVerifier: Reconstruct w' from h MLDSAVerifier->>MLDSAVerifier: Compute Az - t*2^d*s2 MLDSAVerifier->>MLDSAVerifier: Verify lattice bounds MLDSAVerifier-->>Blockchain: valid: bool Blockchain-->>Contract: result: bool ``` ### Direct ML-DSA Verification ```typescript import { MLDSASecurityLevel } from '@btc-vision/btc-runtime/runtime'; const isValid = Blockchain.verifyMLDSASignature( MLDSASecurityLevel.Level2, // Security level signer.mldsaPublicKey, // ML-DSA public key (auto-loaded from address) signature, // 2420-byte signature (for Level2) messageHash // 32-byte message hash ); ``` ### ML-DSA Security Levels | Level | Name | Public Key | Signature | NIST Category | |-------|------|------------|-----------|---------------| | Level2 | ML-DSA-44 | 1,312 bytes | 2,420 bytes | Category 2 (~AES-128) | | Level3 | ML-DSA-65 | 1,952 bytes | 3,309 bytes | Category 3 (~AES-192) | | Level5 | ML-DSA-87 | 2,592 bytes | 4,627 bytes | Category 5 (~AES-256) | **OP_NET uses ML-DSA-44 (Level2) by default.** ## ECDSA Verification (Deprecated) OP_NET now supports ECDSA (secp256k1) signatures for backward compatibility with Ethereum and Bitcoin ecosystems. These methods are **deprecated** and only available when `UNSAFE_QUANTUM_SIGNATURES_ALLOWED` consensus flag is set. ### Ethereum ECDSA (ecrecover model) ```typescript // Verifies using Ethereum ecrecover: recovers signer from (hash, v, r, s) const isValid: bool = Blockchain.verifyECDSASignature( publicKey, // secp256k1 public key (33, 64, or 65 bytes) signature, // 65-byte signature: r(32) || s(32) || v(1) messageHash // 32-byte message hash (typically keccak256) ); ``` ### Bitcoin ECDSA (direct verify model) ```typescript // Verifies directly against public key, enforces BIP-0062 low-S normalization const isValid: bool = Blockchain.verifyBitcoinECDSASignature( publicKey, // secp256k1 public key (33, 64, or 65 bytes) signature, // 64-byte compact signature: r(32) || s(32) messageHash // 32-byte message hash (typically SHA-256 double hash) ); ``` ### ECDSA Sub-Types | Sub-Type | Model | Signature Size | Description | |----------|-------|---------------|-------------| | `ECDSASubType.Ethereum` | ecrecover | 65 bytes (r32 \|\| s32 \|\| v1) | Recovers signer public key from signature | | `ECDSASubType.Bitcoin` | Direct verify | 64 bytes (r32 \|\| s32) | Verifies directly against provided public key | ### Accepted Public Key Formats Both ECDSA methods accept secp256k1 public keys in these formats: | Format | Size | Prefix | Description | |--------|------|--------|-------------| | Compressed | 33 bytes | `0x02` or `0x03` | Standard SEC1 compressed | | Raw | 64 bytes | None | Raw X \|\| Y coordinates, no prefix | | Uncompressed | 65 bytes | `0x04` | Standard SEC1 uncompressed | | Hybrid | 65 bytes | `0x06` or `0x07` | SEC1 hybrid (rewritten to `0x04` on host) | ### ECDSA Deprecation Warning Both ECDSA methods emit a runtime `WARNING` and are gated behind the `UNSAFE_QUANTUM_SIGNATURES_ALLOWED` consensus flag. They will throw a `Revert` if called when unsafe signatures are not allowed. Contracts should migrate to `verifySignature()` with ML-DSA for long-term quantum security. ## Keccak-256 Hashing OP_NET includes a built-in Keccak-256 implementation (Ethereum-compatible, pre-NIST). This is useful for ECDSA-related workflows, Ethereum-style function selectors, and EIP-712 typed data hashing. ```typescript import { keccak256, keccak256Concat, functionSelector, ethAddressFromPubKey } from '@btc-vision/btc-runtime/runtime'; // Basic keccak256 hash const hash: Uint8Array = keccak256(data); // 32-byte digest // Hash concatenated byte arrays (common for abi.encodePacked patterns) const hash2: Uint8Array = keccak256Concat(a, b); // Compute 4-byte Ethereum function selector const sel: Uint8Array = functionSelector('transfer(address,uint256)'); // sel == 0xa9059cbb // Derive Ethereum address from 64-byte uncompressed public key const addr: Uint8Array = ethAddressFromPubKey(publicKey64); // 20-byte address ``` **Important:** This is original Keccak-256 (as used by Ethereum), NOT NIST SHA-3-256. The difference is the domain separation padding byte: Keccak uses `0x01`, SHA-3 uses `0x06`. ## Message Hash Construction When building message hashes for signature verification, use domain separation to prevent cross-contract signature reuse: ```mermaid --- config: theme: dark --- flowchart LR A["Domain Separator"] --> B["Hash Domain"] C["Struct Data"] --> D["Hash Struct"] B --> E["Combine"] D --> E E --> F["Final Hash"] G["Domain Components"] --> B H["Message Components"] --> D ``` ### Domain Separator ```typescript function buildDomainSeparator( name: string, version: string, chainId: u256, contractAddress: Address ): Uint8Array { const writer = new BytesWriter(256); // EIP-712 domain typehash writer.writeBytes(sha256( encodeString('EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)') )); // Domain values writer.writeBytes(sha256(encodeString(name))); writer.writeBytes(sha256(encodeString(version))); writer.writeU256(chainId); writer.writeAddress(contractAddress); return sha256(writer.getBuffer()); } ``` ### Permit Message Hash ```typescript function buildPermitHash( domainSeparator: Uint8Array, owner: Address, spender: Address, value: u256, nonce: u256, deadline: u64 ): Uint8Array { const PERMIT_TYPEHASH = sha256( encodeString('Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)') ); // Build struct hash const structWriter = new BytesWriter(192); structWriter.writeBytes(PERMIT_TYPEHASH); structWriter.writeAddress(owner); structWriter.writeAddress(spender); structWriter.writeU256(value); structWriter.writeU256(nonce); structWriter.writeU64(deadline); const structHash = sha256(structWriter.getBuffer()); // Final hash with domain separator const finalWriter = new BytesWriter(66); finalWriter.writeU8(0x19); finalWriter.writeU8(0x01); finalWriter.writeBytes(domainSeparator); finalWriter.writeBytes(structHash); return sha256(finalWriter.getBuffer()); } ``` ## Complete Contract Example ```typescript import { OP_NET, Blockchain, Calldata, BytesWriter, Revert, sha256, SignaturesMethods } from '@btc-vision/btc-runtime/runtime'; @final class SignatureContract extends OP_NET { @method(ABIDataTypes.BYTES) @returns({ name: 'valid', type: ABIDataTypes.BOOL }) public verifySignature(calldata: Calldata): BytesWriter { const signature = calldata.readBytesWithLength(); // Create the message to verify const message = new BytesWriter(32); message.writeString('Hello, OP_NET!'); const messageHash = sha256(message.getBuffer()); // Verify using consensus-aware method // Automatically uses the sender's public key const isValid = Blockchain.verifySignature( Blockchain.tx.origin, signature, messageHash, SignaturesMethods.MLDSA // Force ML-DSA for quantum resistance ); const writer = new BytesWriter(1); writer.writeBoolean(isValid); return writer; } @method( { name: 'signature', type: ABIDataTypes.BYTES }, { name: 'message', type: ABIDataTypes.BYTES }, ) @returns({ name: 'valid', type: ABIDataTypes.BOOL }) public verifyForOrigin(calldata: Calldata): BytesWriter { const signature = calldata.readBytesWithLength(); const message = calldata.readBytesWithLength(); const messageHash = sha256(message); // Verify signature for the transaction origin (ExtendedAddress) // Note: Blockchain.tx.origin returns ExtendedAddress which supports both // Schnorr (via tweakedPublicKey) and ML-DSA (via mldsaPublicKey) signatures const isValid = Blockchain.verifySignature( Blockchain.tx.origin, // ExtendedAddress from transaction signature, messageHash, SignaturesMethods.Schnorr // Use consensus-aware Schnorr verification ); const writer = new BytesWriter(1); writer.writeBoolean(isValid); return writer; } } ``` ## Solidity vs OP_NET: Signature Verification Comparison OP_NET provides significant advantages over Solidity for signature verification, including quantum-resistant signatures, native Schnorr support, and simplified APIs. ### Feature Comparison Table | Feature | Solidity/EVM | OP_NET | OP_NET Advantage | |---------|--------------|-------|-----------------| | **Primary Signature Scheme** | ECDSA (secp256k1) | Schnorr + ML-DSA + ECDSA | Multiple schemes, quantum-resistant option | | **Quantum Resistance** | Not supported | ML-DSA (FIPS 204) | Future-proof security | | **ECDSA Support** | Only option | Supported (deprecated) | Backward compatibility with Ethereum/Bitcoin | | **Signature Recovery** | `ecrecover()` returns address | Direct verification | Cleaner API | | **Public Key Access** | Must be stored/derived | Automatic via `Address` | No custom storage needed | | **Verification Function** | Multiple parameters (v, r, s) | Single signature bytes | Simpler interface | | **EIP-712 Support** | Manual implementation | Built-in domain separation | Type-safe messages | | **Keccak-256 Hashing** | Native opcode | Built-in runtime module | Ethereum-compatible hashing | | **Batch Verification** | Not native | Supported | Better performance | | **Key Sizes** | 33/65 bytes (secp256k1) | 32 bytes (Schnorr) / 33-65 bytes (ECDSA) / 1,312+ bytes (ML-DSA) | Flexible security | ### Signature Scheme Comparison | Aspect | Solidity (ECDSA) | OP_NET (ECDSA) | OP_NET (Schnorr) | OP_NET (ML-DSA) | |--------|------------------|---------------|-----------------|----------------| | Algorithm | secp256k1 ECDSA | secp256k1 ECDSA | BIP340 Schnorr | FIPS 204 Lattice | | Public Key Size | 33 or 65 bytes | 33, 64, or 65 bytes | 32 bytes | 1,312+ bytes | | Signature Size | 65 bytes (v, r, s) | 64 or 65 bytes | 64 bytes | 2,420+ bytes | | Quantum Safe | No | No | No | **Yes** | | Bitcoin Native | No | Yes (Bitcoin sub-type) | Yes | Yes | | Batch Verification | No | No | Yes | Yes | | Signature Malleability | Yes (fixable) | No (BIP-0062 low-S) | No | No | | Status | Only option | Deprecated | Transition | **Recommended** | ### Capability Matrix | Capability | Solidity | OP_NET | |------------|:--------:|:-----:| | ECDSA verification (Ethereum ecrecover) | Yes | Yes (deprecated) | | ECDSA verification (Bitcoin direct) | No | Yes (deprecated) | | Schnorr verification | No | Yes | | ML-DSA (quantum-safe) verification | No | Yes | | Keccak-256 hashing | Yes (native) | Yes (runtime module) | | Automatic public key loading | No | Yes | | Consensus-aware algorithm selection | No | Yes | | EIP-712 domain separation | Manual | Built-in pattern | | Nonce management | Manual | Manual (with helpers) | | Multi-signature verification | Custom | Built-in loop support | | Signature deadline enforcement | Manual | `Blockchain.block.medianTime` | ### API Comparison #### Solidity: ecrecover ```solidity // Solidity - ecrecover (complex, error-prone) function verifySignature( bytes32 hash, uint8 v, bytes32 r, bytes32 s, address expectedSigner ) public pure returns (bool) { // Must handle v value normalization if (v < 27) { v += 27; } // ecrecover returns address(0) on failure (no error thrown!) address recovered = ecrecover(hash, v, r, s); // Must explicitly check for zero address require(recovered != address(0), "Invalid signature"); return recovered == expectedSigner; // Limitations: // - Returns address(0) on invalid signature (silent failure) // - v, r, s must be extracted from signature bytes // - No quantum resistance // - Signature malleability issues } ``` #### OP_NET: verifySignature ```typescript // OP_NET - verifySignature (simple, safe) function verifySignature( signer: Address, signature: Uint8Array, hash: Uint8Array ): bool { // Single function call - handles everything const isValid = Blockchain.verifySignature( signer, // Address contains public key reference signature, // Full signature bytes hash, // Message hash SignaturesMethods.MLDSA // Force quantum-resistant ML-DSA ); // Returns false on invalid (never throws for invalid sig) return isValid; // Advantages: // - Single function call // - No signature parsing needed // - Automatic public key loading // - Quantum-resistant option // - No malleability issues } ``` ### EIP-712 / EIP-2612 Permit Comparison ```solidity // Solidity (EIP-2612) function permit( address owner, address spender, uint256 value, uint256 deadline, uint8 v, bytes32 r, bytes32 s ) external { require(deadline >= block.timestamp, "Permit expired"); bytes32 digest = keccak256(abi.encodePacked( "\x19\x01", DOMAIN_SEPARATOR, keccak256(abi.encode(PERMIT_TYPEHASH, owner, spender, value, nonces[owner]++, deadline)) )); address recovered = ecrecover(digest, v, r, s); require(recovered == owner, "Invalid signature"); _approve(owner, spender, value); } ``` ```typescript // OP_NET @method( { name: 'owner', type: ABIDataTypes.ADDRESS }, { name: 'spender', type: ABIDataTypes.ADDRESS }, { name: 'value', type: ABIDataTypes.UINT256 }, { name: 'deadline', type: ABIDataTypes.UINT64 }, { name: 'signature', type: ABIDataTypes.BYTES }, ) @emit('Approved') public permit(calldata: Calldata): BytesWriter { const owner = calldata.readAddress(); const spender = calldata.readAddress(); const value = calldata.readU256(); const deadline = calldata.readU64(); const signature = calldata.readBytesWithLength(); if (Blockchain.block.medianTime > deadline) { throw new Revert('Permit expired'); } const nonce = this.nonces.get(owner); this.nonces.set(owner, SafeMath.add(nonce, u256.One)); const digest = this.buildPermitHash(owner, spender, value, nonce, deadline); if (!Blockchain.verifySignature(owner, signature, digest, SignaturesMethods.Schnorr)) { throw new Revert('Invalid signature'); } this._approve(owner, spender, value); return new BytesWriter(0); } ``` ### Security Comparison | Security Aspect | Solidity | OP_NET | |-----------------|----------|-------| | Signature Malleability | Vulnerable (requires OpenZeppelin) | Not vulnerable | | Replay Attack Protection | Manual nonce tracking | Built-in patterns | | Cross-Chain Replay | EIP-712 chain ID (manual) | Network-aware domain | | Zero Address Recovery | Silent failure | Clean boolean return | | Quantum Computer Attack | **Vulnerable** | **Protected (ML-DSA)** | | Key Compromise Recovery | No built-in support | Dual-key architecture | ### Implementation Complexity | Task | Solidity Lines of Code | OP_NET Lines of Code | |------|:----------------------:|:-------------------:| | Basic signature verification | ~15 | ~5 | | EIP-712 domain separator | ~20 | ~15 | | Permit implementation | ~30 | ~20 | | Multi-sig verification | ~50+ | ~15 | | Quantum-safe verification | Not possible | ~5 (same as basic) | ### Error Handling Comparison ```solidity // Solidity - Silent failure with ecrecover function verify(bytes32 hash, uint8 v, bytes32 r, bytes32 s) public view returns (address) { address recovered = ecrecover(hash, v, r, s); // DANGER: recovered can be address(0) on failure! // DANGER: No error thrown, must check explicitly require(recovered != address(0), "Invalid signature"); return recovered; } ``` ```typescript // OP_NET - Clear boolean result function verify(hash: Uint8Array, signature: Uint8Array, signer: Address): bool { // Returns false on invalid signature - no silent failures // Returns false on malformed input - no exceptions return Blockchain.verifySignature(signer, signature, hash, SignaturesMethods.MLDSA); } ``` ### Why OP_NET for Signature Verification? | Solidity Limitation | OP_NET Solution | |---------------------|----------------| | ECDSA only | ECDSA + Schnorr + ML-DSA support | | No quantum resistance | Built-in ML-DSA (FIPS 204) | | Complex v, r, s handling | Single signature bytes parameter | | Must store/derive public keys | Automatic key loading from Address | | Silent ecrecover failures | Clean boolean returns | | Signature malleability | BIP340 Schnorr / BIP-0062 low-S ECDSA (no malleability) | | No keccak256 in runtime | Built-in Ethereum-compatible keccak256 | | Manual EIP-712 implementation | Built-in domain separation patterns | | No consensus-aware selection | Automatic algorithm selection | ## Common Patterns ### Signature-Based Authorization ```typescript @method( { name: 'action', type: ABIDataTypes.UINT256 }, { name: 'deadline', type: ABIDataTypes.UINT64 }, { name: 'signature', type: ABIDataTypes.BYTES }, ) public executeWithSignature(calldata: Calldata): BytesWriter { const action = calldata.readU256(); const deadline = calldata.readU64(); const signature = calldata.readBytesWithLength(); // Check deadline if (Blockchain.block.medianTime > deadline) { throw new Revert('Signature expired'); } // Build message hash (include action + deadline) const message = new BytesWriter(40); message.writeU256(action); message.writeU64(deadline); const messageHash = sha256(message.getBuffer()); // Verify signature from sender if (!Blockchain.verifySignature(Blockchain.tx.origin, signature, messageHash, SignaturesMethods.MLDSA)) { throw new Revert('Invalid signature'); } // Execute action this.executeAction(action); return new BytesWriter(0); } ``` ### Nonce-Based Replay Protection ```typescript private noncesPointer: u16 = Blockchain.nextPointer; private nonces: AddressMemoryMap; @method( { name: 'owner', type: ABIDataTypes.ADDRESS }, { name: 'spender', type: ABIDataTypes.ADDRESS }, { name: 'value', type: ABIDataTypes.UINT256 }, { name: 'deadline', type: ABIDataTypes.UINT64 }, { name: 'signature', type: ABIDataTypes.BYTES }, ) @emit('Approved') public permit(calldata: Calldata): BytesWriter { const owner = calldata.readAddress(); const spender = calldata.readAddress(); const value = calldata.readU256(); const deadline = calldata.readU64(); const signature = calldata.readBytesWithLength(); // Check deadline if (Blockchain.block.medianTime > deadline) { throw new Revert('Permit expired'); } // Get and increment nonce (prevents replay) const nonce = this.nonces.get(owner); this.nonces.set(owner, SafeMath.add(nonce, u256.One)); // Build permit hash const messageHash = this.buildPermitHash(owner, spender, value, nonce, deadline); // Verify signature if (!Blockchain.verifySignature(owner, signature, messageHash, SignaturesMethods.Schnorr)) { throw new Revert('Invalid signature'); } // Set approval this._approve(owner, spender, value); return new BytesWriter(0); } ``` ### Multi-Signature Verification ```typescript @method( { name: 'action', type: ABIDataTypes.BYTES }, { name: 'signers', type: ABIDataTypes.ADDRESS_ARRAY }, { name: 'signatures', type: ABIDataTypes.BYTES_ARRAY }, ) public executeMultiSig(calldata: Calldata): BytesWriter { const action = calldata.readBytesWithLength(); const signers = calldata.readAddressArray(); const signatures = calldata.readBytesArray(); // Build action hash const actionHash = sha256(action); // Verify required signatures let validCount: u32 = 0; for (let i = 0; i < signers.length; i++) { const signer = signers[i]; const signature = signatures[i]; if (this.isAuthorizedSigner(signer)) { if (Blockchain.verifySignature(signer, signature, actionHash, SignaturesMethods.MLDSA)) { validCount++; } } } // Check threshold if (validCount < this.threshold.value) { throw new Revert('Insufficient signatures'); } // Execute this.executeAction(action); return new BytesWriter(0); } ``` ## Security Best Practices ### 1. Always Include Nonces ```typescript // Prevent signature replay const nonce = this.nonces.get(signer); this.nonces.set(signer, SafeMath.add(nonce, u256.One)); // Include nonce in message hash ``` ### 2. Include Deadlines ```typescript // Limit signature validity if (Blockchain.block.medianTime > deadline) { throw new Revert('Signature expired'); } ``` ### 3. Use Domain Separation ```typescript // Prevent cross-contract/cross-chain replay const DOMAIN_SEPARATOR = buildDomainSeparator( 'MyContract', '1', chainId, Blockchain.contract.address ); ``` ### 4. Prefer Quantum-Resistant Verification ```typescript // For high-security operations, force ML-DSA Blockchain.verifySignature(signer, signature, hash, SignaturesMethods.MLDSA); ``` --- **Navigation:** - Previous: [Cross-Contract Calls](./cross-contract-calls.md) - Next: [Quantum Resistance](./quantum-resistance.md)