@smartledger/bsv 5.4.2 → 5.5.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 (93) hide show
  1. package/.mocharc.json +5 -0
  2. package/CHANGELOG.md +66 -0
  3. package/README.md +28 -8
  4. package/bsv-covenant.min.js +8 -8
  5. package/bsv-gdaf.min.js +11 -11
  6. package/bsv-ltp.min.js +11 -11
  7. package/bsv-smartcontract.min.js +8 -8
  8. package/bsv.bundle.js +11 -11
  9. package/bsv.min.js +11 -11
  10. package/lib/smart_contract/authorizers.js +159 -0
  11. package/lib/smart_contract/covenant_helpers.js +3 -3
  12. package/lib/smart_contract/dsl.js +1 -1
  13. package/lib/smart_contract/index.js +9 -8
  14. package/lib/smart_contract/pels.js +1 -0
  15. package/lib/smart_contract/pushtx.js +51 -7
  16. package/lib/smart_contract/token.js +179 -35
  17. package/package.json +3 -1
  18. package/test/address.js +629 -0
  19. package/test/block/block.js +239 -0
  20. package/test/block/blockheader.js +270 -0
  21. package/test/block/merkleblock.js +207 -0
  22. package/test/cli/smoke.js +261 -0
  23. package/test/credentials-test.js +329 -0
  24. package/test/crypto/bn.js +168 -0
  25. package/test/crypto/ecdsa.js +328 -0
  26. package/test/crypto/elliptic-fixed.js +61 -0
  27. package/test/crypto/hash.browser.js +121 -0
  28. package/test/crypto/hash.js +122 -0
  29. package/test/crypto/point.js +193 -0
  30. package/test/crypto/random.js +32 -0
  31. package/test/crypto/security.js +127 -0
  32. package/test/crypto/shamir.js +169 -0
  33. package/test/crypto/signature.js +318 -0
  34. package/test/data/bip69.json +215 -0
  35. package/test/data/bitcoind/base58_keys_invalid.json +152 -0
  36. package/test/data/bitcoind/base58_keys_valid.json +452 -0
  37. package/test/data/bitcoind/blocks.json +27 -0
  38. package/test/data/bitcoind/script_tests.json +2244 -0
  39. package/test/data/bitcoind/sig_canonical.json +7 -0
  40. package/test/data/bitcoind/sig_noncanonical.json +22 -0
  41. package/test/data/bitcoind/tx_invalid.json +177 -0
  42. package/test/data/bitcoind/tx_valid.json +224 -0
  43. package/test/data/blk86756-testnet.dat +0 -0
  44. package/test/data/blk86756-testnet.js +12 -0
  45. package/test/data/blk86756-testnet.json +684 -0
  46. package/test/data/ecdsa.json +230 -0
  47. package/test/data/merkleblocks.js +486 -0
  48. package/test/data/messages.json +22 -0
  49. package/test/data/sighash.json +1004 -0
  50. package/test/data/tx_creation.json +85 -0
  51. package/test/ecies/bitcore-ecies.js +170 -0
  52. package/test/ecies/electrum-ecies.js +206 -0
  53. package/test/encoding/base58.js +131 -0
  54. package/test/encoding/base58check.js +136 -0
  55. package/test/encoding/bufferreader.js +328 -0
  56. package/test/encoding/bufferwriter.js +160 -0
  57. package/test/encoding/varint.js +104 -0
  58. package/test/gdaf/canonicalize.js +140 -0
  59. package/test/hdkeys.js +365 -0
  60. package/test/hdprivatekey.js +330 -0
  61. package/test/hdpublickey.js +293 -0
  62. package/test/index.js +16 -0
  63. package/test/ltp/right.js +198 -0
  64. package/test/message/message.js +190 -0
  65. package/test/mnemonic/data/fixtures.json +300 -0
  66. package/test/mnemonic/mnemonic.js +245 -0
  67. package/test/mnemonic/mocha.opts +1 -0
  68. package/test/mnemonic/pbkdf2.test.js +43 -0
  69. package/test/networks.js +162 -0
  70. package/test/opcode.js +161 -0
  71. package/test/privatekey.js +430 -0
  72. package/test/publickey.js +411 -0
  73. package/test/script/genesis_limits.js +80 -0
  74. package/test/script/interpreter.js +723 -0
  75. package/test/script/script.js +1256 -0
  76. package/test/script/string_ops.js +72 -0
  77. package/test/smart_contract/covenants.js +247 -0
  78. package/test/smart_contract/dsl_debugger.js +103 -0
  79. package/test/smart_contract/token_generalized.js +187 -0
  80. package/test/transaction/deserialize.js +33 -0
  81. package/test/transaction/input/input.js +92 -0
  82. package/test/transaction/input/multisig.js +174 -0
  83. package/test/transaction/input/multisigscripthash.js +111 -0
  84. package/test/transaction/input/publickey.js +68 -0
  85. package/test/transaction/input/publickeyhash.js +59 -0
  86. package/test/transaction/output.js +185 -0
  87. package/test/transaction/sighash.js +71 -0
  88. package/test/transaction/signature.js +127 -0
  89. package/test/transaction/transaction.js +1299 -0
  90. package/test/transaction/unspentoutput.js +92 -0
  91. package/test/util/js.js +76 -0
  92. package/test/util/preconditions.js +79 -0
  93. package/test/vcjwt/interop.js +126 -0
