A node for Bitcoin BLAKE2b testnet4 (txbt4) in plain JavaScript. It starts from an
assumeUTXO snapshot at the fork point and validates every BLAKE2b block forward with the
bitcoin-kernel engine. Node first; the same library is
meant to run in the browser next (OPFS for the snapshot, WebTorrent for transport).
Part of the bitcoin-blake family, next to blaketest (the wallet) and the datstr pool.
-
fetch the snapshot over WebTorrent (magnet with WebRTC and udp trackers, plus an HTTP webseed) and check its sha256. The snapshot is announced on Nostr as a NIP-35 torrent event; see it on ntorrent.
-
verify the snapshot: parse the Core
dumptxoutsetv2 format and recomputehash_serialized_3over all coins, byte for byte asgettxoutsetinfodoes. The result must equal thetxoutset_hashpinned inlib/params.mjs. -
sync: get every post-fork block, validate the header chain (BLAKE2b v2 headers, testnet4 min-difficulty rule, MTP, timewarp), then run the structural and contextual block rules against the UTXO set and apply each block. When a local Knots node is reachable the coin count is cross-checked against its
gettxoutsetinfo.Blocks come from one of two sources (
--source):- http (default, no node needed): a served block file plus index, mirrored into the
data directory with Range requests so a re-run fetches only the tail. Every record is
checked against the index hash and linked to its parent. The newest NIP-333 header
event for the chain (kind 33333,
d=tbtc4b2, from the pinned publisher key, signature verified) is fetched from relays and its 12 headers must agree with the file's tail, so the file cannot quietly serve a different chain. The epoch of headers before the fork comes from a context file the same way. - rpc: the local Knots node, as before.
The block file is kept current by
tools/export-blocks.mjsfrom a node, appending new blocks and unwinding reorgs. Any host that serves the two files with Range requests and CORS will do (a datstr gateway's--filesdirectory does); pass it as--blocks-urlorBLAKETESTNODE_BLOCKS_URL. - http (default, no node needed): a served block file plus index, mirrored into the
data directory with Range requests so a re-run fetches only the tail. Every record is
checked against the index hash and linked to its parent. The newest NIP-333 header
event for the chain (kind 33333,
-
run: the long-running node. It loads its newest checkpoint (a snapshot it wrote itself in the same dumptxoutset format,
hash_serialized_3in the manifest) or the fork snapshot through the packed index, replays its per-block delta log on top, follows the block file every 30 s and on every NIP-333 event, applies new blocks with undo records for the last 100 so a reorg is a pop, appends a delta after each block, writes a full checkpoint every 2016 blocks, and serves a status page,/status.json,/block/<h>,/header/<h>,/coin/<txid>:<vout>and a/tipWebSocket stream on--api(3337). The snapshots it writes are exact: a checkpoint at 151,078 has thehash_serialized_3the Knots node reports at that height, androundtriprewrites the fork snapshot from the packed set byte for byte, same sha256. The daemon holds about 450 MB.
node --max-old-space-size=8192 bin/blaketestnode.mjs run --api 3337 --blocks-url <url> # the daemon (pm2 example in ops/)
node --max-old-space-size=8192 bin/blaketestnode.mjs bench # fetch, verify, sync: no node needed
node bin/blaketestnode.mjs verify --data ./data # snapshot only
node bin/blaketestnode.mjs sync --source rpc # blocks from the local node
node bin/blaketestnode.mjs sync --no-scripts # skip signature checks
node tools/export-blocks.mjs --loop 20 # keep the served block file current (needs a node)
Options: --data <dir> (default ~/.blaketestnode/txbt4), --source http|rpc, --blocks-url <url>,
--webseed <url>, --conf <bitcoin.conf>, --to <height>, --no-scripts; for run also --api <port>,
--poll <seconds>, --checkpoint-every <blocks> (2016). A restart with the index on disk
takes seconds; building the index for a new snapshot takes about 20 s. The engine is loaded from $SCHEMA or
~/bitcoin-desktop/schema; it needs bitcoin-desktop/schema v0.0.27 or later (unified sighash, pay-to-anchor).
browser/index.html is the same node starting in a tab: it fetches the snapshot into the
origin's private file system in parallel ranges (resumable, a journal of finished ranges),
hashes it, then a module worker runs the same parser and index builder over the file
through a sync access handle and keeps the 217 MB index beside it. Nothing leaves the tab
and no server is trusted: the pinned sha256 and hash_serialized_3 decide. Give it a
snapshot URL with ?snapshot= (a plain file with Range and CORS). Measured in Chromium
on this machine: fetch 2 s from a local server, sha256 15 s, parse plus index 37 s.
Storage needed is about 1.1 GB; the page shows the origin's quota first.
With ?blocks= (the served block file, as for the daemon) the tab is a node: the worker
mirrors the block file into OPFS with the same hash and link checks, takes the tip from
the NIP-333 relays and requires it to agree with the file's tail, validates every block
against its own UTXO set with the engine from jsDelivr (signatures included), keeps a
delta log so a reload replays in seconds, then follows the tip every 30 s and on every
NIP-333 event. Measured in Chromium: 797 blocks validated in 48 s, replay 9 s, a new
block applied live. A coin lookup box answers from the tab's own set.
With --mempool-relays wss://a,wss://b the daemon follows kind 23404 events (one transaction
each, chain tag = this chain; a datstr gateway publishes its node's mempool that way) and
validates every one against its own UTXO set before holding it: structure, inputs unspent and
mature, value, scripts with the chain's sighash, a fee floor. What passes is served at
/mempool and /mempool/<txid> and dropped when a block confirms or conflicts with it.
--mempool-publishers pk,pk limits the publishers. node test/mempool-test.mjs runs both ends
through a local relay (it imports the gateway's publisher from $DATSTR_GATEWAY).
lib/template.mjs builds the next block from the node's own state: time after the median time
past, bits from the chain's rules, transactions from the mempool by fee rate within the weight
limit (the RDTS 800,000 while it is active), the coinbase as datstr SPEC 6.1 has it (the split or
the pay scripts, the witness commitment, the datstr commitment output last), the merkle root and a
v2 header. checkTemplate then runs every kernel rule on it; the only one an unmined block may
fail is proof of work. The daemon serves it at /template?pay=<script hex>; the tab has a "build a
block" button. Proven against Knots: a block built by the daemon for height 151,412 was accepted by
both a 29.4.1 and a 29.4.2rc2 node in getblocktemplate proposal mode; so was one built in a
Chromium tab that had fetched the snapshot (36 s), verified it (23 s) and synced 1,104 blocks with
signatures (22 s) on its own, with no server trusted. node test/template-test.mjs
covers a throwaway chain and checks the datstr gateway's builder makes the identical block from the
same template.
lib/webminer.mjs makes the node a datstr gateway of one: it is its own master (a miner
descriptor paying its script), speaks the coordinator's socket (hello, welcome, split,
assignment, ack), builds each height's block with lib/template.mjs and the coordinator's split,
hashes the 80-byte work an ASIC would (workBytes), and signs the kind 23400 share carrying the
header, the coinbase and the merkle path, so the coordinator rebuilds the commitment from the
block this miner built. lib/open.mjs opens a daemon's state read-only in another process.
node test/webminer-test.mjs runs it against a standalone datstr coordinator on the live chain:
the share for height 151,413 was verified by SPEC 8.1 and credited (#1); a coinbase paying the
wrong script was refused as split. In the tab, the "mine" card does the same with hashing
workers (the datstr miner's core from jsDelivr, WebAssembly): a Chromium tab that had replayed its
own delta log to the tip in 1.5 s connected to a coordinator as its own master, built block
151,413 with the coordinator's split, and had 2,072 shares credited in a minute, none refused,
vardiff raising its difficulty from 0.0001 to 0.0016 on the way. Against the live pool's
coordinator (start difficulty 1,000) the same tab, hashing at about 100 MH/s, found nothing for
minutes: at that rate the 32-bit nonce space is exhausted in 42 seconds and a piece of work at a
real difficulty rarely holds a valid nonce at all. The hashers now roll the second nonce word
(header.nonce2, bytes 36..39 of the work) on every wrap, as a rig rolls its extranonce, and the
share carries it; after that the live coordinator credited the tab's shares (#145,218 onward).
Manners: a share must be at the assignment's target (SPEC 8.1), so a tab given a low one cannot
raise it; it sends at most maxSharesPerSecond (10) and drops the rest, counted on the page,
until the pool's vardiff raises its difficulty. A block is never dropped. node test/manners-test.mjs covers it with a fake socket and clock.
| base | 150,307 000000000017ec2251d81c8d2ca401c713e98e85196c7f660a4088a7ca57b1cc (last block shared with Core) |
| txoutset_hash | 372bfcaeef1e93892acccda9e700b00bf90f45d73746d69959d33620a3df5518 |
| coins | 14,230,182 in 9,356,185 txids, 869,836,053 bytes |
| infohash | 242e9b7dcba15cc0ed8f1bc5f06b68da008f87c0 |
| webseed | optional, --webseed <url> or BLAKETESTNODE_WEBSEED; peers and the NIP-35 event carry the rest |
| step | result |
|---|---|
| torrent fetch | 870 MB in 9.8 s, 85 MiB/s (webseed + one peer) |
| sha256 of file | 1.8 s |
| parse + hash_serialized_3 | 14 s (26 s with the old string map); index build 3 s, reload 50 ms |
| block file, 2.4 MB, 766 blocks | 0.8 s fetch, 1.4 s with hash and link checks |
| NIP-333 tip from relays | tip 151,073 verified, 12 tail hashes agree with the file |
| headers 150,308 to 151,073 | 766 validated, 0 failed, 150 ms |
| blocks, scripts off | 763 blocks, 5,045 txs, 2.5 s (300 blocks/s) |
| blocks, scripts on | 51 s, 0 failures and no skipped rules on engine v0.0.27 (16 script failures on v0.0.25) |
| UTXO count vs node | 14,233,524 both, match (rpc cross-check when a node is reachable) |
Post-fork transactions are signed with SIGHASH_ALL | SIGHASH_UNIFIED (0x21), the
fork's replay protection. Engine v0.0.25 fails 16 blocks on that; from v0.0.26
(bitcoin-desktop/schema#92) every block
validates with scripts on. Signature checks are about 48 of the 51 s, pure-JS secp256k1.
lib/params.mjschain and snapshot parameters, the only file that changes for mainnetlib/varint.mjsCore VARINT, CompactSize, amount and script decompressionlib/snapshot.mjssnapshot parser andhash_serialized_3lib/packed.mjsthe UTXO set: a packed index of the snapshot (16 bytes per coin, file order, binary search on a txid prefix with the full txid checked in the file), a spent bitmap, and a side map of new coins; built in one pass with the hash, read back in 50 mslib/delta.mjsper-block deltas appended after each block and replayed on restartlib/bytes.mjs,lib/sha256.mjsbyte helpers and an incremental SHA-256, so the parser and index run unchanged in Node and in a workerlib/snapshot-write.mjs,lib/filebytes.mjsthe Node-only writer and file accessbrowser/index.html,browser/worker.js,browser/blocks.jsthe page, its worker and the block mirror over OPFSlib/fetch.mjsWebTorrent fetch and sha256lib/blockfile.mjs,lib/source.mjsthe block file format and the http/rpc block sourceslib/nip333.mjsthe chain tip from NIP-333 header events, one-shot and livelib/node.mjsthe chain state machine: headers, applied hashes, undo recordslib/state.mjscheckpoints: the UTXO set written as a snapshot with a manifestlib/api.mjs,lib/status.htmlthe HTTP routes, tip stream and status pageops/blaketestnode.config.example.cjspm2 exampletools/export-blocks.mjskeeps the served block file current from a nodelib/engine.mjs,lib/rpc.mjsengine and node RPC loadersbin/blaketestnode.mjsthe CLI