@smartledger/bsv 5.4.2 → 5.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -5,6 +5,52 @@ All notable changes to SmartLedger-BSV will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [5.5.0] - 2026-06-25
9
+
10
+ Minor release. Closes a front-running theft flaw in the ownership-token covenant
11
+ and generalizes the covenant core into a pluggable, multi-output state machine.
12
+
13
+ ### Security
14
+
15
+ - **`SmartContract.Token` ownership is now ECDSA-authorized.** The previous design
16
+ gated a transfer on revealing `ownerSecret` with `SHA256(ownerSecret) ==
17
+ ownerHash` — a hash-lock. The secret was exposed in the mempool on the first
18
+ spend and the spender freely chose the next owner, so a watcher could lift the
19
+ revealed secret and broadcast a competing spend redirecting the token to
20
+ themselves. Ownership is now a key: state is `HASH160(ownerPubKey)` and a
21
+ transfer requires the current owner's signature over the spend
22
+ (`OP_CHECKSIGVERIFY`), which commits via `SIGHASH_ALL` `hashOutputs` to the
23
+ exact recreated output — so altering the destination invalidates the proof.
24
+
25
+ ### Added
26
+
27
+ - **Pluggable ownership — `SmartContract.Authorizers`.** Token ownership is always
28
+ a 20-byte commitment, so the transfer plumbing is identical across schemes:
29
+ - `singleKey()` — signature over the spend (the model above).
30
+ - `multisig(m, n)` — m-of-n group ownership; the key set is committed by hashing
31
+ the canonical redeem script, revealed at spend, bound to the commitment, and
32
+ checked with `OP_CHECKMULTISIG`.
33
+ - `predicate({ commit, emit, unlockArgs })` — escape hatch for custom schemes.
34
+ - **Multi-output (N-output) tokens** — `Token.ownershipTokenMulti` /
35
+ `unlockTransferMulti` let the recreated token output sit among other outputs
36
+ (payments, change, data); the spender reveals the surrounding output bytes and
37
+ the covenant binds them into the committed `hashOutputs`.
38
+ - **`PushTx.grind` configurable nonce** — `{ field: 'nLockTime' | 'sequence' }`,
39
+ so a covenant pinning `nLockTime` for a CLTV timelock can grind the input
40
+ sequence instead. `PushTx.assertSighashAll()` guard added to token/PELS scripts.
41
+
42
+ ### Changed (breaking, `SmartContract.Token` only)
43
+
44
+ - `Token.ownershipToken(fee, owner)` — `owner` is now `HASH160(pubkey)` (use
45
+ `Token.ownerId(key)`), not `SHA256(secret)`.
46
+ - `Token.unlockTransfer(signer, newOwnerHash, spend, satoshis, lockingScript[, opts])`
47
+ — now takes the owner key and the spend (it grinds and signs), not a precomputed
48
+ preimage. The rest of the package's API is unchanged and fully compatible.
49
+
50
+ ### Notes
51
+
52
+ - Full suite **4267 passing**; `lib/` and `test/` lint clean.
53
+
8
54
  ## [5.4.2] - 2026-06-25
9
55
 
10
56
  Patch release. Dependency security maintenance — no API or behavior changes.
package/README.md CHANGED
@@ -37,9 +37,9 @@ SC.enableGenesis() // post-Genesis limits (OP_PUSH_TX need
37
37
  // Self-replicating covenant — every spend recreates the same script (value − fee).
38
38
  const lock = SC.perpetualCovenant(500)
39
39
 
40
- // Stateful ownership token (NFT) — transfer requires the owner's secret,
41
- // rewrites state, perpetuates the token code.
42
- const token = SC.ownershipToken(500, ownerHash)
40
+ // Stateful ownership token (NFT) — transfer requires the owner's ECDSA signature
41
+ // over the spend, rewrites state, perpetuates the token code.
42
+ const token = SC.ownershipToken(500, ownerHash) // ownerHash = SC.Token.ownerId(ownerKey)
43
43
 
44
44
  // Value covenant — forces spend outputs to match a specific hashOutputs.
