@btc-vision/btc-runtime 1.10.10 → 1.10.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/LICENSE +190 -0
  2. package/README.md +258 -137
  3. package/SECURITY.md +226 -0
  4. package/docs/README.md +614 -0
  5. package/docs/advanced/bitcoin-scripts.md +939 -0
  6. package/docs/advanced/cross-contract-calls.md +579 -0
  7. package/docs/advanced/plugins.md +1006 -0
  8. package/docs/advanced/quantum-resistance.md +660 -0
  9. package/docs/advanced/signature-verification.md +715 -0
  10. package/docs/api-reference/blockchain.md +729 -0
  11. package/docs/api-reference/events.md +642 -0
  12. package/docs/api-reference/op20.md +902 -0
  13. package/docs/api-reference/op721.md +819 -0
  14. package/docs/api-reference/safe-math.md +510 -0
  15. package/docs/api-reference/storage.md +731 -0
  16. package/docs/contracts/op-net-base.md +786 -0
  17. package/docs/contracts/op20-token.md +687 -0
  18. package/docs/contracts/op20s-signatures.md +614 -0
  19. package/docs/contracts/op721-nft.md +785 -0
  20. package/docs/contracts/reentrancy-guard.md +787 -0
  21. package/docs/core-concepts/blockchain-environment.md +724 -0
  22. package/docs/core-concepts/decorators.md +466 -0
  23. package/docs/core-concepts/events.md +652 -0
  24. package/docs/core-concepts/pointers.md +391 -0
  25. package/docs/core-concepts/security.md +370 -0
  26. package/docs/core-concepts/storage-system.md +938 -0
  27. package/docs/examples/basic-token.md +745 -0
  28. package/docs/examples/nft-with-reservations.md +1210 -0
  29. package/docs/examples/oracle-integration.md +1212 -0
  30. package/docs/examples/stablecoin.md +1180 -0
  31. package/docs/getting-started/first-contract.md +575 -0
  32. package/docs/getting-started/installation.md +384 -0
  33. package/docs/getting-started/project-structure.md +630 -0
  34. package/docs/storage/memory-maps.md +721 -0
  35. package/docs/storage/stored-arrays.md +714 -0
  36. package/docs/storage/stored-maps.md +686 -0
  37. package/docs/storage/stored-primitives.md +608 -0
  38. package/docs/types/address.md +773 -0
  39. package/docs/types/bytes-writer-reader.md +938 -0
  40. package/docs/types/calldata.md +744 -0
  41. package/docs/types/safe-math.md +403 -0
  42. package/package.json +51 -26
  43. package/runtime/memory/MapOfMap.ts +1 -0
  44. package/runtime/types/SafeMath.ts +121 -1
  45. package/LICENSE.md +0 -21
