Pay and talk when the network won't.
Murmur carries a real shielded Zcash transaction and sealed private notes from device to device with no internet. Whoever reconnects first broadcasts the transaction, and the payment confirms on chain.
Repository: https://github.com/etvjay/murmur. Built for ZECATHON, Shielded Payments track. Zcash testnet only. Judge page: https://murmurz.vercel.app (the static site in
apps/web; deploy notes indocs/operations/DEPLOY_WEB.md).
Murmur is for people who still need to pay and message each other privately when a network is down or shut off: a market stall in a blackout, a clinic after a storm, a team in a region with shutdowns. It is a transport and a spend boundary. It is not a wallet, not a new currency, not a bridge, and not anonymous routing.
Try it in 30 seconds (Node 20+, no install): sh scripts/verify runs everything, and node bin/murmur.js request --address ztestsapling1cvge2ycqhv40jrdgr82qd7nv8q57urzeg83nkw07e20uwzf7j4h4nm344mmrht07nhfvx5uwlnc --amount 3000 prints a payment request. More commands are under Quick start.
Private money and private words both depend on a network someone can switch off. Money over an offline mesh is not new: Bitchat merged a Cashu ecash /pay feature in July 2026, signed Bitcoin and Ethereum transactions have been relayed over Bluetooth and LoRa meshes, and a Bluetooth shielded-ZEC demo has been announced publicly (related work). What Murmur adds for Zcash is narrower, and it is mostly evidence: a shielded payment built with every wallet step offline, carried device to device, and broadcast once by whoever reconnects; an offline invoice layer on top (an itemised invoice sealed to the payer, paid with its id in the memo, settled against the receipt and the chain); retained testnet runs; and a ledger of what is and is not proven.
MERCHANT ZIP 321 payment request (address, amount, memo)
↓
PAYER wallet builds, proves and signs a shielded transaction with the network cut
↓
CARRIER raw transaction + a sealed receipt hop from device to device (UDP)
↓
GATEWAY the first device with internet broadcasts the transaction, once
↓
CHAIN Zcash testnet mines it; the merchant opens the sealed receipt
- The wallet (
zcash-devtool, with a small patch inpatches/) holds keys and builds the transaction. Murmur never touches keys. - The carrier (
src/) moves two packet kinds. Atxpacket is the finished raw transaction; it is public once mined and any relay may read it. Asealedpacket is encrypted to one recipient (X25519 and ChaCha20-Poly1305, with the sender's key mixed into the derivation). Relays cannot open it and there is no recipient tag to link. - The spend policy (
src/policy.js) caps each payment and the total spent while offline. It is checked before the wallet runs. - Only the finished raw transaction travels. A PCZT can hold viewing keys and note details, so it never leaves the payer.
All runs are on a Zcash test network: the one served by testnet.zec.rocks (chain name test, NU7). A public explorer we tried, testnet.zcashexplorer.app, follows a different chain, so it does not show these transactions (GAP-012, evidence/08-chain-comparison). Which network the hackathon treats as canonical is unconfirmed. Full detail in docs/submission/EVIDENCE_INDEX.md.
| Run | What it shows | Result | Evidence |
|---|---|---|---|
| A | A shielded transaction built with the network cut | create, prove, sign, combine and extract all succeeded offline (20,986-byte transaction, unified-address recipient) | evidence/03-acceptance-a |
| B | A transaction carried across the carrier and confirmed | 5,222 bytes, same SHA-256 on both sides, one broadcast, mined in block 4,550,000 | evidence/04-e2e-loopback |
| C | A ZIP 321 payment request paid end to end | every devtool step in a no-network namespace, hand-over over loopback UDP, one broadcast, mined in block 4,550,881; its memo read back on chain (evidence/09-memo-readback) |
evidence/05-zip321-e2e |
| D | An offline invoice: created, sealed to the payer, paid offline, settled | the payer validated the invoice, paid its 3,500 zat total with every devtool step offline and the invoice id in the memo; the merchant matched the receipt to the invoice and the chain; mined in block 4,562,231 | evidence/10-invoice-e2e |
Runs B, C and D used loopback UDP on one machine. Run E used two real devices: a Windows laptop and an Android phone on one Wi-Fi hotspot with no internet exchanged sealed messages and an invoice, and carried a real transaction that was then mined (block 4,573,059). No Bluetooth, range or multi-hop test has been run, and in Run E the owner's machine built and broadcast the transaction; the devices carried it. The judge page's relay is a labeled simulation.
| Authority | Responsibility | What it cannot claim |
|---|---|---|
| Zcash testnet | Consensus and finality of the payment | It does not know the carrier or the receipt |
Wallet (zcash-devtool) |
Keys, note selection, proofs, signatures | It does not know the mesh; it never sees a relay |
Carrier (src/) |
Moving packets, sealing notes, dropping duplicates, hop limits | It cannot create a payment, sign, or prove anything was paid |
| Gateway | Broadcasting a received transaction once | It cannot make a transaction valid or confirmed |
| Relay | Forwarding packets | It cannot read a sealed note or forge an ack; it can drop or delay a packet (a repeat of the same packet is dropped as a duplicate) |
| Sealed receipt | Telling the merchant a payment was handed over | It is not proof of payment. The on-chain transaction is |
| Judge page | Presenting evidence and a labeled simulation | It is not evidence about radio |
- A payer can still spend the same notes elsewhere before broadcast. The spend policy bounds the loss; nothing here is trustless offline settlement.
- Relays see packet sizes, timing and neighbors. Sealed notes hide content and recipient; the shielded transaction hides payer, recipient and amount on chain.
- An offline-built transaction expires 40 blocks after the wallet's last sync, about 15 minutes at the testnet's measured pace of about 22 seconds a block. The library default is
DEFAULT_TX_EXPIRY_DELTA = 40; two builds on 2026-11-02 carried expiry heights 40 and 41 above the tip. A transaction built against a stale tip carried expiry height 4,550,721 and was refused at height 4,550,785. Sync, build, carry and broadcast must fit in the window. - The devtool's PCZT path cannot yet sign for internal-scope change notes (
WrongFvkForNote). - The memo in the ZIP 321 request reached the receiving account on chain, but that was read back inside one wallet we operate (after
wallet enhance), not by an independent party (evidence/09-memo-readback). - An ack proves that the addressed recipient's key sealed it in reply to this message (it echoes a per-run nonce). It does not prove a payment was made: the chain does.
- An invoice is authenticated by the sealed delivery that carried it; a saved invoice file is not itself signed (GAP-023).
- The offline spend total persists across runs only with
--ledger FILE; without it--spentis self-reported per run. - A device listens on all interfaces by default so other devices can reach it;
--bind 127.0.0.1restricts it. - Sealed-note authentication binds keys, not people, and relays can link all notes from one sender because the sender's public key travels in the clear. The cryptography has not been independently audited.
Check any transaction yourself, independent of our wallet: zcash-devtool inspect -l <txid> (or sh scripts/verify-on-chain.sh <txid>). Results are in evidence/07-chain-readback.
Related work, including Offline Protocol's Bluetooth mesh SDK and why Murmur does not use it yet: docs/submission/RELATED_WORK.md.
Every claim and its evidence ceiling: docs/submission/CLAIMS_AND_LIMITS.md, foundry/claims.jsonl, STATUS.md. Open gaps: foundry/gaps.jsonl.
Needs Node 20 or newer (the versions CI runs: 20 and 22). No dependencies.
npm test # 144 tests: 143 pass, 1 skipped (the real-devtool test needs MURMUR_E2E=1 and a funded wallet)
npm run demo # 6-node relay simulation at 30% loss (a simulation, not radio)
sh scripts/verify # tests, ledger consistency, secret scan, web build checkA merchant's payment request needs no wallet and no network. This prints a ZIP 321 URI as one JSON line:
node bin/murmur.js request --address ztestsapling1cvge2ycqhv40jrdgr82qd7nv8q57urzeg83nkw07e20uwzf7j4h4nm344mmrht07nhfvx5uwlnc --amount 3000 --memo hi
# {"kind":"request","uri":"zcash:ztestsapling1cvge2yc...?amount=0.00003&memo=aGk"}A sealed message between two terminals on one machine (loopback, not radio). The listener prints the message and acks; the sender prints the ack:
# terminal A: the listener (creates /tmp/a.json on first use)
node bin/murmur.js listen --port 47101 --identity /tmp/a.json --peer 127.0.0.1:47102 --ack
# terminal B: look up A's public key (it is not a secret), then send to it
PUB=$(node bin/murmur.js pubkey --identity /tmp/a.json)
node bin/murmur.js msg --port 47102 --recipient-pub $PUB --peer 127.0.0.1:47101 --identity /tmp/b.json --text "Paid. Two bags, pickup at 5."An offline invoice, on one machine (the merchant creates and sends it; the payer's listener validates and saves it; settle matches the receipt later):
node bin/murmur.js invoice create --to ztestsapling1cvge2ycqhv40jrdgr82qd7nv8q57urzeg83nkw07e20uwzf7j4h4nm344mmrht07nhfvx5uwlnc \
--item "Two bags of rice:2:1500" --item "Delivery:1:500" --identity /tmp/m.json --out /tmp/inv.json
# the payer listens with: listen --invoice-dir DIR ...; the merchant sends with: invoice send --invoice /tmp/inv.json --recipient-pub HEX ...
# the payer pays with: pay --invoice DIR/<id>.json --recipient-pub MERCHANT_PUB ... (address, amount and memo come from the invoice)
# the merchant settles: invoice settle --invoice /tmp/inv.json --receipt gateway-output.jsonl [--check-chain --devtool PATH]node bin/murmur.js --help lists every command.
Payments need the patched devtool and a funded testnet wallet: see patches/README.md, scripts/wallet-setup.sh and scripts/run-zip321-e2e.sh. The two-device test is docs/operations/acceptance-b.md.
REPO_STRUCTURE.md is the map. In short: src/ and bin/ are the carrier and CLI, test/ the tests, apps/web/ the judge page, evidence/ the immutable run packages, docs/ the canonical documents (invariants, ground truth, ADRs, submission), and foundry/ the claim and gap ledgers.
foundry/ is the claim ledger (claims, gaps and contradictions), not application code; docs/development/EXTENDING.md and CONTRIBUTING.md explain how to change things. Start with PRD.md for scope, docs/canonical/INVARIANTS.md for what must always hold, and STATUS.md for the current verified state.
Licensed under either of Apache License 2.0 or MIT license, at your option. Third-party terms: THIRD_PARTY_NOTICES.md.