@opcat-labs/opcat 2.1.2 → 2.1.3
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/CHANGELOG.md +12 -0
- package/cjs/interpreter/interpreter.cjs +172 -0
- package/cjs/opcode.cjs +4 -0
- package/esm/interpreter/interpreter.js +172 -0
- package/esm/opcode.js +4 -0
- package/package.json +1 -1
- package/test/opcode.cjs +3 -2
- package/test/script/interpreter.cjs +169 -0
- package/test/script/script.cjs +11 -1
- package/types/interpreter/interpreter.d.cts +14 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# @opcat-labs/opcat
|
|
2
2
|
|
|
3
|
+
## 2.1.3
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- refactor: reuse Interpreter methods for checkDataSig and add signData utility
|
|
8
|
+
- Refactor checkDataSig.ts to use Interpreter.checkDataSigSignatureEncoding() and checkPubkeyEncoding()
|
|
9
|
+
- Add signData(privateKey, message) utility for Oracle scenarios
|
|
10
|
+
- Add signDataWithInternalKey() using INTERNAL_KEY from ContextUtils
|
|
11
|
+
- Add end-to-end contract tests for CheckDataSig
|
|
12
|
+
- Add OP_CHECKSIGFROMSTACK and OP_CHECKSIGFROMSTACKVERIFY opcode tests
|
|
13
|
+
- Add TypeScript type declarations for checkDataSigSignatureEncoding and isDER
|
|
14
|
+
|
|
3
15
|
## 2.1.2
|
|
4
16
|
|
|
5
17
|
### Patch Changes
|
|
@@ -8,6 +8,7 @@ var BN = require('../crypto/bn.cjs');
|
|
|
8
8
|
var Hash = require('../crypto/hash.cjs');
|
|
9
9
|
var Signature = require('../crypto/signature.cjs');
|
|
10
10
|
var PublicKey = require('../publickey.cjs');
|
|
11
|
+
var ECDSA = require('../crypto/ecdsa.cjs');
|
|
11
12
|
var Stack = require('./stack.cjs');
|
|
12
13
|
var Transaction = require('../transaction/index.cjs');
|
|
13
14
|
|
|
@@ -443,6 +444,118 @@ Interpreter.prototype.checkPubkeyEncoding = function (buf) {
|
|
|
443
444
|
return true;
|
|
444
445
|
};
|
|
445
446
|
|
|
447
|
+
|
|
448
|
+
/**
|
|
449
|
+
* Validates pure DER signature encoding (without sighash type).
|
|
450
|
+
* Used for OP_CHECKSIGFROMSTACK which expects signatures without trailing sighash byte.
|
|
451
|
+
* @param {Buffer} buf - The buffer containing the signature to verify
|
|
452
|
+
* @returns {boolean} True if the signature is valid DER-encoded, false otherwise
|
|
453
|
+
* @static
|
|
454
|
+
*/
|
|
455
|
+
Interpreter.isDER = function (buf) {
|
|
456
|
+
if (buf.length < 8) {
|
|
457
|
+
// Non-canonical signature: too short (min DER sig is 8 bytes)
|
|
458
|
+
return false;
|
|
459
|
+
}
|
|
460
|
+
if (buf.length > 72) {
|
|
461
|
+
// Non-canonical signature: too long (max DER sig is 72 bytes without sighash)
|
|
462
|
+
return false;
|
|
463
|
+
}
|
|
464
|
+
if (buf[0] !== 0x30) {
|
|
465
|
+
// Non-canonical signature: wrong type
|
|
466
|
+
return false;
|
|
467
|
+
}
|
|
468
|
+
if (buf[1] !== buf.length - 2) {
|
|
469
|
+
// Non-canonical signature: wrong length marker (for pure DER, length = buf.length - 2)
|
|
470
|
+
return false;
|
|
471
|
+
}
|
|
472
|
+
var nLenR = buf[3];
|
|
473
|
+
if (5 + nLenR >= buf.length) {
|
|
474
|
+
// Non-canonical signature: S length misplaced
|
|
475
|
+
return false;
|
|
476
|
+
}
|
|
477
|
+
var nLenS = buf[5 + nLenR];
|
|
478
|
+
if (nLenR + nLenS + 6 !== buf.length) {
|
|
479
|
+
// Non-canonical signature: R+S length mismatch (for pure DER, total = R + S + 6)
|
|
480
|
+
return false;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
var R = buf.slice(4);
|
|
484
|
+
if (buf[4 - 2] !== 0x02) {
|
|
485
|
+
// Non-canonical signature: R value type mismatch
|
|
486
|
+
return false;
|
|
487
|
+
}
|
|
488
|
+
if (nLenR === 0) {
|
|
489
|
+
// Non-canonical signature: R length is zero
|
|
490
|
+
return false;
|
|
491
|
+
}
|
|
492
|
+
if (R[0] & 0x80) {
|
|
493
|
+
// Non-canonical signature: R value negative
|
|
494
|
+
return false;
|
|
495
|
+
}
|
|
496
|
+
if (nLenR > 1 && R[0] === 0x00 && !(R[1] & 0x80)) {
|
|
497
|
+
// Non-canonical signature: R value excessively padded
|
|
498
|
+
return false;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
var S = buf.slice(6 + nLenR);
|
|
502
|
+
if (buf[6 + nLenR - 2] !== 0x02) {
|
|
503
|
+
// Non-canonical signature: S value type mismatch
|
|
504
|
+
return false;
|
|
505
|
+
}
|
|
506
|
+
if (nLenS === 0) {
|
|
507
|
+
// Non-canonical signature: S length is zero
|
|
508
|
+
return false;
|
|
509
|
+
}
|
|
510
|
+
if (S[0] & 0x80) {
|
|
511
|
+
// Non-canonical signature: S value negative
|
|
512
|
+
return false;
|
|
513
|
+
}
|
|
514
|
+
if (nLenS > 1 && S[0] === 0x00 && !(S[1] & 0x80)) {
|
|
515
|
+
// Non-canonical signature: S value excessively padded
|
|
516
|
+
return false;
|
|
517
|
+
}
|
|
518
|
+
return true;
|
|
519
|
+
};
|
|
520
|
+
|
|
521
|
+
|
|
522
|
+
/**
|
|
523
|
+
* Checks if a signature encoding is valid for OP_CHECKSIGFROMSTACK.
|
|
524
|
+
* Unlike checkSignatureEncoding, this expects pure DER signatures without sighash type.
|
|
525
|
+
* @param {Buffer} buf - The signature buffer to validate
|
|
526
|
+
* @returns {boolean} True if valid, false otherwise (sets errstr on failure)
|
|
527
|
+
*/
|
|
528
|
+
Interpreter.prototype.checkDataSigSignatureEncoding = function (buf) {
|
|
529
|
+
var sig;
|
|
530
|
+
|
|
531
|
+
// Empty signature is allowed
|
|
532
|
+
if (buf.length === 0) {
|
|
533
|
+
return true;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
// For OP_CHECKSIGFROMSTACK, use pure DER validation (no sighash type)
|
|
537
|
+
if (
|
|
538
|
+
(this.flags &
|
|
539
|
+
(Interpreter.SCRIPT_VERIFY_DERSIG |
|
|
540
|
+
Interpreter.SCRIPT_VERIFY_LOW_S |
|
|
541
|
+
Interpreter.SCRIPT_VERIFY_STRICTENC)) !==
|
|
542
|
+
0 &&
|
|
543
|
+
!Interpreter.isDER(buf)
|
|
544
|
+
) {
|
|
545
|
+
this.errstr = 'SCRIPT_ERR_SIG_DER_INVALID_FORMAT';
|
|
546
|
+
return false;
|
|
547
|
+
} else if ((this.flags & Interpreter.SCRIPT_VERIFY_LOW_S) !== 0) {
|
|
548
|
+
sig = Signature.fromDER(buf);
|
|
549
|
+
if (!sig.hasLowS()) {
|
|
550
|
+
this.errstr = 'SCRIPT_ERR_SIG_DER_HIGH_S';
|
|
551
|
+
return false;
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
// Note: No STRICTENC hashtype check for OP_CHECKSIGFROMSTACK since it has no sighash type
|
|
555
|
+
|
|
556
|
+
return true
|
|
557
|
+
};
|
|
558
|
+
|
|
446
559
|
/**
|
|
447
560
|
* Checks if a buffer is minimally encoded (see https://github.com/bitcoincashorg/spec/blob/master/may-2018-reenabled-opcodes.md#op_bin2num) as a number.
|
|
448
561
|
* @param {Buffer} buf - The buffer to check.
|
|
@@ -1979,6 +2092,65 @@ Interpreter.prototype.step = function (scriptType) {
|
|
|
1979
2092
|
}
|
|
1980
2093
|
break;
|
|
1981
2094
|
|
|
2095
|
+
case Opcode.OP_CHECKSIGFROMSTACK:
|
|
2096
|
+
case Opcode.OP_CHECKSIGFROMSTACKVERIFY:
|
|
2097
|
+
// Stack order (bottom to top): <sig> <msg> <pubKey>
|
|
2098
|
+
// (sig msg pubkey -- bool) for OP_CHECKSIGFROMSTACK
|
|
2099
|
+
// (sig msg pubkey -- ) for OP_CHECKSIGFROMSTACKVERIFY
|
|
2100
|
+
// Note: Unlike OP_CHECKSIG, signatures are pure DER without sighash type
|
|
2101
|
+
if (this.stack.length < 3) {
|
|
2102
|
+
this.errstr = 'SCRIPT_ERR_INVALID_STACK_OPERATION';
|
|
2103
|
+
return false;
|
|
2104
|
+
}
|
|
2105
|
+
|
|
2106
|
+
bufPubkey = stacktop(-1); // pubKey at top
|
|
2107
|
+
var bufMsg = stacktop(-2); // msg in middle
|
|
2108
|
+
bufSig = stacktop(-3); // sig at bottom
|
|
2109
|
+
|
|
2110
|
+
// Use checkDataSigSignatureEncoding for pure DER validation (no sighash type)
|
|
2111
|
+
if (!this.checkDataSigSignatureEncoding(bufSig) || !this.checkPubkeyEncoding(bufPubkey)) {
|
|
2112
|
+
return false;
|
|
2113
|
+
}
|
|
2114
|
+
|
|
2115
|
+
fSuccess = false;
|
|
2116
|
+
try {
|
|
2117
|
+
// Compute SHA256 of msg (single hash, not double)
|
|
2118
|
+
// Reverse to little-endian format (same as checkSig) for signature verification
|
|
2119
|
+
var hashbuf = Hash.sha256(bufMsg).reverse();
|
|
2120
|
+
|
|
2121
|
+
// Use fromDER for pure DER signatures (no sighash type)
|
|
2122
|
+
sig = Signature.fromDER(bufSig);
|
|
2123
|
+
pubkey = PublicKey.fromBuffer(bufPubkey, false);
|
|
2124
|
+
|
|
2125
|
+
// Verify signature using ECDSA with little endian
|
|
2126
|
+
fSuccess = ECDSA.verify(hashbuf, sig, pubkey, 'little');
|
|
2127
|
+
} catch (e) {
|
|
2128
|
+
// invalid sig or pubkey
|
|
2129
|
+
fSuccess = false;
|
|
2130
|
+
}
|
|
2131
|
+
|
|
2132
|
+
if (!fSuccess && this.flags & Interpreter.SCRIPT_VERIFY_NULLFAIL && bufSig.length) {
|
|
2133
|
+
this.errstr = 'SCRIPT_ERR_NULLFAIL';
|
|
2134
|
+
return false;
|
|
2135
|
+
}
|
|
2136
|
+
|
|
2137
|
+
// Pop all 3 elements
|
|
2138
|
+
this.stack.pop();
|
|
2139
|
+
this.stack.pop();
|
|
2140
|
+
this.stack.pop();
|
|
2141
|
+
|
|
2142
|
+
if (opcodenum === Opcode.OP_CHECKSIGFROMSTACKVERIFY) {
|
|
2143
|
+
if (!fSuccess) {
|
|
2144
|
+
this.errstr = 'SCRIPT_ERR_CHECKSIGFROMSTACKVERIFY';
|
|
2145
|
+
return false;
|
|
2146
|
+
}
|
|
2147
|
+
// VERIFY variant doesn't push anything on success
|
|
2148
|
+
} else {
|
|
2149
|
+
// Push result for OP_CHECKSIGFROMSTACK
|
|
2150
|
+
this.stack.push(fSuccess ? Interpreter.getTrue() : Interpreter.getFalse());
|
|
2151
|
+
}
|
|
2152
|
+
break;
|
|
2153
|
+
|
|
1982
2154
|
default:
|
|
1983
2155
|
this.errstr = 'SCRIPT_ERR_BAD_OPCODE';
|
|
1984
2156
|
return false;
|
package/cjs/opcode.cjs
CHANGED
|
@@ -264,6 +264,10 @@ Opcode.map = {
|
|
|
264
264
|
OP_CHECKLOCKTIMEVERIFY: 177,
|
|
265
265
|
OP_CHECKSEQUENCEVERIFY: 178,
|
|
266
266
|
|
|
267
|
+
// OPCAT signature from stack
|
|
268
|
+
OP_CHECKSIGFROMSTACK: 186, // 0xba
|
|
269
|
+
OP_CHECKSIGFROMSTACKVERIFY: 187, // 0xbb
|
|
270
|
+
|
|
267
271
|
// expansion
|
|
268
272
|
OP_NOP1: 176,
|
|
269
273
|
OP_NOP2: 177,
|
|
@@ -7,6 +7,7 @@ import BN from '../crypto/bn.js';
|
|
|
7
7
|
import Hash from '../crypto/hash.js';
|
|
8
8
|
import Signature from '../crypto/signature.js';
|
|
9
9
|
import PublicKey from '../publickey.js';
|
|
10
|
+
import ECDSA from '../crypto/ecdsa.js';
|
|
10
11
|
import Stack from './stack.js';
|
|
11
12
|
import Transaction from '../transaction/index.js';
|
|
12
13
|
|
|
@@ -442,6 +443,118 @@ Interpreter.prototype.checkPubkeyEncoding = function (buf) {
|
|
|
442
443
|
return true;
|
|
443
444
|
};
|
|
444
445
|
|
|
446
|
+
|
|
447
|
+
/**
|
|
448
|
+
* Validates pure DER signature encoding (without sighash type).
|
|
449
|
+
* Used for OP_CHECKSIGFROMSTACK which expects signatures without trailing sighash byte.
|
|
450
|
+
* @param {Buffer} buf - The buffer containing the signature to verify
|
|
451
|
+
* @returns {boolean} True if the signature is valid DER-encoded, false otherwise
|
|
452
|
+
* @static
|
|
453
|
+
*/
|
|
454
|
+
Interpreter.isDER = function (buf) {
|
|
455
|
+
if (buf.length < 8) {
|
|
456
|
+
// Non-canonical signature: too short (min DER sig is 8 bytes)
|
|
457
|
+
return false;
|
|
458
|
+
}
|
|
459
|
+
if (buf.length > 72) {
|
|
460
|
+
// Non-canonical signature: too long (max DER sig is 72 bytes without sighash)
|
|
461
|
+
return false;
|
|
462
|
+
}
|
|
463
|
+
if (buf[0] !== 0x30) {
|
|
464
|
+
// Non-canonical signature: wrong type
|
|
465
|
+
return false;
|
|
466
|
+
}
|
|
467
|
+
if (buf[1] !== buf.length - 2) {
|
|
468
|
+
// Non-canonical signature: wrong length marker (for pure DER, length = buf.length - 2)
|
|
469
|
+
return false;
|
|
470
|
+
}
|
|
471
|
+
var nLenR = buf[3];
|
|
472
|
+
if (5 + nLenR >= buf.length) {
|
|
473
|
+
// Non-canonical signature: S length misplaced
|
|
474
|
+
return false;
|
|
475
|
+
}
|
|
476
|
+
var nLenS = buf[5 + nLenR];
|
|
477
|
+
if (nLenR + nLenS + 6 !== buf.length) {
|
|
478
|
+
// Non-canonical signature: R+S length mismatch (for pure DER, total = R + S + 6)
|
|
479
|
+
return false;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
var R = buf.slice(4);
|
|
483
|
+
if (buf[4 - 2] !== 0x02) {
|
|
484
|
+
// Non-canonical signature: R value type mismatch
|
|
485
|
+
return false;
|
|
486
|
+
}
|
|
487
|
+
if (nLenR === 0) {
|
|
488
|
+
// Non-canonical signature: R length is zero
|
|
489
|
+
return false;
|
|
490
|
+
}
|
|
491
|
+
if (R[0] & 0x80) {
|
|
492
|
+
// Non-canonical signature: R value negative
|
|
493
|
+
return false;
|
|
494
|
+
}
|
|
495
|
+
if (nLenR > 1 && R[0] === 0x00 && !(R[1] & 0x80)) {
|
|
496
|
+
// Non-canonical signature: R value excessively padded
|
|
497
|
+
return false;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
var S = buf.slice(6 + nLenR);
|
|
501
|
+
if (buf[6 + nLenR - 2] !== 0x02) {
|
|
502
|
+
// Non-canonical signature: S value type mismatch
|
|
503
|
+
return false;
|
|
504
|
+
}
|
|
505
|
+
if (nLenS === 0) {
|
|
506
|
+
// Non-canonical signature: S length is zero
|
|
507
|
+
return false;
|
|
508
|
+
}
|
|
509
|
+
if (S[0] & 0x80) {
|
|
510
|
+
// Non-canonical signature: S value negative
|
|
511
|
+
return false;
|
|
512
|
+
}
|
|
513
|
+
if (nLenS > 1 && S[0] === 0x00 && !(S[1] & 0x80)) {
|
|
514
|
+
// Non-canonical signature: S value excessively padded
|
|
515
|
+
return false;
|
|
516
|
+
}
|
|
517
|
+
return true;
|
|
518
|
+
};
|
|
519
|
+
|
|
520
|
+
|
|
521
|
+
/**
|
|
522
|
+
* Checks if a signature encoding is valid for OP_CHECKSIGFROMSTACK.
|
|
523
|
+
* Unlike checkSignatureEncoding, this expects pure DER signatures without sighash type.
|
|
524
|
+
* @param {Buffer} buf - The signature buffer to validate
|
|
525
|
+
* @returns {boolean} True if valid, false otherwise (sets errstr on failure)
|
|
526
|
+
*/
|
|
527
|
+
Interpreter.prototype.checkDataSigSignatureEncoding = function (buf) {
|
|
528
|
+
var sig;
|
|
529
|
+
|
|
530
|
+
// Empty signature is allowed
|
|
531
|
+
if (buf.length === 0) {
|
|
532
|
+
return true;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
// For OP_CHECKSIGFROMSTACK, use pure DER validation (no sighash type)
|
|
536
|
+
if (
|
|
537
|
+
(this.flags &
|
|
538
|
+
(Interpreter.SCRIPT_VERIFY_DERSIG |
|
|
539
|
+
Interpreter.SCRIPT_VERIFY_LOW_S |
|
|
540
|
+
Interpreter.SCRIPT_VERIFY_STRICTENC)) !==
|
|
541
|
+
0 &&
|
|
542
|
+
!Interpreter.isDER(buf)
|
|
543
|
+
) {
|
|
544
|
+
this.errstr = 'SCRIPT_ERR_SIG_DER_INVALID_FORMAT';
|
|
545
|
+
return false;
|
|
546
|
+
} else if ((this.flags & Interpreter.SCRIPT_VERIFY_LOW_S) !== 0) {
|
|
547
|
+
sig = Signature.fromDER(buf);
|
|
548
|
+
if (!sig.hasLowS()) {
|
|
549
|
+
this.errstr = 'SCRIPT_ERR_SIG_DER_HIGH_S';
|
|
550
|
+
return false;
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
// Note: No STRICTENC hashtype check for OP_CHECKSIGFROMSTACK since it has no sighash type
|
|
554
|
+
|
|
555
|
+
return true;
|
|
556
|
+
};
|
|
557
|
+
|
|
445
558
|
/**
|
|
446
559
|
* Checks if a buffer is minimally encoded (see https://github.com/bitcoincashorg/spec/blob/master/may-2018-reenabled-opcodes.md#op_bin2num) as a number.
|
|
447
560
|
* @param {Buffer} buf - The buffer to check.
|
|
@@ -1978,6 +2091,65 @@ Interpreter.prototype.step = function (scriptType) {
|
|
|
1978
2091
|
}
|
|
1979
2092
|
break;
|
|
1980
2093
|
|
|
2094
|
+
case Opcode.OP_CHECKSIGFROMSTACK:
|
|
2095
|
+
case Opcode.OP_CHECKSIGFROMSTACKVERIFY:
|
|
2096
|
+
// Stack order (bottom to top): <sig> <msg> <pubKey>
|
|
2097
|
+
// (sig msg pubkey -- bool) for OP_CHECKSIGFROMSTACK
|
|
2098
|
+
// (sig msg pubkey -- ) for OP_CHECKSIGFROMSTACKVERIFY
|
|
2099
|
+
// Note: Unlike OP_CHECKSIG, signatures are pure DER without sighash type
|
|
2100
|
+
if (this.stack.length < 3) {
|
|
2101
|
+
this.errstr = 'SCRIPT_ERR_INVALID_STACK_OPERATION';
|
|
2102
|
+
return false;
|
|
2103
|
+
}
|
|
2104
|
+
|
|
2105
|
+
bufPubkey = stacktop(-1); // pubKey at top
|
|
2106
|
+
var bufMsg = stacktop(-2); // msg in middle
|
|
2107
|
+
bufSig = stacktop(-3); // sig at bottom
|
|
2108
|
+
|
|
2109
|
+
// Use checkDataSigSignatureEncoding for pure DER validation (no sighash type)
|
|
2110
|
+
if (!this.checkDataSigSignatureEncoding(bufSig) || !this.checkPubkeyEncoding(bufPubkey)) {
|
|
2111
|
+
return false;
|
|
2112
|
+
}
|
|
2113
|
+
|
|
2114
|
+
fSuccess = false;
|
|
2115
|
+
try {
|
|
2116
|
+
// Compute SHA256 of msg (single hash, not double)
|
|
2117
|
+
// Reverse to little-endian format (same as checkSig) for signature verification
|
|
2118
|
+
var hashbuf = Hash.sha256(bufMsg).reverse();
|
|
2119
|
+
|
|
2120
|
+
// Use fromDER for pure DER signatures (no sighash type)
|
|
2121
|
+
sig = Signature.fromDER(bufSig);
|
|
2122
|
+
pubkey = PublicKey.fromBuffer(bufPubkey, false);
|
|
2123
|
+
|
|
2124
|
+
// Verify signature using ECDSA with little endian
|
|
2125
|
+
fSuccess = ECDSA.verify(hashbuf, sig, pubkey, 'little');
|
|
2126
|
+
} catch (e) {
|
|
2127
|
+
// invalid sig or pubkey
|
|
2128
|
+
fSuccess = false;
|
|
2129
|
+
}
|
|
2130
|
+
|
|
2131
|
+
if (!fSuccess && this.flags & Interpreter.SCRIPT_VERIFY_NULLFAIL && bufSig.length) {
|
|
2132
|
+
this.errstr = 'SCRIPT_ERR_NULLFAIL';
|
|
2133
|
+
return false;
|
|
2134
|
+
}
|
|
2135
|
+
|
|
2136
|
+
// Pop all 3 elements
|
|
2137
|
+
this.stack.pop();
|
|
2138
|
+
this.stack.pop();
|
|
2139
|
+
this.stack.pop();
|
|
2140
|
+
|
|
2141
|
+
if (opcodenum === Opcode.OP_CHECKSIGFROMSTACKVERIFY) {
|
|
2142
|
+
if (!fSuccess) {
|
|
2143
|
+
this.errstr = 'SCRIPT_ERR_CHECKSIGFROMSTACKVERIFY';
|
|
2144
|
+
return false;
|
|
2145
|
+
}
|
|
2146
|
+
// VERIFY variant doesn't push anything on success
|
|
2147
|
+
} else {
|
|
2148
|
+
// Push result for OP_CHECKSIGFROMSTACK
|
|
2149
|
+
this.stack.push(fSuccess ? Interpreter.getTrue() : Interpreter.getFalse());
|
|
2150
|
+
}
|
|
2151
|
+
break;
|
|
2152
|
+
|
|
1981
2153
|
default:
|
|
1982
2154
|
this.errstr = 'SCRIPT_ERR_BAD_OPCODE';
|
|
1983
2155
|
return false;
|
package/esm/opcode.js
CHANGED
|
@@ -264,6 +264,10 @@ Opcode.map = {
|
|
|
264
264
|
OP_CHECKLOCKTIMEVERIFY: 177,
|
|
265
265
|
OP_CHECKSEQUENCEVERIFY: 178,
|
|
266
266
|
|
|
267
|
+
// OPCAT signature from stack
|
|
268
|
+
OP_CHECKSIGFROMSTACK: 186, // 0xba
|
|
269
|
+
OP_CHECKSIGFROMSTACKVERIFY: 187, // 0xbb
|
|
270
|
+
|
|
267
271
|
// expansion
|
|
268
272
|
OP_NOP1: 176,
|
|
269
273
|
OP_NOP2: 177,
|
package/package.json
CHANGED
package/test/opcode.cjs
CHANGED
|
@@ -83,8 +83,9 @@ describe('Opcode', function () {
|
|
|
83
83
|
});
|
|
84
84
|
|
|
85
85
|
describe('@map', function () {
|
|
86
|
-
it('should have a map containing
|
|
87
|
-
|
|
86
|
+
it('should have a map containing 120 elements', function () {
|
|
87
|
+
// 118 base opcodes + 2 new CHECKSIGFROMSTACK opcodes
|
|
88
|
+
Object.keys(Opcode.map).length.should.equal(120);
|
|
88
89
|
});
|
|
89
90
|
});
|
|
90
91
|
|
|
@@ -731,4 +731,173 @@ describe('Interpreter', function () {
|
|
|
731
731
|
testTxs(txValid, true);
|
|
732
732
|
//testTxs(txInvalid, false);
|
|
733
733
|
});
|
|
734
|
+
|
|
735
|
+
describe('OP_CHECKSIGFROMSTACK', function () {
|
|
736
|
+
var crypto = opcat.crypto;
|
|
737
|
+
var Hash = crypto.Hash;
|
|
738
|
+
var ECDSA = crypto.ECDSA;
|
|
739
|
+
|
|
740
|
+
it('should verify a valid signature with OP_CHECKSIGFROMSTACK', function () {
|
|
741
|
+
// Generate keypair
|
|
742
|
+
var privKey = opcat.PrivateKey.fromRandom();
|
|
743
|
+
var pubKey = privKey.toPublicKey();
|
|
744
|
+
|
|
745
|
+
// Message to sign
|
|
746
|
+
var message = Buffer.from('Test message for OP_CHECKSIGFROMSTACK', 'utf8');
|
|
747
|
+
|
|
748
|
+
// Hash the message with SHA256 (single hash) and reverse for little-endian
|
|
749
|
+
// The BVM does sha256(msg).reverse() before ECDSA.verify
|
|
750
|
+
var msgHash = Hash.sha256(message).reverse();
|
|
751
|
+
|
|
752
|
+
// Sign the hash
|
|
753
|
+
var signature = ECDSA.sign(msgHash, privKey, 'little');
|
|
754
|
+
|
|
755
|
+
// Use pure DER signature (NO sighash type for OP_CHECKSIGFROMSTACK)
|
|
756
|
+
var sigDER = signature.toDER();
|
|
757
|
+
|
|
758
|
+
// Build script: <sig> <msg> <pubkey> OP_CHECKSIGFROMSTACK
|
|
759
|
+
var scriptSig = new Script()
|
|
760
|
+
.add(sigDER)
|
|
761
|
+
.add(message)
|
|
762
|
+
.add(pubKey.toBuffer());
|
|
763
|
+
|
|
764
|
+
var scriptPubkey = new Script()
|
|
765
|
+
.add(Opcode.OP_CHECKSIGFROMSTACK);
|
|
766
|
+
|
|
767
|
+
// Create interpreter and verify
|
|
768
|
+
var interp = new Interpreter();
|
|
769
|
+
var result = interp.verify(scriptSig, scriptPubkey);
|
|
770
|
+
|
|
771
|
+
result.should.equal(true);
|
|
772
|
+
interp.errstr.should.equal('');
|
|
773
|
+
});
|
|
774
|
+
|
|
775
|
+
it('should fail with invalid signature using OP_CHECKSIGFROMSTACK', function () {
|
|
776
|
+
// Generate two keypairs
|
|
777
|
+
var privKey = opcat.PrivateKey.fromRandom();
|
|
778
|
+
var pubKey = privKey.toPublicKey();
|
|
779
|
+
var wrongPrivKey = opcat.PrivateKey.fromRandom();
|
|
780
|
+
|
|
781
|
+
// Message to sign
|
|
782
|
+
var message = Buffer.from('Test message', 'utf8');
|
|
783
|
+
|
|
784
|
+
// Hash the message with SHA256 and reverse for little-endian
|
|
785
|
+
var msgHash = Hash.sha256(message).reverse();
|
|
786
|
+
|
|
787
|
+
// Sign with wrong private key - use pure DER (no sighash type)
|
|
788
|
+
var signature = ECDSA.sign(msgHash, wrongPrivKey, 'little');
|
|
789
|
+
var sigDER = signature.toDER();
|
|
790
|
+
|
|
791
|
+
// Build script with correct pubkey but wrong signature
|
|
792
|
+
var scriptSig = new Script()
|
|
793
|
+
.add(sigDER)
|
|
794
|
+
.add(message)
|
|
795
|
+
.add(pubKey.toBuffer());
|
|
796
|
+
|
|
797
|
+
var scriptPubkey = new Script()
|
|
798
|
+
.add(Opcode.OP_CHECKSIGFROMSTACK);
|
|
799
|
+
|
|
800
|
+
var interp = new Interpreter();
|
|
801
|
+
var result = interp.verify(scriptSig, scriptPubkey);
|
|
802
|
+
|
|
803
|
+
result.should.equal(false);
|
|
804
|
+
});
|
|
805
|
+
|
|
806
|
+
it('should verify with OP_CHECKSIGFROMSTACKVERIFY and continue execution', function () {
|
|
807
|
+
// Generate keypair
|
|
808
|
+
var privKey = opcat.PrivateKey.fromRandom();
|
|
809
|
+
var pubKey = privKey.toPublicKey();
|
|
810
|
+
|
|
811
|
+
// Message to sign
|
|
812
|
+
var message = Buffer.from('VERIFY test', 'utf8');
|
|
813
|
+
var msgHash = Hash.sha256(message).reverse();
|
|
814
|
+
var signature = ECDSA.sign(msgHash, privKey, 'little');
|
|
815
|
+
// Use pure DER signature (no sighash type)
|
|
816
|
+
var sigDER = signature.toDER();
|
|
817
|
+
|
|
818
|
+
// Build script: <sig> <msg> <pubkey> OP_CHECKSIGFROMSTACKVERIFY OP_1
|
|
819
|
+
var scriptSig = new Script()
|
|
820
|
+
.add(sigDER)
|
|
821
|
+
.add(message)
|
|
822
|
+
.add(pubKey.toBuffer());
|
|
823
|
+
|
|
824
|
+
var scriptPubkey = new Script()
|
|
825
|
+
.add(Opcode.OP_CHECKSIGFROMSTACKVERIFY)
|
|
826
|
+
.add(Opcode.OP_1);
|
|
827
|
+
|
|
828
|
+
var interp = new Interpreter();
|
|
829
|
+
var result = interp.verify(scriptSig, scriptPubkey);
|
|
830
|
+
|
|
831
|
+
result.should.equal(true);
|
|
832
|
+
});
|
|
833
|
+
|
|
834
|
+
it('should fail OP_CHECKSIGFROMSTACKVERIFY with invalid signature', function () {
|
|
835
|
+
// Generate keypair
|
|
836
|
+
var privKey = opcat.PrivateKey.fromRandom();
|
|
837
|
+
var pubKey = privKey.toPublicKey();
|
|
838
|
+
var wrongPrivKey = opcat.PrivateKey.fromRandom();
|
|
839
|
+
|
|
840
|
+
var message = Buffer.from('VERIFY fail test', 'utf8');
|
|
841
|
+
var msgHash = Hash.sha256(message).reverse();
|
|
842
|
+
var signature = ECDSA.sign(msgHash, wrongPrivKey, 'little');
|
|
843
|
+
// Use pure DER signature (no sighash type)
|
|
844
|
+
var sigDER = signature.toDER();
|
|
845
|
+
|
|
846
|
+
var scriptSig = new Script()
|
|
847
|
+
.add(sigDER)
|
|
848
|
+
.add(message)
|
|
849
|
+
.add(pubKey.toBuffer());
|
|
850
|
+
|
|
851
|
+
var scriptPubkey = new Script()
|
|
852
|
+
.add(Opcode.OP_CHECKSIGFROMSTACKVERIFY)
|
|
853
|
+
.add(Opcode.OP_1);
|
|
854
|
+
|
|
855
|
+
var interp = new Interpreter();
|
|
856
|
+
var result = interp.verify(scriptSig, scriptPubkey);
|
|
857
|
+
|
|
858
|
+
result.should.equal(false);
|
|
859
|
+
interp.errstr.should.equal('SCRIPT_ERR_CHECKSIGFROMSTACKVERIFY');
|
|
860
|
+
});
|
|
861
|
+
|
|
862
|
+
it('should fail with insufficient stack elements', function () {
|
|
863
|
+
// Only provide 2 elements instead of 3
|
|
864
|
+
var scriptSig = new Script()
|
|
865
|
+
.add(Buffer.from('signature', 'utf8'))
|
|
866
|
+
.add(Buffer.from('message', 'utf8'));
|
|
867
|
+
|
|
868
|
+
var scriptPubkey = new Script()
|
|
869
|
+
.add(Opcode.OP_CHECKSIGFROMSTACK);
|
|
870
|
+
|
|
871
|
+
var interp = new Interpreter();
|
|
872
|
+
var result = interp.verify(scriptSig, scriptPubkey);
|
|
873
|
+
|
|
874
|
+
result.should.equal(false);
|
|
875
|
+
interp.errstr.should.equal('SCRIPT_ERR_INVALID_STACK_OPERATION');
|
|
876
|
+
});
|
|
877
|
+
|
|
878
|
+
it('should work with empty message', function () {
|
|
879
|
+
var privKey = opcat.PrivateKey.fromRandom();
|
|
880
|
+
var pubKey = privKey.toPublicKey();
|
|
881
|
+
|
|
882
|
+
// Empty message
|
|
883
|
+
var message = Buffer.from('', 'utf8');
|
|
884
|
+
var msgHash = Hash.sha256(message).reverse();
|
|
885
|
+
var signature = ECDSA.sign(msgHash, privKey, 'little');
|
|
886
|
+
// Use pure DER signature (no sighash type)
|
|
887
|
+
var sigDER = signature.toDER();
|
|
888
|
+
|
|
889
|
+
var scriptSig = new Script()
|
|
890
|
+
.add(sigDER)
|
|
891
|
+
.add(message)
|
|
892
|
+
.add(pubKey.toBuffer());
|
|
893
|
+
|
|
894
|
+
var scriptPubkey = new Script()
|
|
895
|
+
.add(Opcode.OP_CHECKSIGFROMSTACK);
|
|
896
|
+
|
|
897
|
+
var interp = new Interpreter();
|
|
898
|
+
var result = interp.verify(scriptSig, scriptPubkey);
|
|
899
|
+
|
|
900
|
+
result.should.equal(true);
|
|
901
|
+
});
|
|
902
|
+
});
|
|
734
903
|
});
|
package/test/script/script.cjs
CHANGED
|
@@ -871,7 +871,8 @@ describe('Script', function () {
|
|
|
871
871
|
|
|
872
872
|
describe('#add and #prepend', function () {
|
|
873
873
|
it('should add these ops', function () {
|
|
874
|
-
|
|
874
|
+
// 186 (0xba) is now OP_CHECKSIGFROMSTACK
|
|
875
|
+
Script().add(1).add(10).add(186).toString().should.equal('1 0x0a OP_CHECKSIGFROMSTACK');
|
|
875
876
|
Script().add(Buffer.from('03e8', 'hex')).toString().should.equal('2 0x03e8');
|
|
876
877
|
Script().add('OP_CHECKMULTISIG').toString().should.equal('OP_CHECKMULTISIG');
|
|
877
878
|
Script().add('OP_1').add('OP_2').toString().should.equal('OP_1 OP_2');
|
|
@@ -894,6 +895,15 @@ describe('Script', function () {
|
|
|
894
895
|
.should.equal('OP_4 OP_2 OP_1 OP_3');
|
|
895
896
|
});
|
|
896
897
|
|
|
898
|
+
it('should recognize OP_CHECKSIGFROMSTACK and OP_CHECKSIGFROMSTACKVERIFY opcodes', function () {
|
|
899
|
+
// Test adding by opcode number
|
|
900
|
+
Script().add(186).toString().should.equal('OP_CHECKSIGFROMSTACK');
|
|
901
|
+
Script().add(187).toString().should.equal('OP_CHECKSIGFROMSTACKVERIFY');
|
|
902
|
+
// Test adding by opcode name
|
|
903
|
+
Script().add('OP_CHECKSIGFROMSTACK').toString().should.equal('OP_CHECKSIGFROMSTACK');
|
|
904
|
+
Script().add('OP_CHECKSIGFROMSTACKVERIFY').toString().should.equal('OP_CHECKSIGFROMSTACKVERIFY');
|
|
905
|
+
});
|
|
906
|
+
|
|
897
907
|
it('should add these push data', function () {
|
|
898
908
|
var buf = Buffer.alloc(1);
|
|
899
909
|
buf.fill(0);
|
|
@@ -118,6 +118,13 @@ declare class Interpreter {
|
|
|
118
118
|
* @returns {boolean} True if valid, false otherwise (with error string set).
|
|
119
119
|
*/
|
|
120
120
|
checkPubkeyEncoding(buf: Buffer): boolean;
|
|
121
|
+
/**
|
|
122
|
+
* Checks if a signature encoding is valid for OP_CHECKSIGFROMSTACK.
|
|
123
|
+
* Unlike checkSignatureEncoding, this expects pure DER signatures without sighash type.
|
|
124
|
+
* @param {Buffer} buf - The signature buffer to validate
|
|
125
|
+
* @returns {boolean} True if valid, false otherwise (sets errstr on failure)
|
|
126
|
+
*/
|
|
127
|
+
checkDataSigSignatureEncoding(buf: Buffer): boolean;
|
|
121
128
|
/**
|
|
122
129
|
* Evaluates a script by executing each opcode step-by-step.
|
|
123
130
|
* Performs size checks on the script and stacks before execution.
|
|
@@ -169,6 +176,13 @@ declare class Interpreter {
|
|
|
169
176
|
declare namespace Interpreter {
|
|
170
177
|
function getTrue(): Buffer;
|
|
171
178
|
function getFalse(): Buffer;
|
|
179
|
+
/**
|
|
180
|
+
* Validates pure DER signature encoding (without sighash type).
|
|
181
|
+
* Used for OP_CHECKSIGFROMSTACK which expects signatures without trailing sighash byte.
|
|
182
|
+
* @param {Buffer} buf - The buffer containing the signature to verify
|
|
183
|
+
* @returns {boolean} True if the signature is valid DER-encoded, false otherwise
|
|
184
|
+
*/
|
|
185
|
+
function isDER(buf: Buffer): boolean;
|
|
172
186
|
let MAX_SCRIPT_ELEMENT_SIZE: number;
|
|
173
187
|
let MAXIMUM_ELEMENT_SIZE: number;
|
|
174
188
|
let LOCKTIME_THRESHOLD: number;
|