@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.
- package/LICENSE +190 -0
- package/README.md +258 -137
- package/SECURITY.md +226 -0
- package/docs/README.md +614 -0
- package/docs/advanced/bitcoin-scripts.md +939 -0
- package/docs/advanced/cross-contract-calls.md +579 -0
- package/docs/advanced/plugins.md +1006 -0
- package/docs/advanced/quantum-resistance.md +660 -0
- package/docs/advanced/signature-verification.md +715 -0
- package/docs/api-reference/blockchain.md +729 -0
- package/docs/api-reference/events.md +642 -0
- package/docs/api-reference/op20.md +902 -0
- package/docs/api-reference/op721.md +819 -0
- package/docs/api-reference/safe-math.md +510 -0
- package/docs/api-reference/storage.md +731 -0
- package/docs/contracts/op-net-base.md +786 -0
- package/docs/contracts/op20-token.md +687 -0
- package/docs/contracts/op20s-signatures.md +614 -0
- package/docs/contracts/op721-nft.md +785 -0
- package/docs/contracts/reentrancy-guard.md +787 -0
- package/docs/core-concepts/blockchain-environment.md +724 -0
- package/docs/core-concepts/decorators.md +466 -0
- package/docs/core-concepts/events.md +652 -0
- package/docs/core-concepts/pointers.md +391 -0
- package/docs/core-concepts/security.md +370 -0
- package/docs/core-concepts/storage-system.md +938 -0
- package/docs/examples/basic-token.md +745 -0
- package/docs/examples/nft-with-reservations.md +1210 -0
- package/docs/examples/oracle-integration.md +1212 -0
- package/docs/examples/stablecoin.md +1180 -0
- package/docs/getting-started/first-contract.md +575 -0
- package/docs/getting-started/installation.md +384 -0
- package/docs/getting-started/project-structure.md +630 -0
- package/docs/storage/memory-maps.md +721 -0
- package/docs/storage/stored-arrays.md +714 -0
- package/docs/storage/stored-maps.md +686 -0
- package/docs/storage/stored-primitives.md +608 -0
- package/docs/types/address.md +773 -0
- package/docs/types/bytes-writer-reader.md +938 -0
- package/docs/types/calldata.md +744 -0
- package/docs/types/safe-math.md +403 -0
- package/package.json +51 -26
- package/runtime/memory/MapOfMap.ts +1 -0
- package/runtime/types/SafeMath.ts +121 -1
- 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)
|