sl-bsv
v3.0.0
Published
Bitcoin SV library with a consensus-faithful Script interpreter. Fork of bsv 1.5.6 brought up to Genesis and Chronicle rules, verified against the SV Node vectors.
Maintainers
Readme
sl-bsv
A Bitcoin SV library whose Script interpreter is held to the reference node's behaviour, vector by vector.
npm install sl-bsvThis is a Bitcoin SV library. It tracks BSV consensus as the network runs it — not Bitcoin Core's rules, and not BCH's. What each upgrade changed, and where each rule lives in this codebase, is written up in docs/genesis.md and docs/chronicle.md.
It is a fork of moneybutton/bsv 1.5.6, which was written before Genesis and had drifted two protocol upgrades behind the network. This library brings it forward: Genesis (2020) and Chronicle (activated 7 April 2026 at block 943,816) are both implemented, and the interpreter is measured against the SV Node corpus on every test run.
Status
| | | | --- | --- | | SV Node script vectors (v1.2.2) | 1483 / 1483 | | SV Node transaction vectors | 161 / 161 | | SV Node base58 and key vectors | 112 / 112 | | False accepts (we accept, the node rejects) | 0 | | Transaction digest vectors, both algorithms | 1000 / 1000 | | Result codes matching the node | 600 / 600 rejecting vectors | | Tests | 3959 | | Runtime dependency advisories | 0 | | Coverage | 98% lines, 95% branches | | secp256k1 cross-checked against | OpenSSL | | Opcode table cross-checked against | the BSV wiki |
The corpus is copied verbatim from the node's src/test/data and lives under
test/data/bitcoin-sv, with provenance
recorded. Every file in it is exercised, on every npm test.
Usage
Keys and addresses
const bsv = require('sl-bsv')
const privateKey = bsv.PrivateKey.fromRandom()
const address = privateKey.toAddress().toString()
const wif = privateKey.toWIF()Building and signing a transaction
const tx = new bsv.Transaction()
.from({
txId: '...',
outputIndex: 0,
script: bsv.Script.buildPublicKeyHashOut(publicKey).toHex(),
satoshis: 100000
})
.to(address, 90000)
.change(address)
.sign(privateKey)
tx.verify() // trueRunning the interpreter directly
The interpreter needs to be told which rules apply, because they depend on when the output being spent was created — not on when you are running the code.
const { Interpreter } = bsv.Script
const flags =
Interpreter.SCRIPT_UTXO_AFTER_GENESIS | // the utxo was created post-Genesis
Interpreter.SCRIPT_UTXO_AFTER_CHRONICLE | // ...and post-Chronicle
Interpreter.SCRIPT_CHRONICLE | // the spending txn is in a Chronicle block
Interpreter.SCRIPT_ENABLE_SIGHASH_FORKID
const interpreter = new Interpreter()
const ok = interpreter.verify(scriptSig, scriptPubKey, tx, 0, flags, satoshisBN)
if (!ok) console.error(interpreter.errstr) // e.g. 'SCRIPT_ERR_SCRIPTNUM_OVERFLOW'The UTXO_AFTER_* flags follow the output; SCRIPT_GENESIS and
SCRIPT_CHRONICLE follow the spending transaction. The node draws the same
distinction, and mixing them up silently changes which rules you are enforcing.
Consensus notes
The full account of each upgrade is in docs/genesis.md and docs/chronicle.md, including where every rule is implemented and what proves it. In brief:
Script numbers are arbitrary precision. Genesis raised the four-byte limit
to 750KB and Chronicle to 32MB, so arithmetic on satoshi amounts and on
OP_PUSH_TX preimage fields works. They are backed by native BigInt.
// 8-byte satoshi arithmetic, impossible before Genesis
new bsv.Script()
.add(new bsv.crypto.BN('2100000000000000').toScriptNumBuffer())
.add(new bsv.crypto.BN(2).toScriptNumBuffer())
.add(bsv.Opcode.OP_DIV)Chronicle gates its relaxations on transaction version. A version 1 transaction keeps every malleability rule — MINIMALIF, LOW_S, NULLFAIL, NULLDUMMY, CLEANSTACK, push-only unlocking scripts. They lift only at version 2 and above, which is what makes the upgrade backward compatible.
Genesis stopped P2SH outputs being created, and raised the transaction size
limit from 1MB to 1GB. Both are era-dependent, and Transaction#verify takes
the era because the object cannot know it:
tx.verify()
// 'transaction txout 0 is pay-to-script-hash, which Genesis disallows'
tx.verify({ afterGenesis: false }) // the pre-2020 rules, for historical txsThe default is the post-Genesis reading, because that is the network a transaction being built now belongs to. A default describing rules the network stopped applying in 2020 would report transactions as valid that no node accepts.
Genesis reverted the locktime opcodes. OP_CHECKLOCKTIMEVERIFY and
OP_CHECKSEQUENCEVERIFY are upgradable NOPs for outputs created after Genesis,
so a post-Genesis script containing one does not enforce a locktime.
Chronicle restored five opcodes in the old OP_NOP4..OP_NOP8 slots:
OP_SUBSTR, OP_LEFT, OP_RIGHT, OP_LSHIFTNUM, OP_RSHIFTNUM. Before
Chronicle they still behave as the upgradable NOPs they were. OP_2MUL and
OP_2DIV are re-enabled, and OP_VER / OP_VERIF / OP_VERNOTIF became
transaction-version operations.
SIGHASH_CHRONICLE (0x20) selects the original digest algorithm instead of
BIP143, and both may appear within one transaction.
The opcodes BSV restored in 2018 — OP_MUL, OP_DIV, OP_MOD,
OP_INVERT, OP_LSHIFT, OP_RSHIFT, OP_CAT, OP_SPLIT, OP_AND, OP_OR,
OP_XOR, OP_NUM2BIN, OP_BIN2NUM — are enabled unconditionally, with no
flag to set. OP_2MUL and OP_2DIV are the only opcodes the node disables at
all, and only before Chronicle.
Security
No runtime dependency has a known advisory. Two changes in 2.0.0 are the reason:
elliptic is gone. It carried a critical advisory — private key extraction
in ECDSA when signing a malformed input — among others. secp256k1 now runs on
@noble/curves.
Base58 decoding validates its own characters. base-x 3 looked characters
up in a 256-entry table and treated a miss as a sentinel; any character above
U+00FF indexed past the end, returned undefined rather than the sentinel, and
so skipped the invalid-character branch. A Unicode homograph in an address was
caught only by the Base58Check checksum. Base58.decode now rejects anything
outside the Bitcoin alphabet before decoding, regardless of what the
implementation underneath does.
Reporting a vulnerability: open an issue at github.com/codenlighten/sl-bsv/issues for anything already public, and for anything not yet public please make initial contact privately.
Modules
The library is CommonJS. Both forms work:
const bsv = require('sl-bsv')
import bsv, { PrivateKey, Script } from 'sl-bsv'The ESM entry point re-exports the same module instance rather than a second
build of it. That matters more than it sounds: this library makes 99
instanceof checks, and a dual CommonJS/ESM package would give an importing
caller its own copy of every class, so every one of those would silently return
false across the two. test/esm-exports.js asserts that identity holds and
that the two entry points export the same names.
Note that lib/ is internal. It is reachable, but only with an explicit file
extension — require('sl-bsv/lib/crypto/hash.js'), not .../hash.
TypeScript
Type definitions ship with the package and are declared in types, so nothing
extra is needed. test/types/consumer.ts compiles the README's examples under
strict on every CI run — the definitions this library inherited had gone five
major versions stale and described a module name the package no longer used, so
they are now held to something.
Browser
Prebuilt bundles are published in the package root: bsv.min.js, and
bsv-ecies.min.js, bsv-message.min.js, bsv-mnemonic.min.js, each of which
expects bsv.min.js to be loaded first.
Script numbers require BigInt, so the floor is Chrome 67+, Firefox 68+,
Safari 14+, Edge 79+. There is no build that supports IE.
Development
npm test # lint, unit tests, and the consensus ratchet
npm run test:types # typecheck a consumer against bsv.d.ts
npm run coverage # V8 coverage, with a floor
npm run vectors:gap # how far the interpreter is from the node
npm run build && npm run smoke # build the bundles and exercise themAll of these run in CI. The suite and the ratchet run on Node 20, 22 and 24;
the rest run once, alongside a check that the committed bundles match a fresh
build, an audit of the runtime dependencies, an install of the packed tarball
into an empty project, and a mutation gate that introduces real defects into
lib/ and fails if the suite stays green.
npm test does not cover what webpack emits, so npm run smoke loads each
bundle in a bare browser-shaped context and puts it through the operations it
exists for. That check exists because a bundle once broke while every test
stayed green.
The ratchet
test/consensus/script-vectors.js runs the SV Node corpus on every test run
and compares against test/consensus/known-failures.json. That list may only
shrink: an unlisted failure is a regression, a listed vector that now passes
means the list is stale, and both fail the build. False accepts are held at
zero outright rather than through the list, since that is the direction that
can cost money.
It also compares the node's result code, exactly — matching the outcome is not the same as failing for the right reason, and the gap between the two had been hiding real bugs.
interpreter.errstr carries the node's code. Where this library can be more
specific than the node — an evaluation ending false has four distinguishable
causes, all of which the node calls EVAL_FALSE — the narrower reason is on
interpreter.errdetail:
interpreter.errstr // 'SCRIPT_ERR_EVAL_FALSE'
interpreter.errdetail // 'SCRIPT_ERR_EVAL_FALSE_IN_STACK'Changelog
2.0.0
Breaking. Read the migration notes below before upgrading.
- Genesis and Chronicle consensus rules implemented; 1483/1483 SV Node script vectors, 0 false accepts.
- Script numbers are arbitrary precision, backed by native
BigInt(750KB post-Genesis, 32MB post-Chronicle). - secp256k1 moved from
ellipticto@noble/curves. Point addition is ~10x faster and public key derivation ~2x; signature verification is ~17% slower, which is the backend's own cost. Base58.decoderejects characters outside the Bitcoin alphabet.bs584 → 5 andbn.js4.11.9 → 4.12.5, clearing the last advisories.- New result codes:
SCRIPTNUM_OVERFLOW,SCRIPTNUM_MINENCODE,MOD_BY_ZERO,ILLEGAL_CHRONICLE. Interpreter#errstrnow always carries the node's result code; the narrower names this library used to report there moved toInterpreter#errdetail.Transaction#verifyapplies the post-Genesis rules by default: a pay-to-script-hash output is rejected, and the size limit is 1GB rather than 1MB. Pass{ afterGenesis: false }for the earlier rules.SCRIPT_ENABLE_MONOLITH_OPCODESandSCRIPT_ENABLE_MAGNETIC_OPCODESare deprecated no-ops. The opcodes they gated are enabled unconditionally, as they are on the network; setting the flags is harmless and selects nothing.- Browser bundles built with webpack 5 and emitted as ES6.
Migrating from 1.x
bsv.deps.ellipticis removed. Usebsv.crypto.Point, which keeps the same surface —mul,add,mulAdd,getX,getY— and still returnsBNcoordinates.Base58.decodenow throwsNon-base58 characteron input it previously accepted and decoded to something arbitrary. Anything relying on the lenient behaviour needs to handle the error.Point.prototype.toBufferthrows for the point at infinity rather than returning a compressed key of02and 32 zero bytes.Point.prototype.toStringreturns<EC Point x: … y: …>where it previously returned[object Object].- The interpreter's internal script numbers are
BigInt. This is visible if you callInterpreter.prototype.scriptNum,checkLockTimeorcheckSequencedirectly; they take and returnBigInt, notBN. The publicverify/evaluateAPI is unchanged and the stack is stillBuffers. Script#add(number)rejects a number that cannot be an opcode. It takes an opcode, not a value to push — anything outside 0–255 previously built a script that could not be serialised. To push a number, use aBuffer, orBN#toScriptNumBuffer()for a script number.- A truncated push is rejected consistently. Previously a script built with one evaluated as valid while the identical bytes parsed from the wire were rejected.
Transaction#verifynow rejects P2SH outputs by default, because Genesis did. Anything building or checking such an output must either stop or pass{ afterGenesis: false }deliberately.- Deep imports into
lib/now need the file extension.require('sl-bsv/lib/crypto/hash')becomesrequire('sl-bsv/lib/crypto/hash.js'), and a directory such assl-bsv/lib/cryptono longer resolves to itsindex.js. This is the cost of declaring anexportsmap, which is what makesimport { PrivateKey } from 'sl-bsv'possible at all. The documented entry points — the package root andsl-bsv/ecies,/message,/mnemonic— are unaffected. - Node 20.19+ and a
BigInt-capable browser are required. The floor comes from @noble/curves 2.x, which this library's EC math rests on. - Scripts spending post-Genesis or post-Chronicle outputs need the
corresponding
SCRIPT_UTXO_AFTER_*flags to be validated correctly. Omitting them means the pre-Genesis rules are enforced.
1.5.0
- Add build files into repo.
1.4.0
- Change default fee to 0.5 sat/byte
1.3.0
- Remove limit on OP_RETURN size
1.1.0
- Refactor code related to buffers and get rid of bufferUtil
- Deprecate p2sh
- Add .Mnemonic to bsv object
1.0.0
- Bump to 1.0 release as per the suggestion of @mathiasrw
0.30.2
- Added addSafeData to Transaction.
0.30.1
- Enforce buffer check for Electrum ECIES decryption.
- Clean up script folder (no API breaking changes).
- Documentation improvements.
0.30.0
- Fix transaction size calculation.
0.29.2
- Throw error on invalid hex strings in script
0.29.1
- Add support for new OP_RETURN style: buildSafeDataOut and isSafeDataOut (and getData)
0.27.2
- Add support for Stress Test Network (STN).
v0.27.1
- Replace lodash methods with inline pure javascript methods.
v0.27.0
- Remove version guard. This should fix the "two versions of bsv" error that people often get. Note that it is poor practice to use incompatible versions of bsv. To send objects from one version of the library to another, always serialize to a string or buffer first. Do not send objects from one version to another. This due to frequent use of "instanceof" inside the library.
v0.26.5
- lodash optimization and overall size optimization of bsv.min.js
- fix isFinal
- fix non-dust amount example
- minor ECIES API issue
v0.26.4
- Use ECDSA.signWithCalcI(...) convenience method inside Message.
v0.26.3
- Add ECDSA.signWithCalcI(...) convenience method.
v0.26.2
- Add Mnemonic.fromString(string).
- Add convenience method for ECDSA.signRandomK (mostly for demo purposes).
- Add convenience methods Message.sign and message.verify.
- Move large portions of the documentation to docs.moneybutton.com.
v0.26.1
- Add .fromRandom() method for Mnemonic.
v0.26.0
- Remove the (already deprecated) .derive() method from HDPrivateKey and HDPublicKey. If you rely on this, please switch to .deriveNonCompliantChild(). If you do not already rely on this, you should use .deriveChild() instead.
- HDPrivateKey / HDPublicKey toHex() and fromHex()
- HDPrivateKey.fromRandom()
- Remove Base32 (this was only used for cashaddr and is now obsolete).
v0.25.0
- Remove support for cashaddr completely. This saves size in the bundle.
- Private key .toString() method now returns WIF, which makes it compatible with the corresponding .fromString(wif) method.
- Private key and public key classes now have toHex() and fromHex(hex) methods.
Credits and licence
MIT. This is a fork of bsv by Ryan X. Charles and Money Button, which in turn descends from bitcore-lib by BitPay and from Bitcoin Core. The copyright notices in LICENSE are unchanged.
Consensus behaviour is measured against
bitcoin-sv, whose test vectors are
redistributed here under the same MIT licence. The corpus is byte-identical
between v1.2.0 and v1.2.2, the current release; v1.2.2 changed the
interpreter only cosmetically.
