Skip to content
bitcoin-blakePublic

About

Payment channels in a browser tab, Lightning-style: funded, watched and settled by the tab's own node (BLAKE2b testnet4)

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

40 Commits

Folders and files

Repository files navigation

Hitch

Payment channels in a browser tab, in the Lightning shape. A 2-of-2 funding output on txbt4 (the BLAKE2b testnet4), asymmetric commitment transactions with a revocable to_local, penalties for old states, cooperative and forced closes. The tab's own node funds, watches and settles; two tabs pay each other over the relays without touching the chain.

Live: https://bitcoin-blake.github.io/hitch/

What it is, and is not

Hitch is the Lightning construction without the Lightning network. There are no Lightning nodes on this chain to talk to, and a tab has no TCP, so the peer is another tab and the transport is Nostr. It is a demonstration of the mechanics on a chain where everything else, the node, the mempool and the mining, already runs in a tab: Reef, Bight, Winch. It is not compatible with Lightning peers, routes only through a hub you have a channel with (no gossip, no pathfinding, no onion), and is not private: channel messages are signed, not encrypted, so amounts and memos are readable on the relays.

How it works

  • The keys. Reef's key in this browser when there is one (same origin, same storage), so Reef's coins fund channels and Reef sees what comes back; else a key Hitch keeps. The same key signs the channel's transactions and the relay messages, so the node id is the key.
  • Funding. The funder builds a transaction from its on-chain coins to a taproot output whose one leaf is multi_a(2, a, b) under an unspendable internal key, and keeps it back until the other side has signed the funder's first commitment. Then it goes out as a kind 23503 event, which a sidestr producer with a node broadcasts; the tab sees it confirm in its own UTXO set.
  • Commitments. Each side holds its own commitment for the current state: its balance in a to_local with two leaves (its own key after delay blocks, or a revocation key at once) and the other's balance paid straight to the other's key. The revocation key is a two-party key: the other side's basepoint (announced when the channel opens) plus the owner's per-state point, so the owner can never use the leaf on its own commitment and the other side can only once it holds the per-state secret. A state is replaced by an update: the payer signs the payee's next commitment, the payee signs the payer's and reveals its old per-state secret, the payer reveals its own. Per-state points are announced one state ahead so the payer can sign first. An update the other side will not sign comes back as a reject, and the sender remembers the state it signed as one the other side might still publish. A signature or a secret leaves a node only after the state it belongs to is saved. Every transaction is built by lib/channel.mjs and checked by the kernel's interpreter before it is used; test/channel-test.mjs runs the constructions and pins the scripts as golden vectors; test/peer-test.mjs and test/adversarial-test.mjs run three peers in memory through payments, routing, lost and reordered messages, a collision, a cheat, a restart and a forced close with an HTLC in flight.
  • Closing. Cooperative: both sign one transaction paying the balances to the keys. Forced: publish your latest commitment; your share waits delay blocks, then the tab sweeps it. Cheating: publish an old commitment; the other tab, watching the funding output through its own node, recognises the revoked state, spends its to_local with the revealed secret, and takes it all. The Cheat button (Settings → Options → Developer) exists to show that. After any close the tab follows every output of the closing transaction until its own claims are confirmed, and an HTLC output the other side takes with the preimage teaches the tab that preimage.
  • Invoices. hitch1… strings naming the node, an amount, a payment hash, the hubs the node has channels with, and an expiry; paying one is an HTLC that the issuer settles with the preimage.

Fees are a fixed amount per channel transaction, agreed at open: commitments and cooperative closes are paid by the funder, and each side pays the fee on what it claims afterwards (sweeps, HTLC claims, penalties). The delay is 6 blocks by default, about two hours here; a tab refuses channels with a shorter one, the hub accepts 3. A payment is final only when the payer has revoked the state before it; until then nothing is forwarded, settled or announced. An HTLC a tab can claim goes to the chain delay + 3 blocks before its expiry if the settle is not acknowledged, whatever else is pending, because the claim on its own commitment waits the delay and the other side's refund does not. A tab with an open channel must be opened at least once every delay blocks: an old state published while it is closed for longer goes unpunished. Keep it open while anything is in flight or a close is being watched. A funding that has not confirmed in a day is set aside (it opens if it ever confirms); a proposal nobody funds is abandoned after six hours.

The first run (30 September 2026)

Two tabs on one machine, each its own node, over the live txbt4 chain with blocks about twenty minutes apart.

step block what happened
open 152,081 A proposed 100,000 sat to B; accepted, both first commitments signed, funding published, in 8 s
pay – A paid B's 5,000 sat invoice; B pushed 1,000 back; state 2, both revocation secrets exchanged
close 152,082 A asked, B signed and published; 4,000 sat to B's key, the rest to A's
cheat 152,083 on a second channel A paid 30,000 then published its revoked state 0
penalty 152,084 B's tab found the old commitment in its own chain within 40 s, spent its to_local with the revealed secret: 99,400 sat to B, accepted by the Knots node

Through a hub

HTLCs are in since the second night (30 September 2026): a hash-locked output in each commitment with three leaves (the receiver with the preimage, the offerer after the expiry, the revocation key), the commitment's owner waiting the delay on its own claims so a revoked commitment's HTLCs can still be punished. An invoice carries the payment hash and the node ids of the issuer's hubs; paying it is an HTLC on a channel to the issuer, or to one of its hubs with the route, and the issuer settles with the preimage. bin/hub.mjs runs the same protocol in Node over the local node's RPC: it accepts channels (open one to it with a push if you want it to pay you back), forwards HTLCs for a fee and carries settles and fails back. The protocol is lib/peer.mjs and lib/route.mjs, pure; test/peer-test.mjs runs A, a hub and B in memory.