@@ -0,0 +1,715 @@
1
+ # Signature Verification
2
+
3
+ OPNet supports multiple signature schemes for authentication and authorization. This guide covers Schnorr signatures, quantum-resistant ML-DSA, and common verification patterns.
4
+
5
+ ## Overview
6
+
7
+ ```typescript
8
+ import { Blockchain } from '@btc-vision/btc-runtime/runtime';
9
+
10
+ // Consensus-aware signature verification (recommended)
11
+ // Uses Schnorr during transition period, ML-DSA after quantum deadline
12
+ const isValid: bool = Blockchain.verifySignature(
13
+ Blockchain.tx.origin, // Address or ExtendedAddress
14
+ signature, // Signature bytes
15
+ messageHash, // 32-byte message hash
16
+ false // forceMLDSA: false = auto, true = require ML-DSA
17
+ );
18
+
19
+ // Force quantum-resistant verification (always uses ML-DSA)
20
+ const isValidQuantum: bool = Blockchain.verifySignature(
21
+ Blockchain.tx.origin,
22
+ signature,
23
+ messageHash,
24
+ true // Force ML-DSA regardless of consensus flags
25
+ );
26
+ ```
27
+
28
+ ## Signature Scheme Comparison
29
+
30
+ OPNet supports both traditional Schnorr signatures and quantum-resistant ML-DSA:
31
+
32
+ ```mermaid
33
+ ---
34
+ config:
35
+ theme: dark
36
+ ---
37
+ flowchart LR
38
+ subgraph OPNet["OPNet Signature Verification"]
39
+ subgraph Schnorr["Schnorr - Traditional"]
40
+ S1["Public Key: 32 bytes"]
41
+ S2["Signature: 64 bytes"]
42
+ S3["Security: Classical only"]
43
+ S4["Status: Deprecated"]
44
+ end
45
+
46
+ subgraph MLDSA["ML-DSA - Quantum-Resistant"]
47
+ M1["Public Key: 1,312 bytes"]
48
+ M2["Signature: 2,420 bytes"]
49
+ M3["Security: Post-quantum"]
50
+ M4["Status: Recommended"]
51
+ end
52
+
53
+ Q["Quantum Computer Threat"] -.->|"Breaks"| S3
54
+ Q -.->|"Cannot break"| M3
55
+ end
56
+ ```
57
+
58
+ ## The verifySignature Method
59
+
60
+ The recommended approach for all signature verification:
61
+
62
+ ```typescript
63
+ Blockchain.verifySignature(
64
+ address: ExtendedAddress, // Signer's address (contains both key references)
65
+ signature: Uint8Array, // Signature bytes
66
+ hash: Uint8Array, // 32-byte message hash
67
+ forceMLDSA: boolean = false // Force quantum-resistant verification
68
+ ): boolean
69
+ ```
70
+
71
+ **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:
72
+ - `tweakedPublicKey` (32 bytes) - for Schnorr/Taproot signatures
73
+ - `mldsaPublicKey` (1,312 bytes for Level2) - for quantum-resistant ML-DSA signatures
74
+
75
+ **Behavior:**
76
+ - When `forceMLDSA = false`: Uses Schnorr if `UNSAFE_QUANTUM_SIGNATURES_ALLOWED` consensus flag is set, otherwise ML-DSA
77
+ - When `forceMLDSA = true`: Always uses ML-DSA (quantum-resistant)
78
+
79
+ The method automatically:
80
+ 1. Loads the appropriate public key from the address
81
+ 2. Selects the correct verification algorithm based on consensus rules
82
+ 3. Handles all internal key formatting
83
+
84
+ ## Schnorr Verification
85
+
86
+ When using Schnorr signatures (during transition period), the verification follows BIP340:
87
+
88
+ ```mermaid
89
+ ---
90
+ config:
91
+ theme: dark
92
+ ---
93
+ sequenceDiagram
94
+ participant Contract as Contract
95
+ participant Blockchain as OPNet Runtime
96
+ participant SchnorrVerifier as Schnorr Verifier
97
+ participant ExtendedAddress as ExtendedAddress
98
+
99
+ Contract->>Blockchain: verifySignature(address, sig, hash, false)
100
+ Note over Blockchain: Check consensus flags
101
+ Blockchain->>Blockchain: UNSAFE_QUANTUM_SIGNATURES_ALLOWED?
102
+
103
+ Blockchain->>ExtendedAddress: Load tweakedPublicKey
104
+ ExtendedAddress-->>Blockchain: 32-byte Schnorr key
105
+
106
+ Blockchain->>SchnorrVerifier: verify(pubkey, signature, hash)
107
+ Note over SchnorrVerifier: BIP340 verification
108
+ SchnorrVerifier->>SchnorrVerifier: Compute R from signature
109
+ SchnorrVerifier->>SchnorrVerifier: Compute challenge e
110
+ SchnorrVerifier->>SchnorrVerifier: Verify s*G = R + e*P
111
+
112
+ SchnorrVerifier-->>Blockchain: valid: bool
113
+ Blockchain-->>Contract: result: bool
114
+ ```
115
+
116
+ ### Low-Level Schnorr Verification (Deprecated)
117
+
118
+ ```typescript
119
+ // Deprecated - use Blockchain.verifySignature() instead
120
+ const isValid = Blockchain.verifySchnorrSignature(
121
+ extendedAddress, // ExtendedAddress (contains tweaked public key)
122
+ signature, // 64-byte Schnorr signature
123
+ messageHash // 32-byte message hash
124
+ );
125
+ ```
126
+
127
+ ## ML-DSA Verification
128
+
129
+ When using quantum-resistant ML-DSA signatures, the verification follows FIPS 204:
130
+
131
+ ```mermaid
132
+ ---
133
+ config:
134
+ theme: dark
135
+ ---
136
+ sequenceDiagram
137
+ participant Contract as Contract
138
+ participant Blockchain as OPNet Runtime
139
+ participant MLDSAVerifier as ML-DSA Verifier
140
+ participant Address as Address
141
+
142
+ Contract->>Blockchain: verifySignature(address, sig, hash, true)
143
+ Note over Blockchain: forceMLDSA = true
144
+
145
+ Blockchain->>Address: Load mldsaPublicKey
146
+ Note over Address: SHA256 hash stored in address
147
+ Address->>Address: Lazy load full public key
148
+ Address-->>Blockchain: 1,312-byte ML-DSA public key
149
+
150
+ Blockchain->>MLDSAVerifier: verify(Level2, pubkey, sig, hash)
151
+ Note over MLDSAVerifier: FIPS 204 ML-DSA-44
152
+ MLDSAVerifier->>MLDSAVerifier: Decode signature (s1, s2, h)
153
+ MLDSAVerifier->>MLDSAVerifier: Reconstruct w' from h
154
+ MLDSAVerifier->>MLDSAVerifier: Compute Az - t*2^d*s2
155
+ MLDSAVerifier->>MLDSAVerifier: Verify lattice bounds
156
+
157
+ MLDSAVerifier-->>Blockchain: valid: bool
158
+ Blockchain-->>Contract: result: bool
159
+ ```
160
+
161
+ ### Direct ML-DSA Verification
162
+
163
+ ```typescript
164
+ import { MLDSASecurityLevel } from '@btc-vision/btc-runtime/runtime';
165
+
166
+ const isValid = Blockchain.verifyMLDSASignature(
167
+ MLDSASecurityLevel.Level2, // Security level
168
+ signer.mldsaPublicKey, // ML-DSA public key (auto-loaded from address)
169
+ signature, // 2420-byte signature (for Level2)
170
+ messageHash // 32-byte message hash
171
+ );
172
+ ```
173
+
174
+ ### ML-DSA Security Levels
175
+
176
+ | Level | Name | Public Key | Signature | NIST Category |
177
+ |-------|------|------------|-----------|---------------|
178
+ | Level2 | ML-DSA-44 | 1,312 bytes | 2,420 bytes | Category 2 (~AES-128) |
179
+ | Level3 | ML-DSA-65 | 1,952 bytes | 3,309 bytes | Category 3 (~AES-192) |
180
+ | Level5 | ML-DSA-87 | 2,592 bytes | 4,627 bytes | Category 5 (~AES-256) |
181
+
182
+ **OPNet uses ML-DSA-44 (Level2) by default.**
183
+
184
+ ## Message Hash Construction
185
+
186
+ When building message hashes for signature verification, use domain separation to prevent cross-contract signature reuse:
187
+
188
+ ```mermaid
189
+ ---
190
+ config:
191
+ theme: dark
192
+ ---
193
+ flowchart LR
194
+ A["Domain Separator"] --> B["Hash Domain"]
195
+ C["Struct Data"] --> D["Hash Struct"]
196
+ B --> E["Combine"]
197
+ D --> E
198
+ E --> F["Final Hash"]
199
+
200
+ G["Domain Components"] --> B
201
+ H["Message Components"] --> D
202
+ ```
203
+
204
+ ### Domain Separator
205
+
206
+ ```typescript
207
+ function buildDomainSeparator(
208
+ name: string,
209
+ version: string,
210
+ chainId: u256,
211
+ contractAddress: Address
212
+ ): Uint8Array {
213
+ const writer = new BytesWriter(256);
214
+
215
+ // EIP-712 domain typehash
216
+ writer.writeBytes(sha256(
217
+ encodeString('EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)')
218
+ ));
219
+
220
+ // Domain values
221
+ writer.writeBytes(sha256(encodeString(name)));
222
+ writer.writeBytes(sha256(encodeString(version)));
223
+ writer.writeU256(chainId);
224
+ writer.writeAddress(contractAddress);
225
+
226
+ return sha256(writer.getBuffer());
227
+ }
228
+ ```
229
+
230
+ ### Permit Message Hash
231
+
232
+ ```typescript
233
+ function buildPermitHash(
234
+ domainSeparator: Uint8Array,
235
+ owner: Address,
236
+ spender: Address,
237
+ value: u256,
238
+ nonce: u256,
239
+ deadline: u64
240
+ ): Uint8Array {
241
+ const PERMIT_TYPEHASH = sha256(
242
+ encodeString('Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)')
243
+ );
244
+
245
+ // Build struct hash
246
+ const structWriter = new BytesWriter(192);
247
+ structWriter.writeBytes(PERMIT_TYPEHASH);
248
+ structWriter.writeAddress(owner);
249
+ structWriter.writeAddress(spender);
250
+ structWriter.writeU256(value);
251
+ structWriter.writeU256(nonce);
252
+ structWriter.writeU64(deadline);
253
+ const structHash = sha256(structWriter.getBuffer());
254
+
255
+ // Final hash with domain separator
256
+ const finalWriter = new BytesWriter(66);
257
+ finalWriter.writeU8(0x19);
258
+ finalWriter.writeU8(0x01);
259
+ finalWriter.writeBytes(domainSeparator);
260
+ finalWriter.writeBytes(structHash);
261
+
262
+ return sha256(finalWriter.getBuffer());
263
+ }
264
+ ```
265
+
266
+ ## Complete Contract Example
267
+
268
+ ```typescript
269
+ import { OP_NET, Blockchain, Calldata, BytesWriter, Revert, sha256 } from '@btc-vision/btc-runtime/runtime';
270
+
271
+ @final
272
+ class SignatureContract extends OP_NET {
273
+
274
+ @method(ABIDataTypes.BYTES)
275
+ @returns({ name: 'valid', type: ABIDataTypes.BOOL })
276
+ public verifySignature(calldata: Calldata): BytesWriter {
277
+ const signature = calldata.readBytesWithLength();
278
+
279
+ // Create the message to verify
280
+ const message = new BytesWriter(32);
281
+ message.writeString('Hello, OPNet!');
282
+ const messageHash = sha256(message.getBuffer());
283
+
284
+ // Verify using consensus-aware method
285
+ // Automatically uses the sender's public key
286
+ const isValid = Blockchain.verifySignature(
287
+ Blockchain.tx.origin,
288
+ signature,
289
+ messageHash,
290
+ true // Force ML-DSA for quantum resistance
291
+ );
292
+
293
+ const writer = new BytesWriter(1);
294
+ writer.writeBoolean(isValid);
295
+ return writer;
296
+ }
297
+
298
+ @method(
299
+ { name: 'signature', type: ABIDataTypes.BYTES },
300
+ { name: 'message', type: ABIDataTypes.BYTES },
301
+ )
302
+ @returns({ name: 'valid', type: ABIDataTypes.BOOL })
303
+ public verifyForOrigin(calldata: Calldata): BytesWriter {
304
+ const signature = calldata.readBytesWithLength();
305
+ const message = calldata.readBytesWithLength();
306
+
307
+ const messageHash = sha256(message);
308
+
309
+ // Verify signature for the transaction origin (ExtendedAddress)
310
+ // Note: Blockchain.tx.origin returns ExtendedAddress which supports both
311
+ // Schnorr (via tweakedPublicKey) and ML-DSA (via mldsaPublicKey) signatures
312
+ const isValid = Blockchain.verifySignature(
313
+ Blockchain.tx.origin, // ExtendedAddress from transaction
314
+ signature,
315
+ messageHash,
316
+ false // Use consensus-aware verification
317
+ );
318
+
319
+ const writer = new BytesWriter(1);
320
+ writer.writeBoolean(isValid);
321
+ return writer;
322
+ }
323
+ }
324
+ ```
325
+
326
+ ## Solidity vs OPNet: Signature Verification Comparison
327
+
328
+ OPNet provides significant advantages over Solidity for signature verification, including quantum-resistant signatures, native Schnorr support, and simplified APIs.
329
+
330
+ ### Feature Comparison Table
331
+
332
+ | Feature | Solidity/EVM | OPNet | OPNet Advantage |
333
+ |---------|--------------|-------|-----------------|
334
+ | **Primary Signature Scheme** | ECDSA (secp256k1) | Schnorr + ML-DSA | Quantum-resistant option |
335
+ | **Quantum Resistance** | Not supported | ML-DSA (FIPS 204) | Future-proof security |
336
+ | **Signature Recovery** | `ecrecover()` returns address | Direct verification | Cleaner API |
337
+ | **Public Key Access** | Must be stored/derived | Automatic via `Address` | No custom storage needed |
338
+ | **Verification Function** | Multiple parameters (v, r, s) | Single signature bytes | Simpler interface |
339
+ | **EIP-712 Support** | Manual implementation | Built-in domain separation | Type-safe messages |
340
+ | **Batch Verification** | Not native | Supported | Better performance |
341
+ | **Key Sizes** | 33/65 bytes (secp256k1) | 32 bytes (Schnorr) / 1,312+ bytes (ML-DSA) | Flexible security |
342
+
343
+ ### Signature Scheme Comparison
344
+
345
+ | Aspect | Solidity (ECDSA) | OPNet (Schnorr) | OPNet (ML-DSA) |
346
+ |--------|------------------|-----------------|----------------|
347
+ | Algorithm | secp256k1 ECDSA | BIP340 Schnorr | FIPS 204 Lattice |
348
+ | Public Key Size | 33 or 65 bytes | 32 bytes | 1,312+ bytes |
349
+ | Signature Size | 65 bytes (v, r, s) | 64 bytes | 2,420+ bytes |
350
+ | Quantum Safe | No | No | **Yes** |
351
+ | Bitcoin Native | No | Yes | Yes |
352
+ | Batch Verification | No | Yes | Yes |
353
+ | Signature Malleability | Yes (fixable) | No | No |
354
+
355
+ ### Capability Matrix
356
+
357
+ | Capability | Solidity | OPNet |
358
+ |------------|:--------:|:-----:|
359
+ | ECDSA verification | Yes | No (not needed) |
360
+ | Schnorr verification | No | Yes |
361
+ | ML-DSA (quantum-safe) verification | No | Yes |
362
+ | Automatic public key loading | No | Yes |
363
+ | Consensus-aware algorithm selection | No | Yes |
364
+ | EIP-712 domain separation | Manual | Built-in pattern |
365
+ | Nonce management | Manual | Manual (with helpers) |
366
+ | Multi-signature verification | Custom | Built-in loop support |
367
+ | Signature deadline enforcement | Manual | `Blockchain.block.medianTime` |
368
+
369
+ ### API Comparison
370
+
371
+ #### Solidity: ecrecover
372
+
373
+ ```solidity
374
+ // Solidity - ecrecover (complex, error-prone)
375
+ function verifySignature(
376
+ bytes32 hash,
377
+ uint8 v,
378
+ bytes32 r,
379
+ bytes32 s,
380
+ address expectedSigner
381
+ ) public pure returns (bool) {
382
+ // Must handle v value normalization
383
+ if (v < 27) {
384
+ v += 27;
385
+ }
386
+
387
+ // ecrecover returns address(0) on failure (no error thrown!)
388
+ address recovered = ecrecover(hash, v, r, s);
389
+
390
+ // Must explicitly check for zero address
391
+ require(recovered != address(0), "Invalid signature");
392
+
393
+ return recovered == expectedSigner;
394
+
395
+ // Limitations:
396
+ // - Returns address(0) on invalid signature (silent failure)
397
+ // - v, r, s must be extracted from signature bytes
398
+ // - No quantum resistance
399
+ // - Signature malleability issues
400
+ }
401
+ ```
402
+
403
+ #### OPNet: verifySignature
404
+
405
+ ```typescript
406
+ // OPNet - verifySignature (simple, safe)
407
+ function verifySignature(
408
+ signer: Address,
409
+ signature: Uint8Array,
410
+ hash: Uint8Array
411
+ ): bool {
412
+ // Single function call - handles everything
413
+ const isValid = Blockchain.verifySignature(
414
+ signer, // Address contains public key reference
415
+ signature, // Full signature bytes
416
+ hash, // Message hash
417
+ true // Force quantum-resistant ML-DSA
418
+ );
419
+
420
+ // Returns false on invalid (never throws for invalid sig)
421
+ return isValid;
422
+
423
+ // Advantages:
424
+ // - Single function call
425
+ // - No signature parsing needed
426
+ // - Automatic public key loading
427
+ // - Quantum-resistant option
428
+ // - No malleability issues
429
+ }
430
+ ```
431
+
432
+ ### EIP-712 / EIP-2612 Permit Comparison
433
+
434
+ ```solidity
435
+ // Solidity (EIP-2612)
436
+ function permit(
437
+ address owner,
438
+ address spender,
439
+ uint256 value,
440
+ uint256 deadline,
441
+ uint8 v,
442
+ bytes32 r,
443
+ bytes32 s
444
+ ) external {
445
+ require(deadline >= block.timestamp, "Permit expired");
446
+ bytes32 digest = keccak256(abi.encodePacked(
447
+ "\x19\x01",
448
+ DOMAIN_SEPARATOR,
449
+ keccak256(abi.encode(PERMIT_TYPEHASH, owner, spender, value, nonces[owner]++, deadline))
450
+ ));
451
+ address recovered = ecrecover(digest, v, r, s);
452
+ require(recovered == owner, "Invalid signature");
453
+ _approve(owner, spender, value);
454
+ }
455
+ ```
456
+
457
+ ```typescript
458
+ // OPNet
459
+ @method(
460
+ { name: 'owner', type: ABIDataTypes.ADDRESS },
461
+ { name: 'spender', type: ABIDataTypes.ADDRESS },
462
+ { name: 'value', type: ABIDataTypes.UINT256 },
463
+ { name: 'deadline', type: ABIDataTypes.UINT64 },
464
+ { name: 'signature', type: ABIDataTypes.BYTES },
465
+ )
466
+ @emit('Approved')
467
+ public permit(calldata: Calldata): BytesWriter {
468
+ const owner = calldata.readAddress();
469
+ const spender = calldata.readAddress();
470
+ const value = calldata.readU256();
471
+ const deadline = calldata.readU64();
472
+ const signature = calldata.readBytesWithLength();
473
+
474
+ if (Blockchain.block.medianTime > deadline) {
475
+ throw new Revert('Permit expired');
476
+ }
477
+
478
+ const nonce = this.nonces.get(owner);
479
+ this.nonces.set(owner, SafeMath.add(nonce, u256.One));
480
+
481
+ const digest = this.buildPermitHash(owner, spender, value, nonce, deadline);
482
+
483
+ if (!Blockchain.verifySignature(owner, signature, digest, false)) {
484
+ throw new Revert('Invalid signature');
485
+ }
486
+
487
+ this._approve(owner, spender, value);
488
+ return new BytesWriter(0);
489
+ }
490
+ ```
491
+
492
+ ### Security Comparison
493
+
494
+ | Security Aspect | Solidity | OPNet |
495
+ |-----------------|----------|-------|
496
+ | Signature Malleability | Vulnerable (requires OpenZeppelin) | Not vulnerable |
497
+ | Replay Attack Protection | Manual nonce tracking | Built-in patterns |
498
+ | Cross-Chain Replay | EIP-712 chain ID (manual) | Network-aware domain |
499
+ | Zero Address Recovery | Silent failure | Clean boolean return |
500
+ | Quantum Computer Attack | **Vulnerable** | **Protected (ML-DSA)** |
501
+ | Key Compromise Recovery | No built-in support | Dual-key architecture |
502
+
503
+ ### Implementation Complexity
504
+
505
+ | Task | Solidity Lines of Code | OPNet Lines of Code |
506
+ |------|:----------------------:|:-------------------:|
507
+ | Basic signature verification | ~15 | ~5 |
508
+ | EIP-712 domain separator | ~20 | ~15 |
509
+ | Permit implementation | ~30 | ~20 |
510
+ | Multi-sig verification | ~50+ | ~15 |
511
+ | Quantum-safe verification | Not possible | ~5 (same as basic) |
512
+
513
+ ### Error Handling Comparison
514
+
515
+ ```solidity
516
+ // Solidity - Silent failure with ecrecover
517
+ function verify(bytes32 hash, uint8 v, bytes32 r, bytes32 s) public view returns (address) {
518
+ address recovered = ecrecover(hash, v, r, s);
519
+ // DANGER: recovered can be address(0) on failure!
520
+ // DANGER: No error thrown, must check explicitly
521
+ require(recovered != address(0), "Invalid signature");
522
+ return recovered;
523
+ }
524
+ ```
525
+
526
+ ```typescript
527
+ // OPNet - Clear boolean result
528
+ function verify(hash: Uint8Array, signature: Uint8Array, signer: Address): bool {
529
+ // Returns false on invalid signature - no silent failures
530
+ // Returns false on malformed input - no exceptions
531
+ return Blockchain.verifySignature(signer, signature, hash, true);
532
+ }
533
+ ```
534
+
535
+ ### Why OPNet for Signature Verification?
536
+
537
+ | Solidity Limitation | OPNet Solution |
538
+ |---------------------|----------------|
539
+ | ECDSA only | Schnorr + ML-DSA support |
540
+ | No quantum resistance | Built-in ML-DSA (FIPS 204) |
541
+ | Complex v, r, s handling | Single signature bytes parameter |
542
+ | Must store/derive public keys | Automatic key loading from Address |
543
+ | Silent ecrecover failures | Clean boolean returns |
544
+ | Signature malleability | BIP340 Schnorr (no malleability) |
545
+ | Manual EIP-712 implementation | Built-in domain separation patterns |
546
+ | No consensus-aware selection | Automatic algorithm selection |
547
+
548
+ ## Common Patterns
549
+
550
+ ### Signature-Based Authorization
551
+
552
+ ```typescript
553
+ @method(
554
+ { name: 'action', type: ABIDataTypes.UINT256 },
555
+ { name: 'deadline', type: ABIDataTypes.UINT64 },
556
+ { name: 'signature', type: ABIDataTypes.BYTES },
557
+ )
558
+ public executeWithSignature(calldata: Calldata): BytesWriter {
559
+ const action = calldata.readU256();
560
+ const deadline = calldata.readU64();
561
+ const signature = calldata.readBytesWithLength();
562
+
563
+ // Check deadline
564
+ if (Blockchain.block.medianTime > deadline) {
565
+ throw new Revert('Signature expired');
566
+ }
567
+
568
+ // Build message hash (include action + deadline)
569
+ const message = new BytesWriter(40);
570
+ message.writeU256(action);
571
+ message.writeU64(deadline);
572
+ const messageHash = sha256(message.getBuffer());
573
+
574
+ // Verify signature from sender
575
+ if (!Blockchain.verifySignature(Blockchain.tx.origin, signature, messageHash, true)) {
576
+ throw new Revert('Invalid signature');
577
+ }
578
+
579
+ // Execute action
580
+ this.executeAction(action);
581
+
582
+ return new BytesWriter(0);
583
+ }
584
+ ```
585
+
586
+ ### Nonce-Based Replay Protection
587
+
588
+ ```typescript
589
+ private noncesPointer: u16 = Blockchain.nextPointer;
590
+ private nonces: AddressMemoryMap;
591
+
592
+ @method(
593
+ { name: 'owner', type: ABIDataTypes.ADDRESS },
594
+ { name: 'spender', type: ABIDataTypes.ADDRESS },
595
+ { name: 'value', type: ABIDataTypes.UINT256 },
596
+ { name: 'deadline', type: ABIDataTypes.UINT64 },
597
+ { name: 'signature', type: ABIDataTypes.BYTES },
598
+ )
599
+ @emit('Approved')
600
+ public permit(calldata: Calldata): BytesWriter {
601
+ const owner = calldata.readAddress();
602
+ const spender = calldata.readAddress();
603
+ const value = calldata.readU256();
604
+ const deadline = calldata.readU64();
605
+ const signature = calldata.readBytesWithLength();
606
+
607
+ // Check deadline
608
+ if (Blockchain.block.medianTime > deadline) {
609
+ throw new Revert('Permit expired');
610
+ }
611
+
612
+ // Get and increment nonce (prevents replay)
613
+ const nonce = this.nonces.get(owner);
614
+ this.nonces.set(owner, SafeMath.add(nonce, u256.One));
615
+
616
+ // Build permit hash
617
+ const messageHash = this.buildPermitHash(owner, spender, value, nonce, deadline);
618
+
619
+ // Verify signature
620
+ if (!Blockchain.verifySignature(owner, signature, messageHash, false)) {
621
+ throw new Revert('Invalid signature');
622
+ }
623
+
624
+ // Set approval
625
+ this._approve(owner, spender, value);
626
+
627
+ return new BytesWriter(0);
628
+ }
629
+ ```
630
+
631
+ ### Multi-Signature Verification
632
+
633
+ ```typescript
634
+ @method(
635
+ { name: 'action', type: ABIDataTypes.BYTES },
636
+ { name: 'signers', type: ABIDataTypes.ADDRESS_ARRAY },
637
+ { name: 'signatures', type: ABIDataTypes.BYTES_ARRAY },
638
+ )
639
+ public executeMultiSig(calldata: Calldata): BytesWriter {
640
+ const action = calldata.readBytesWithLength();
641
+ const signers = calldata.readAddressArray();
642
+ const signatures = calldata.readBytesArray();
643
+
644
+ // Build action hash
645
+ const actionHash = sha256(action);
646
+
647
+ // Verify required signatures
648
+ let validCount: u32 = 0;
649
+ for (let i = 0; i < signers.length; i++) {
650
+ const signer = signers[i];
651
+ const signature = signatures[i];
652
+
653
+ if (this.isAuthorizedSigner(signer)) {
654
+ if (Blockchain.verifySignature(signer, signature, actionHash, true)) {
655
+ validCount++;
656
+ }
657
+ }
658
+ }
659
+
660
+ // Check threshold
661
+ if (validCount < this.threshold.value) {
662
+ throw new Revert('Insufficient signatures');
663
+ }
664
+
665
+ // Execute
666
+ this.executeAction(action);
667
+
668
+ return new BytesWriter(0);
669
+ }
670
+ ```
671
+
672
+ ## Security Best Practices
673
+
674
+ ### 1. Always Include Nonces
675
+
676
+ ```typescript
677
+ // Prevent signature replay
678
+ const nonce = this.nonces.get(signer);
679
+ this.nonces.set(signer, SafeMath.add(nonce, u256.One));
680
+ // Include nonce in message hash
681
+ ```
682
+
683
+ ### 2. Include Deadlines
684
+
685
+ ```typescript
686
+ // Limit signature validity
687
+ if (Blockchain.block.medianTime > deadline) {
688
+ throw new Revert('Signature expired');
689
+ }
690
+ ```
691
+
692
+ ### 3. Use Domain Separation
693
+
694
+ ```typescript
695
+ // Prevent cross-contract/cross-chain replay
696
+ const DOMAIN_SEPARATOR = buildDomainSeparator(
697
+ 'MyContract',
698
+ '1',
699
+ chainId,
700
+ Blockchain.contract.address
701
+ );
702
+ ```
703
+
704
+ ### 4. Prefer Quantum-Resistant Verification
705
+
706
+ ```typescript
707
+ // For high-security operations, force ML-DSA
708
+ Blockchain.verifySignature(signer, signature, hash, true);
709
+ ```
710
+
711
+ ---
712
+
713
+ **Navigation:**
714
+ - Previous: [Cross-Contract Calls](./cross-contract-calls.md)
715
+ - Next: [Quantum Resistance](./quantum-resistance.md)