Super Testnet v0
A small browser-local Bitcoin protocol lab: create a test-only wallet, mine fake BTC, sign a MIME inscription, see it in that wallet’s collectibles, and follow its transactions into a local block.
This is a private, single-browser simulation inspired by Supertestnet. It uses real Bitcoin transaction serialization, Schnorr signatures and Taproot commit/reveal envelopes. It is not Bitcoin Core, public testnet, signet, regtest consensus, an Ord indexer, or production custody. There is no mainnet mode and no external broadcast path.
Run locally
Requires Node.js 20.19+ (Node 22 recommended) and npm.
npm ci
npm run dev
# Open http://localhost:5173
For the exact production server:
npm run build
npm start
# Open http://localhost:8080
Use localhost or HTTPS: secure-context Web Crypto and Web Locks are required. Chromium is the verified browser; recent browsers supporting Web Locks and Array.toReversed should work. No API keys, Bitcoin node, faucet, CDN, database, account or environment file is needed. Runtime assets are local. Installing dependencies requires network access; the built app makes no third-party requests. It is not an installable/offline service-worker app.
Complete the loop
- Create test wallet, or load a previously saved Super Testnet wallet JSON. Wallet creation never signs a transaction.
- Mine now: receive 50 fake BTC immediately, without coinbase maturity.
- Choose text/plain, application/json, text/html, image/svg+xml, image/png, image/jpeg or image/webp. Edit text or upload a local file of up to 16,384 bytes (16 KiB). No remote fetching. Optionally select an owned inscription as its immutable parent. Add arbitrary JSON metadata independently of the body: it becomes real CBOR in repeated ord tag-5 fields, each at most 520 bytes. The app caps metadata at 2,048 CBOR bytes (64 nesting levels); invalid JSON, non-finite numbers and oversized documents are rejected before signing. Blank metadata is omitted; JSON
nullis a present metadata value. Review inscription shows the payload and costs without signing. - Sign & inscribe explicitly signs a commit and a reveal locally. Fees total 1,000 fake sats; 1,000 fake sats remain in the inscription output. Both transactions enter the local mempool atomically. A child reveal also signs the parent input and returns its full value to the same wallet. After successful persistence the content, file, parent, metadata, review and byte counters reset; failures preserve the composer.
- The inscription immediately appears as pending in this wallet’s collectibles. Mine again to confirm both transactions in one block.
- Open the inscription, its reveal, its commit, or its block in the integrated explorer. Raw transaction hex and witnesses are visible. Reload to verify persistence.
Wallet ownership is derived from the unspent reveal output’s script, not a disconnected UI list. Commits spend ordinary change. Child reveals explicitly spend and preserve an owned parent output, tracked at its current outpoint. Parent and children links support nested lineage, including pending ancestors. Loading another wallet on the same chain displays only that wallet’s collectibles.
Save test wallet exports its tagged test key, not chain history. Save complete lab exports both wallet and chain, including pending transactions, and is the portable recovery option. Loading a complete lab replaces local state after an explicit prompt. Clearing site data/resetting the lab destroys local state unless you have a backup. Explorer hash links work only with the matching local chain; they are not public explorer links. A different origin (including localhost vs Fly) has separate browser storage; use a complete lab backup to move it.
Metadata, local indexes and API
Collectible cards and details show metadata decoded from inscription witnesses. Metadata is data; indexers are interpretation. MetadataIndex reconstructs every inscription event from confirmed block raw transactions in chain order, followed by raw mempool transactions. Events include id, content type, status, height/block hash, metadata, presence flag and parent. No UI metadata sidecar is persisted. A top-level string metadata.protocol is the default discovery convention; other fields and values have no built-in semantics. The Indexes surface shows counts, discovered names and the ordered feed for a selected protocol.
The optional event-counter reducer derives counts and ordered ids from only the selected protocol's events, displayed separately. Reducers are trusted application code registered above the base index, never downloaded or executed from inscriptions. Adding a reducer requires a source-code change; this is a seam for future metaprotocol experiments, not a sidechain consensus engine. Native ord metaprotocol tag 7 is supported as an independent inscription field. It is authoritative for protocol grouping; metadata.protocol remains a compatibility fallback when tag 7 is absent.
The LOCAL / IN-BROWSER API explorer calls LocalOrdAPI.request(path) and displays/copies its {status, contentType, body} response. No HTTP request is made. Fly/the static server cannot read browser state. Available paths:
/content/:id: terminal delegate MIME and exactUint8Arraybody;inscriptionIdremains the requested id andcontentIdidentifies the terminal source. The explorer renders bytes plus an escaped text preview, never executes content./r/inscription/:id: decoded event, including JSON metadata, delegate/delegatedBy, synthetic sat, current location, reinscription position and ordered same-sat inscription ids. Metadata and identity always belong to the requested inscription./r/metadata/:id: JSON string of CBOR hex, ornullwhen absent./r/parents/:id,/r/children/:id:{ids}for immediate lineage (all local results, no pagination)./r/tx/:txid:{txid, hex, height};/r/blockheight: local height./r/blockhash/:height,/r/blockinfo/:height-or-hash: local hash or synthetic block info with ordered transaction ids (timestamps in milliseconds)./local/sat/:number: synthetic sat location and ordered inscription stack;/local/utxo/:txid:vout: unspent value, half-open sat ranges and inscription ids./local/metadata-index: stats and ordered events;/local/protocols: names and counts;/local/protocol?name=...: matching ordered events. URL-encode protocol names.
Unknown paths/resources return 404; non-local paths return 400. This deliberately partial ord-like API is not wire-compatible with all ord schemas. Metadata encoding follows the ord metadata convention; path naming and CBOR-hex metadata responses follow the ord endpoint reference. HTML/SVG previews use the restricted local bridge described below.
Delegates, recursion and synthetic sats
The optional delegate is ord tag 11, using the same reversed txid/minimal little-endian index as parent tag 3. Existing and syntactically valid future ids are accepted. /content/:id follows chains, returns 404 for an absent target and deterministic 508 for loops. Cards/details link both directions. Delegation changes only served bytes/type, never the requesting inscription's metadata, lineage, sat or self-reference.
Use recursive demo fills a usable HTML template. Inline HTML/SVG scripts execute as an async worker program inside a sandboxed iframe. Use await fetch('/r/blockheight'), fetch('/content/'+id) and standard Response methods. ord.id and ord.contentPath refer to the requested inscription, including delegated previews; ord.render(text) writes escaped text to the preview. The MessageChannel bridge permits only the documented /content and /r read paths (no query strings, URLs, local administrative endpoints or mutations). CSP also blocks direct XHR, WebSocket and import requests. Workers cannot navigate or access any DOM; the iframe runs only the trusted renderer. General browser DOM scripts and automatic URL rewriting are deliberately unsupported. Scripts never receive wallet secrets or signing capabilities.
Sat identity is synthetic-local-fifo, not a Bitcoin ordinal number. Height h issues [ (h-1)*5,000,000,000, h*5,000,000,000 ). Ordered input ranges flow into ordered outputs; leftover fee ranges retire instead of being paid to a miner. This replaceable range adapter is replayed from raw transactions, not saved UI state. New parent reveals put the parent's input and output first, preserving its exact sat offset; the new inscription uses a pointer to its own output. Legacy envelopes still decode and their sat positions follow their actual serialized FIFO flow.
Select Reinscribe an owned sat to spend and return its current output while appending an inscription to that same sat. The original body and id remain, stack positions start at zero and ordering survives mining/replay/spends. A new 1,000-sat output is also created by the fixed commit/reveal template; reinscription targets the carried sat, not that new output. Parent and reinscription selection are mutually exclusive. The reinscription target determines its pointer.
Advanced pointer tag 2 is an unsigned 64-bit zero-based position across all reveal outputs. Encoding is little-endian with trailing zero bytes omitted (zero is empty); a pointer beyond total output value falls back to the first output sat. There is one inscription per reveal. Multiple inscriptions on one sat come from separate reveals. Actual Bitcoin subsidy schedules, miner fee recycling, multiple envelopes/reveal, arbitrary DOM recursion and regtest/public backends are deferred.
Mining at your pace
The miner defaults OFF, with 10s / 30s / 60s choices and Mine now. Preference and next deadline persist in lab state/backups; old backups default OFF. Auto and manual mining use the same Web Locks → read latest lab → mutate → atomic save path. Auto mining rechecks the persisted deadline inside the lock, so tabs sharing the origin cannot mine the same scheduled block. Manual mining resets the next deadline. Each block clears pending commit/reveal packages naturally.
A tab must stay open; background throttling/suspension can delay mining. Reopening an overdue lab mines at most one block, then starts a fresh interval, without a catch-up burst. Auto mining signs nothing. A failed save publishes no partial state and the UI reports the error.
Planned backend progression
Local lab → regtest (Super Regtest) → signet/testnet → mainnet is a future selector progression, not a list of implemented modes. Chain, wallet/signing, raw metadata index, trusted reducer and API adapters stay separate. A later network adapter must explicitly supply chain data and wallet capabilities; a hosted server cannot implicitly access browser storage. Regtest is not implemented. Public networks and mainnet need separate security and signing designs; this v0 has no production keys/funds, external broadcast or hidden signing.
Native tag-7 compatibility, full ord indexing and public-network adapters remain future passes. Metadata supports the JSON data model encoded in CBOR, not every CBOR feature (no byte strings, tags, indefinite collections or arbitrary map keys). The narrow local envelope validator accepts the app's encoding, not arbitrary external ord transactions.
Architecture
src/adapters/wallet.js: test-wallet creation/loading, regtest-formatbcrt1paddress and explicit signing primitive. Only tagged v0 test backups can be imported; there is no arbitrary production-key input.src/adapters/chain.js: instant 50-BTC coinbase, UTXO set, package validation, mempool, synthetic blocks and replayable snapshots. Validates transaction shape, values, spent inputs, default Schnorr signatures and the supported Taproot script/control block. Exposesmine,submitPackage,coins,balance,snapshot.src/adapters/inscription.js: MIME/byte envelope with optional parent tag 3 and chunked CBOR metadata tag 5, commit/reveal construction, confirmation/ownership view.preparedoes not sign;inscribeis invoked only after the user’s explicit signing action.src/adapters/metadata.js,metadata-index.js,local-ord-api.js: bounded JSON/CBOR codec, byte-derived raw index and trusted reducer boundary, in-process read API.src/miner.js: persisted schedule and deadline checks, invoked throughmutateLabin storage for both mining modes.src/upstream/primitives.js: small attributed adaptations of Supertestnet’s input/output helpers. See provenance.LocalRPC: in-process read facade forgetblockcount,getblockhash,getblock,getrawtransaction. It is deliberately partial, not wire-compatible bitcoind JSON-RPC. A future Core or node_faker adapter can replace this boundary.src/storage.js: versionedlocalStoragesnapshot; index/UTXO reconstruction with transaction validation on load. One atomic storage write per operation; failed validation/quota writes do not publish partial in-memory state. Web Locks serialize mutations across tabs; storage events refresh other tabs.server.js: dependency-free static file server with security headers and/healthz. No signing, chain state, RPC, database or wallet secrets on the server.
Exact v0 limitations
- Blocks are custom JSON hashed with SHA-256, not Bitcoin block headers. No proof of work, difficulty, merkle tree, reorgs, peers, chain synchronization, coinbase maturity, halving or full consensus. Fixed 50-BTC rewards; fixed 500-sat fees per non-coinbase transaction are burned.
- Only this app’s single-input Taproot key-path and narrow
ordMIME script-path templates, plus explicit two-input child/reinscription reveals, are accepted. No general Script interpreter, legacy/SegWit-v0, timelocks, arbitrary transactions, fee estimation, transfer UI or replace-by-fee. Regtest-format addresses do not make the chain compatible with Bitcoin Core. Never fund these addresses elsewhere. - One inscription per reveal, 1,000-sat postage, 16 KiB body cap and 520-byte script pushes. Parent tag 3 encodes reversed txid bytes plus a minimal little-endian index; validation requires the referenced parent input, its signature, and preservation in the same wallet. Only one parent per child. Parent identity and lineage survive relocated outputs and cannot be edited. Signed key-path spends carry sat identities and stacks through FIFO ranges. Inscribed sats cannot be discarded into fees. No full ord indexing, BRC-20 execution, transfer UI or external Ord compatibility guarantee.
- Text/JSON previews use escaped text; JSON is not schema-validated. HTML/SVG markup uses a restrictive element/attribute allowlist in sandboxed frames. Inscription scripts run only in an isolated worker, with local fetch and text-only ord.render; no DOM, navigation, external imports or network requests. Rendering is intentionally lossy (no linked images, fonts or embedded frames). Reviews stay inert until explicit signing. Raster previews use local blobs; MIME declarations do not guarantee valid image data. Body bytes are preserved exactly even when previews cannot render them.
- One active wallet per origin. Test secret and chain are unencrypted in browser storage and JSON backups. No mnemonic, password, hardware wallet, key derivation, custody service or hardened security audit. Tagged JSON is an accidental-import guard, not proof a key has never been used elsewhere. Never put production secrets in these files.
- No persistent server database, volume or shared chain. Browser storage quotas apply; large histories replay synchronously and eventually become slow. A maximum of 1,000 pending transactions (500 inscriptions) per block; complete lab imports capped at 5 MB. Built for small experiments.
- Mining signs nothing; only Sign & inscribe signs the two displayed operations. Restoring state verifies signatures but does not create new ones. Exporting backups exposes the fake wallet key in the downloaded file.
- No network security or economic assurances: a user controlling their browser can rewrite their entire private lab. Validation is useful for testing this flow, not an adversarial consensus boundary.
Verification
Verified for this pass: 30 adapter tests passed (npm test), 26 desktop/mobile browser tests passed (npm run test:e2e), and npm run build passed. Chromium, missing runtime libraries and fonts were staged under /tmp; no system packages or deployment were changed. Run:
npm test
npx playwright install --with-deps chromium
npm run test:e2e
The adapter suite covers ownership, real signed commit/reveal construction, confirmation, RPC inspection, pending/confirmed restoration, malformed imports, MIME/byte round-trips and size limits, parent ownership/preservation, fraudulent tags, untagged parent spends, nested lineage, bad signatures, modified payload/control blocks, over-spending, atomic rollback, double-spending and collectible protection.
Playwright tests the production build/server, at desktop and Pixel 7 mobile sizes: full loop, review-before-signing, pending persistence, transaction/block navigation, complete backup/reset/restore, wallet imports, invalid input, two-tab concurrent mining, composer reset and failure preservation, non-text uploads, parent navigation, sandbox preview safety, no third-party requests and no horizontal overflow. Additional tests cover CBOR vectors/chunking/caps, sidecar-independent metadata indexing, protocol grouping/reducer ordering, local API reads, miner persistence/countdown/manual reset and two-tab deadline races. Screenshots/traces are written to ignored test-results/.
Independent Fly deployment
Dockerfile builds static assets and runs a non-root Node static server. fly.toml targets only supertestnet, one shared CPU, 256 MB RAM, HTTPS, health checks, scale-to-zero, and no volume or database. No Teahub app, service, secrets or network integration is configured. No custom domain is configured.
From a worker with authorized Fly CLI/account access, create the new app in the intended organization, then deploy:
fly auth whoami
fly apps create supertestnet --org YOUR_AUTHORIZED_ORG
fly deploy --app supertestnet --config fly.toml --ha=false
fly status --app supertestnet
Use --ha=false to avoid a redundant second machine. If the app name already exists, verify its ownership and purpose before deploying; do not overwrite an unrelated app. Expected URL after successful creation: https://supertestnet.fly.dev.
Current worker deployment status: not deployed. Neither fly nor flyctl is installed/on PATH, no default ~/.fly configuration is present, and no Fly credential environment variables are configured. Authorized account access could not be established; no app was created and no credentials were invented. Docker is also unavailable here, so the image was not built locally; the identical production Node server is exercised by Playwright. Fly app-name availability and deployment remain unverified.
Configuration references: Fly app configuration, Fly deploy options.
Attribution and licenses
See PROVENANCE.md for upstream commit pins, exactly what was adapted, what was only inspiration, and the node_faker decision. License texts ship in public/licenses/ and the app links them. No upstream UI, network relay, mainnet API or remote script loader was imported.