@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.
- package/CHANGELOG.md +115 -0
- package/README.md +38 -38
- package/bin/cli.js +35 -5
- package/bsv-covenant.min.js +6 -6
- package/bsv-gdaf.min.js +2 -2
- package/bsv-ltp.min.js +2 -2
- package/bsv-shamir.min.js +1 -1
- package/bsv-smartcontract.min.js +6 -6
- package/bsv-statuslist.min.js +5 -5
- package/bsv-vcjwt.min.js +1 -1
- package/bsv.bundle.js +2 -2
- package/bsv.d.ts +283 -10
- package/bsv.min.js +2 -2
- package/docs/MODULE_REFERENCE_COMPLETE.md +27 -27
- package/docs/advanced/UTXO_MANAGER_GUIDE.md +1 -1
- package/docs/getting-started/INSTALLATION.md +23 -23
- package/docs/getting-started/QUICK_START.md +7 -7
- package/docs/migration/FROM_BSV_1_5_6.md +5 -5
- package/index.js +4 -0
- package/lib/crypto/ecdsa.js +13 -0
- package/lib/crypto/shamir.js +16 -4
- package/lib/gdaf/did-resolver.js +3 -1
- package/lib/gdaf/smartledger-anchor.js +154 -12
- package/lib/ltp/anchor.js +16 -3
- package/lib/ltp/obligation.js +56 -8
- package/lib/ltp/proof.js +4 -2
- package/lib/ltp/right.js +86 -11
- package/lib/ordinals/README.md +170 -0
- package/lib/ordinals/bsv20.js +183 -0
- package/lib/ordinals/index.js +49 -0
- package/lib/ordinals/inscription.js +159 -0
- package/lib/ordinals/ordlock.js +535 -0
- package/lib/smart_contract/builder.js +27 -3
- package/lib/smart_contract/covenant.js +37 -10
- package/lib/smart_contract/covenant_builder.js +7 -5
- package/lib/smart_contract/index.js +28 -15
- package/lib/smart_contract/preimage.js +7 -7
- package/lib/smart_contract/pushtx.js +47 -7
- package/lib/smart_contract/token.js +69 -1
- package/lib/spv/headerchain.js +77 -0
- package/lib/spv/index.js +16 -0
- package/lib/spv/merkleproof.js +108 -0
- package/lib/statuslist/index.js +65 -24
- package/lib/vcjwt/index.js +6 -0
- package/package.json +1 -1
- package/test/credentials-test.js +22 -19
- package/test/crypto/shamir.js +9 -1
- package/test/crypto/shamir_rngcap.js +29 -0
- package/test/gdaf/anchor_spv.js +163 -0
- package/test/ltp/verify_failclosed.js +134 -0
- package/test/ordinals/bsv20.js +116 -0
- package/test/ordinals/inscription.js +71 -0
- package/test/ordinals/ordlock.js +482 -0
- package/test/smart_contract/extract_field.js +61 -0
- package/test/smart_contract/nonenforcing_guard.js +44 -0
- package/test/smart_contract/ordinal_transfer.js +83 -0
- package/test/smart_contract/sighash_marketplace.js +110 -0
- package/test/spv/headerchain.js +86 -0
- package/test/spv/merkleproof.js +133 -0
- 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
|
+
})
|