@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
Markdown
# 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)