@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.
- package/.mocharc.json +5 -0
- package/CHANGELOG.md +66 -0
- package/README.md +28 -8
- package/bsv-covenant.min.js +8 -8
- package/bsv-gdaf.min.js +11 -11
- package/bsv-ltp.min.js +11 -11
- package/bsv-smartcontract.min.js +8 -8
- package/bsv.bundle.js +11 -11
- package/bsv.min.js +11 -11
- package/lib/smart_contract/authorizers.js +159 -0
- package/lib/smart_contract/covenant_helpers.js +3 -3
- package/lib/smart_contract/dsl.js +1 -1
- package/lib/smart_contract/index.js +9 -8
- package/lib/smart_contract/pels.js +1 -0
- package/lib/smart_contract/pushtx.js +51 -7
- package/lib/smart_contract/token.js +179 -35
- package/package.json +3 -1
- package/test/address.js +629 -0
- package/test/block/block.js +239 -0
- package/test/block/blockheader.js +270 -0
- package/test/block/merkleblock.js +207 -0
- package/test/cli/smoke.js +261 -0
- package/test/credentials-test.js +329 -0
- package/test/crypto/bn.js +168 -0
- package/test/crypto/ecdsa.js +328 -0
- package/test/crypto/elliptic-fixed.js +61 -0
- package/test/crypto/hash.browser.js +121 -0
- package/test/crypto/hash.js +122 -0
- package/test/crypto/point.js +193 -0
- package/test/crypto/random.js +32 -0
- package/test/crypto/security.js +127 -0
- package/test/crypto/shamir.js +169 -0
- package/test/crypto/signature.js +318 -0
- package/test/data/bip69.json +215 -0
- package/test/data/bitcoind/base58_keys_invalid.json +152 -0
- package/test/data/bitcoind/base58_keys_valid.json +452 -0
- package/test/data/bitcoind/blocks.json +27 -0
- package/test/data/bitcoind/script_tests.json +2244 -0
- package/test/data/bitcoind/sig_canonical.json +7 -0
- package/test/data/bitcoind/sig_noncanonical.json +22 -0
- package/test/data/bitcoind/tx_invalid.json +177 -0
- package/test/data/bitcoind/tx_valid.json +224 -0
- package/test/data/blk86756-testnet.dat +0 -0
- package/test/data/blk86756-testnet.js +12 -0
- package/test/data/blk86756-testnet.json +684 -0
- package/test/data/ecdsa.json +230 -0
- package/test/data/merkleblocks.js +486 -0
- package/test/data/messages.json +22 -0
- package/test/data/sighash.json +1004 -0
- package/test/data/tx_creation.json +85 -0
- package/test/ecies/bitcore-ecies.js +170 -0
- package/test/ecies/electrum-ecies.js +206 -0
- package/test/encoding/base58.js +131 -0
- package/test/encoding/base58check.js +136 -0
- package/test/encoding/bufferreader.js +328 -0
- package/test/encoding/bufferwriter.js +160 -0
- package/test/encoding/varint.js +104 -0
- package/test/gdaf/canonicalize.js +140 -0
- package/test/hdkeys.js +365 -0
- package/test/hdprivatekey.js +330 -0
- package/test/hdpublickey.js +293 -0
- package/test/index.js +16 -0
- package/test/ltp/right.js +198 -0
- package/test/message/message.js +190 -0
- package/test/mnemonic/data/fixtures.json +300 -0
- package/test/mnemonic/mnemonic.js +245 -0
- package/test/mnemonic/mocha.opts +1 -0
- package/test/mnemonic/pbkdf2.test.js +43 -0
- package/test/networks.js +162 -0
- package/test/opcode.js +161 -0
- package/test/privatekey.js +430 -0
- package/test/publickey.js +411 -0
- package/test/script/genesis_limits.js +80 -0
- package/test/script/interpreter.js +723 -0
- package/test/script/script.js +1256 -0
- package/test/script/string_ops.js +72 -0
- package/test/smart_contract/covenants.js +247 -0
- package/test/smart_contract/dsl_debugger.js +103 -0
- package/test/smart_contract/token_generalized.js +187 -0
- package/test/transaction/deserialize.js +33 -0
- package/test/transaction/input/input.js +92 -0
- package/test/transaction/input/multisig.js +174 -0
- package/test/transaction/input/multisigscripthash.js +111 -0
- package/test/transaction/input/publickey.js +68 -0
- package/test/transaction/input/publickeyhash.js +59 -0
- package/test/transaction/output.js +185 -0
- package/test/transaction/sighash.js +71 -0
- package/test/transaction/signature.js +127 -0
- package/test/transaction/transaction.js +1299 -0
- package/test/transaction/unspentoutput.js +92 -0
- package/test/util/js.js +76 -0
- package/test/util/preconditions.js +79 -0
- 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
|
-
//
|
|
24
|
-
//
|
|
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,
|
|
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
|
-
|
|
77
|
-
|
|
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 = '
|
|
363
|
-
SmartContract.description = '
|
|
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
|
|
133
|
-
* fixed-length DER.
|
|
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,
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
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:
|
|
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 :
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
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
|
-
*
|
|
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||
|
|
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
|
-
*
|
|
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
|
-
|
|
33
|
-
|
|
34
|
-
if (
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
52
|
-
s.add(Opcode.OP_DUP).add(n(4)).add(n(
|
|
53
|
-
s.add(
|
|
54
|
-
|
|
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
|
-
//
|
|
57
|
-
s
|
|
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
|
-
|
|
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
|
-
/**
|
|
79
|
-
|
|
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
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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.
|
|
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",
|