First routed payment, 30 September 2026, on the live chain: A opened 100,000 sat to the estate's hub, B opened 100,000 with 50,000 pushed to the hub (block 152,086); B issued a 20,000 sat invoice naming the hub; A's HTLC of 20,010 crossed the hub as an HTLC of 20,000 to B, B settled with the preimage, the hub settled upstream; 25 seconds end to end, nothing on the chain.

What the page shows and does

The Send page keeps a list of payments by payment hash with each one's outcome: in flight, sent (the other side took it with the preimage, off the chain or on it), failed (with the reason, or refunded on the chain after the expiry) or not made (the update was set aside after a collision or a rejection). A payment that was set aside is not dead: the signature given for it binds until the other side revokes that state number, so the other side could still acknowledge it late and it would then take effect; wait for the outcome before paying the same invoice again, and the page refuses to pay an invoice that is in flight or already sent. Every revocation point a side announces comes with a proof of possession (a signature by its secret over the channel id and the point's place), so neither side can choose a point that cancels the other's contribution to the two-party key. A funding that has not confirmed after an hour can be cancelled by the funder (the same coins spent back to its key with a higher fee); the channel can be forgotten once the cancel is confirmed, never before, because a funding that confirms later would lock the coins in a 2-of-2 output nobody can spend. Coins a funding has spent are not picked for another funding while it is unconfirmed. One tab of an origin runs the channels; a second tab of the same origin is read-only (a Web Lock decides), because two tabs signing over the same documents would wedge every channel. A cooperative close is final after six blocks; until then the tab keeps watching the funding output in case a reorganisation replaces the close with an old commitment.

Running the tests, running a hub, keeping it safe

npm test runs the three suites (test/channel-test.mjs against the kernel's interpreter, test/peer-test.mjs and test/adversarial-test.mjs with three peers in memory). They import the kernel, the codec and the sidestr library from sibling checkouts: SCHEMA (bitcoin-desktop/schema), BLAKETESTNODE (bitcoin-blake/blaketestnode) and SIDESTR_LIB (sidestr/spec/siding/lib), defaulting to ~/bitcoin-desktop/schema, ~/remote/github.com/bitcoin-blake/blaketestnode and ~/remote/github.com/sidestr/spec/siding/lib. The page pins those three by commit; the hub imports the working trees named by the same variables, so pin your checkouts before running one.

A hub (bin/hub.mjs) keeps its key in the file named by --key-file (mode 600, refused otherwise) and its channels in --data (channels.json, the previous copy as .bak, finished channels in archive.jsonl, the block cursor in cursor.json). It refuses to start with a key that is not the one its channel documents were made with. Back up the key file and the data directory together, off the host, after every session that opened or closed a channel; restoring an older channels.json and force-closing from it is a cheat the other side will punish, so resync first (the status page says when the hub started from the backup copy). The status page (--status PORT, localhost only) lists every channel and HTLC with its deadline, the block cursor, the relays, a reconciliation of open channels against the node's view, and answers 503 while the chain view is stale, a save has failed or the backup copy is in use. Admission is capped per key, in all, unfunded, per hour and by delay (--max-per-peer, --max-channels, --max-unfunded, --open-rate, --max-delay); a proposal nobody funds within twenty minutes is dropped, and a funding the node has not seen in the mempool or the chain within thirty minutes is set aside (it is still looked for during a day and taken up if it turns up). Both a tab and the hub count a channel open at two confirmations.

What each warning on the status page means, and what to do: the chain view is stale — the node's RPC has not answered for ten minutes; HTLC updates and opens are held until it does, check the node. the last save failed — the data directory is not writable or full; nothing that binds the hub is sent until a save succeeds; fix the disk, do not close anything meanwhile. running from the backup copy of the channel file — channels.json was missing or unreadable and .bak (the previous save) is in use; the block cursor was discarded so the walk covers everything watched; let the hub resync with every peer (it does on start) and compare each channel's n with the peer before closing anything. open channels hold X sat but the node sees Y sat unspent — a funding output is spent but the walk has not yet classified the spend (a close pending in the mempool shows this way too); if it persists past a block, look at the channel with spentBy null. N unsent transaction(s) — the node refused a broadcast; read the log line for the reason (usually a fee or a not-yet-valid timelock) and it is retried every tick. htlc N has K blocks left — a deadline approaches; the protocol closes on its own at delay + 3 blocks, but check the peer is reachable. no relay is connected — messages neither arrive nor leave; check the network. X is spent-unknown / bad-funding — the funding output was spent by a transaction the hub cannot classify, or holds a different value than declared; inspect on the explorer. update N unanswered after K tries — the peer is offline or refusing silently; the channel force-closes on its own only for HTLC deadlines. part of the penalty was lost — a cheating peer swept some outputs before the penalty landed. There is no log rotation in the pm2 configuration: add pm2-logrotate or rotate hitch-hub.log yourself.

Name

A hitch ties a line to something. Reef the knot, Bight the slack, Winch the pull, Hitch the tie.

Licence

AGPL-3.0-or-later.

About

Payment channels in a browser tab, Lightning-style: funded, watched and settled by the tab's own node (BLAKE2b testnet4)

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages