@smartledger/bsv 5.5.3 → 6.0.1
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 +142 -0
- package/README.md +38 -38
- package/bin/cli.js +35 -5
- package/bsv-covenant.min.js +6 -6
- package/bsv-gdaf.min.js +2 -2
- package/bsv-ltp.min.js +2 -2
- package/bsv-shamir.min.js +1 -1
- package/bsv-smartcontract.min.js +6 -6
- package/bsv-statuslist.min.js +5 -5
- package/bsv-vcjwt.min.js +1 -1
- package/bsv.bundle.js +2 -2
- package/bsv.d.ts +289 -10
- package/bsv.min.js +2 -2
- package/docs/MODULE_REFERENCE_COMPLETE.md +27 -27
- package/docs/advanced/UTXO_MANAGER_GUIDE.md +1 -1
- package/docs/getting-started/INSTALLATION.md +23 -23
- package/docs/getting-started/QUICK_START.md +7 -7
- package/docs/migration/FROM_BSV_1_5_6.md +5 -5
- package/index.js +4 -0
- package/lib/crypto/ecdsa.js +13 -0
- package/lib/crypto/shamir.js +16 -4
- package/lib/gdaf/did-resolver.js +3 -1
- package/lib/gdaf/smartledger-anchor.js +154 -12
- package/lib/ltp/anchor.js +16 -3
- package/lib/ltp/obligation.js +56 -8
- package/lib/ltp/proof.js +4 -2
- package/lib/ltp/right.js +86 -11
- package/lib/ordinals/README.md +170 -0
- package/lib/ordinals/bsv20.js +185 -0
- package/lib/ordinals/index.js +49 -0
- package/lib/ordinals/inscription.js +159 -0
- package/lib/ordinals/ordlock.js +560 -0
- package/lib/smart_contract/builder.js +27 -3
- package/lib/smart_contract/covenant.js +37 -10
- package/lib/smart_contract/covenant_builder.js +7 -5
- package/lib/smart_contract/index.js +28 -15
- package/lib/smart_contract/preimage.js +7 -7
- package/lib/smart_contract/pushtx.js +47 -7
- package/lib/smart_contract/token.js +69 -1
- package/lib/spv/headerchain.js +77 -0
- package/lib/spv/index.js +16 -0
- package/lib/spv/merkleproof.js +108 -0
- package/lib/statuslist/index.js +65 -24
- package/lib/vcjwt/index.js +6 -0
- package/package.json +1 -1
- package/test/credentials-test.js +22 -19
- package/test/crypto/shamir.js +9 -1
- package/test/crypto/shamir_rngcap.js +29 -0
- package/test/gdaf/anchor_spv.js +163 -0
- package/test/ltp/verify_failclosed.js +134 -0
- package/test/ordinals/bsv20.js +126 -0
- package/test/ordinals/inscription.js +71 -0
- package/test/ordinals/ordlock.js +552 -0
- package/test/smart_contract/extract_field.js +61 -0
- package/test/smart_contract/nonenforcing_guard.js +44 -0
- package/test/smart_contract/ordinal_transfer.js +83 -0
- package/test/smart_contract/sighash_marketplace.js +110 -0
- package/test/spv/headerchain.js +86 -0
- package/test/spv/merkleproof.js +133 -0
- package/test/statuslist/failclosed.js +78 -0
|
@@ -1,15 +1,27 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* SmartContract.Covenant Class
|
|
3
3
|
* ============================
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
4
|
+
*
|
|
5
|
+
* ⚠️ NON-ENFORCING BY DESIGN — NOT A REAL COVENANT. The locking script this class
|
|
6
|
+
* builds is:
|
|
7
|
+
*
|
|
8
|
+
* OP_DUP OP_HASH256 <creationPreimageHash> OP_EQUALVERIFY OP_DROP <pubkey> OP_CHECKSIG
|
|
9
|
+
*
|
|
10
|
+
* `creationPreimageHash` is a CONSTANT baked in at creation time, and the spend
|
|
11
|
+
* simply re-pushes the stored creation preimage to satisfy it before OP_DROP throws
|
|
12
|
+
* it away. Nothing binds the SPENDING transaction's outputs or value — the script
|
|
13
|
+
* reduces to plain P2PK. It does NOT "constrain where coins can go" the way a
|
|
14
|
+
* covenant must.
|
|
15
|
+
*
|
|
16
|
+
* For real, spend-binding covenants use the OP_PUSH_TX primitives, which generate
|
|
17
|
+
* the signature in-script from the pushed preimage so a passing OP_CHECKSIG proves
|
|
18
|
+
* the preimage IS this spend:
|
|
19
|
+
* - SmartContract.PushTx.valueCovenant(expectedHashOutputs) — bind outputs.
|
|
20
|
+
* - SmartContract.Token.ownershipToken / perpetualCovenant — stateful NFT.
|
|
21
|
+
*
|
|
22
|
+
* This class is retained only as a workflow/storage demo. createFromP2PKH() throws
|
|
23
|
+
* unless you pass { allowNonEnforcing: true } to the constructor, acknowledging the
|
|
24
|
+
* script enforces no spend constraint.
|
|
13
25
|
*/
|
|
14
26
|
|
|
15
27
|
'use strict'
|
|
@@ -54,7 +66,13 @@ function Covenant(privateKey, options) {
|
|
|
54
66
|
this.covenantUtxoPath = path.join(this.storageDir, 'covenant_utxos.json')
|
|
55
67
|
|
|
56
68
|
this.sighashType = bsv.crypto.Signature.SIGHASH_ALL | bsv.crypto.Signature.SIGHASH_FORKID
|
|
57
|
-
|
|
69
|
+
|
|
70
|
+
// The locking script this class builds does NOT constrain the spend (see the
|
|
71
|
+
// class doc). Require explicit acknowledgement so no one ships it as a real
|
|
72
|
+
// covenant. Use SmartContract.PushTx.valueCovenant / SmartContract.Token for
|
|
73
|
+
// genuine spend-binding covenants.
|
|
74
|
+
this.allowNonEnforcing = options.allowNonEnforcing === true
|
|
75
|
+
|
|
58
76
|
// Ensure storage directory exists (Node.js only)
|
|
59
77
|
if (fs) {
|
|
60
78
|
this._ensureStorageDir()
|
|
@@ -67,6 +85,15 @@ function Covenant(privateKey, options) {
|
|
|
67
85
|
* @returns {Object} Creation result with transaction and covenant UTXO
|
|
68
86
|
*/
|
|
69
87
|
Covenant.prototype.createFromP2PKH = function(utxo) {
|
|
88
|
+
if (!this.allowNonEnforcing) {
|
|
89
|
+
throw new Error(
|
|
90
|
+
'SmartContract.Covenant builds a NON-ENFORCING script that reduces to plain ' +
|
|
91
|
+
'P2PK — it does not constrain the spend. Use SmartContract.PushTx.valueCovenant ' +
|
|
92
|
+
'(output binding) or SmartContract.Token / perpetualCovenant (stateful covenants) ' +
|
|
93
|
+
'for real covenants. To use this demo anyway, construct with ' +
|
|
94
|
+
'{ allowNonEnforcing: true }.'
|
|
95
|
+
)
|
|
96
|
+
}
|
|
70
97
|
// Reconstruct P2PKH script if not provided
|
|
71
98
|
if (!utxo.script) {
|
|
72
99
|
utxo.script = Covenant.reconstructP2pkhScript(this.address)
|
|
@@ -70,6 +70,9 @@ class CovenantBuilder {
|
|
|
70
70
|
*/
|
|
71
71
|
dup() { this.operations.push('OP_DUP'); return this }
|
|
72
72
|
drop() { this.operations.push('OP_DROP'); return this }
|
|
73
|
+
// OP_NIP removes the SECOND item, keeping the top. After `OP_SPLIT` the RIGHT
|
|
74
|
+
// part is on top, so keeping it (dropping the left) requires OP_NIP, not OP_DROP.
|
|
75
|
+
nip() { this.operations.push('OP_NIP'); return this }
|
|
73
76
|
swap() { this.operations.push('OP_SWAP'); return this }
|
|
74
77
|
over() { this.operations.push('OP_OVER'); return this }
|
|
75
78
|
rot() { this.operations.push('OP_ROT'); return this }
|
|
@@ -186,21 +189,20 @@ class CovenantBuilder {
|
|
|
186
189
|
.push(field.offsetFromEnd)
|
|
187
190
|
.sub()
|
|
188
191
|
.split()
|
|
189
|
-
.
|
|
192
|
+
.nip() // OP_SPLIT leaves the RIGHT part on top; drop the LEFT with OP_NIP
|
|
190
193
|
.push(field.length)
|
|
191
194
|
.split()
|
|
192
|
-
.drop() //
|
|
195
|
+
.drop() // field is now the LEFT part; drop the trailing remainder
|
|
193
196
|
} else if (field.strategy === 'DYNAMIC') {
|
|
194
197
|
if (fieldName === 'scriptLen') {
|
|
195
198
|
// Extract scriptLen (varint after outpoint)
|
|
196
199
|
this.push(104) // Fixed left part (4+32+32+36)
|
|
197
200
|
.split()
|
|
198
|
-
.
|
|
201
|
+
.nip() // keep the RIGHT part (tail from offset 104); OP_NIP not OP_DROP
|
|
199
202
|
// For now, assume 1-byte varint
|
|
200
203
|
.push(1)
|
|
201
204
|
.split()
|
|
202
|
-
.
|
|
203
|
-
.drop() // Keep just the length byte
|
|
205
|
+
.drop() // length byte is the LEFT part; drop the trailing remainder
|
|
204
206
|
} else {
|
|
205
207
|
throw new Error(`Dynamic extraction for ${fieldName} not implemented`)
|
|
206
208
|
}
|
|
@@ -2,22 +2,27 @@
|
|
|
2
2
|
* SmartContract Module
|
|
3
3
|
* ===================
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* with comprehensive SIGHASH flag support and error handling.
|
|
5
|
+
* Covenant functionality for Bitcoin SV (BSV): BIP-143 preimage parsing and
|
|
6
|
+
* OP_PUSH_TX covenants that constrain a transaction's own spend.
|
|
8
7
|
*
|
|
9
|
-
*
|
|
10
|
-
* -
|
|
11
|
-
*
|
|
12
|
-
* -
|
|
13
|
-
*
|
|
14
|
-
* -
|
|
15
|
-
*
|
|
16
|
-
* -
|
|
17
|
-
* -
|
|
18
|
-
*
|
|
19
|
-
* -
|
|
20
|
-
* -
|
|
8
|
+
* ── ENFORCING COVENANT API (canonical — these actually bind the spend) ──
|
|
9
|
+
* - PushTx: the audited OP_PUSH_TX core (in-script sig gen + OP_CHECKSIG). Now
|
|
10
|
+
* supports configurable SIGHASH (pushTxCore({sighashType}), assertSighashType).
|
|
11
|
+
* - policy() / DSL: declarative covenant DSL that compiles to a verified OP_PUSH_TX
|
|
12
|
+
* locking script — the recommended high-level entry point.
|
|
13
|
+
* - Token: stateful ownership-token (NFT) covenants + ordinals (ordinalLock,
|
|
14
|
+
* transferOrdinal). Authorizers: pluggable single-key / multisig / predicate.
|
|
15
|
+
* - valueCovenant / perpetualCovenant / PELS / Locks: purpose-built primitives.
|
|
16
|
+
* - CovenantHelpers: rawPreimage, verify, fundAndSpend, sighash helpers.
|
|
17
|
+
*
|
|
18
|
+
* ── DEPRECATED, NON-ENFORCING (do NOT use for real covenants) ──
|
|
19
|
+
* - Covenant, Builder: build a constant hash-lock that reduces to plain P2PK and
|
|
20
|
+
* binds nothing about the spend. Both throw unless constructed with
|
|
21
|
+
* { allowNonEnforcing: true }. Retained only as workflow/demo scaffolding.
|
|
22
|
+
*
|
|
23
|
+
* ── TOOLING (analysis / debugging, not covenant logic) ──
|
|
24
|
+
* - Preimage, SIGHASH, CovenantBuilder (low-level JS→ASM), OpcodeMap, ScriptUtils,
|
|
25
|
+
* StackExaminer, ScriptInterpreter, ScriptTester, UTXOGenerator, Debugger.
|
|
21
26
|
*/
|
|
22
27
|
|
|
23
28
|
'use strict'
|
|
@@ -82,6 +87,14 @@ SmartContract.ownershipToken = function (fee, ownerPubKeyHash) {
|
|
|
82
87
|
SmartContract.valueCovenant = function (expectedHashOutputs) {
|
|
83
88
|
return SmartContract.PushTx.valueCovenant(expectedHashOutputs)
|
|
84
89
|
}
|
|
90
|
+
// Ordinal covenant locking script (value-preserving; safe for 1-sat ordinals).
|
|
91
|
+
SmartContract.ordinalLock = function (owner, auth) {
|
|
92
|
+
return SmartContract.Token.ordinalLock(owner, auth)
|
|
93
|
+
}
|
|
94
|
+
// Transfer a 1-sat (or N-sat) ordinal without burning it — see Token.transferOrdinal.
|
|
95
|
+
SmartContract.transferOrdinal = function (signer, newOwnerHash, spend, ordinalSats, lockingScript, opts) {
|
|
96
|
+
return SmartContract.Token.transferOrdinal(signer, newOwnerHash, spend, ordinalSats, lockingScript, opts)
|
|
97
|
+
}
|
|
85
98
|
|
|
86
99
|
// Convenience constructor methods
|
|
87
100
|
SmartContract.createCovenant = function (privateKey, options) {
|
|
@@ -529,8 +529,8 @@ Preimage.prototype._generateRightExtractionASM = function(fieldName, includeComm
|
|
|
529
529
|
instructions = instructions.concat([
|
|
530
530
|
splitPoint + ' OP_SUB # Calculate split point: [preimage, split_point]',
|
|
531
531
|
'OP_SPLIT # Split: [left_part, right_part]',
|
|
532
|
-
'
|
|
533
|
-
targetLen + ' OP_SPLIT # Extract field: [
|
|
532
|
+
'OP_NIP # Drop left (OP_SPLIT leaves right on top): [right_part]',
|
|
533
|
+
targetLen + ' OP_SPLIT # Extract field: [' + fieldName + ', remaining]',
|
|
534
534
|
'OP_DROP # Clean up: [' + fieldName + ']',
|
|
535
535
|
'# ✅ Result: ' + fieldName + ' is now on top of stack'
|
|
536
536
|
])
|
|
@@ -538,7 +538,7 @@ Preimage.prototype._generateRightExtractionASM = function(fieldName, includeComm
|
|
|
538
538
|
instructions = instructions.concat([
|
|
539
539
|
splitPoint + ' OP_SUB',
|
|
540
540
|
'OP_SPLIT',
|
|
541
|
-
'
|
|
541
|
+
'OP_NIP',
|
|
542
542
|
targetLen + ' OP_SPLIT',
|
|
543
543
|
'OP_DROP'
|
|
544
544
|
])
|
|
@@ -569,7 +569,7 @@ Preimage.prototype._generateLeftExtractionASM = function(fieldName, includeComme
|
|
|
569
569
|
instructions = [
|
|
570
570
|
'# 🔄 Extract ' + fieldName + ' from LEFT side (bidirectional strategy)',
|
|
571
571
|
fieldInfo.offset + ' OP_SPLIT # Skip to field: [prefix, remainder]',
|
|
572
|
-
'
|
|
572
|
+
'OP_NIP # Drop prefix (OP_SPLIT leaves remainder on top): [remainder]',
|
|
573
573
|
fieldInfo.len + ' OP_SPLIT # Extract field: [' + fieldName + ', suffix]',
|
|
574
574
|
'OP_DROP # Clean up: [' + fieldName + ']',
|
|
575
575
|
'# ✅ Result: ' + fieldName + ' is now on top of stack'
|
|
@@ -577,7 +577,7 @@ Preimage.prototype._generateLeftExtractionASM = function(fieldName, includeComme
|
|
|
577
577
|
} else {
|
|
578
578
|
instructions = [
|
|
579
579
|
fieldInfo.offset + ' OP_SPLIT',
|
|
580
|
-
'
|
|
580
|
+
'OP_NIP',
|
|
581
581
|
fieldInfo.len + ' OP_SPLIT',
|
|
582
582
|
'OP_DROP'
|
|
583
583
|
]
|
|
@@ -596,7 +596,7 @@ Preimage.prototype._generateDynamicExtractionASM = function(parsed, includeComme
|
|
|
596
596
|
return [
|
|
597
597
|
'# 🎯 Extract scriptCode DYNAMICALLY with CompactSize varint support',
|
|
598
598
|
leftZone + ' OP_SPLIT # Skip left zone + scriptLen varint: [left_zone, remainder]',
|
|
599
|
-
'
|
|
599
|
+
'OP_NIP # Drop left (OP_SPLIT leaves remainder on top): [remainder]',
|
|
600
600
|
parsed.scriptLen + ' OP_SPLIT # Extract scriptCode: [scriptCode, right_zone]',
|
|
601
601
|
'OP_DROP # Clean up: [scriptCode]',
|
|
602
602
|
'# ✅ Result: scriptCode extracted with ' + parsed.scriptLenSize + '-byte varint awareness'
|
|
@@ -611,7 +611,7 @@ Preimage.prototype._generateScriptLenExtractionASM = function(parsed, includeCom
|
|
|
611
611
|
return [
|
|
612
612
|
'# 🎯 Extract scriptLen CompactSize varint (' + parsed.scriptLenSize + ' bytes)',
|
|
613
613
|
'104 OP_SPLIT # Skip left fixed fields: [left_zone, remainder]',
|
|
614
|
-
'
|
|
614
|
+
'OP_NIP # Drop left (OP_SPLIT leaves remainder on top): [remainder]',
|
|
615
615
|
parsed.scriptLenSize + ' OP_SPLIT # Extract varint: [scriptLen_varint, suffix]',
|
|
616
616
|
'OP_DROP # Clean up: [scriptLen_varint]',
|
|
617
617
|
'# ✅ Result: CompactSize varint (decode off-chain to get ' + parsed.scriptLen + ')'
|
|
@@ -38,7 +38,6 @@ var N_LE = Buffer.concat([Buffer.from(N.toBuffer()).reverse(), Buffer.from([0x00
|
|
|
38
38
|
|
|
39
39
|
// fixed DER prefix: SEQUENCE(0x44) INTEGER(0x20) r=Gx INTEGER(0x20)
|
|
40
40
|
var DER_PREFIX = Buffer.concat([Buffer.from([0x30, 0x44, 0x02, 0x20]), Gx, Buffer.from([0x02, 0x20])])
|
|
41
|
-
var SIGHASH_BYTE = Buffer.from([SIGHASH]) // 0x41
|
|
42
41
|
|
|
43
42
|
/** Reverse a fixed n-byte buffer on top of the stack (big-endian <-> little-endian). */
|
|
44
43
|
function reverseBytes (script, n) {
|
|
@@ -48,11 +47,36 @@ function reverseBytes (script, n) {
|
|
|
48
47
|
return script
|
|
49
48
|
}
|
|
50
49
|
|
|
50
|
+
// Common SIGHASH flag combinations (all include FORKID, required on BSV). The
|
|
51
|
+
// marketplace/partially-signed pattern uses SINGLE|ANYONECANPAY: the signer commits
|
|
52
|
+
// only to its own input and its own output (the output at the same index), leaving
|
|
53
|
+
// a counterparty free to add funding inputs and change outputs without invalidating
|
|
54
|
+
// the signature.
|
|
55
|
+
var Signature = bsv.crypto.Signature
|
|
56
|
+
var SIGHASH_ALL_FORKID = Signature.SIGHASH_ALL | Signature.SIGHASH_FORKID // 0x41
|
|
57
|
+
var SIGHASH_SINGLE_FORKID = Signature.SIGHASH_SINGLE | Signature.SIGHASH_FORKID // 0x43
|
|
58
|
+
var SIGHASH_SINGLE_ANYONECANPAY_FORKID =
|
|
59
|
+
Signature.SIGHASH_SINGLE | Signature.SIGHASH_ANYONECANPAY | Signature.SIGHASH_FORKID // 0xc3
|
|
60
|
+
// ALL|ANYONECANPAY commits to ALL outputs (so the covenant can pin the FULL output
|
|
61
|
+
// set) while leaving inputs open for a counterparty to add funding. This is the flag
|
|
62
|
+
// the 1Sat Ordinals OrdLock uses: the buyer supplies the surrounding outputs and the
|
|
63
|
+
// covenant binds the seller's payment output into the committed hashOutputs.
|
|
64
|
+
var SIGHASH_ALL_ANYONECANPAY_FORKID =
|
|
65
|
+
Signature.SIGHASH_ALL | Signature.SIGHASH_ANYONECANPAY | Signature.SIGHASH_FORKID // 0xc1
|
|
66
|
+
|
|
51
67
|
/**
|
|
52
68
|
* Append the in-script signature generator + verifier ("PUSH_TX core").
|
|
53
69
|
* Pre: top of stack = preimage. Post: top = OP_CHECKSIG result.
|
|
70
|
+
*
|
|
71
|
+
* @param {Script} script
|
|
72
|
+
* @param {object} [opts]
|
|
73
|
+
* @param {number} [opts.sighashType=0x41] the SIGHASH flag byte baked into the
|
|
74
|
+
* synthetic signature. OP_CHECKSIG derives the spend's sighash using THIS flag,
|
|
75
|
+
* so the spender must push the matching BIP-143 preimage (grind with the same
|
|
76
|
+
* sighashType). Defaults to SIGHASH_ALL|FORKID — existing covenants are unchanged.
|
|
54
77
|
*/
|
|
55
|
-
function pushTxCore (script) {
|
|
78
|
+
function pushTxCore (script, opts) {
|
|
79
|
+
var sighashType = (opts && opts.sighashType) || SIGHASH
|
|
56
80
|
script.add(Opcode.OP_HASH256) // z = HASH256(preimage), 32B BE
|
|
57
81
|
reverseBytes(script, 32) // -> little-endian = e. The grind guarantees e is
|
|
58
82
|
// already positive and minimally encoded (z[0] in 0x01..0x7f), so NO 0x00 sign
|
|
@@ -70,7 +94,7 @@ function pushTxCore (script) {
|
|
|
70
94
|
script.add(Buffer.from([0x30, 0x44, 0x02, 0x20])).add(Opcode.OP_SWAP).add(Opcode.OP_CAT) // 30440220||Gx
|
|
71
95
|
script.add(Buffer.from([0x02, 0x20])).add(Opcode.OP_CAT) // ||0220 => DER prefix
|
|
72
96
|
script.add(Opcode.OP_SWAP).add(Opcode.OP_CAT) // DER prefix || s_be
|
|
73
|
-
script.add(
|
|
97
|
+
script.add(Buffer.from([sighashType])).add(Opcode.OP_CAT) // || sighash flag => full DER sig
|
|
74
98
|
script.add(Opcode.OP_FROMALTSTACK).add(Opcode.OP_CHECKSIG) // pubkey ; verify against P = G
|
|
75
99
|
return script
|
|
76
100
|
}
|
|
@@ -88,8 +112,7 @@ function extractHashOutputs (script) {
|
|
|
88
112
|
}
|
|
89
113
|
|
|
90
114
|
// The BIP-143 preimage ends with nHashType as a 4-byte LE word; for SIGHASH_ALL|
|
|
91
|
-
// FORKID that word is 0x41 00 00 00.
|
|
92
|
-
var SIGHASH_WORD = Buffer.from([SIGHASH, 0x00, 0x00, 0x00])
|
|
115
|
+
// FORKID that word is 0x41 00 00 00. assertSighashType builds this word per flag.
|
|
93
116
|
|
|
94
117
|
/**
|
|
95
118
|
* Assert the spend is SIGHASH_ALL|FORKID (0x41). Pre/Post: top of stack = preimage
|
|
@@ -104,8 +127,19 @@ var SIGHASH_WORD = Buffer.from([SIGHASH, 0x00, 0x00, 0x00])
|
|
|
104
127
|
* flag assumption explicit and fails fast — and survives any refactor of the core.
|
|
105
128
|
*/
|
|
106
129
|
function assertSighashAll (script) {
|
|
130
|
+
return assertSighashType(script, SIGHASH)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Assert the spend uses a specific SIGHASH flag. Pre/Post: top of stack = preimage
|
|
135
|
+
* (left untouched). The BIP-143 preimage's trailing 4-byte LE nHashType word must
|
|
136
|
+
* equal `sighashType`. Use with pushTxCore({ sighashType }) + grind({ sighashType })
|
|
137
|
+
* to build covenants under SIGHASH_SINGLE|ANYONECANPAY (marketplace) etc.
|
|
138
|
+
*/
|
|
139
|
+
function assertSighashType (script, sighashType) {
|
|
140
|
+
var word = Buffer.from([sighashType & 0xff, 0x00, 0x00, 0x00])
|
|
107
141
|
script.add(Opcode.OP_DUP).add(scriptNum(4)).add(Opcode.OP_RIGHT)
|
|
108
|
-
.add(
|
|
142
|
+
.add(word).add(Opcode.OP_EQUALVERIFY)
|
|
109
143
|
return script
|
|
110
144
|
}
|
|
111
145
|
|
|
@@ -171,6 +205,7 @@ function grind (spend, inputIndex, lockingScript, satoshis, opts) {
|
|
|
171
205
|
var maxTries = opts.maxTries || 5000
|
|
172
206
|
var start = opts.start || 0
|
|
173
207
|
var field = opts.field || 'nLockTime'
|
|
208
|
+
var sighashType = opts.sighashType || SIGHASH
|
|
174
209
|
if (field !== 'nLockTime' && field !== 'sequence') {
|
|
175
210
|
throw new Error("grind field must be 'nLockTime' or 'sequence'")
|
|
176
211
|
}
|
|
@@ -179,7 +214,7 @@ function grind (spend, inputIndex, lockingScript, satoshis, opts) {
|
|
|
179
214
|
var nonce = start + i
|
|
180
215
|
if (field === 'nLockTime') spend.nLockTime = nonce
|
|
181
216
|
else input.sequenceNumber = 0xfffffffe - nonce // stay < 0xffffffff (non-final)
|
|
182
|
-
var preimage = H.rawPreimage(spend, inputIndex, lockingScript, satoshis)
|
|
217
|
+
var preimage = H.rawPreimage(spend, inputIndex, lockingScript, satoshis, sighashType)
|
|
183
218
|
if (sFromPreimage(preimage)) return { preimage: preimage, tries: i + 1, field: field, nonce: nonce }
|
|
184
219
|
}
|
|
185
220
|
throw new Error('OP_PUSH_TX grind failed after ' + maxTries + ' tries')
|
|
@@ -197,6 +232,11 @@ module.exports = {
|
|
|
197
232
|
authenticator: authenticator,
|
|
198
233
|
extractHashOutputs: extractHashOutputs,
|
|
199
234
|
assertSighashAll: assertSighashAll,
|
|
235
|
+
assertSighashType: assertSighashType,
|
|
236
|
+
SIGHASH_ALL_FORKID: SIGHASH_ALL_FORKID,
|
|
237
|
+
SIGHASH_SINGLE_FORKID: SIGHASH_SINGLE_FORKID,
|
|
238
|
+
SIGHASH_SINGLE_ANYONECANPAY_FORKID: SIGHASH_SINGLE_ANYONECANPAY_FORKID,
|
|
239
|
+
SIGHASH_ALL_ANYONECANPAY_FORKID: SIGHASH_ALL_ANYONECANPAY_FORKID,
|
|
200
240
|
hashOutputs: hashOutputs,
|
|
201
241
|
valueCovenant: valueCovenant,
|
|
202
242
|
sFromPreimage: sFromPreimage,
|
|
@@ -190,6 +190,16 @@ function unlockTransfer (signer, newOwnerHash, spend, satoshis, lockingScript, o
|
|
|
190
190
|
var auth = opts.auth || A.singleKey()
|
|
191
191
|
var inputIndex = opts.inputIndex || 0
|
|
192
192
|
if (newOwnerHash.length !== 20) throw new Error('newOwnerHash must be 20 bytes (HASH160)')
|
|
193
|
+
// Guardrail: the single-output form forces the recreated output value to
|
|
194
|
+
// (input - fee), so a 1-sat UTXO — e.g. a 1-sat ordinal — cannot survive any
|
|
195
|
+
// fee: the satoshi carrying the inscription is burned. Refuse it and point to
|
|
196
|
+
// the value-preserving path. Opt out with { allowLowValue: true } if you really
|
|
197
|
+
// mean a single-output, fee-absorbing token on a tiny UTXO.
|
|
198
|
+
if (satoshis <= 1 && !opts.allowLowValue) {
|
|
199
|
+
throw new Error('unlockTransfer: single-output token sets output = input - fee, ' +
|
|
200
|
+
'which burns a ' + satoshis + '-sat UTXO (e.g. a 1-sat ordinal). Use ' +
|
|
201
|
+
'Token.transferOrdinal / ownershipTokenMulti to preserve the satoshi.')
|
|
202
|
+
}
|
|
193
203
|
P.grind(spend, inputIndex, lockingScript, satoshis, opts.grind)
|
|
194
204
|
var preimage = H.rawPreimage(spend, inputIndex, lockingScript, satoshis)
|
|
195
205
|
var args = auth.unlockArgs(spend, inputIndex, lockingScript, satoshis, signer)
|
|
@@ -228,10 +238,68 @@ function unlockTransferMulti (signer, newOwnerHash, spend, satoshis, lockingScri
|
|
|
228
238
|
return us
|
|
229
239
|
}
|
|
230
240
|
|
|
241
|
+
/**
|
|
242
|
+
* Locking script for a transferable ordinal held in a covenant. This is exactly
|
|
243
|
+
* the multi-output token (the single-output form burns 1-sat UTXOs), named for
|
|
244
|
+
* intent: the token output carries its OWN value, so the ordinal's satoshi is
|
|
245
|
+
* preserved while fees are paid from separate funding inputs/outputs.
|
|
246
|
+
*/
|
|
247
|
+
function ordinalLock (owner, auth) { return ownershipTokenMulti(owner, auth) }
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Transfer a 1-sat (or N-sat) ordinal held under an ordinalLock/ownershipTokenMulti
|
|
251
|
+
* covenant WITHOUT burning it. Derives the multi-output layout from the spend the
|
|
252
|
+
* caller has already built, and enforces that the recreated token output carries
|
|
253
|
+
* EXACTLY the ordinal's satoshis so the inscribed sat survives.
|
|
254
|
+
*
|
|
255
|
+
* The caller builds `spend` with the ordinal covenant input at `opts.inputIndex`
|
|
256
|
+
* (default 0), the recreated token output at `opts.tokenIndex` (default 0, so the
|
|
257
|
+
* ordinal's first sat maps to the token output's first sat under ordinal FIFO),
|
|
258
|
+
* funding input(s) for the fee, and a change output. Fees come from the funding,
|
|
259
|
+
* never from the ordinal.
|
|
260
|
+
*
|
|
261
|
+
* @param {*} signer authorizer signer (PrivateKey for single-key).
|
|
262
|
+
* @param {Buffer} newOwnerHash 20-byte commitment of the next owner.
|
|
263
|
+
* @param {Transaction} spend the spend tx (already carrying the outputs).
|
|
264
|
+
* @param {number} ordinalSats satoshis on the ordinal UTXO (default 1).
|
|
265
|
+
* @param {Script} lockingScript the ordinal covenant script being spent.
|
|
266
|
+
* @param {object} [opts] { auth, inputIndex=0, tokenIndex=0, grind }.
|
|
267
|
+
* @returns {Script} the unlocking script for the ordinal input.
|
|
268
|
+
*/
|
|
269
|
+
function transferOrdinal (signer, newOwnerHash, spend, ordinalSats, lockingScript, opts) {
|
|
270
|
+
opts = opts || {}
|
|
271
|
+
if (ordinalSats == null) ordinalSats = 1
|
|
272
|
+
var tokenIndex = opts.tokenIndex || 0
|
|
273
|
+
var outs = spend.outputs || []
|
|
274
|
+
var tokenOut = outs[tokenIndex]
|
|
275
|
+
if (!tokenOut) {
|
|
276
|
+
throw new Error('transferOrdinal: spend has no output at tokenIndex ' + tokenIndex)
|
|
277
|
+
}
|
|
278
|
+
// Anti-burn: the recreated token output must carry EXACTLY the ordinal's value.
|
|
279
|
+
if (tokenOut.satoshis !== ordinalSats) {
|
|
280
|
+
throw new Error('transferOrdinal: token output value (' + tokenOut.satoshis +
|
|
281
|
+
' sat) must equal the ordinal value (' + ordinalSats + ' sat) to avoid ' +
|
|
282
|
+
'burning the inscription')
|
|
283
|
+
}
|
|
284
|
+
// Derive the covenant layout: serialized outputs before/after the token output.
|
|
285
|
+
function ser (o) { return o.toBufferWriter().toBuffer() }
|
|
286
|
+
var before = Buffer.concat(outs.slice(0, tokenIndex).map(ser))
|
|
287
|
+
var after = Buffer.concat(outs.slice(tokenIndex + 1).map(ser))
|
|
288
|
+
var tokenValue = new bsv.crypto.BN(ordinalSats).toBuffer({ endian: 'little', size: 8 })
|
|
289
|
+
|
|
290
|
+
return unlockTransferMulti(signer, newOwnerHash, spend, ordinalSats, lockingScript, {
|
|
291
|
+
before: before,
|
|
292
|
+
after: after,
|
|
293
|
+
tokenValue: tokenValue
|
|
294
|
+
}, opts)
|
|
295
|
+
}
|
|
296
|
+
|
|
231
297
|
module.exports = {
|
|
232
298
|
ownershipToken: ownershipToken,
|
|
233
299
|
ownershipTokenMulti: ownershipTokenMulti,
|
|
300
|
+
ordinalLock: ordinalLock,
|
|
234
301
|
ownerId: ownerId,
|
|
235
302
|
unlockTransfer: unlockTransfer,
|
|
236
|
-
unlockTransferMulti: unlockTransferMulti
|
|
303
|
+
unlockTransferMulti: unlockTransferMulti,
|
|
304
|
+
transferOrdinal: transferOrdinal
|
|
237
305
|
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
/**
|
|
3
|
+
* SPV block-header chain verification — the confirmations half of the SPV story.
|
|
4
|
+
*
|
|
5
|
+
* verifyTxInclusion proves a tx is in a SUPPLIED header; this proves that header is
|
|
6
|
+
* buried under real work by verifying a chain of consecutive headers: each links to
|
|
7
|
+
* the previous (prevHash), each meets its own proof-of-work target, and (optionally)
|
|
8
|
+
* the chain is anchored at a hash the caller independently trusts.
|
|
9
|
+
*
|
|
10
|
+
* TRUST MODEL — read this. It validates linkage + PER-HEADER proof-of-work, but NOT
|
|
11
|
+
* the difficulty-retargeting schedule. A determined attacker can mine cheap headers
|
|
12
|
+
* at an artificially easy `bits`, so PoW-per-header alone is not sufficient in an
|
|
13
|
+
* adversarial setting. For real assurance pass `opts.trustedHash` — a block hash you
|
|
14
|
+
* already trust (from your own node, or a hardcoded checkpoint) that the chain's tip
|
|
15
|
+
* (or anchor) must match; the headers around it are then real work extending a block
|
|
16
|
+
* you trust. Full difficulty-retarget validation is intentionally out of scope here.
|
|
17
|
+
*/
|
|
18
|
+
var bsv = require('../..')
|
|
19
|
+
|
|
20
|
+
function rev (b) { return Buffer.from(b).reverse() }
|
|
21
|
+
|
|
22
|
+
function toHeader (h) {
|
|
23
|
+
if (h && typeof h.validProofOfWork === 'function') return h
|
|
24
|
+
if (Buffer.isBuffer(h)) return bsv.BlockHeader.fromBuffer(h)
|
|
25
|
+
if (typeof h === 'string') return bsv.BlockHeader.fromBuffer(Buffer.from(h, 'hex'))
|
|
26
|
+
throw new Error('each header must be a BlockHeader, an 80-byte Buffer, or hex')
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* @param {Array} headers consecutive headers, oldest→newest (BlockHeader/Buffer/hex).
|
|
31
|
+
* @param {object} [opts]
|
|
32
|
+
* requirePow {boolean=true} verify each header meets its bits target.
|
|
33
|
+
* trustedHash {string} a block hash the chain's tip or anchor must equal.
|
|
34
|
+
* @returns {{ valid, reason?, count, anchorHash, tipHash, work }}
|
|
35
|
+
*/
|
|
36
|
+
function verifyHeaderChain (headers, opts) {
|
|
37
|
+
opts = opts || {}
|
|
38
|
+
if (!Array.isArray(headers) || headers.length === 0) {
|
|
39
|
+
throw new Error('headers must be a non-empty array')
|
|
40
|
+
}
|
|
41
|
+
var hs = headers.map(toHeader)
|
|
42
|
+
var requirePow = opts.requirePow !== false
|
|
43
|
+
var work = 0
|
|
44
|
+
|
|
45
|
+
for (var i = 0; i < hs.length; i++) {
|
|
46
|
+
if (requirePow && !hs[i].validProofOfWork()) {
|
|
47
|
+
return { valid: false, reason: 'invalid proof-of-work at index ' + i, count: hs.length }
|
|
48
|
+
}
|
|
49
|
+
work += hs[i].getDifficulty()
|
|
50
|
+
if (i > 0) {
|
|
51
|
+
var linkOk = rev(hs[i].prevHash).toString('hex').toLowerCase() === hs[i - 1].id.toLowerCase()
|
|
52
|
+
if (!linkOk) {
|
|
53
|
+
return { valid: false, reason: 'broken link at index ' + i, count: hs.length }
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
var anchorHash = hs[0].id
|
|
59
|
+
var tipHash = hs[hs.length - 1].id
|
|
60
|
+
|
|
61
|
+
if (opts.trustedHash) {
|
|
62
|
+
var t = String(opts.trustedHash).toLowerCase()
|
|
63
|
+
if (t !== tipHash.toLowerCase() && t !== anchorHash.toLowerCase()) {
|
|
64
|
+
return {
|
|
65
|
+
valid: false,
|
|
66
|
+
reason: 'chain is not anchored at the trusted hash',
|
|
67
|
+
count: hs.length,
|
|
68
|
+
anchorHash: anchorHash,
|
|
69
|
+
tipHash: tipHash
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return { valid: true, count: hs.length, anchorHash: anchorHash, tipHash: tipHash, work: work }
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
module.exports = { verifyHeaderChain: verifyHeaderChain }
|
package/lib/spv/index.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
/**
|
|
3
|
+
* SPV — trustless Simplified Payment Verification helpers.
|
|
4
|
+
*
|
|
5
|
+
* Currently: Merkle inclusion proofs (verify a tx is in a PoW-backed block without
|
|
6
|
+
* a full node). Built on the fork's BlockHeader / MerkleBlock primitives.
|
|
7
|
+
*/
|
|
8
|
+
var merkleproof = require('./merkleproof')
|
|
9
|
+
var headerchain = require('./headerchain')
|
|
10
|
+
|
|
11
|
+
module.exports = {
|
|
12
|
+
merkleRootFromBranch: merkleproof.merkleRootFromBranch,
|
|
13
|
+
verifyMerkleProof: merkleproof.verifyMerkleProof,
|
|
14
|
+
verifyTxInclusion: merkleproof.verifyTxInclusion,
|
|
15
|
+
verifyHeaderChain: headerchain.verifyHeaderChain
|
|
16
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
/**
|
|
3
|
+
* SPV Merkle inclusion proofs.
|
|
4
|
+
*
|
|
5
|
+
* Trustlessly verify that a transaction is included in a block, given a Merkle
|
|
6
|
+
* branch proof and a trusted block header — no full node and no trust in the data
|
|
7
|
+
* provider. This is the real inclusion check that replaces the "trust the caller's
|
|
8
|
+
* txData" anchor stubs: a provider (explorer / node) can lie about a tx being mined,
|
|
9
|
+
* but it cannot forge a Merkle branch that hashes to a proof-of-work-backed header's
|
|
10
|
+
* merkle root.
|
|
11
|
+
*
|
|
12
|
+
* Byte order: `txid`, branch `nodes` and `merkleRoot` are DISPLAY-order hex (big
|
|
13
|
+
* endian, as block explorers show them). Bitcoin hashes internally in little-endian,
|
|
14
|
+
* so we reverse on the way in/out. Nodes are combined with double-SHA256, matching
|
|
15
|
+
* lib/block/{block,merkleblock}.js. A node of '*' (or '' / null) means "duplicate the
|
|
16
|
+
* working hash" — Bitcoin's odd-node rule, per the TSC Merkle Proof standard.
|
|
17
|
+
*/
|
|
18
|
+
var bsv = require('../..')
|
|
19
|
+
var Hash = bsv.crypto.Hash
|
|
20
|
+
|
|
21
|
+
var HEX32 = /^[0-9a-fA-F]{64}$/
|
|
22
|
+
|
|
23
|
+
function rev (buf) { return Buffer.from(buf).reverse() }
|
|
24
|
+
function toInternal (hex) { return rev(Buffer.from(hex, 'hex')) } // display -> internal LE
|
|
25
|
+
function toDisplay (buf) { return rev(buf).toString('hex') } // internal LE -> display
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Recompute the Merkle root from a branch proof.
|
|
29
|
+
* @param {string} txid display-order txid hex (64 hex chars)
|
|
30
|
+
* @param {number} index 0-based position of the tx within the block
|
|
31
|
+
* @param {Array<string>} nodes sibling hashes, leaf->root (display hex; '*' = duplicate)
|
|
32
|
+
* @returns {string} the computed merkle root (display-order hex)
|
|
33
|
+
*/
|
|
34
|
+
function merkleRootFromBranch (txid, index, nodes) {
|
|
35
|
+
if (!HEX32.test(String(txid))) throw new Error('txid must be 32-byte hex')
|
|
36
|
+
if (!Number.isInteger(index) || index < 0) throw new Error('index must be a non-negative integer')
|
|
37
|
+
nodes = nodes || []
|
|
38
|
+
var cur = toInternal(txid)
|
|
39
|
+
var idx = index
|
|
40
|
+
for (var i = 0; i < nodes.length; i++) {
|
|
41
|
+
var node = nodes[i]
|
|
42
|
+
var sib
|
|
43
|
+
if (node === '*' || node === '' || node == null) {
|
|
44
|
+
sib = cur // odd-node: the working hash is duplicated
|
|
45
|
+
} else {
|
|
46
|
+
if (!HEX32.test(String(node))) throw new Error('proof node ' + i + ' must be 32-byte hex or "*"')
|
|
47
|
+
sib = toInternal(node)
|
|
48
|
+
}
|
|
49
|
+
// idx even -> current is the LEFT child; idx odd -> current is the RIGHT child.
|
|
50
|
+
cur = (idx & 1)
|
|
51
|
+
? Hash.sha256sha256(Buffer.concat([sib, cur]))
|
|
52
|
+
: Hash.sha256sha256(Buffer.concat([cur, sib]))
|
|
53
|
+
idx = Math.floor(idx / 2)
|
|
54
|
+
}
|
|
55
|
+
return toDisplay(cur)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Verify a Merkle branch proof against an expected root.
|
|
60
|
+
* @param {object} proof { txid, index, nodes, merkleRoot } (all display-order hex)
|
|
61
|
+
* @returns {boolean}
|
|
62
|
+
*/
|
|
63
|
+
function verifyMerkleProof (proof) {
|
|
64
|
+
if (!proof || !proof.merkleRoot) throw new Error('proof.merkleRoot is required')
|
|
65
|
+
var computed = merkleRootFromBranch(proof.txid, proof.index, proof.nodes)
|
|
66
|
+
return computed.toLowerCase() === String(proof.merkleRoot).toLowerCase()
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Verify a transaction is included in a block: branch -> root, root ==
|
|
71
|
+
* header.merkleRoot, and (unless disabled) the header meets its PoW target.
|
|
72
|
+
*
|
|
73
|
+
* NOTE: this proves inclusion in the SUPPLIED header. Confirming that header is on
|
|
74
|
+
* the honest chain (height / confirmations) requires a trusted header chain, which
|
|
75
|
+
* the caller supplies out of band.
|
|
76
|
+
*
|
|
77
|
+
* @param {object} params { txid, index, nodes, header, requirePow=true }
|
|
78
|
+
* header: a bsv.BlockHeader, an 80-byte Buffer, or 80-byte hex.
|
|
79
|
+
* @returns {{ valid:boolean, rootMatches:boolean, powValid:boolean, merkleRoot:string, blockHash:string }}
|
|
80
|
+
*/
|
|
81
|
+
function verifyTxInclusion (params) {
|
|
82
|
+
var header = params.header
|
|
83
|
+
if (Buffer.isBuffer(header) || typeof header === 'string') {
|
|
84
|
+
header = bsv.BlockHeader.fromBuffer(Buffer.isBuffer(header) ? header : Buffer.from(header, 'hex'))
|
|
85
|
+
}
|
|
86
|
+
if (!header || !header.merkleRoot) throw new Error('a valid block header is required')
|
|
87
|
+
|
|
88
|
+
var headerRoot = toDisplay(header.merkleRoot) // header stores the root internal-LE
|
|
89
|
+
var computed = merkleRootFromBranch(params.txid, params.index, params.nodes)
|
|
90
|
+
var rootMatches = computed.toLowerCase() === headerRoot.toLowerCase()
|
|
91
|
+
|
|
92
|
+
var requirePow = params.requirePow !== false
|
|
93
|
+
var powValid = header.validProofOfWork()
|
|
94
|
+
|
|
95
|
+
return {
|
|
96
|
+
valid: rootMatches && (!requirePow || powValid),
|
|
97
|
+
rootMatches: rootMatches,
|
|
98
|
+
powValid: powValid,
|
|
99
|
+
merkleRoot: computed,
|
|
100
|
+
blockHash: header.id
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
module.exports = {
|
|
105
|
+
merkleRootFromBranch: merkleRootFromBranch,
|
|
106
|
+
verifyMerkleProof: verifyMerkleProof,
|
|
107
|
+
verifyTxInclusion: verifyTxInclusion
|
|
108
|
+
}
|