@@ -0,0 +1,159 @@
1
+ 'use strict'
2
+ /**
3
+ * SmartContract.Authorizers — pluggable ownership-authorization schemes for the
4
+ * stateful Token covenant.
5
+ *
6
+ * A token carries a single 20-byte owner commitment in its scriptCode. By
7
+ * factoring out ONLY the block that proves "the spender is the committed owner of
8
+ * THIS spend", the same covenant can be owned by a single key, an m-of-n group, or
9
+ * any future custom predicate — with the state-rebuild plumbing (read commit =
10
+ * chunk[4:24], rewrite with the next owner's 20-byte commit) identical for every
11
+ * scheme.
12
+ *
13
+ * An Authorizer is `{ name, commit, emit, unlockArgs }`:
14
+ * - commit(descriptor) -> Buffer(20)
15
+ * the 20-byte owner id for an owner descriptor (a key, a key set, ...).
16
+ * - emit(script)
17
+ * the in-script proof. At entry the stack is [ ...authArgs, ownerCommit ]
18
+ * (ownerCommit on top). The block MUST consume authArgs + ownerCommit and
19
+ * VERIFY authorization over this spend (use *VERIFY opcodes — it must not
20
+ * leave a fall-through boolean), leaving whatever sits below authArgs
21
+ * (the next-owner hash) untouched. It must also leave the altstack as it
22
+ * found it.
23
+ * - unlockArgs(spend, inputIndex, lock, sats, signer) -> Array<Buffer|opcode>
24
+ * the authArgs pushed (deepest-first) in the unlock script, which the Token
25
+ * slots as <newOwnerHash> <...authArgs> <preimage>.
26
+ *
27
+ * All schemes commit to a fixed 20-byte hash, so a transfer's chosen next owner is
28
+ * always just a 20-byte value the covenant splices into the recreated scriptCode.
29
+ */
30
+
31
+ var bsv = require('../..')
32
+ var Hash = require('../crypto/hash')
33
+ var H = require('./covenant_helpers')
34
+
35
+ var Opcode = bsv.Opcode
36
+ var n = H.scriptNum
37
+
38
+ function pubBuffer (key) {
39
+ if (key instanceof bsv.PrivateKey) return key.toPublicKey().toBuffer()
40
+ if (key instanceof bsv.PublicKey) return key.toBuffer()
41
+ return key // already a Buffer
42
+ }
43
+
44
+ /* -------------------------------------------------------------------------- */
45
+ /* single-key: owner is one key; authorize with its signature over the spend. */
46
+ /* -------------------------------------------------------------------------- */
47
+ function singleKey () {
48
+ return {
49
+ name: 'single-key',
50
+ commit: function (key) { return Hash.sha256ripemd160(pubBuffer(key)) },
51
+ emit: function (s) {
52
+ // stack: [.., sig, pubkey, ownerCommit]
53
+ s.add(Opcode.OP_OVER).add(Opcode.OP_HASH160).add(Opcode.OP_EQUALVERIFY) // HASH160(pubkey) == ownerCommit
54
+ s.add(Opcode.OP_CHECKSIGVERIFY) // ECDSA signature over THIS spend
55
+ return s
56
+ },
57
+ unlockArgs: function (spend, inputIndex, lock, sats, ownerKey) {
58
+ var sig = H.signInput(spend, ownerKey, inputIndex, lock, sats)
59
+ return [sig, ownerKey.toPublicKey().toBuffer()] // deepest-first: sig, then pubkey
60
+ }
61
+ }
62
+ }
63
+
64
+ /* -------------------------------------------------------------------------- */
65
+ /* multisig: owner is an m-of-n key set, committed by hashing the canonical */
66
+ /* multisig redeem script. The set is revealed at spend, bound to the */
67
+ /* commitment, and checked with OP_CHECKMULTISIG. */
68
+ /* -------------------------------------------------------------------------- */
69
+
70
+ /** Canonical m-of-n redeem-script bytes: OP_m <33||pk>..<33||pk> OP_n OP_CHECKMULTISIG. */
71
+ function redeemScript (m, pubkeys) {
72
+ var nn = pubkeys.length
73
+ if (m < 1 || m > nn || nn < 1 || nn > 16) throw new Error('multisig requires 1 <= m <= n <= 16')
74
+ var s = new bsv.Script().add(Opcode['OP_' + m])
75
+ pubkeys.forEach(function (p) { s.add(Buffer.from(p)) }) // each emitted as 0x21||pk (33-byte push)
76
+ s.add(Opcode['OP_' + nn]).add(Opcode.OP_CHECKMULTISIG)
77
+ return s.toBuffer()
78
+ }
79
+
80
+ function multisig (m, nKeys) {
81
+ if (m < 1 || m > nKeys || nKeys < 1 || nKeys > 16) throw new Error('multisig requires 1 <= m <= n <= 16')
82
+ return {
83
+ name: m + '-of-' + nKeys + '-multisig',
84
+ m: m,
85
+ n: nKeys,
86
+ /** descriptor: an ordered array of n public keys (PublicKey|PrivateKey|Buffer). */
87
+ commit: function (keys) {
88
+ if (keys.length !== nKeys) throw new Error('expected ' + nKeys + ' public keys')
89
+ return Hash.sha256ripemd160(redeemScript(m, keys.map(pubBuffer)))
90
+ },
91
+ emit: function (s) {
92
+ // stack: [.., dummy, sig_1..sig_m, redeem, ownerCommit] (redeem = canonical bytes)
93
+ s.add(Opcode.OP_OVER).add(Opcode.OP_HASH160).add(Opcode.OP_EQUALVERIFY) // HASH160(redeem) == ownerCommit
94
+ // strip the leading OP_m byte and the trailing OP_n OP_CHECKMULTISIG (2 bytes),
95
+ // leaving body = (0x21||pubkey) repeated n times.
96
+ s.add(n(1)).add(Opcode.OP_SPLIT).add(Opcode.OP_NIP)
97
+ s.add(Opcode.OP_SIZE).add(n(2)).add(Opcode.OP_SUB).add(Opcode.OP_SPLIT).add(Opcode.OP_DROP)
98
+ // peel each 34-byte (0x21||pubkey) unit, drop the push opcode, park the pubkey.
99
+ // Parking pushes pk1..pkn onto the altstack (pkn on top), so popping them back
100
+ // yields pkn..pk1 — the pubkey just under OP_n is pk1. CHECKMULTISIG therefore
101
+ // encounters keys pk1, pk2, ... pkn; the unlock provides sigs in the matching
102
+ // (descending key-index) order, see unlockArgs.
103
+ for (var i = 0; i < this.n; i++) {
104
+ s.add(n(34)).add(Opcode.OP_SPLIT) // [.., unit34, rest]
105
+ s.add(Opcode.OP_SWAP).add(n(1)).add(Opcode.OP_SPLIT).add(Opcode.OP_NIP) // [.., rest, pubkey]
106
+ s.add(Opcode.OP_TOALTSTACK)
107
+ }
108
+ s.add(Opcode.OP_DROP) // the now-empty remainder
109
+ // rebuild the CHECKMULTISIG stack: [.., dummy, sigs.., m, pkn..pk1, n]
110
+ s.add(n(this.m))
111
+ for (i = 0; i < this.n; i++) s.add(Opcode.OP_FROMALTSTACK)
112
+ s.add(n(this.n))
113
+ s.add(Opcode.OP_CHECKMULTISIGVERIFY)
114
+ return s
115
+ },
116
+ unlockArgs: function (spend, inputIndex, lock, sats, signer) {
117
+ // signer: { keys: [n ordered pubkeys], signWith: [>= m private keys] }
118
+ var keys = signer.keys
119
+ if (keys.length !== nKeys) throw new Error('expected ' + nKeys + ' public keys in signer.keys')
120
+ var redeem = redeemScript(m, keys.map(pubBuffer))
121
+ // emit() encounters keys pk1..pkn, so the m signatures must be pushed in
122
+ // descending key-index order (topmost sig = smallest-index signer).
123
+ var keyHex = keys.map(pubBuffer).map(function (b) { return b.toString('hex') })
124
+ var indexed = signer.signWith.map(function (priv) {
125
+ var idx = keyHex.indexOf(priv.toPublicKey().toBuffer().toString('hex'))
126
+ if (idx < 0) throw new Error('signWith key is not in the committed key set')
127
+ return { idx: idx, priv: priv }
128
+ }).sort(function (a, b) { return b.idx - a.idx }).slice(0, m)
129
+ if (indexed.length < m) throw new Error('need at least ' + m + ' signing keys')
130
+ var sigs = indexed.map(function (e) { return H.signInput(spend, e.priv, inputIndex, lock, sats) })
131
+ // deepest-first: dummy, then the m sigs (descending key index), then redeem.
132
+ return [Opcode.OP_0].concat(sigs).concat([redeem])
133
+ }
134
+ }
135
+ }
136
+
137
+ /* -------------------------------------------------------------------------- */
138
+ /* predicate: escape hatch for arbitrary ownership schemes. Supply your own */
139
+ /* { commit, emit, unlockArgs } honoring the Authorizer contract above. */
140
+ /* -------------------------------------------------------------------------- */
141
+ function predicate (spec) {
142
+ if (!spec || typeof spec.emit !== 'function' || typeof spec.unlockArgs !== 'function' ||
143
+ typeof spec.commit !== 'function') {
144
+ throw new Error('predicate authorizer needs { commit, emit, unlockArgs } functions')
145
+ }
146
+ return {
147
+ name: spec.name || 'predicate',
148
+ commit: spec.commit,
149
+ emit: spec.emit,
150
+ unlockArgs: spec.unlockArgs
151
+ }
152
+ }
153
+
154
+ module.exports = {
155
+ singleKey: singleKey,
156
+ multisig: multisig,
157
+ predicate: predicate,
158
+ redeemScript: redeemScript
159
+ }
@@ -19,9 +19,9 @@ var SIGHASH = Signature.SIGHASH_ALL | Signature.SIGHASH_FORKID // 0x41
19
19
 
