Releases: knobcore/bopwire
musicchain player — swarm-v2 transfer + per-stream rewards (2026-06-29)
Build from branch per-stream-rewards @ b169b85. Swarm Transfer v2 (binary multi-source, per-piece verify, 4 Mbit/s/seeder, stop/resume + fast-start fixes), private DHT, per-stream 5-lane rewards (artist+node+discoverer+seeder+mini-node), secure wallet.transfer. APK = Android; ZIP = Windows x64 (unzip, run musicchain_player.exe).
v0.9.6 — DB2 discovery cutover (presence + libraries replace swarm.hello)
v0.9.6 — DB2 discovery cutover (presence + libraries replace swarm.hello)
Discovery and streaming no longer ride on the ephemeral swarm-index re-announce. A player publishes its library to DB2 once (durable, wallet-signed) and, on connect, sends a tiny signed presence.hello binding its wallet to its live peer id. The node drives Discover straight from DB2 — "say hi, your library appears."
What changed
presence.hello—"mcprs1" ‖ wallet(20) ‖ ts(8 LE) ‖ peer_id, wallet-signed. The node verifies it and binds wallet → live peer id (self-declared so relayed players bind correctly), evicting on disconnect.songs.listnow returns only chain songs that an online wallet's DB2 library currently holds;swarm_size= live DB2 holder count. Metadata is unchanged (it always came from the on-chain registry, never the swarm).stream.openresolves serving peers from DB2 holders ∩ presence.library.deltareply carriesunknown[](your published hashes not yet on chain); the player re-registers those viafingerprint.submit— the oldswarm.hellochain-resubmit, re-homed.- Removed the
swarm.hellofamily (hello / digest / add / remove / leave) on both node and player; library add/remove now flow through the DB2 versioned snapshot. Presence lifecycle for relayed players is kept viaswarm.peer_online/offline.
Heads-up — this is a full cutover
Discovery is now DB2-only. A player must be on v0.9.6 (sends presence.hello) to appear in Discover and be streamable. Older builds won't show up until updated. Install this build; on connect your library republishes to DB2 and reappears in Discover automatically.
Artifacts
musicchain-player-v0.9.6-arm64.apk— Android (arm64-v8a, signed)musicchain-player-v0.9.6-windows-x64.zip— Windows desktop player (x64)
v0.9.5 — DB2: gossip-replicated libraries + playlists (off-chain, wallet-keyed)
v0.9.5 — DB2: gossip-replicated libraries + playlists (off-chain, wallet-keyed)
A new off-chain store ("DB2") that propagates each wallet's library and playlists across every node the way the chain propagates blocks — but outside consensus. A player just says hi to the network; the node already holds the list. Groundwork laid in this release; built from main @ d310dd7.
Gossip-replicated libraries (off-chain, wallet-keyed)
- Each wallet's library is a compressed Roaring bitmap of song content-hashes (~2–4 bytes/song), in a separate leveldb keyspace outside the chain.
- The wallet is the sole writer of its own record → eventually-consistent gossip (wallet-signed deltas), no block ordering or quorum.
- The wire speaks 32-byte content-hashes, so nodes need no agreement on id numbering — ids are local intern handles.
- A reverse index (song → who-has-it) powers discovery; the player publishes its wallet-signed library on (re)connect.
Ordered playlists — same mechanism
- Wallet-signed, gossip-replicated ordered playlists (
mcpls1canonical bytes). - Player UI: playlists are the third facet — Artist / Genre / Playlists — in both the Discover and My Library tabs. Selecting a playlist fills the same track pane an album opens into; long-press / right-click a playlist chip to Add songs / Rename / Delete, and a "New playlist" chip creates one. Discover resolves a playlist's hashes to discoverable chain songs; My Library resolves them to your local files — each in the playlist's own order.
Anti-entropy (offline-node catch-up)
- The flood covers the online mesh; this converges a node that was offline during an edit. On peer connect each side exchanges a
{key→version}summary and pushes back every record the other is behind on (mirrors themod.sync_sincepattern). - Libraries now replace by version snapshot, so removals propagate — not just additive deltas.
- Idempotent re-publish on reconnect: edits made while the home node was unreachable still converge, without re-flooding unchanged records.
Artifacts
musicchain-player-v0.9.5-arm64.apk— Android (arm64-v8a, signed)musicchain-player-v0.9.5-windows-x64.zip— Windows desktop player (x64)
DB2 is entirely off-chain and additive — consensus, blocks, and the existing chain are unchanged.
v0.9.4 — play rewards + fingerprint matcher + node.hello connectivity gate
MusicChain v0.9.4
Android APK (arm64-v8a) attached — sideload musicchain-player-v0.9.4-arm64.apk.
Play rewards fixed
A buggy container/ID3 tag could register a garbage song duration (e.g. 184 h). Because the play-reward gate needs 50% of the registered duration, such songs were permanently un-rewardable — every real play scored "below_threshold". Now sanity-gated on both sides:
- player: reject implausible durations at registration (prefer tag, fall back to PCM math, else 0)
- node: treat an implausible on-chain duration as unreliable and fall back to the fixed legacy listen threshold — so songs already registered with a bad duration reward again on a genuine play
Fingerprint matcher fixed (false positives + slow validation)
- offset-alignment bug in
similarity()(mixed match-count from one offset with overlap-length from another — inflated different-song scores AND deflated same-song transcodes) - minimum-overlap guard (kills silence-tail flukes)
- same-song threshold 0.55 → 0.70, hoisted to one shared constant used by both the consensus dup-check and the swarm-join probe
- swarm-join probe early-exits on first match (perf)
- player: only count a scan as "matched" when the node names a chain song with a real swarm
Network / consensus (Model 1)
node.hellofull-node identity handshake + connectivity-gated propagation (an isolated node holds INV/DHT fan-out and flushes when a full-node peer appears; minting is never gated)- retired the
validator_enabledproducer/follower gate — every node mints on content (song blocks + tx-carrying heartbeat blocks), converging on the heaviest valid chain - discovery: the library re-announces on every network change (songs no longer go undiscoverable while still online)
Runs against a freshly re-bootstrapped genesis (the fingerprint/threshold change is consensus-affecting). Build full/mini nodes from source.
v0.9.3 — Model 1 + #10 relay triangulation + attestation + mesh/crash hardening
MusicChain v0.9.3
Android player APK (arm64-v8a) attached below — sideload musicchain-player-v0.9.3-arm64.apk.
Highlights
- Model 1 deterministic consensus — vote-free; blocks re-derived from content + heaviest-valid-chain fork choice (genesis is the only operator-seeded block).
- #10 relay-reward triangulation — per-byte relay credit via broker
delivery_id+ signed minirelay.report+ signed playerrelay.receipt(1 MC / 10 MB). - Structural device attestation (#5) — hardware fingerprint (desktop native FFI / Android ANDROID_ID+Build) inside the wallet-signed bundle +
session.start. - DHT player-to-player transfer un-nerfed — VPS-empty gate removed + DHT sources are now dialed (connect-then-validate).
- Mesh stability — 16 verified instability bugs fixed (pending-relay reaper, route race, reconnect hysteresis, propagator trims, etc.).
- Android crash hardening — wallet FFI NULL-deref, exceptions escaping FFI callbacks, binary-frame bound, feed() ANR, defensive JSON casts, JNI
GetShortArrayElements.
Full node + mini-node build from source (cmake --build build-linux). Chain format is v3 (consensus-breaking; wipe prior data).
v0.9.2 — 24-agent hardening (mini-nodes-are-routers)
Hardening pass landing the "mini-nodes are routers" architecture cleanly. Web/browser surfaces removed; librats frozen at v0.2.0; mini-node + full-node + player all tightened against the failure modes seen in the prior week.
Cellular wedge fix (three layers)
- Android
REQUEST_IGNORE_BATTERY_OPTIMIZATIONSone-shot prompt so Doze stops throttling the Dart isolate - Discovery refresh dropped 60s → 20s + paired bidirectional
mini.pingto fill the gap between librats's 15s TCP keepalive and the application-layer heartbeat (carrier NAT was silently closing idle mappings in that window) - Watchdog evicts stale
_miniNodePeerIdsand_relayViawhenvalidatedPeerIds.isEmptyso the next refresh repopulates from scratch instead of routing to ghost peer_ids
Mini-node (router) hardening
- Routes table now has a 10-min TTL + 30s reaper thread; was: routes lingered forever after the publishing full node disappeared
- 'F'-tag binary relay token-bucket rate-limit (50 MB bucket, 10 MB/s refill per peer)
- Dead-route eviction on
relay.forwardsend-failure +{status:dead_route}reply so the player re-picks a different full node immediately RatsLinkwatchdog tick 3s → 1s; route republish on every validated-peer count increase (not just first-up); eager 127.0.0.1 bootstrap in addition to the colocated-VPS loopback fallback (librats blocks dialing own public IP)
Full node (server/tracker) hardening
SwarmIndex::evict_peerwired to disconnect callback so online entries no longer linger 20 min after a silent socket deathrats_announce_for_hash(sha1(content_hash))fires on everyfingerprint.submit— DHT-based content discovery is now live on the seeder sideRelayCreditTrackerpre-capscountat 1M per tx (chain ceiling) with carry-over of the overflow into the next sweep
Player hardening
bestMiniNodePeerIdpicks lowestload_score(was: arbitraryvalidatedPeerIds.first)- Direct-when-reachable carve-out: desktop player whose own routes.get echo shows
reachability=='direct'tries direct librats dial before falling back to relay findContentSeeders(contentHash)adapter delegates tofindHashHolders(sha1(...))for DHT content lookupstream.opendisk-streamed (was:file.readAsBytesloaded whole song into RAM); 4-chunk pacing; cancel API wired to peer disconnect- Disconnect now triggers
cancelStreamsForPeerso the player stops blasting bytes at a dead receiver
Removed
musicchain_web/entirely (browser/web player path abandoned)ws_mini_gateway,ws_audio_bridge,ws_tcp_relay,ws_bridge,audio_fetch_handler(browser-facing C++)- All browser-protocol docs under
musicchain/docs/ - Full-node
--ws-portflag and the port 9090 WebSocket bridge
Frozen
deps/librats/reset byte-for-byte to v0.2.0 (commit 246557c); future limitations are wrapped at the call site (rats_link.cpp,rats_client.dart), never inside librats
Docs + memory
- New canonical reference:
musicchain/ARCHITECTURE.md - Memory bank updated:
feedback-no-web-version,feedback-dont-modify-librats,feedback-load-aware-routing,feedback-no-stale-peer-routing,project-architecture-doc
APK
Bundled below: musicchain-player-v0.9.2-arm64-v8a.apk (87.5 MB). Install:
adb install -r musicchain-player-v0.9.2-arm64-v8a.apk
First launch will prompt for battery-optimization exemption (Doze).
Known limitations (also tracked in ARCHITECTURE.md §8)
- RELAY_REWARD still per-stream, not per-byte
- No application-layer ACKs on binary relay (backpressure is time-paced, not credit-based)
- Single VPS in current dev/test deployment; multi-VPS mesh ready in code but not exercised here
v0.9.1 — BlockPropagator + dynamic quorum + relay-credit + wallet save
musicchain v0.9.1 — BlockPropagator + dynamic quorum + relay-credit + wallet save-to-file
Core (musicchain):
- Replace SyncManager with BlockPropagator: bitcoin-style block distribution (block.hello / getblocks / inv / getdata / data) over librats typed messages, with DHT bootstrap and per-block DHT announce for multi-source catch-up.
- Dynamic confirmation quorum scaling with peer count: solo = 1, +1 per peer, capped at MAX_CONFIRMATIONS = 5.
- Block.signing_hash() so verifiers check signatures against the header-minus-confirmations hash that producers actually sign.
- Mini-node carries its wallet in mini.hello so the full node can credit the right address on RelayRewardTx via RelayCreditTracker.
Player (musicchain_player):
- Wallet first-launch show-seed step gets a Save to a file button alongside Copy to clipboard (uses file_picker bytes for mobile sandbox, manual writeAsString for desktop).
- Direct librats connect to the home node from LibratsDiscovery so player RPCs route peer-to-peer rather than tunneling through the VPS.
- NDK bumped to 28.2.13676358 to match the :jni plugin.
APK: arm64-v8a only, ~83 MB, signed with the debug keys (replace before production).
v0.2.0 — moderator system, offline play-proof
Squashed v0.2.0 release. See the README for build instructions per target.
What's new since v0.1.0
- Chain-level moderator system (FOUNDER / OP / VOICE) with passphrase founder bootstrap and no on-chain handles.
- Offline play-proof: continuous local heartbeat + network / BSSID / battery / screen-on capture, signed bundle posted via
offline.play_proof.submiton reconnect. Ten bot-pattern heuristics stubbed for the node-side validator. - Player UX: album-play queues the full sorted album, library rows show play count, Connect button long-press disconnects, banner shows full rats peer id + STUN-observed public address.
- Per-target build scripts under
scripts/for all five binaries plus systemd install scripts for the two Linux node modes.
Binaries
| Target | File |
|---|---|
| Windows full node | musicchain-node-windows-x64.zip |
| Windows desktop player | musicchain-player-windows-x64.zip |
| Android player | musicchain-player-android-arm64.apk |
| Linux full node | musicchain-node-linux-x64.tar.gz |
| Linux mini node | musicchain-mini-node-linux-x64.tar.gz |
musicchain v0.1.0-20260610
musicchain — peer-to-peer music with on-chain royalties
I've been quietly building a peer-to-peer music platform for the last
few months. First public binaries are up, multi-container audio support
landed today, and the system finally feels coherent enough to write
about. Here's what it is and where it stands.
The shape of it
Songs live in two places at once. The chain holds a tiny block per
song: a Chromaprint fingerprint, content hash, ID3 metadata, royalty
splits, and a few timestamps. About 3 KB per block — small enough that
a Spotify-scale 100 M-track catalog would weigh in around 310 GB on
chain, same order of magnitude as a portable backup drive.
The audio bytes live with players. When you scan a folder, the
player computes the fingerprint locally and either matches you into an
existing swarm or registers a new block. When you press play on
someone else's track, the chain tells you which peers are currently
serving it and your player pulls the bytes peer-to-peer over librats.
That bisection is the whole trick. The chain is the catalog and the
royalty ledger; the swarm is the CDN.
Containers shipped today
mp3, ogg, flac, m4a, aac, opus, wav, aiff, wma, ape, mka — anything
libmpv decodes, the player scans and serves. Format detection rides on
extension, stamps into a 1-byte enum on the song block, and
round-trips back as the right file extension on download so other
players hand the right bytes to their decoder.
Discovery, online-only
There's no central song registry. The Discover tab is generated live
from "who's connected right now and what fingerprints did they vouch
for." Close the phone app — peers see your songs vanish from their
Discover within seconds. Open it again — they reappear.
The state pipeline runs end-to-end on rats connection callbacks plus a
small delta protocol: an unchanged library re-announces itself in one
96-byte round trip — a SHA-256 digest, no list bytes. New file? Single
swarm.add event. Delete a file? Single swarm.remove. The
"flood every fingerprint on every boot" pattern that the prototype
shipped with is gone.
The mini-nodes — our VPSes — gossip routes and online-state between
themselves over a librats mesh. Stand up another bootstrap VPS, hand it
a --peer-vps flag pointing at any existing one, and the mesh figures
out the rest. No single point of failure on the catalog surface.
Royalty model
Pre-10 000 plays per song (the discovery tier):
- Listener earns 1 token as discoverer
- Serving node earns 1 token
- Artist's 1 token routes to a per-artist escrow, releasable by a
moderator. Quality gate for the first decade of plays.
Post-10 000:
- Artist and serving node each get 1 spendable token
- No discoverer credit
- Listener burns a dynamic amount that scales cubically with total
supply between a 1 B floor and a 2 B hard cap — zero burn while the
network is bootstrapping, exponential pressure as we approach the
ceiling, mint refused entirely past it.
A play counts when timestamped heartbeats prove the listener consumed
at least 50 % of the song's unique timestamp range. Replay-the-chorus-
on-loop earns nothing; only union coverage of distinct seconds counts.
Seeking around still works as expected — listening to seconds 0–60 and
then 90–150 is 120 seconds of credit, not the wall-clock duration.
NAT and transport
librats over TCP today, with a custom keepalive (~30 s dead-socket
detection) and a duplicate-peer-eviction patch for the dial→disconnect
storm we hit when the watchdog re-dialed faster than half-open cleanup.
Cellular peers can't ICE-punch through symmetric NAT on the
US carrier we're testing on, so cellular traffic falls back to VPS
relay — TURN-shaped, with a binary forward path for audio chunks so
relayed downloads don't pay a base64 tax. Direct rats connections still
work on home wifi and most non-cellular networks.
Planned but not in this release: QUIC + libdatachannel ICE signaling
for proper hole punching on phones.
What this isn't
Honest list:
- One moderator. The escrow-release model needs a real council before
it scales beyond hobby use. - No algorithmic discovery. Discover is a live chain catalog filtered
by who's online — no recommendations, no playlists from someone
else's taste graph. - Mobile peer connections relay through VPS on symmetric cellular NAT.
- Search is plain-substring against title / artist / genre / album.
Where to try it
Binaries on the latest GitHub release:
musicchain-player-windows-x64.zip— Windows desktop. Unzip
anywhere, runmusicchain_player.exe.musicchain-player.apk— Android sideload. Needs READ_MEDIA_AUDIO
(Android 13+) or "All files access" for folders outside/Music.
Workflow:
- Create a wallet (one password, auto-loaded thereafter via the OS
keyring). - Open My Library, tap the folder-plus icon, add a music folder.
- Tap refresh — files get fingerprinted on the device and join the
chain catalog. Songs that match existing chain entries swarm-join
silently; new ones queue for the next block. - Switch to Discover. Pick an artist or genre, drill in, pick an
album. The track list opens in the bottom pane (drag the handle to
resize). - Tap a track to stream, or long-press / right-click an album to
download the whole thing into your library.
If you find a song you want to keep, hit download — it'll pull from
the swarm and add to your local library. From there, you serve it too.
That's the entire growth model.
Built solo, in C++ for the home node + VPS mini-node and Flutter for
the player. Comments, issues, and "this crashed when I…" reports
welcome on the repo.