45
45
  const vlock = SC.valueCovenant(SC.PushTx.hashOutputs(requiredOutputs))
@@ -54,7 +54,8 @@ const ok = SC.verifyScript(unlockScript, lockingScript, tx, inputIndex, satoshis
54
54
  |---|---|
55
55
  | `PushTx` | nChain WP1605 OP_PUSH_TX — `authenticator()`, `valueCovenant()`, `hashOutputs()`, `extractHashOutputs()`, `grind()` |
56
56
  | `PELS` | Perpetually Enforcing Locking Scripts — `perpetualCovenant(fee)` |
57
- | `Token` | Stateful ownership token (NFT) — `ownershipToken(fee, ownerHash)` |
57
+ | `Token` | Stateful ownership token (NFT) — `ownershipToken(fee, owner[, auth])`, `ownershipTokenMulti(owner[, auth])`, `ownerId(key)`, `unlockTransfer(...)`, `unlockTransferMulti(...)` |
58
+ | `Authorizers` | Pluggable token ownership — `singleKey()`, `multisig(m, n)`, `predicate({...})` |
58
59
  | `Locks` | Hash-lock, P2PKH, CLTV time-lock, m-of-n multisig, HTLC |
59
60
  | `CovenantHelpers` | Consensus-flag `verify()` harness, raw BIP-143 preimage, signing, fund/spend scaffolding |
60
61
 
@@ -729,11 +730,30 @@ const pels = SC.perpetualCovenant(500) // fee in satoshis deducted each hop
729
730
 
730
731
  ### Ownership Tokens (NFT) — v4.2.0 API
731
732
  ```javascript
732
- // Stateful ownership token. Owner is carried as on-chain state; transfer
733
- // requires the current owner's secret and rewrites state, perpetuating
734
- // the token code across the chain of spends.
735
- const ownerHash = bsv.crypto.Hash.sha256ripemd160(currentOwnerPubKey.toBuffer())
733
+ // Stateful ownership token. Owner is carried as on-chain state (HASH160 of the
734
+ // owner's public key); transfer requires the current owner's ECDSA SIGNATURE over
735
+ // the spend (OP_CHECKSIG) and rewrites state, perpetuating the token code across
736
+ // the chain of spends. The signature commits to the chosen next owner, so a
737
+ // mempool watcher cannot redirect a pending transfer (no hash-lock front-running).
738
+ const ownerHash = SC.Token.ownerId(currentOwnerKey) // = HASH160(pubkey)
736
739
  const token = SC.ownershipToken(500, ownerHash)
740
+ // To spend it forward to `nextOwnerHash`, the current owner signs:
741
+ // tokenInput.setScript(SC.Token.unlockTransfer(currentOwnerKey, nextOwnerHash, spendTx, sats, token))
742
+
743
+ // Pluggable ownership — the SAME covenant, owned by an m-of-n group. Ownership is
744
+ // committed as a 20-byte hash either way, so the transfer plumbing is identical.
745
+ const ms = SC.Authorizers.multisig(2, 3)
746
+ const groupToken = SC.Token.ownershipToken(500, ms.commit([pkA, pkB, pkC]), ms)
747
+ // transfer requires any 2 of the 3 keys:
748
+ // SC.Token.unlockTransfer({ keys: [pkA, pkB, pkC], signWith: [skA, skC] },
749
+ // nextOwnerHash, spendTx, sats, groupToken, { auth: ms })
750
+ // Custom schemes: SC.Authorizers.predicate({ commit, emit, unlockArgs }).
751
+
752
+ // N-output: recreate the token alongside other outputs (payments, change, data).
753
+ // The spender reveals the surrounding output bytes; the covenant binds them all.
754
+ const multi = SC.Token.ownershipTokenMulti(ownerHash)
755
+ // SC.Token.unlockTransferMulti(currentOwnerKey, nextOwnerHash, spendTx, sats, multi,
756
+ // { before: serializedOutputsBeforeToken, after: serializedOutputsAfterToken, tokenValue: le8(value) })
737
757
  ```
738
758
 
739
759
  ### End-to-end verification