20
20
  // Post-Genesis consensus + STANDARD RELAY flags — what mainnet miners actually
21
21
  // enforce. Includes SCRIPT_VERIFY_MINIMALDATA (every push must be minimal) and
22
- // SCRIPT_VERIFY_LOW_S (canonical signatures). Verifying with these locally
23
- // catches non-relayable covenants BEFORE broadcast — covenants verified here
24
- // have been deployed and spent on BSV mainnet.
22
+ // SCRIPT_VERIFY_LOW_S (canonical signatures). Verifying with these flags locally
23
+ // mirrors mainnet relay/consensus policy, so a covenant that passes here is
24
+ // expected to be accepted on broadcast — catch non-relayable scripts before then.
25
25
  function flags () {
26
26
  var I = bsv.Script.Interpreter
27
27
  return I.SCRIPT_VERIFY_P2SH |
@@ -124,6 +124,6 @@ function policy () { return new Policy() }
124
124
 
125
125
  // Terminal shortcuts for the non-composable whole-covenant patterns.
126
126
  policy.perpetual = function (fee) { return PELS.pelsCovenant(fee) }
127
- policy.token = function (fee, ownerHash) { return Token.ownershipToken(fee, ownerHash) }
127
+ policy.token = function (fee, ownerPubKeyHash) { return Token.ownershipToken(fee, ownerPubKeyHash) }
128
128
 
129
129
  module.exports = { policy: policy, Policy: Policy }
@@ -44,6 +44,7 @@ var SmartContract = {
44
44
  PushTx: require('./pushtx'),
45
45
  PELS: require('./pels'),
46
46
  Token: require('./token'),
47
+ Authorizers: require('./authorizers'),
47
48
 
48
49
  // Declarative covenant DSL + stack debugger (v4.5.0)
49
50
  DSL: require('./dsl'),
@@ -72,9 +73,10 @@ SmartContract.verifyScript = function (unlockingScript, lockingScript, opts) {
72
73
  SmartContract.perpetualCovenant = function (fee) {
73
74
  return SmartContract.PELS.pelsCovenant(fee)
74
75
  }
75
- // Stateful ownership token (NFT) locking script.
76
- SmartContract.ownershipToken = function (fee, ownerHash) {
77
- return SmartContract.Token.ownershipToken(fee, ownerHash)
76
+ // Stateful ownership token (NFT) locking script. ownerPubKeyHash = HASH160 of the
77
+ // owner's public key (SmartContract.Token.ownerId); transfers are ECDSA-authorized.
78
+ SmartContract.ownershipToken = function (fee, ownerPubKeyHash) {
79
+ return SmartContract.Token.ownershipToken(fee, ownerPubKeyHash)
78
80
  }
79
81
  // OP_PUSH_TX value/output covenant: coins can only go where the covenant says.
80
82
  SmartContract.valueCovenant = function (expectedHashOutputs) {
@@ -358,9 +360,9 @@ SmartContract.utils.decodeCompactSize = function (buffer, offset) {
358
360
  return SmartContract.Preimage.decodeCompactSize(buffer, offset)
359
361
  }
360
362
 
361
- // Version information
362
- SmartContract.version = 'v1.0.0'
363
- SmartContract.description = 'Enterprise Smart Contract Framework for Bitcoin SV'
363
+ // Version information — tracks the package version so the two never drift.
364
+ SmartContract.version = require('../../package.json').version
365
+ SmartContract.description = 'Smart Contract Framework for Bitcoin SV'
364
366
 
365
367
  // Feature flags
366
368
  SmartContract.features = {
@@ -385,8 +387,7 @@ SmartContract.features = {
385
387
  STACK_EXAMINATION: true,
386
388
  SCRIPT_DEBUGGING: true,
387
389
  STEP_BY_STEP_EXECUTION: true,
388
- INTERACTIVE_DEBUGGING: true,
389
- PRODUCTION_READY: true
390
+ INTERACTIVE_DEBUGGING: true
390
391
  }
391
392
 
392
393
  // Standard debug method aliases for compatibility
@@ -36,6 +36,7 @@ function pelsCovenant (fee) {
36
36
  s.add(Opcode.OP_DUP)
37
37
  P.pushTxCore(s)
38
38
  s.add(Opcode.OP_VERIFY) // stack: [preimage]
39
+ P.assertSighashAll(s) // fail fast unless this spend is SIGHASH_ALL|FORKID (0x41)
39
40
 
40
41
  // 2. scriptChunk = preimage[104 : len-52]
41
42
  s.add(Opcode.OP_DUP)
@@ -87,6 +87,28 @@ function extractHashOutputs (script) {
87
87
  return script
88
88
  }
89
89
 
90
+ // 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])
93
+
94
+ /**
95
+ * Assert the spend is SIGHASH_ALL|FORKID (0x41). Pre/Post: top of stack = preimage
96
+ * (left untouched). These covenants read fixed offsets (104 / 52 / 40) that are
97
+ * meaningful only for the 0x41 layout, and the OP_PUSH_TX core already pins the
98
+ * flag implicitly (its synthetic sig carries 0x41, so OP_CHECKSIG only accepts a
99
+ * preimage hashing to the 0x41 sighash). Note the offsets themselves are invariant
100
+ * in the number of INPUTS — hashPrevouts/hashSequence are digests of all inputs,
101
+ * so the preimage is the same length for 1 or N inputs; the covenant simply does
102
+ * not constrain sibling inputs, by design. What it does assume is a single
103
+ * recreated OUTPUT (HASH256(nextOutput) == hashOutputs). This guard makes the
104
+ * flag assumption explicit and fails fast — and survives any refactor of the core.
105
+ */
106
+ function assertSighashAll (script) {
107
+ script.add(Opcode.OP_DUP).add(scriptNum(4)).add(Opcode.OP_RIGHT)
108
+ .add(SIGHASH_WORD).add(Opcode.OP_EQUALVERIFY)
109
+ return script
110
+ }
111
+
90
112
  /** BIP-143 hashOutputs (SIGHASH_ALL) for a set of Transaction.Output objects. */
91
113
  function hashOutputs (outputs) {
92
114
  var ser = Buffer.concat(outputs.map(function (o) { return o.toBufferWriter().toBuffer() }))
@@ -129,15 +151,36 @@ function sFromPreimage (preimage) {
129
151
  }
130
152
 
131
153
  /**
132
- * Grind the spend tx (vary nLockTime) until the in-script signature is a clean
133
- * fixed-length DER. Mutates spend.nLockTime; returns { preimage, tries }.
154
+ * Grind a malleable field of the spend until the in-script OP_PUSH_TX signature
155
+ * is a clean, canonical (low-S) fixed-length DER. Returns { preimage, tries,
156
+ * field, nonce }; mutates the chosen field on `spend`.
157
+ *
158
+ * @param {object} [opts] grind options (a bare number is accepted as legacy maxTries):
159
+ * - maxTries {number} attempts before giving up (default 5000).
160
+ * - start {number} first nonce value to try (default 0).
161
+ * - field {'nLockTime'|'sequence'} which field carries the grind nonce
162
+ * (default 'nLockTime'). Use 'sequence' when the covenant pins nLockTime for
163
+ * a real CLTV absolute timelock: the input's sequence is swept down from
164
+ * 0xfffffffe (kept non-final, so CLTV still triggers) instead of clobbering
165
+ * nLockTime. (Not for CSV/relative-locktime covenants, which encode meaning
166
+ * in the sequence number itself.)
134
167
  */
135
- function grind (spend, inputIndex, lockingScript, satoshis, maxTries) {
136
- maxTries = maxTries || 5000
137
- for (var t = 0; t < maxTries; t++) {
138
- spend.nLockTime = t
168
+ function grind (spend, inputIndex, lockingScript, satoshis, opts) {
169
+ if (typeof opts === 'number') opts = { maxTries: opts } // back-compat: legacy maxTries arg
170
+ opts = opts || {}
171
+ var maxTries = opts.maxTries || 5000
172
+ var start = opts.start || 0
173
+ var field = opts.field || 'nLockTime'
174
+ if (field !== 'nLockTime' && field !== 'sequence') {
175
+ throw new Error("grind field must be 'nLockTime' or 'sequence'")
176
+ }
177
+ var input = spend.inputs[inputIndex]
178
+ for (var i = 0; i < maxTries; i++) {
179
+ var nonce = start + i
180
+ if (field === 'nLockTime') spend.nLockTime = nonce
181
+ else input.sequenceNumber = 0xfffffffe - nonce // stay < 0xffffffff (non-final)
139
182
  var preimage = H.rawPreimage(spend, inputIndex, lockingScript, satoshis)
140
- if (sFromPreimage(preimage)) return { preimage: preimage, tries: t + 1 }
183
+ if (sFromPreimage(preimage)) return { preimage: preimage, tries: i + 1, field: field, nonce: nonce }
141
184
  }
142
185
  throw new Error('OP_PUSH_TX grind failed after ' + maxTries + ' tries')
143
186
  }
@@ -153,6 +196,7 @@ module.exports = {
153
196
  pushTxCore: pushTxCore,
154
197
  authenticator: authenticator,
155
198
  extractHashOutputs: extractHashOutputs,
199
+ assertSighashAll: assertSighashAll,
156
200
  hashOutputs: hashOutputs,
157
201
  valueCovenant: valueCovenant,
158
202
  sFromPreimage: sFromPreimage,
@@ -4,40 +4,69 @@
4
4
  *
5
5
  * Carries mutable STATE in its own locking script and enforces a STATE-
6
6
  * TRANSITION rule on every spend.
7
- * State : 32-byte owner id = SHA256(ownerSecret).
8
- * Transition rule: to spend, the owner reveals `ownerSecret`; the spend must
9
- * recreate the SAME token code with a (possibly new) owner,
10
- * paying value-fee forward. A transferable, perpetual NFT.
7
+ * State : 20-byte owner id (an Authorizer commitment — by default the
8
+ * HASH160 of the owner's public key).
9
+ * Transition rule: to spend, the current owner proves authorization over THIS
10
+ * spend (an ECDSA signature, an m-of-n multisig, or any custom
11
+ * predicate), and the spend must recreate the SAME token code
12
+ * with a (possibly new) owner. A transferable, perpetual NFT.
11
13
  *
12
- * Layout: <ownerHash:32> OP_DROP <CODE>. The leading push embeds the state so it
14
+ * Why authorization is a proof over the spend, not a revealed secret. An earlier
15
+ * design gated the spend on revealing `ownerSecret` with SHA256(ownerSecret)==
16
+ * ownerHash — a hash-lock. The secret is exposed in the mempool on the first
17
+ * spend, and because the spender freely chooses the next owner, a watcher can lift
18
+ * the revealed secret and broadcast a competing spend redirecting the token to
19
+ * themselves (front-running theft). Binding authorization to a signature over the
20
+ * spend fixes this: the signature commits (via SIGHASH_ALL hashOutputs) to the
21
+ * exact recreated output, including the chosen next owner, so a third party who
22
+ * alters the destination invalidates the proof. See ./authorizers.
23
+ *
24
+ * Layout: <ownerCommit:20> OP_DROP <CODE>. The leading push embeds the state so it
13
25
  * rides inside `scriptCode`; OP_DROP discards the runtime copy because CODE reads
14
26
  * the authoritative state from the authenticated preimage. Within the scriptCode
15
27
  * chunk (varint||script, 3-byte varint for ~480B scripts):
16
- * [0:4] = varint||0x20 (prefix), [4:36] = ownerHash, [36:] = 0x75||CODE (suffix)
28
+ * [0:4] = varint||0x14 (prefix), [4:24] = ownerCommit, [24:] = 0x75||CODE
17
29
  * A transfer rebuilds the chunk as prefix || newOwnerHash || suffix.
18
30
  *
19
- * Unlocking script: <ownerSecret> <newOwnerHash> <preimage>.
31
+ * Two output shapes:
32
+ * ownershipToken(fee, owner[, auth]) — single recreated output, value
33
+ * forwarded as (input - fee). The classic perpetual NFT.
34
+ * ownershipTokenMulti(owner[, auth]) — the recreated token output may sit
35
+ * among other outputs (payments, change, data). The spender reveals the
36
+ * surrounding output bytes and the token's value; the covenant binds them
37
+ * into the committed hashOutputs. Value conservation is left to the network.
38
+ *
20
39
  * Requires post-Genesis limits (SmartContract.enableGenesis()).
21
40
  */
22
41
 
23
42
  var bsv = require('../..')
24
- var Hash = require('../crypto/hash')
25
43
  var P = require('./pushtx')
26
44
  var H = require('./covenant_helpers')
45
+ var A = require('./authorizers')
27
46
 
28
47
  var Script = bsv.Script
29
48
  var Opcode = bsv.Opcode
30
49
  var n = H.scriptNum
31
50
 
32
- /** Build the token locking script for a given owner and per-hop fee. */
33
- function ownershipToken (fee, ownerHash) {
34
- if (ownerHash.length !== 32) throw new Error('ownerHash must be 32 bytes (SHA256)')
35
- var s = new Script()
36
- s.add(Buffer.from(ownerHash)) // <ownerHash:32>
37
- s.add(Opcode.OP_DROP)
51
+ function resolveCommit (owner, auth) {
52
+ var commit = Buffer.isBuffer(owner) ? owner : auth.commit(owner)
53
+ if (commit.length !== 20) throw new Error('owner commitment must be 20 bytes (HASH160)')
54
+ return commit
55
+ }
56
+
57
+ /** Push a possibly-empty buffer MINIMALDATA-cleanly (empty => OP_0). */
58
+ function pushData (s, buf) { return buf.length ? s.add(Buffer.from(buf)) : s.add(Opcode.OP_0) }
38
59
 
39
- // CODE — stack at entry: [ownerSecret, newOwnerHash, preimage]
60
+ /**
61
+ * Authenticated head shared by both output shapes. Pre: top of stack = preimage,
62
+ * with the scheme's authArgs and the next-owner hash beneath it. Post: consumes
63
+ * the preimage + authArgs, verifies owner authorization, and leaves the next-owner
64
+ * hash (and any lower unlock items) with the scriptChunk on top; altstack carries
65
+ * [value8, hashOutputs].
66
+ */
67
+ function emitAuthHead (s, auth) {
40
68
  s.add(Opcode.OP_DUP); P.pushTxCore(s); s.add(Opcode.OP_VERIFY) // authenticate
69
+ P.assertSighashAll(s) // fail fast unless this spend is SIGHASH_ALL|FORKID (0x41)
41
70
 
42
71
  // park value (8B @ offsetFromEnd 52) and hashOutputs (32B @ offsetFromEnd 40)
43
72
  s.add(Opcode.OP_DUP)
@@ -48,16 +77,53 @@ function ownershipToken (fee, ownerHash) {
48
77
  s.add(n(104)).add(Opcode.OP_SPLIT).add(Opcode.OP_NIP)
49
78
  .add(Opcode.OP_SIZE).add(n(52)).add(Opcode.OP_SUB).add(Opcode.OP_SPLIT).add(Opcode.OP_DROP)
50
79
 
51
- // owner authorization: SHA256(ownerSecret) == ownerHash = chunk[4:36] (OP_SUBSTR)
52
- s.add(Opcode.OP_DUP).add(n(4)).add(n(32)).add(Opcode.OP_SUBSTR)
53
- s.add(n(3)).add(Opcode.OP_ROLL)
54
- s.add(Opcode.OP_SHA256).add(Opcode.OP_EQUALVERIFY) // [newOwnerHash, scriptChunk]
80
+ // expose the authoritative ownerCommit (= chunk[4:24]) and park the chunk
81
+ s.add(Opcode.OP_DUP).add(n(4)).add(n(20)).add(Opcode.OP_SUBSTR) // [.., scriptChunk, ownerCommit]
82
+ s.add(Opcode.OP_SWAP).add(Opcode.OP_TOALTSTACK) // alt:[value8, hashOutputs, scriptChunk]; [.., authArgs, ownerCommit]
83
+
84
+ auth.emit(s) // consumes authArgs + ownerCommit, VERIFYs authorization over this spend
55
85
 
56
- // rebuild chunk with new owner: prefix(4) || newOwnerHash || suffix
57
- s.add(n(4)).add(Opcode.OP_SPLIT).add(n(32)).add(Opcode.OP_SPLIT).add(Opcode.OP_NIP)
86
+ s.add(Opcode.OP_FROMALTSTACK) // restore scriptChunk ; alt:[value8, hashOutputs]
87
+ return s
88
+ }
89
+
90
+ /**
91
+ * Rebuild the scriptChunk with the next owner. Pre: [.., newOwnerHash, scriptChunk].
92
+ * Post: [.., nextChunk] = prefix(4) || newOwnerHash(20) || suffix.
93
+ */
94
+ function emitRebuild (s) {
95
+ s.add(n(4)).add(Opcode.OP_SPLIT).add(n(20)).add(Opcode.OP_SPLIT).add(Opcode.OP_NIP) // [.., newOwnerHash, prefix, suffix]
58
96
  s.add(Opcode.OP_TOALTSTACK) // park suffix
59
97
  s.add(Opcode.OP_SWAP).add(Opcode.OP_CAT) // prefix || newOwnerHash
60
98
  s.add(Opcode.OP_FROMALTSTACK).add(Opcode.OP_CAT) // || suffix => nextChunk
99
+ return s
100
+ }
101
+
102
+ function assertVarintLen (s) {
103
+ var len = s.toBuffer().length
104
+ if (len <= 252 || len > 0xffff) {
105
+ throw new Error('token script length must use a 3-byte varint (253..65535); got ' + len)
106
+ }
107
+ return s
108
+ }
109
+
110
+ /**
111
+ * Single-output perpetual NFT: the spend's one output recreates the token with a
112
+ * (possibly new) owner and value = (input - fee).
113
+ *
114
+ * @param {number} fee satoshis deducted at each hop.
115
+ * @param {*} owner a 20-byte commitment Buffer, or an owner descriptor the
116
+ * authorizer can commit (a key, a key set, ...).
117
+ * @param {object} [auth] an Authorizer (default Authorizers.singleKey()).
118
+ */
119
+ function ownershipToken (fee, owner, auth) {
120
+ auth = auth || A.singleKey()
121
+ var s = new Script()
122
+ s.add(resolveCommit(owner, auth)) // <ownerCommit:20>
123
+ s.add(Opcode.OP_DROP)
124
+
125
+ emitAuthHead(s, auth) // stack: [newOwnerHash, scriptChunk] ; alt:[value8, hashOutputs]
126
+ emitRebuild(s) // stack: [nextChunk]
61
127
 
62
128
  // newValue = value - fee (8B LE); nextOutput = newValue || nextChunk
63
129
  s.add(Opcode.OP_FROMALTSTACK) // hashOutputs
@@ -68,26 +134,104 @@ function ownershipToken (fee, ownerHash) {
68
134
  s.add(Opcode.OP_HASH256)
69
135
  s.add(Opcode.OP_EQUAL)
70
136
 
71
- var len = s.toBuffer().length
72
- if (len <= 252 || len > 0xffff) {
73
- throw new Error('token script length must use a 3-byte varint (253..65535); got ' + len)
74
- }
75
- return s
137
+ return assertVarintLen(s)
76
138
  }
77
139
 
78
- /** Owner identifier from a secret. */
79
- function ownerId (secret) { return Hash.sha256(secret) }
140
+ /**
141
+ * Multi-output NFT: the recreated token output may sit among other outputs. The
142
+ * spender reveals the serialized outputs before and after the token output and the
143
+ * token output's value; the covenant binds them into the committed hashOutputs.
144
+ * Value conservation across the whole tx is enforced by the network, not the script.
145
+ *
146
+ * @param {*} owner commitment Buffer or descriptor (see ownershipToken).
147
+ * @param {object} [auth] an Authorizer (default Authorizers.singleKey()).
148
+ */
149
+ function ownershipTokenMulti (owner, auth) {
150
+ auth = auth || A.singleKey()
151
+ var s = new Script()
152
+ s.add(resolveCommit(owner, auth))
153
+ s.add(Opcode.OP_DROP)
154
+
155
+ emitAuthHead(s, auth)
156
+ // stack: [before, after, tokenValue8, newOwnerHash, scriptChunk] ; alt:[value8, hashOutputs]
157
+ emitRebuild(s)
158
+ // stack: [before, after, tokenValue8, nextChunk]
159
+
160
+ s.add(Opcode.OP_CAT) // tokenValue8 || nextChunk => tokenOutput ; [before, after, tokenOutput]
161
+ s.add(Opcode.OP_SWAP).add(Opcode.OP_CAT) // tokenOutput || after ; [before, tokenOutput||after]
162
+ s.add(Opcode.OP_CAT) // before || tokenOutput || after => allOutputs
80
163
 
81
- /** Unlocking script for a transfer. */
82
- function unlockTransfer (ownerSecret, newOwnerHash, preimage) {
83
- return new Script()
84
- .add(Buffer.from(ownerSecret))
85
- .add(Buffer.from(newOwnerHash))
86
- .add(Buffer.from(preimage))
164
+ s.add(Opcode.OP_FROMALTSTACK) // hashOutputs ; alt:[value8]
165
+ s.add(Opcode.OP_FROMALTSTACK).add(Opcode.OP_DROP) // discard the input value (not used in multi mode)
166
+ s.add(Opcode.OP_SWAP).add(Opcode.OP_HASH256).add(Opcode.OP_EQUAL) // HASH256(allOutputs) == hashOutputs
167
+
168
+ return assertVarintLen(s)
169
+ }
170
+
171
+ /** Owner identifier from a single key: HASH160(ownerPubKey). For other schemes use the authorizer's commit(). */
172
+ function ownerId (key) { return A.singleKey().commit(key) }
173
+
174
+ /**
175
+ * Build the unlocking script for a single-output transfer. Grinds the spend so the
176
+ * in-script OP_PUSH_TX signature is canonical, THEN proves authorization — the
177
+ * owner's proof must commit to the final tx (nLockTime + outputs), which is what
178
+ * binds the chosen `newOwnerHash` and defeats front-running.
179
+ *
180
+ * @param {*} signer descriptor passed to the authorizer's unlockArgs
181
+ * (a PrivateKey for single-key; { keys, signWith } for multisig).
182
+ * @param {Buffer} newOwnerHash 20-byte commitment of the NEXT owner.
183
+ * @param {Transaction} spend the spend tx (its single output recreates the token).
184
+ * @param {number} satoshis value of the token UTXO being spent.
185
+ * @param {Script} lockingScript the token script being spent.
186
+ * @param {object} [opts] { auth, inputIndex=0, grind }.
187
+ */
188
+ function unlockTransfer (signer, newOwnerHash, spend, satoshis, lockingScript, opts) {
189
+ opts = opts || {}
190
+ var auth = opts.auth || A.singleKey()
191
+ var inputIndex = opts.inputIndex || 0
192
+ if (newOwnerHash.length !== 20) throw new Error('newOwnerHash must be 20 bytes (HASH160)')
193
+ P.grind(spend, inputIndex, lockingScript, satoshis, opts.grind)
194
+ var preimage = H.rawPreimage(spend, inputIndex, lockingScript, satoshis)
195
+ var args = auth.unlockArgs(spend, inputIndex, lockingScript, satoshis, signer)
196
+ var us = new Script().add(Buffer.from(newOwnerHash))
197
+ args.forEach(function (a) { us.add(a) })
198
+ us.add(Buffer.from(preimage))
199
+ return us
200
+ }
201
+
202
+ /**
203
+ * Unlocking script for a multi-output transfer (see ownershipTokenMulti).
204
+ *
205
+ * @param {object} layout { before, after, tokenValue } — `before`/`after` are the
206
+ * serialized Output bytes surrounding the token output, and
207
+ * `tokenValue` is the 8-byte LE value of the token output.
208
+ * The spend tx MUST actually contain those outputs in order.
209
+ */
210
+ function unlockTransferMulti (signer, newOwnerHash, spend, satoshis, lockingScript, layout, opts) {
211
+ opts = opts || {}
212
+ var auth = opts.auth || A.singleKey()
213
+ var inputIndex = opts.inputIndex || 0
214
+ if (newOwnerHash.length !== 20) throw new Error('newOwnerHash must be 20 bytes (HASH160)')
215
+ if (!layout || layout.tokenValue == null || layout.tokenValue.length !== 8) {
216
+ throw new Error('layout.tokenValue must be the 8-byte LE value of the token output')
217
+ }
218
+ P.grind(spend, inputIndex, lockingScript, satoshis, opts.grind)
219
+ var preimage = H.rawPreimage(spend, inputIndex, lockingScript, satoshis)
220
+ var args = auth.unlockArgs(spend, inputIndex, lockingScript, satoshis, signer)
221
+ var us = new Script()
222
+ pushData(us, layout.before || Buffer.alloc(0))
223
+ pushData(us, layout.after || Buffer.alloc(0))
224
+ us.add(Buffer.from(layout.tokenValue))
225
+ us.add(Buffer.from(newOwnerHash))
226
+ args.forEach(function (a) { us.add(a) })
227
+ us.add(Buffer.from(preimage))
228
+ return us
87
229
  }
88
230
 
89
231
  module.exports = {
90
232
  ownershipToken: ownershipToken,
233
+ ownershipTokenMulti: ownershipTokenMulti,
91
234
  ownerId: ownerId,
92
- unlockTransfer: unlockTransfer
235
+ unlockTransfer: unlockTransfer,
236
+ unlockTransferMulti: unlockTransferMulti
93
237
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@smartledger/bsv",
3
- "version": "5.4.2",
3
+ "version": "5.5.1",
4
4
  "description": "🚀 Complete Bitcoin SV development framework with legally-recognizable DID:web + W3C VC-JWT toolkit, Legal Token Protocol (LTP), Global Digital Attestation Framework (GDAF), StatusList2021 revocation, and 16 flexible loading options. Standards-based credentials with ES256/ES256K support, on-chain BSV anchoring, and comprehensive Bitcoin SV API. Perfect for legal tokens, verifiable credentials, DeFi, smart contracts, and secure Bitcoin applications.",
5
5
  "author": "SmartLedger Technology <hello@smartledger.technology> (https://smartledger.technology)",
6
6
  "homepage": "https://github.com/codenlighten/smartledger-bsv#readme",
@@ -72,6 +72,8 @@
72
72
  "cdn": "bsv.min.js",
73
73
  "files": [
74
74
  "index.js",
75
+ "test/",
76
+ ".mocharc.json",
75
77
  "lib/",
76
78
  "utilities/*.js",
77
79
  "utilities/README.md",