@smartledger/bsv 5.5.3 → 6.0.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.
Files changed (60) hide show
  1. package/CHANGELOG.md +115 -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 +283 -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 +183 -0
  30. package/lib/ordinals/index.js +49 -0
  31. package/lib/ordinals/inscription.js +159 -0
  32. package/lib/ordinals/ordlock.js +535 -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 +116 -0
  52. package/test/ordinals/inscription.js +71 -0
  53. package/test/ordinals/ordlock.js +482 -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
@@ -0,0 +1,134 @@
1
+ 'use strict'
2
+
3
+ /* global describe, it */
4
+
5
+ // Regression guard for the C1–C4 fail-open verification bugs. Before the fix,
6
+ // LTP signature verification returned the ECDSA instance (always truthy) or
7
+ // skipped the JWS check entirely (a `// TODO`), so a forged token verified as
8
+ // valid. These tests feed known-bad signatures/issuers and assert valid===false.
9
+
10
+ var expect = require('chai').expect
11
+ var bsv = require('../../')
12
+
13
+ var LTP = bsv.LTP
14
+ var RightToken = LTP.Right
15
+ var Obligation = LTP.Obligation
16
+
17
+ function newDid () {
18
+ var key = bsv.PrivateKey.fromRandom()
19
+ return { key: key, did: 'did:key:' + key.toPublicKey().toString() }
20
+ }
21
+
22
+ describe('LTP verification fails closed (C1–C4 regression)', function () {
23
+ it('a genuinely signed right token still verifies as valid', function () {
24
+ var issuer = newDid()
25
+ var result = RightToken.prepareRightToken(
26
+ 'EquityShare', issuer.did, issuer.did,
27
+ { spv: 'spv-001', units: 1000 }, issuer.key, {}
28
+ )
29
+ expect(result.success).to.equal(true)
30
+ var v = RightToken.prepareRightTokenVerification(result.rightToken, {})
31
+ expect(v.verification.valid, JSON.stringify(v.verification.errors)).to.equal(true)
32
+ })
33
+
34
+ it('a tampered JWS signature is rejected (C4)', function () {
35
+ var issuer = newDid()
36
+ var result = RightToken.prepareRightToken(
37
+ 'EquityShare', issuer.did, issuer.did, { spv: 'x' }, issuer.key, {}
38
+ )
39
+ var token = result.rightToken
40
+ // Flip the signature: replace it with a signature over different data by a
41
+ // different key, re-encoded as a valid-looking JWS.
42
+ var attacker = newDid()
43
+ var parts = token.proof.jws.split('.')
44
+ var bogusSig = bsv.crypto.ECDSA.sign(
45
+ bsv.crypto.Hash.sha256(Buffer.from('not the token')), attacker.key
46
+ )
47
+ token.proof.jws = parts[0] + '.' + parts[1] + '.' + bogusSig.toDER().toString('base64url')
48
+
49
+ var v = RightToken.prepareRightTokenVerification(token, {})
50
+ expect(v.verification.valid).to.equal(false)
51
+ })
52
+
53
+ it('a token re-signed by an attacker but attributed to the victim issuer is rejected (C4 binding)', function () {
54
+ var victim = newDid()
55
+ var attacker = newDid()
56
+ var result = RightToken.prepareRightToken(
57
+ 'PropertyTitle', victim.did, victim.did, { parcel: '1 Main' }, victim.key, {}
58
+ )
59
+ var token = result.rightToken
60
+ // Attacker re-signs the exact token hash with THEIR key, but leaves issuer =
61
+ // victim. Verification must bind to the victim issuer key and reject.
62
+ var sig = bsv.crypto.ECDSA.sign(
63
+ bsv.crypto.Hash.sha256(Buffer.from(RightToken._canonicalizeToken(token))), attacker.key
64
+ )
65
+ var parts = token.proof.jws.split('.')
66
+ token.proof.jws = parts[0] + '.' + parts[1] + '.' + sig.toDER().toString('base64url')
67
+
68
+ var v = RightToken.prepareRightTokenVerification(token, {})
69
+ expect(v.verification.valid).to.equal(false)
70
+ })
71
+
72
+ it('prepareRightTokenTransfer rejects a transfer signed by a non-owner (ownership binding)', function () {
73
+ var owner = newDid()
74
+ var attacker = newDid()
75
+ var res = RightToken.prepareRightToken(
76
+ 'EquityShare', owner.did, owner.did, { spv: 't-1' }, owner.key, {}
77
+ )
78
+ var token = res.rightToken
79
+ // Attacker tries to transfer a token they do not own → must be refused.
80
+ var bad = RightToken.prepareRightTokenTransfer(token, attacker.did, attacker.key, {})
81
+ expect(bad.success).to.equal(false)
82
+ // The genuine owner can transfer.
83
+ var good = RightToken.prepareRightTokenTransfer(token, attacker.did, owner.key, {})
84
+ expect(good.success, JSON.stringify(good.error)).to.equal(true)
85
+ })
86
+
87
+ it('verifyOwnership rejects a signature by the wrong key (C2)', function () {
88
+ var owner = newDid()
89
+ var attacker = newDid()
90
+ var Resolver = require('../../lib/gdaf/did-resolver')
91
+ var did = 'did:smartledger:' + owner.key.toPublicKey().toString()
92
+ var msg = 'transfer authorization'
93
+ // Signature by attacker, checked against owner's DID → must be false.
94
+ var badSig = bsv.crypto.ECDSA.sign(bsv.crypto.Hash.sha256(Buffer.from(msg)), attacker.key)
95
+ expect(Resolver.verifyOwnership(did, badSig.toString(), msg)).to.equal(false)
96
+ })
97
+
98
+ it('verifyAnchor refuses to fabricate success without a chain provider (C3)', async function () {
99
+ var Anchor = require('../../lib/gdaf/smartledger-anchor')
100
+ var threw = false
101
+ try {
102
+ await Anchor.verifyAnchor('deadbeef', 'cafe')
103
+ } catch (e) {
104
+ threw = true
105
+ }
106
+ expect(threw).to.equal(true)
107
+ })
108
+
109
+ it('a genuinely signed obligation token still verifies as valid', function () {
110
+ var issuer = newDid()
111
+ var ob = Obligation.prepareObligationToken(
112
+ 'PaymentObligation', issuer.did, issuer.did, { amount: 1 }, issuer.key, {}
113
+ )
114
+ expect(ob.success).to.equal(true)
115
+ var v = Obligation.prepareObligationVerification(ob.obligationToken, {})
116
+ expect(v.verification.valid, JSON.stringify(v.verification && v.verification.errors)).to.equal(true)
117
+ })
118
+
119
+ it('a tampered obligation signature is rejected (C4)', function () {
120
+ var issuer = newDid()
121
+ var attacker = newDid()
122
+ var ob = Obligation.prepareObligationToken(
123
+ 'PaymentObligation', issuer.did, issuer.did, { amount: 1 }, issuer.key, {}
124
+ )
125
+ var tok = ob.obligationToken
126
+ var sig = bsv.crypto.ECDSA.sign(
127
+ bsv.crypto.Hash.sha256(Buffer.from('forged')), attacker.key
128
+ )
129
+ var parts = tok.proof.jws.split('.')
130
+ tok.proof.jws = parts[0] + '.' + parts[1] + '.' + sig.toDER().toString('base64url')
131
+ var v = Obligation.prepareObligationVerification(tok, {})
132
+ expect(v.verification.valid).to.equal(false)
133
+ })
134
+ })
@@ -0,0 +1,116 @@
1
+ 'use strict'
2
+
3
+ /* global describe, it */
4
+
5
+ // BSV-20 / BSV-21 fungible-token inscriptions: build/parse round-trips + validation.
6
+
7
+ require('chai').should()
8
+ var bsv = require('../..')
9
+ var B = bsv.Ordinals.BSV20
10
+ var Ord = bsv.Ordinals
11
+
12
+ describe('Ordinals BSV-20 fungible tokens', function () {
13
+ var key = bsv.PrivateKey.fromRandom()
14
+ var address = key.toAddress()
15
+
16
+ it('deploys a v1 ticker token and round-trips through parse', function () {
17
+ var s = B.buildDeploy({ address: address, tick: 'ORDI', max: '21000000', lim: '1000', dec: 18 })
18
+ var p = B.parseBsv20(s)
19
+ p.p.should.equal('bsv-20')
20
+ p.op.should.equal('deploy')
21
+ p.tick.should.equal('ORDI')
22
+ p.max.should.equal('21000000')
23
+ p.lim.should.equal('1000')
24
+ p.dec.should.equal('18')
25
+ // The payload rides in an inscription with the bsv-20 content-type.
26
+ Ord.parseInscription(s).contentType.should.equal('application/bsv-20')
27
+ // The base lock is the owner's P2PKH.
28
+ Ord.parseInscription(s).lock.toHex().should.equal(bsv.Script.buildPublicKeyHashOut(address).toHex())
29
+ })
30
+
31
+ it('mints and transfers a v1 ticker token', function () {
32
+ var mint = B.parseBsv20(B.buildMint({ address: address, tick: 'ORDI', amt: '1000' }))
33
+ mint.op.should.equal('mint')
34
+ mint.tick.should.equal('ORDI')
35
+ mint.amt.should.equal('1000')
36
+
37
+ var xfer = B.parseBsv20(B.buildTransfer({ address: address, tick: 'ORDI', amt: '250' }))
38
+ xfer.op.should.equal('transfer')
39
+ xfer.tick.should.equal('ORDI')
40
+ xfer.amt.should.equal('250')
41
+ })
42
+
43
+ it('deploy+mints a BSV-21 (id-based) token and transfers by id', function () {
44
+ var dm = B.parseBsv20(B.buildDeployMint({ address: address, amt: '1000000', dec: 8, sym: 'XYZ' }))
45
+ dm.op.should.equal('deploy+mint')
46
+ dm.amt.should.equal('1000000')
47
+ dm.dec.should.equal('8')
48
+ dm.sym.should.equal('XYZ')
49
+
50
+ var id = 'a'.repeat(64) + '_0'
51
+ var xfer = B.parseBsv20(B.buildTransfer({ address: address, id: id, amt: '5' }))
52
+ xfer.op.should.equal('transfer')
53
+ xfer.id.should.equal(id)
54
+ xfer.amt.should.equal('5')
55
+ ;(xfer.tick === undefined).should.equal(true)
56
+ })
57
+
58
+ it('preserves integer amounts larger than 2^53 exactly (string, never a JS number)', function () {
59
+ var huge = '99999999999999999999999999'
60
+ var p = B.parseBsv20(B.buildMint({ address: address, tick: 'BIG', amt: huge }))
61
+ p.amt.should.equal(huge)
62
+ })
63
+
64
+ it('accepts numeric amounts and normalizes them to strings', function () {
65
+ var p = B.parseBsv20(B.buildMint({ address: address, tick: 'NUM', amt: 42 }))
66
+ p.amt.should.equal('42')
67
+ })
68
+
69
+ it('builds a 1-sat token output', function () {
70
+ var out = B.createMintOutput({ address: address, tick: 'ORDI', amt: '1' })
71
+ out.satoshis.should.equal(1)
72
+ B.isBsv20(out.script).should.equal(true)
73
+ })
74
+
75
+ it('round-trips through hex serialization', function () {
76
+ var s = B.buildDeploy({ address: address, tick: 'HEX', max: '1000' })
77
+ var p = B.parseBsv20(s.toHex())
78
+ p.tick.should.equal('HEX')
79
+ p.max.should.equal('1000')
80
+ })
81
+
82
+ it('parses a raw JSON string or an already-parsed object', function () {
83
+ var json = '{"p":"bsv-20","op":"mint","tick":"ORDI","amt":"1"}'
84
+ B.parseBsv20(json).amt.should.equal('1')
85
+ B.parseBsv20({ p: 'bsv-20', op: 'mint', tick: 'ORDI', amt: '1' }).op.should.equal('mint')
86
+ })
87
+
88
+ it('isBsv20 is false for a plain inscription and a P2PKH script', function () {
89
+ var plain = Ord.buildInscription({ address: address, contentType: 'text/plain', content: 'hi' })
90
+ B.isBsv20(plain).should.equal(false)
91
+ B.isBsv20(bsv.Script.buildPublicKeyHashOut(address)).should.equal(false)
92
+ ;(B.parseBsv20(plain) === null).should.equal(true)
93
+ })
94
+
95
+ describe('validation', function () {
96
+ it('rejects a tick longer than 4 UTF-8 bytes', function () {
97
+ ;(function () { B.buildDeploy({ address: address, tick: 'TOOLONG', max: '1' }) }).should.throw(/1–4 UTF-8 bytes/)
98
+ })
99
+ it('rejects a non-integer / negative amount', function () {
100
+ ;(function () { B.buildMint({ address: address, tick: 'ORDI', amt: '1.5' }) }).should.throw(/non-negative integer/)
101
+ ;(function () { B.buildMint({ address: address, tick: 'ORDI', amt: -5 }) }).should.throw(/non-negative integer/)
102
+ })
103
+ it('rejects a zero amount for mint/transfer', function () {
104
+ ;(function () { B.buildMint({ address: address, tick: 'ORDI', amt: '0' }) }).should.throw(/greater than zero/)
105
+ })
106
+ it('rejects dec out of the 0…18 range', function () {
107
+ ;(function () { B.buildDeploy({ address: address, tick: 'ORDI', max: '1', dec: 19 }) }).should.throw(/dec must be/)
108
+ })
109
+ it('rejects a malformed BSV-21 id', function () {
110
+ ;(function () { B.buildTransfer({ address: address, id: 'notanid', amt: '1' }) }).should.throw(/txid.*vout|<txid>/)
111
+ })
112
+ it('requires an owner (address or lock)', function () {
113
+ ;(function () { B.buildMint({ tick: 'ORDI', amt: '1' }) }).should.throw(/address or a lock/)
114
+ })
115
+ })
116
+ })
@@ -0,0 +1,71 @@
1
+ 'use strict'
2
+
3
+ /* global describe, it */
4
+
5
+ // 1Sat Ordinals inscription build/parse round-trip + output construction.
6
+
7
+ require('chai').should()
8
+ var bsv = require('../..')
9
+ var Ord = bsv.Ordinals
10
+
11
+ describe('Ordinals inscriptions', function () {
12
+ var key = bsv.PrivateKey.fromRandom()
13
+ var address = key.toAddress()
14
+
15
+ it('builds a P2PKH inscription and round-trips through parse', function () {
16
+ var s = Ord.buildInscription({
17
+ address: address, contentType: 'text/plain', content: 'hello ordinals'
18
+ })
19
+ var parsed = Ord.parseInscription(s)
20
+ parsed.should.be.an('object')
21
+ parsed.contentType.should.equal('text/plain')
22
+ parsed.contentText.should.equal('hello ordinals')
23
+ // The base lock is exactly the P2PKH for the address.
24
+ parsed.lock.toHex().should.equal(bsv.Script.buildPublicKeyHashOut(address).toHex())
25
+ })
26
+
27
+ it('preserves binary content and content-type exactly', function () {
28
+ var content = bsv.crypto.Random.getRandomBuffer(64)
29
+ var s = Ord.buildInscription({ address: address, contentType: 'image/png', content: content })
30
+ var parsed = Ord.parseInscription(s)
31
+ parsed.contentType.should.equal('image/png')
32
+ parsed.content.equals(content).should.equal(true)
33
+ })
34
+
35
+ it('round-trips through hex serialization', function () {
36
+ var s = Ord.buildInscription({ address: address, contentType: 'application/json', content: '{"a":1}' })
37
+ var parsed = Ord.parseInscription(s.toHex())
38
+ parsed.contentText.should.equal('{"a":1}')
39
+ parsed.contentType.should.equal('application/json')
40
+ })
41
+
42
+ it('accepts a custom base lock script', function () {
43
+ var lock = bsv.Script.buildPublicKeyHashOut(address)
44
+ var s = Ord.buildInscription({ lock: lock, contentType: 'text/plain', content: 'x' })
45
+ Ord.isInscription(s).should.equal(true)
46
+ Ord.parseInscription(s).lock.toHex().should.equal(lock.toHex())
47
+ })
48
+
49
+ it('isInscription is false for a plain P2PKH script', function () {
50
+ var p2pkh = bsv.Script.buildPublicKeyHashOut(address)
51
+ Ord.isInscription(p2pkh).should.equal(false)
52
+ var isNull = Ord.parseInscription(p2pkh) === null
53
+ isNull.should.equal(true)
54
+ })
55
+
56
+ it('createInscriptionOutput is a 1-sat output carrying the inscription', function () {
57
+ var out = Ord.createInscriptionOutput({ address: address, contentType: 'text/plain', content: 'hi' })
58
+ out.satoshis.should.equal(1)
59
+ Ord.isInscription(out.script).should.equal(true)
60
+ })
61
+
62
+ it('batchInscriptionOutputs builds one output per item', function () {
63
+ var outs = Ord.batchInscriptionOutputs([
64
+ { address: address, contentType: 'text/plain', content: 'a' },
65
+ { address: address, contentType: 'text/plain', content: 'b' }
66
+ ])
67
+ outs.length.should.equal(2)
68
+ Ord.parseInscription(outs[0].script).contentText.should.equal('a')
69
+ Ord.parseInscription(outs[1].script).contentText.should.equal('b')
70
+ })
71
+ })