Back to list
Development Update — August 11
Today’s headline is a measured, fleet-wide efficiency win: a single quiet-feed keepalive that ends a churn pattern which had been re-handshaking healthy CXO connections every ninety seconds across the whole network — and, built on it, moving dmsg client-entry registration off its periodic HTTP-over-dmsg re-PUTs and onto a persistent CXO feed. Both attack the same cost: the Noise + post-quantum handshake that was dominating dmsg-discovery’s CPU. Alongside them the browser dmsg carrier finally converges from WebSocket to WebTransport, and the in-tab wasm visor grows a real shell. On the Skycoin side, the skycoin-web wallet’s unit-test suite came back from a single working spec to 1146 — reviving the golden-vector check that a browser-restored wallet holds the same coins as a node-restored one — the skycoin-lite wasm cipher stopped swallowing its own panics, and the cipher artifact learned to report the commit it was built from.
Skywire: The Ninety-Second Churn
3826 feat(cxo): quiet-feed heartbeat republish to keep subscriber connections alive fixes a churn that was tearing down and re-dialing CXO connections — a full noise + ML-KEM-768 handshake — every ~90 seconds on any quiet feed, fleet-wide. The root cause is a dead keepalive: the CXO node’s idle watchdog closes a Conn after 90s with no inbound message, but no keepalive is actually sent — conf.Pings (118s) is dead config no transport consumes, and 118s > 90s regardless — so a healthy connection whose feed simply hasn’t changed in 90s (transport graph, dmsg-server list, idle telemetry) looks dead and gets re-handshaked on next use. Fleet-wide that churn dominated the dmsg-discovery host’s CPU (measured ~90%, almost entirely GC of the ML-KEM key generation and secp256k1 handshake allocations) and inflated dmsg-server inbound bandwidth. The fix has the publisher re-publish its current Root as a no-change heartbeat when a feed goes quiet, so subscribers keep receiving an inbound Root within the idle window and hold the connection open. The interval is 45s — under both the 90s node watchdog and the 60s subscriber reconnect-on-quiet watchdog, with margin — and busy feeds publish via the dirty path and reset the clock so they never pay it, while a quiet feed’s republish is a warm encode-cache walk with no re-serialize. It’s rollout-safe because the heartbeat is an ordinary Root every node already handles (no new message type, no unknown-type disconnect in a mixed-version fleet), and it applies to all publishers via the shared treestore.Publisher, so both ends heartbeat and the connection stays alive bidirectionally.
Skywire: Registration over CXO
3828 feat(dmsg): registration-over-CXO lands the foundation: dmsg client-entry registration rides periodic HTTP-over-dmsg PUTs to dmsg-discovery, and each PUT is a fresh dmsg stream with a full noise + ML-KEM-768 handshake — fleet-wide, those PUTs dominate discovery CPU for the same reason the quiet-feed churn did. This moves registration onto a persistent CXO feed kept warm by the #3826 heartbeat: a visor registers once and re-publishes only when its delegated-server set actually changes, mirroring the telemetry→TPD fan-in. It ships as an idempotent dual-write — a visor publisher on a new registration CXO port, a dmsg-disc aggregator modeled on the proven TPD cxoaggregator, and an IngestEntryFromCXO that mirrors setEntry’s validation but treats a stale-or-equal sequence as a silent no-op so HTTP and CXO never conflict (ValidateIteration requires strictly greater, so whichever path lands first wins), with a visor only permitted to publish its own client entry. 3829 feat(dmsg): registration-over-CXO always-on + HTTP keepalive stretch turns it from opt-in dual-write into the real load reduction and drops both gates — there is one dmsg-discovery, so a server-side flag buys no A/B, and the aggregator is inert without publishers. The actual win: while the visor’s CXO announce-conn to dmsg-disc is healthy, the dmsg client stretches its periodic no-change keepalive re-registration from ~1 minute to 30 minutes (well under the 60-minute entry TTL), cutting the periodic handshake rate ~30×; delegated-server changes still publish immediately over both paths, and if the feed conn drops health falls back within a 95s window and the frequent HTTP keepalive resumes. CXO carries the keepalive between the rare HTTP re-PUTs by refreshing the stored entry’s TTL and uptime heartbeat on an equal-sequence Root. Registration can’t break because HTTP stays authoritative and, at a 30-minute re-PUT, keeps the entry alive by itself even if CXO ingest silently stalls.
Skywire: wss → WebTransport, Finally
3830 fix(dmsg): browser wss→WebTransport upgrade actually converges fixes a browser wasm visor staying on wss forever, even to dmsg servers that advertise WebTransport. A browser must bootstrap over wss — the seed entries carry only a stable wss:// domain URL, and a server’s WT cert is self-signed with a ~2-week life so its cert hash rotates and can’t be baked into the seed, meaning WT capability and the current hash are knowable only from live discovery reached after the wss bootstrap. The old upgrade routine never used that: it only closed wss sessions and relied on Serve re-dialing WT-preferred, but Serve re-dials from the ws-only seed list, so it never saw AddressWT and just re-dialed wss — a permanent lock, confirmed live on the harness holding wss to two servers that both advertised WT in discovery. UpgradeBrowserSessions now re-resolves each server’s current discovery entry and dials a WT session from that fresh entry; newest-session-wins means a successful WT session atomically replaces the wss, a WT failure falls back to wss, and the server is backed off 5 minutes so an unreachable or UDP-blocked WT listener doesn’t thrash — replace-in-place, so the session count never drops.
Skywire: A Real Shell in the Visor Tab
3823 third_party: vendor the browser-terminal Go ports brings in the libraries behind an in-browser terminal — a Go/wasm port of xterm.js 6.0.0, the websh shell with ~45 applets, and js/wasm forks of mvdan.cc/sh and spf13/afero — following the existing third_party/ pattern (source in place, no nested go.mod, excluded from lint), wired into nothing yet. 3824 wasm-visor: a real shell in the visor tab then replaces the bespoke one-command-per-line “visor cli” REPL with websh: a Bash/POSIX interpreter over a virtual filesystem drawn by the xterm.js Go port, with the visor’s API as applets (pk about visors net health apps tps routes hvapi) whose JSON output pipes into the shell’s own jq, awk, grep, sed and friends. The elegant part is that it needs no second wasm binary: a terminal needs a DOM but the visor normally runs in a DOM-less SharedWorker, so this one wasm carries both roles and picks at startup — role visor (default, unchanged) versus role shell (installs only skywireShell.open(el), skips the visor entirely) — instantiated lazily from the wasm URL the tab already caches, with the applets calling hvCore.ServeHTTP directly when this instance is the visor and the public proxy otherwise. 3827 chore(deps): update dependencies pins skycoin and refreshes the module graph underneath all of it.
Skycoin: The Wallet’s Cipher Suite, Revived
3020 skycoin-web: revive the unit test suite (1 spec → 1146) brings back a suite that had rotted to a single working spec: the other 61 were excluded from the build because they no longer compiled — async from @angular/core/testing (removed in v12), the split @angular/material barrel, @angular/http (removed in v8), and no strict-mode annotations anywhere. All 62 compile and pass now (1146 specs in ~17s), and three of the revived failures were real bugs: CipherProvider read .error off whatever the wasm returned and threw a TypeError because a bad seed came back as null; HistoryService built BigNumbers straight from API fields and bignumber.js v11 throws where v10 returned NaN, so viewing history with an unconfirmed transaction (no calculated_hours on its inputs) could throw; and an onboarding component dereferenced a non-static @ViewChild during the first change-detection pass, before the child resolved. The suite that made it worth doing is cipher.provider.lib.spec.ts, which checks the browser cipher against src/cipher/testsuite/testdata — the same golden vectors the Go implementation is checked against, 1000 addresses plus 11 seed chains — staged from the Go testdata rather than kept as a drift-prone copy, because nothing else in the tree verifies that a wallet restored in the browser holds the same coins as one restored by the node, and a break there is silent. 3021 skycoin-lite: stop the wasm cipher swallowing its own panics is the cause behind the CipherProvider failure: every entry point in main_wasm.go recovered from a panic into an unnamed return, so the deferred function could not change it — and everything under liteclient reports failure by panicking (the Must* constructors, the verification helpers) — so a bad seed or malformed input reached JavaScript as null rather than the {error: string} every caller is written against. Named returns carry the recovered value out now, and the PR restores SkycoinCipherExtras (the nine addressFrom*/verify*/signHash functions the GopherJS build had published all along, +155 KB against an already-linked package) so the signature half of the golden-vector suite can run against the cipher the wallet actually ships — with four new specs pinning the error contract directly, three of which fail against the previous wasm with Expected null not to be null, the bug reproduced.
Skycoin: One Copy of the wasm Cipher, and It Says What Built It
3022 skycoin-web: keep one copy of the wasm cipher, where it is embedded collapses a file that lived in four places — two Go packages read at runtime (standard Go and TinyGo, selected by build tag) and two hand-maintained bundle copies read by nothing — which had already drifted, the shipped copy matching neither generated artifact. cmd/skycoin-web answers /assets/scripts/skycoin-lite.wasm and wasm_exec.js from the embedded Go package ahead of the static handler, so every binary that serves the wallet goes through that path and the bundle’s copy was 4.7 MB of committed dead weight; the Go packages are the only copies now, the two paths that don’t go through the Go server (ng serve, karma) stage them into a gitignored path every run so the drift cannot return, and make build-wasm rebuilds both toolchains and puts each wasm_exec.js next to the wasm it belongs to. 3023 skycoin-lite: report what the wasm cipher was built from then makes the artifact self-describing the way skywire’s wasm-visor is: a go build inside a work tree already stamps the module version and the vcs.* settings into the binary, so debug.ReadBuildInfo() returns them at runtime with no ldflags injected, and main_wasm.go publishes that as SkycoinCipher.version with the short commit shown in the wallet’s top bar. It adds a make check-wasm that CI runs, rejecting any committed wasm reporting vcs.modified=true — a dirty tree that no commit describes and nobody can reproduce — which is a plain-text read of the stamp rather than a rebuild-and-compare (impossible here, since the commit a wasm is built at is always earlier than the one carrying it, and Go is not byte-reproducible across toolchain versions); it fails on the wasm currently on develop, built from a dirty tree in #3022, and the two-step fix — commit source, rebuild, commit wasm — is inherent to the mechanism. One caveat is recorded for the next day: TinyGo records none of this, so its artifact stays unstamped until the fork gets involved. Rounding out the day, 3019 corrects DEVELOPMENT.md to describe the release workflow that actually runs — a v*-tag GitHub Action that creates the release, builds each platform in a matrix, cross-compiles the hardware-wallet utility against musl, and uploads each archive with its .sha256 — replacing text that still credited Travis and a manual master-branch build against an electron/ client removed two PRs earlier, with a note that several pages under docs/ are generated and editing the staged copy silently disappears.
Skycoin: Off Webpack, Onto esbuild
The same builder swap that reached Skywire’s manager UI landed across Skycoin’s three Angular front-ends this day, retiring the deprecated webpack builder for @angular/build (esbuild). 3011 moved the block explorer, 3012 the desktop wallet (src/gui/static), and 3013 the web wallet — and with the last of them, all three are off webpack entirely. Each removes @angular-builders/custom-webpack, @angular-devkit/build-angular, and the directly-pinned webpack, and points build, serve, extract-i18n, and karma at @angular/build instead. The custom extra-webpack.config.js was deleted rather than ported — all it did was alias a Node polyfill that the esbuild builder doesn’t need. The security dividend is concrete: the desktop wallet’s npm audit drops from 10 moderate advisories to 3 and the web wallet’s from 14 to 3, because the removed alerts — including an elliptic advisory that had no patched version available at all — arrived through crypto-browserify, which existed only to satisfy webpack’s resolve.fallback and vanished with the config. 3015 fixes two errors that had been logging on every page of the desktop wallet, both predating the front-end work and found by audit rather than a test: the bignumber.js 10→11 bump (from #2990) made the constructor throw on input that v10 returned NaN for, so the validity check written for v10 — construct first, isNaN() after — was unreachable and an empty form field threw before it could be rejected.