@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.
Files changed (60) hide show
  1. package/CHANGELOG.md +142 -0
  2. package/README.md +38 -38
  3. package/bin/cli.js +35 -5
  4. package/bsv-covenant.min.js +6 -6
  5. package/bsv-gdaf.min.js +2 -2
  6. package/bsv-ltp.min.js +2 -2
  7. package/bsv-shamir.min.js +1 -1
  8. package/bsv-smartcontract.min.js +6 -6
  9. package/bsv-statuslist.min.js +5 -5
  10. package/bsv-vcjwt.min.js +1 -1
  11. package/bsv.bundle.js +2 -2
  12. package/bsv.d.ts +289 -10
  13. package/bsv.min.js +2 -2
  14. package/docs/MODULE_REFERENCE_COMPLETE.md +27 -27
  15. package/docs/advanced/UTXO_MANAGER_GUIDE.md +1 -1
  16. package/docs/getting-started/INSTALLATION.md +23 -23
  17. package/docs/getting-started/QUICK_START.md +7 -7
  18. package/docs/migration/FROM_BSV_1_5_6.md +5 -5
  19. package/index.js +4 -0
  20. package/lib/crypto/ecdsa.js +13 -0
  21. package/lib/crypto/shamir.js +16 -4
  22. package/lib/gdaf/did-resolver.js +3 -1
  23. package/lib/gdaf/smartledger-anchor.js +154 -12
  24. package/lib/ltp/anchor.js +16 -3
  25. package/lib/ltp/obligation.js +56 -8
  26. package/lib/ltp/proof.js +4 -2
  27. package/lib/ltp/right.js +86 -11
  28. package/lib/ordinals/README.md +170 -0
  29. package/lib/ordinals/bsv20.js +185 -0
  30. package/lib/ordinals/index.js +49 -0
  31. package/lib/ordinals/inscription.js +159 -0
  32. package/lib/ordinals/ordlock.js +560 -0
  33. package/lib/smart_contract/builder.js +27 -3
  34. package/lib/smart_contract/covenant.js +37 -10
  35. package/lib/smart_contract/covenant_builder.js +7 -5
  36. package/lib/smart_contract/index.js +28 -15
  37. package/lib/smart_contract/preimage.js +7 -7
  38. package/lib/smart_contract/pushtx.js +47 -7
  39. package/lib/smart_contract/token.js +69 -1
  40. package/lib/spv/headerchain.js +77 -0
  41. package/lib/spv/index.js +16 -0
  42. package/lib/spv/merkleproof.js +108 -0
  43. package/lib/statuslist/index.js +65 -24
  44. package/lib/vcjwt/index.js +6 -0
  45. package/package.json +1 -1
  46. package/test/credentials-test.js +22 -19
  47. package/test/crypto/shamir.js +9 -1
  48. package/test/crypto/shamir_rngcap.js +29 -0
  49. package/test/gdaf/anchor_spv.js +163 -0
  50. package/test/ltp/verify_failclosed.js +134 -0
  51. package/test/ordinals/bsv20.js +126 -0
  52. package/test/ordinals/inscription.js +71 -0
  53. package/test/ordinals/ordlock.js +552 -0
  54. package/test/smart_contract/extract_field.js +61 -0
  55. package/test/smart_contract/nonenforcing_guard.js +44 -0
  56. package/test/smart_contract/ordinal_transfer.js +83 -0
  57. package/test/smart_contract/sighash_marketplace.js +110 -0
  58. package/test/spv/headerchain.js +86 -0
  59. package/test/spv/merkleproof.js +133 -0
  60. package/test/statuslist/failclosed.js +78 -0
@@ -1,15 +1,27 @@
1
1
  /**
2
2
  * SmartContract.Covenant Class
3
3
  * ============================
4
- *
5
- * Production-ready covenant framework with:
6
- * - Complete P2PKH → Covenant → Spending workflow
7
- * - Local UTXO storage and portfolio management
8
- * - WhatsOnChain API integration ready
9
- * - BIP-143 preimage validation
10
- * - Script.Interpreter compliance
11
- *
12
- * Based on examples/covenants2/preimage_covenant_utils.js
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
- .drop() // Drop left part
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() // Drop remaining, keep field
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
- .drop() // Drop left part
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
- .swap()
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
- * Production-ready covenant functionality for Bitcoin SV (BSV).
6
- * Implements BIP-143 transaction preimage parsing and covenant operations
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
- * Classes:
10
- * - Covenant: Core covenant creation and spending logic
11
- * - Preimage: BIP-143 preimage parsing and validation with field extraction
12
- * - SIGHASH: SIGHASH flag detection and analysis
13
- * - Builder: High-level covenant construction utilities
14
- * - UTXOGenerator: Real BSV UTXO generation for authentic testing
15
- * - ScriptTester: Local script execution and debugging capabilities
16
- * - CovenantBuilder: JavaScript-to-Bitcoin Script covenant generator
17
- * - OpcodeMap: Comprehensive Bitcoin Script opcode mapping
18
- * - ScriptUtils: Script analysis, conversion, and optimization utilities
19
- * - StackExaminer: Stack state examination and step-by-step execution debugging
20
- * - ScriptInterpreter: Interactive script debugging with full verification support
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
- 'OP_DROP # Drop left: [right_part]',
533
- targetLen + ' OP_SPLIT # Extract field: [remaining, ' + fieldName + ']',
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
- 'OP_DROP',
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
- 'OP_DROP # Drop prefix: [remainder]',
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
- 'OP_DROP',
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
- 'OP_DROP # Drop left: [remainder]',
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
- 'OP_DROP # Drop left: [remainder]',
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(SIGHASH_BYTE).add(Opcode.OP_CAT) // || sighash flag => full DER sig
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(SIGHASH_WORD).add(Opcode.OP_EQUALVERIFY)
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 }
@@ -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
+ }