-
Morphit v1.21.0
StableAll checks were successfulmorphit-ci / Supply-chain audit gate (every advisory triaged, none fixable in range) (push) Successful in 41smorphit-ci / TypeScript typecheck (sweep all workspaces) (push) Successful in 1m25smorphit-ci / apps/web svelte-check (svelte-kit sync + svelte-aware tsc) (push) Successful in 57smorphit-ci / Integration tests (real Postgres 16) (push) Successful in 9m14smorphit-ci / ansible-lint (playbook quality gate) (push) Successful in 57smorphit-ci / Smoke suite (run-smokes.sh, triple-pulse) (push) Successful in 1h12m35smorphit-release / Build + publish release tarball (push) Successful in 35m56sreleased this
2026-10-05 20:43:23 +00:00 | 3 commits to main since this releaseMorphit v1.21.0
A full security and privacy review of the whole codebase, done the way a hostile expert would: 19
independent reviewers, a red team against a running instance, a fresh threat model, and four rounds of
fixes, each checked by reviewers who had not seen the fix. Every fix comes with a test that was run
against the old code and seen failing.Upgrading: clearnet servers run
sudo morphit-ops upgradeas usual (see "Every node" below for the
new--questionsand--heals). Zero-clearnet servers must use the signed offline bundle — follow
"Zero-clearnet nodes" below step by step. There is one database migration (new indexes, and stored
push-subscription browser details removed); it runs by itself at start-up.New consensus rules switch on at 2026-11-01 00:00 UTC (by block timestamp, so every indexer agrees).
Please upgrade every instance before then. The rules are listed under "Marketplace" below.Indexer: things one user or one node could break
- One cheap on-chain operation could stop every indexer for good. Two duplicate fee-bearing operations
in one transaction, or a deeply nested JSON operation of about 6 KB, made every indexer retry the same
block forever. Both now fail only that operation; indexing goes on. - A single RPC node could feed the indexer unsigned "official" operations (a release record or a new
RPC node list) and so add its own nodes to the pool or change the treasury. Official operations are now
accepted only with a valid signature from the pinned key. - Trusted reads (posting keys, snapshots) need operators to agree, and a stale answer never wins. A
value is confirmed only when at least two operator names agree and form a majority; an answer that is
provably older is overruled, and a lone disagreement delays confirmation instead of deciding it.
Operators are counted by node name. - One node answering errors no longer freezes indexing; the pool moves on to the next node.
- Peer price federation works. A field-name mismatch meant no peer price was ever counted.
- Memory and load limits: RPC replies are capped by what was asked for, account-history reads have
per-client and total limits (413 reply_too_large,503 history_busy), the indexer has a 1536 MiB
memory cap, and the database gets per-request timeouts, missing indexes and no JIT stalls.
Privacy
- BunkerWeb contacts no one. As shipped it sent blocked visitors' IP addresses, full URLs and request
headers to its maker's service, looked every visitor up in public DNS blocklists, and downloaded lists and
databases from third parties. All of that is off, on new and upgraded servers alike. - Fee checks and prices go over Tor first. BTC fees are checked with onion block explorers, XMR fees
with onion xmrblocks explorers, and BTC/XMR prices come from the agreeing median of five Haveno and Bisq
price nodes over Tor. Clearnet sources are only a fallback, and never used on zero-clearnet nodes —
which now offer BTC and XMR fees again. - The relay no longer writes visitor network prefixes or account names to its log, stores no browser
details with push subscriptions, and only sends Web Push to the real push services. - The browser's release check: two small requests to one node, once a day, shared by all tabs. The
browser now checks the release signature itself, so one node cannot forge it, and a release older than
the running site never supplies fee addresses. - Nothing about who you are survives Sign Out or a "just this session" sign-in in another tab, the
profile cache or chat state; account-named server answers are no longer cached by the browser. - The public health pages show only status; host memory, disk and counters need the local header.
- Every instance uses its own address in page links and previews; zero-clearnet instances carry no
clearnet address at all.
Chat
- Messages now prove who sent them. New messages use an authenticated format, so the operator's server
cannot insert a message "from" the other trader. Older messages still open but are marked unverified and
never offer "Pay now" or change a trade. - "Verify peer" is a 60-digit safety number (it was 64 bits, which a determined attacker could match).
Compare once more after upgrading. - A changed chat key always asks you first, Lock keeps your comparisons, and a replaced key can only
open old messages.
Keys and sign-in
- The page's Content-Security-Policy no longer allows inline or eval'd scripts.
- Removing or hardening a YubiKey can no longer be undone by a later password change; the YubiKey
path also asks for your 2FA code when 2FA is on. - Decrypted keys are no longer written to disk during a page reload.
Marketplace
- A stranger can no longer mark your order "paid"; payments are checked against the amount you asked
and the actual counterparty, and a payment with no amount asked shows as "received", not "paid". - Prices shown in the app are live, from the indexer; when none is known the pay field stays blank.
- Rules that switch on at 2026-11-01 00:00 UTC: deeper-than-64-level payloads and U+FFFE/U+FFFF text
are rejected; a review must cite an order the two of you discussed or completed; attestor loyalty counts
only the treasury's share; feature bids under the step are queued, and only live, paid orders rank;
display and operator names are checked for look-alikes of reserved names; profile fields are validated;
chat read receipts are bounded; an order replacement offering only disabled payment methods is refused.
Servers
- Services no longer run as root where they don't need to, and the service account can no longer gain
root through files it owns. - TLS certificates renew on BunkerWeb servers (they could not before), and hardening no longer cuts off
Docker's forwarding. - Zero-clearnet servers enforce it: only Tor and i2pd may reach the internet; DNS goes nowhere but the
box itself; the Matrix alert bot is refused unless it goes over Tor to an onion homeserver. - Upgrades verify the release against the on-chain record or the pinned signing key on every path, and
zero-clearnet upgrades install nothing from the internet. - Branded instances get their own link-preview image, drawn from their logo and name.
Repository
- Internal working notes, personal details and process records are no longer in the public repository.
The threat model (docs/audit/2026-10-*) and docs were rewritten to match the code; claims that were not
true were corrected or removed. - Vulnerability reports: Matrix direct message to
@agorise:matrix.orgonly (see SECURITY.md).
Every node: questions and repairs after the upgrade
The repair part of the upgrade never waits for an answer now. A question asked part-way through could hold up
the upgrade or cut it off. On the upgrade to this release, the old upgrader's own two questions (described below) still
wait unless you pass--yes.Where the repairs used to ask, they now take the safe choice and tell you:
- the journal is left as it is;
- on a tor-only node, a Matrix bot on a clearnet homeserver is stopped;
- plain-text backups are left as they are.
sudo morphit-ops upgrade --questions, run from a terminal, asks those questions. Each upgrade lists the
questions it left on its last lines.When a line says "the next
sudo morphit-ops upgradetries again", that is now true even when you are already on
the newest release. A plainsudo morphit-ops upgradeon an up-to-date box prints "✓ Already on the latest
release." and then re-checks this release's repairs; nothing is downloaded or installed.--check-onlyand--json
still only look.sudo morphit-ops upgrade --healsdoes the same directly, without checking for a release first. Use it when a
line asks you to, or when an upgrade says its repair step ran out of time.Tor-only node, Matrix bot kept on a clearnet homeserver (
KEEP-CLEARNET): the rule that lets only Tor and i2pd
reach the internet is then not loaded on that node, and the upgrade says the node is not zero-clearnet while the bot
is kept. The rule would cut the bot off, including the DNS lookups it needs. To get the rule:- Move the bot to a homeserver on the node or a
.onionone:sudo morphit-ops matrix setup. - Run
sudo rm /etc/morphit/matrix-bot.tor-only-decision. - Run
sudo morphit-ops upgrade --heals.
Zero-clearnet nodes: upgrade from the signed offline bundle
On a zero-clearnet node, do not run the plain upgrade for this release. That includes menu option 2 and a bare
sudo morphit-ops upgrade. The upgrader already on your box (v1.20.3 or older) would then install the packages withnpm ci
from the npm registry over the clearnet.Use the offline bundle instead. The release job builds it, and it is signed with the release key your box already
trusts, so nothing on the box touches the clearnet and npm is not used.On your own computer (any computer with internet), download the two files from the release page. The bundle is
about 250 MB: 246.5 MB measured when it carries its Node.js and Kubo runtimes. Without the runtimes it is
135.0 MB; Kubo v0.42.0 is 54.6 MB and Node.js v22.22.2 about 57 MB.curl -fLO https://git.agorise.net/agorise/morphit/releases/download/v1.21.0/morphit-v1.21.0-offline.tar.gz curl -fLO https://git.agorise.net/agorise/morphit/releases/download/v1.21.0/morphit-v1.21.0-offline.tar.gz.ascCopy both to the node, logging in the way you normally do. Use
scp -O, not plainscp:scp -O morphit-v1.21.0-offline.tar.gz morphit-v1.21.0-offline.tar.gz.asc <you>@<your-node>:/tmp/On the node, in a terminal:
-
Check that the node can verify the signature. Each command must print something, and neither may say
"No such file" or "command not found":gpg --version | head -1 ls /opt/morphit/.forgejo/release-signers/agorise.asc -
Run the upgrade from the bundle.
--yesanswers the two questions the old upgrader asks; they would
otherwise wait for you with no time limit:sudo morphit-ops upgrade --from-file=/tmp/morphit-v1.21.0-offline.tar.gz --yesYou should see these three lines, in this order:
✓ Integrity verified (gpg-signature).Offline bundle detected (prebuilt node_modules) — skipping npm ci; no registry needed.Using the prebuilt web frontend shipped in the release (no rebuild).
Let it run until
✓ Success — your Morphit server is now running v1.21.0(about two to three minutes).The rest of the output comes from the old upgrader already on your box, which cannot be changed. Read it like this:
- Without
--yes, it asks "Apply upgrade from v1.20.x to v1.21.0? … This will: … run npm ci, rebuild + redeploy
the web frontend …". That text is old. With the bundle, nothing is installed from npm and nothing is rebuilt
(see the two lines above). Answery. - Without
--yes, a box that has never had a warrant canary may also ask "Set one up now, right here on this
box? …". Answern. The upgrade then goes on, and you can set a canary up any time later with
sudo bash /opt/morphit/scripts/canary/setup.sh. - If your Matrix bot is set up, it prints "Matrix alert username configured — enabling + restarting
morphit-matrix-bot…". On a tor-only node whose bot uses a clearnet homeserver, ignore it. The bot refuses to
start, and the background checks (step 4) switch it off again. To keep the bot on its clearnet homeserver
anyway, use step 3. - It ends with "Congratulations! … nothing else to do". That is not true for this release: carry on with
step 3.
-
Answer the questions the upgrade did not stop for: the old relay log lines, the Matrix bot, and plain-text
backups. Any it does not need to ask, it skips:sudo morphit-ops upgrade --questions -
Read what ran after the services restarted:
sudo cat /var/log/morphit/after-upgrade-heal.logThe last line must be
Done.. If it is not, the checks are still running: they wait up to 15 minutes for the
services to restart and then up to 10 minutes for the background web repairs. Wait a minute and read it again. -
Once
Done.is there, run the repairs once more. This picks up whatever had to wait until Docker pulls through
Tor, which the checks in step 4 set up. One example is the frontend's newer nginx base, if a line said the
frontend "stays on its current nginx base for now". Nothing is downloaded over the clearnet:sudo morphit-ops upgrade --heals -
Remove the two files:
rm /tmp/morphit-v1.21.0-offline.tar.gz /tmp/morphit-v1.21.0-offline.tar.gz.asc
If something goes wrong:
Cannot verify the integrity of release v1.21.0: nothing was changed. The two files must sit side by side
under exactly these names. Download the.ascagain and repeat step 2. If the same error also says "No on-chain
hash available … Start it with: sudo systemctl start morphit-indexer", ignore that hint. the old indexer never
stores the bundle's hash, so starting it does not help. Only the.ascdoes.Could not import release-signer key agorise.asc: gpg could not start its helper. Run step 2 as
sudo env TMPDIR=/tmp morphit-ops upgrade --from-file=/tmp/morphit-v1.21.0-offline.tar.gz --yes.- A line starting
The upgrade stopped its heal step at the time limit: wait until the upgrade has finished,
then runsudo morphit-ops upgrade --heals.
What this upgrade changes on installed servers, by itself
-
BunkerWeb no longer contacts anyone. These were already off: BunkerNet, DNS blocklists, the reverse-DNS
black/white/grey lists, the anonymous report and third-party captchas. Now:- its daily GeoIP download (db-ip.com), release check (api.github.com) and "preview" Pro-plugin download
(assets.bunkerity.com) are replaced: the upgrade mounts Morphit's own job lists into BunkerWeb's scheduler, and
country rules use the GeoIP file inside the BunkerWeb image; - reverse scan and plugin downloads stay off;
- BunkerWeb's in-memory list of the last 100 blocked requests is off (
USE_METRICS=no). That list held each
visitor's address, full URL and browser with no time limit.
- its daily GeoIP download (db-ip.com), release check (api.github.com) and "preview" Pro-plugin download
-
BunkerWeb's blacklist is off, and part of it cannot be replaced. BunkerWeb checks every blacklist entry only
after a reverse-DNS lookup of each visitor's address, which tells outside DNS servers who visits your site. With
the blacklist off:- Addresses and networks you listed (
BLACKLIST_IP) keep working. The upgrade moves them into an nginxdeny
rule (CUSTOM_CONF_SERVER_HTTP_morphit_ip_blocks) and says so. - Network (ASN) blocks, reverse-DNS name blocks, user-agent and URL lists no longer apply. The upgrade names
each one it finds. Ansible-installed servers hadBLACKLIST_ASN=AS14061 AS24940 AS16276(DigitalOcean, Hetzner,
OVH); visitors from those networks, VPN users among them, are no longer turned away. - The Tor-exit list and the downloaded bad-bot list are no longer used.
- Country blocks (
BLACKLIST_COUNTRY) still work, from the GeoIP file on the server.
- Addresses and networks you listed (
-
Kubo (IPFS) takes no settings from the internet. AutoConf, HTTP routers (cid.contact, delegated-ipfs.dev) and
DoH resolvers are off. A clearnet node seeds over the public DHT, joined through the standard bootstrap peers. -
Ubuntu's own fetches a server does not need are off on every node:
- the login banner's news (motd.ubuntu.com) and apt's Ubuntu Pro news;
- the release-upgrade check at login (changelogs.ubuntu.com;
Prompt=neverin
/etc/update-manager/release-upgrades). To move to a new Ubuntu release later, set it back toPrompt=ltsand
runsudo do-release-upgrade; - fwupd's firmware-list refresh and pollinate;
- rkhunter's data-file update. Ubuntu's rkhunter cannot download one anyway; its data comes with the apt package.
snapd and Ubuntu Pro's status check stay on: installed snaps and a Pro subscription need them.
-
Zero-clearnet nodes can reach their own local network again. The rule that lets only Tor and i2pd reach the
internet now also allows the local network (10/8, 172.16/12, 192.168/16, IPv6 fc00::/7), so a NAS, a printer or
your laptop work as before. DNS goes nowhere but the box itself: it is refused to the local network, to
link-local addresses (home routers often announce their fe80:: address as the resolver) and to the internet, over
IPv4 and IPv6, from every program — Tor, i2pd and the containers included (none of them needs it). -
No Docker Hub download on a zero-clearnet node. The frontend moves to this release's pinned nginx base image
only when that image is already on the box, can be loaded from avendor/docker/file you put there, or can be
fetched with Docker going through Tor. Otherwise it keeps its current base and says so; once Docker pulls through
Tor,sudo morphit-ops upgrade --healson the server switches it. (The release's offline bundle carries no Docker
images.) -
Running the indexer and relay as their own users (not root) is now checked where each one really listens, and
each gets up to two minutes to start. A service that runs as its user but has not answered yet keeps running as
its user, and the upgrade says so. Only a service that is down, keeps restarting or runs as someone else is put
back on root. -
The indexer has a memory cap (1536 MiB, systemd
MemoryMax), like the relay (512 MiB) and the MCP server
(256 MiB). A burst of large requests can no longer take the whole box. The cap is about twice the most the indexer
was measured to use honestly.sudo morphit-ops upgradechecks that the running indexer carries it; if the
indexer happens to use more than 80% of the cap at that moment, nothing is changed and the upgrade says so.
The indexer now reads its own cap, so the optional flow catch-up sizes its buffer to fit. -
Served web files stay with the canary user. The service-user heal no longer hands
apps/web/staticback to
root (it already leftapps/web/buildalone).
Releasing (maintainer)
The offline bundle. The release job now builds it itself, without Docker, and attaches it with its
.sha256.
Without the bundle the release is not published. The job signs the tarball and the bundle only when the
MORPHIT_RELEASE_SIGNING_KEYsecret is set; otherwise it attaches no.asc, and a node running this release
installs later ones by the SHA-256 in @morphit's signed on-chain record. For this release only, the maintainer signs
the bundle and attaches its.asc, because the upgrader on v1.20.x zero-clearnet nodes accepts the bundle only with
a signature.The bundle carries no OS packages or Docker images. A fully offline fresh install still needs the full appliance
bundle:bash scripts/build-offline-bundle.shon an Ubuntu 24.04 box with Docker. That build is optional and not
part of the release steps.Moved tags. The release job checks that the tag still points at what you signed. If a release for the tag already
exists, the job attaches to it only when it was built from the same signed tag object. A tag moved back to an older
signed object after publishing is therefore refused before anything is attached, and nothing is published for it.Block 4 also checks that the release job built the very tag you pushed in Block 2. If it stops with "the tag was
moved after it was pushed", do not broadcast. Delete that release on git.agorise.net, codeberg.org and gitea.com,
because its tarball, bundle and signatures are already published there.Neither check adds a command.
Downloads
-
Source code (ZIP)
0 downloads
-
Source code (TAR.GZ)
0 downloads
- One cheap on-chain operation could stop every indexer for good. Two duplicate fee-bearing operations
-
Morphit v1.20.3 Stable
released this
2026-10-02 04:48:05 +00:00 | 7 commits to main since this releaseMorphit v1.20.3
Visitors' browsers contact a Blurt node far less often, and pages download less. There are no
database changes.Privacy
- The release check reaches a Blurt node at most once a day. To make sure an operator is not
keeping visitors on an old build, each browser asks a public Blurt node for Morphit's signed
release record — the one request that leaves the site, and that node sees the visitor's IP. It
ran on every visit. A good answer is now remembered in the browser for 24 hours (and asked again
right after the site updates), so further visits that day contact no one. - That request is much smaller: it reads @morphit's last 100 history entries instead of 10,000
(a few KB instead of about 115 KB), and looks further back only if it must. - The FAQ says exactly what goes out ("Does anything see or leak my IP address…"), in every
language: at most once a day, two small requests.
Faster pages
- The served build's version file is downloaded once per page load, not twice. The release
check and the update check both need it, and it is about 80 KB.
Release fixes
- Zero-clearnet instances can always upgrade. v1.20.2 was first broadcast without its IPFS name,
and a Tor/I2P-only instance refused it. The release builder now always includes the name, warns
when a release has no IPFS address, and refuses an IPFS record left over from an earlier release.
A zero-clearnet upgrade also accepts a release located by its IPFS address alone. - The release build retries the IPFS tool download (and has a second source), so a slow
download no longer leaves a release without an IPFS address. - The ceremony's Block 4 clears values an earlier ceremony left in the terminal.
Upgrading
Run
sudo morphit-ops upgradeon each server. Nothing needs doing by hand.Downloads
-
Source code (ZIP)
0 downloads
-
Source code (TAR.GZ)
0 downloads
- The release check reaches a Blurt node at most once a day. To make sure an operator is not
-
Morphit v1.20.2 Stable
released this
2026-10-02 00:51:51 +00:00 | 8 commits to main since this releaseMorphit v1.20.2
Pages load faster and are easier to read, Bitcoin fees go to a separate address for each order,
Monero fee checks no longer depend on two websites, and the compare page works again. There are
no database changes.Faster pages
- Each page downloads about half the text it used to. The FAQ, the privacy guides, "run a
node" and the cheat sheet now load only with their own page. Before, every page downloaded all
of them (and, in any language but English, the English copy too): about 100 KB less per page,
and 200 KB less for a visitor reading another language. - The logo shows first. The browser now fetches the header logo before the app's code, keeps
its space reserved while it loads (nothing jumps), and downloads it once instead of twice. Its
occasional shine is drawn by the graphics card instead of repainting the logo. - Fonts: one font file was downloaded twice under two names; it is now downloaded once, all
three fonts start loading at once, and the browser keeps them for a month. - The update check waits. The check for a newer version (an 80 KB file) no longer competes with
the page while it loads, and no longer runs on every switch back to the tab. - On a simulated slow phone, the home page's PageSpeed score went from 61 to 77–80; accessibility
and SEO from 96 and 92 to 100.
Easier to read and to find
- Grey text is readable. The muted grey used for hundreds of labels was too faint on the dark
background (3.4:1); it now meets the 4.5:1 readability standard everywhere. Operator colour
themes get the same lift. - A missing file is a real "not found". Asking for a file that does not exist (for example
/.well-known/ai-catalog.jsonor/favicon.ico) used to return the app's page, so search
engines and AI tools were told those files exist. They now get a proper 404. - Screen readers: the "Start" button says what it does (sign in or create an account), the
logo links no longer announce an English "home" in every language, and the footer's headings are
in the right order. - Pages are isolated from other sites' windows (Cross-Origin-Opener-Policy).
- The privacy guide list showed a raw text key for goods trades (which have no guide). Fixed.
New — fees
- Each Bitcoin fee gets its own address. This release pins the treasury's Bitcoin account key.
From this release on, every order paid in BTC gets a fresh address of that account, and a
payment to it can only ever count for that one order. Nobody can claim someone else's payment. - More sources check Monero fees, and payers don't get stuck.
- Three public Monero nodes join the three explorers. A node gets only the transaction id (never
the payer's transaction key), and the indexer checks the payment itself, so a node cannot fake
one. - Two sources still have to agree. If only ONE can be reached for two hours and nothing
contradicts it, its answer is accepted once the payment is 10 blocks deep. Sources that
disagree never settle it; the log names what each one said. - The upgrade removes the three explorers that stopped answering (still listed on nodes set up
before v1.20.0) and adds the newer sources, once. A default you remove later stays removed.
- Three public Monero nodes join the three explorers. A node gets only the transaction id (never
Fixed
- The compare page. It could not reach other instances at all: each page's security policy
lets the browser talk only to its own instance, so the request was refused before it left
("Failed to fetch"). The instance now fetches the other instance's orderbook itself, only for
instances registered on chain, over their onion address when they have one. Messages are clear
and in every language. - Pages switching language by themselves. On a slow connection, a browser set to another
language could turn an English page into that language a few seconds after it loaded. The
page's own language now always wins. - The treasury setup scripts. The Bitcoin key script lost the key when run as documented, and
the Monero self-test stopped before printing anything when run from the repo folder. Both work
now. The Bitcoin script also refuses an account that has ever received coins. - A dependency. devalue (used by SvelteKit) is updated to 5.9.4 for three advisories.
New — checks
- Block check (report only). For every block, the indexer recomputes the chain's own
fingerprints (the transactions' merkle root, the block id and its link to the block before) and
counts whether they match what the RPC node served. Nothing is refused yet.sudo morphit-ops healthshows it as "Block check".
Upgrading
Run
sudo morphit-ops upgradeon each server. Nothing needs doing by hand.- The Monero source list is updated by the upgrade where needed.
- The web container is rebuilt from this release's config (missing files, font caching, the
opener policy) — on clearnet and on Tor/I2P-only servers alike. - Check
sudo morphit-ops health→ "Block check" a day after upgrading.
Downloads
-
Source code (ZIP)
0 downloads
-
Source code (TAR.GZ)
0 downloads
- Each page downloads about half the text it used to. The FAQ, the privacy guides, "run a
-
Morphit v1.20.1 Stable
released this
2026-10-01 03:16:50 +00:00 | 9 commits to main since this releaseMorphit v1.20.1
A fix release for problems found while upgrading the live instances to v1.20.0. There are no
database changes and no changes to on-chain operations.Fixed — upgrades and operations
- BunkerWeb could stop taking any new settings.
- Morphit's firewall exception for its API was stored twice on some servers. With two copies,
BunkerWeb rejects every new config it builds and quietly keeps serving the last one that
worked. On one instance that had been true for three weeks, so settings changed since then,
including the one that stops visitors choosing their own IP address, never took effect. - The upgrade now keeps exactly one copy and removes the others, after a backup of BunkerWeb's
database. It then checks on the live site that the API still gets through and the rest of
the site is still protected, and puts the copies back if not. - The upgrade now reads BunkerWeb's own verdict on every change. If BunkerWeb refuses a config,
you see nginx's reason instead of a vague "not applied yet".
- Morphit's firewall exception for its API was stored twice on some servers. With two copies,
- Slow BunkerWeb servers finish their settings change.
- Where BunkerWeb's downloads time out, it needs minutes to rebuild its config after a change,
which is longer than the upgrade could wait. The change was always put back. - On a BunkerWeb server the privacy and header settings are now applied in the background, for
as long as BunkerWeb needs. The upgrade shows the progress while it can, and
sudo morphit-ops statusshows the result under "Web proxy (BunkerWeb)".
- Where BunkerWeb's downloads time out, it needs minutes to rebuild its config after a change,
- A relay that stopped could stay stopped. The relay could end without an error, so systemd
did not restart it, and the upgrade then skipped it because it was not running. One instance's
relay was down for three days this way, and sign-ups there failed.- The relay and the indexer now restart after any exit.
- A relay that ends without being asked to logs why and exits with an error.
- The upgrade starts an enabled relay that is not running, and checks that it stays up.
- The sign-up limits work. The relay could not reach its state folder, so the daily sign-up
ceiling was never saved and aSIGNUPS_DISABLEDfile was never seen. The folder is now
/var/lib/morphit-relay. The upgrade moves anything already in the old one, so paused sign-ups
stay paused, and/var/lib/morphit/relaynow points to the new folder. - The release ceremony's payload step installs its packages first (
npm ci), so it cannot
run against outdated ones on the maintainer's computer.
Upgrading
Run
sudo morphit-ops upgradeon each server. Nothing needs doing by hand.- On a BunkerWeb server, the upgrade ends with the result of the background settings change,
or says it is still running. Check it later withsudo morphit-ops status. - Signed-up counts: the first day after the upgrade starts a fresh daily count, because the
old count was never saved.
Downloads
-
Source code (ZIP)
0 downloads
-
Source code (TAR.GZ)
0 downloads
- BunkerWeb could stop taking any new settings.
-
Morphit v1.20.0 Stable
released this
2026-09-30 18:02:27 +00:00 | 10 commits to main since this releaseMorphit v1.20.0
A major release. BLURT-paid orders now show on every instance, not only where they were posted.
Bitcoin and Monero fees can be tied to the order they pay for. You can sign in with a QR code
across instances. Each instance can set its own colour theme. Tor-only nodes keep even the
operating system off clearnet. It also carries a whole-system security, privacy and reliability
review.Four database migrations (v62–v65) run by themselves. The operator-register, release, order and
stranger-fee operations gain new optional fields. Older indexers ignore these fields, and nothing
that used to be valid stops being valid.New
- Orders paid in BLURT show on every instance.
- Before: each instance counted the operator's 90 % share of a listing fee only if it went
to its own fees account. An order posted through morphitir was therefore hidden as
"underpaid" on morphit.io, and first messages between users of different instances were
dropped. - Now: an instance publishes its fees account in its on-chain registration, and every
instance accepts payments to it.sudo morphit-ops upgradepublishes it by itself. The
10 % treasury share is still required. - Existing orders appear too. Once an operator has upgraded, its users' live orders
from before — including those paid on v1.19 — appear on every upgraded instance within
minutes, if their 90 % share went to the first fees account the operator registers. An
order whose share went to a fees account the operator had switched away from before
upgrading stays on its own instance only. Expired orders do not come back, and first
messages already dropped stay lost. - Which operators are accepted: every operator whose on-chain registration names a fees
account — the same registrations the Operators page shows. A new instance needs nothing
from anyone else. - An instance that never set a fees account keeps sending 100 % to the treasury.
- Before: each instance counted the operator's 90 % share of a listing fee only if it went
- Bitcoin fees get their own address for every order, once the treasury's extended public
key (xpub) is pinned in a release.- Users just pay the address shown, by QR code or link, and no longer paste a transaction
ID. - Nobody can claim someone else's payment.
- Payments no longer all go to one public address.
- Every instance and the browser compute the same address from chain data.
- "I've paid — check now" re-checks at once.
- For the treasury:
sudo morphit-ops treasury btcshows the gap limit to set in the
treasury wallet.
- Users just pay the address shown, by QR code or link, and no longer paste a transaction
- Monero fees work.
- Before: the transaction proof Morphit asked for could never be checked by the block
explorers, so every Monero-paid order ended up "missing". - Now: you prove the payment with the transaction key your wallet shows. There is help for
Feather, the Monero GUI and CLI, Cake Wallet and Monerujo. - Tying the payment to its order: once the treasury's main address is pinned in a release,
you pay a Monero integrated address made for your order, so a copied transaction cannot pay
for anyone else's order.
- Before: the transaction proof Morphit asked for could never be checked by the block
- Monero block explorers updated.
localmonero.co/blocksnow forwards to moneroblocks.info, which works differently, so it
is dropped. monerohash.com/explorer and exploremonero.com no longer answer and are dropped
too.- moneroblocks.info is back in a new way: it only sees the transaction ID. Your instance
checks the payment itself with the transaction key: the output, the amount (against the
amount commitment recorded on-chain) and the order binding. - The defaults are now xmrchain.net, moneroexplorer.org and moneroblocks.info.
- Two of them must now agree before a Monero fee counts as paid (it used to be one), so
no single explorer can decide it. An instance that lists only one explorer keeps accepting
one explorer's answer and says so when it starts;MORPHIT_INDEXER_XMR_MIN_SUCCESSFUL_RESPONSES
still sets it explicitly. - If you set your own list in
MORPHIT_INDEXER_XMR_EXPLORER_URLS, remove those three
explorers. For moneroblocks.info, writeraw-tx+https://moneroblocks.info.
- Sign in with a QR code across instances.
- Your phone, signed in on one instance, can approve a login on another instance's page. The
QR code must come from an instance in the Morphit directory. - Your phone only talks to its own instance, which passes the sealed message on (over Tor or
I2P where needed). The other instance never sees your phone's address. - QR sign-in now also works on .onion and .i2p pages.
- Your phone, signed in on one instance, can approve a login on another instance's page. The
- Your instance, your colours.
- For example:
sudo morphit-ops branding apply --theme-from '#f3dca0' --theme-to '#bb872f'
(on the server), or a ready-made theme such as--theme champagne-gold. - Every colour the site uses is derived from these, and each is checked for readable
contrast: buttons, links, cards, hover and focus, chat bubbles, the page background and its
corner glows. - Nothing is rebuilt and the build-integrity check stays green.
- With no theme set, Morphit looks exactly as before.
- For example:
- Tor-only nodes keep the operating system off clearnet too.
- System updates are fetched over Tor.
- The clock is set from onion sites over Tor instead of public time servers.
- Ubuntu's news fetches are switched off.
- The upgrade switches an existing node, checks it works, and puts everything back if it
doesn't.
- IPFS clean-up. Old release and snapshot copies are unpinned every week. The current and
previous release and recent snapshots are always kept.
Fixed — critical
- One malformed character could stop every indexer. Chain data containing a NUL character
made the database refuse the whole block, and indexers retried it forever. Such text is now
handled the same way on every node, and indexing never stops on it. - Fast-sync no longer rejects honest snapshots that contain orders or fees signed with an
active key.
Fixed — privacy
- Servers no longer write visitors' IP addresses to disk.
- Before, BunkerWeb kept an access log with every visitor's address in Docker's log files,
with no size limit. The frontend's web server logged the forwarded visitor address, and
bare-metal installs logged every page and API call. - Now access logging is off everywhere. BunkerWeb's log line no longer contains an address,
and Docker keeps no BunkerWeb log. The exception: if CrowdSec reads that log, a small
rotating one (5 MB) is kept for it. - To watch BunkerWeb live without storing anything, run this on the server:
sudo docker attach --no-stdin --sig-proxy=false bunkerweb.
- Before, BunkerWeb kept an access log with every visitor's address in Docker's log files,
- Tor and I2P visitors now get the same browser protections as everyone else. Their pages
used to be served with no Content-Security-Policy and no anti-framing header. The onion and
I2P policy allows the page itself plus the seven onion RPC nodes. - The QR login camera works again on Ansible-installed BunkerWeb sites. BunkerWeb's default
policy blocked it in Chrome-based browsers. (Firefox ignores that header.) - Hidden-only nodes stop two leftover clearnet contacts. Publishing a snapshot no longer asks
ipfs.io, and the hourly IPFS pin no longer waits 15 minutes for the public network.
Fixed — money
- The relay can no longer pay the same thing twice.
- If an RPC node lost its reply, the relay used to sign and send a second, different
transaction. The same happened when a node rejected a transfer that another node had
already accepted. Welcome bonuses, the 2 BLURT signup dust and power-ups could be paid two
or three times. - Now every payment is signed once and the same bytes are offered to each node. Anything
uncertain is settled by reading the chain before anything is sent again.
- If an RPC node lost its reply, the relay used to sign and send a second, different
- Signups keep their limits through restarts and bad nodes.
- The daily signup ceiling was never saved on installed boxes, so it reset on every restart.
TheSIGNUPS_DISABLEDswitch did nothing for the same reason. - Both now live in
/var/lib/morphit/relay, which is created by itself. - Signups from one address can no longer beat the per-address limit by arriving together.
- One invite can no longer create two accounts.
- A misbehaving RPC node can no longer push creations past the ceiling.
- The daily signup ceiling was never saved on installed boxes, so it reset on every restart.
- The relay refuses a sudden account-creation fee spike (more than 1.5× the configured fee)
instead of paying it.- New error codes:
relay_fee_spikeandbroadcast_outcome_unknown. The second one means
"we can't tell yet whether your account was created — try again in a minute with the same
name". A retry with the same name is safe and costs nothing extra.
- New error codes:
- Typing
12,50into an amount no longer becomes 1250.- Every amount box now understands a comma or a point as the decimal mark, and Persian,
Arabic and full-width digits. - A number that could be read two ways, like
1,234, is refused with both readings shown.
- Every amount box now understands a comma or a point as the decimal mark, and Persian,
- Expired orders no longer raise your listing fee. The indexer never marks orders as
expired, so every order that ran out used to count as live forever. The fee grew 1.5× for
each one, up to a lockout. - Real BTC/XMR fee payments can't be pushed out of the re-check queue by a flood of fake
ones. A made-up XMR transaction is now marked missing, like BTC. A fee paid exactly at the
floor is no longer rejected by rounding. - Finishing a trade automatically keeps the buyer's trade credit. An automatic completion
used to drop it.
Fixed — security
- Notification links can't send you to another site any more.
- Remember me.
- The copy of your keys kept for a page reload now expires after 30 seconds and only
survives a real reload. - Going Back to Morphit after leaving now asks for your password again.
- The copy of your keys kept for a page reload now expires after 30 seconds and only
- Lock session locks every open Morphit tab, not just the current one.
- The Active-key prompt refuses your owner key, even when the account uses one key for
both. - Changing the auto-lock time takes effect at once.
- Keys are wiped from memory when a 2FA code is required or wrong.
- The indexer no longer shows unsigned chat live, and a single RPC node can no longer plant a
posting key. Keys read from blocks are confirmed by two RPC operators before chat
verification relies on them. - Fast chat hardening.
- Clearnet fast-chat pushes connect only to the address that was checked to be public.
https://onion and I2P addresses are refused at registration and dialled correctly if
already registered.
- The federation directory.
- Censored instances are no longer dropped as dead one day after they last registered.
- A peer can no longer fill the directory with oversized or fake data.
- Behind BunkerWeb, one visitor can no longer rate-limit everyone. All visitors used to
share one limit bucket. - Server-side fixes.
- Several root-owned files could be redirected by a local account through symbolic links.
Among them: the upgrade's temporary folder, the branding ownership change, and two marker
files. - The first-boot helper ran any user's canary script as root.
- Several root-owned files could be redirected by a local account through symbolic links.
- Patched libraries. undici is now 7.29.1: before, a hostile peer could send the indexer a
compressed reply that expands until memory runs out. brace-expansion and ip-address are also
updated for newly published advisories.
Fixed — upgrades and operations
- The upgrade finds BunkerWeb by what it is, not by its name, so hand-made stacks (like
morphit.io's) are handled correctly.- It only restarts BunkerWeb, its scheduler and the frontend, and never the whole stack.
- It uses every compose file you started the stack with.
- If it can't tell which container is which, it changes nothing and says so.
- The upgrade checks that services actually stay up after a restart. A brief automatic
restart while the database starts is fine. A rollback now also brings back a service that
crashed. - The upgrade now also refreshes the helper scripts in
/usr/local/lib/morphit/. It also
opens the IPFS port (4001, TCP and UDP) on clearnet IPFS hosts that were installed before it
was added. - Every RPC use goes through the full node pool, with the healthiest node first:
morphit-opslookups and registration, the canary, and the release broadcast scripts. On a
tor-only box only hidden nodes are used. The browser on an onion page now uses all seven
onion RPC nodes, not two. - 16 old one-off debug and patch scripts in
ops/were removed. Several of them broke
today's indexer if run. - Honest messages.
morphit-ops doctorno longer tells you to remove an RPC node that is only briefly down.- The firewall check no longer says "OK" when it could not reach the site.
morphit-ops --versionshows the real version.branding statusasks for sudo instead of guessing.
- Plain-HTTP I2P visitors see a calm note where the browser does not allow a feature (2FA
codes, copy buttons), instead of an error. - Other.
- The homepage's "What Morphit is built around" heading is removed in all 10 languages. The
seven cards stay, and their titles are now proper headings for screen readers. - "agorist" is no longer translated, and Blurt is spelled Blurt in Persian.
- The homepage's "What Morphit is built around" heading is removed in all 10 languages. The
Upgrading
Run
sudo morphit-ops upgradeon each server. Nothing needs doing by hand.- Fees account. The upgrade publishes your instance's fees account in its operator
registration by itself. It keeps your current on-chain name, address and contact exactly as
they are.- If it can't unlock the relay key without you, it prints one line. Then run
sudo morphit-ops registeron that server. - Check the result with
sudo morphit-ops status→ "Fees account (federation)".
- If it can't unlock the relay key without you, it prints one line. Then run
- On BunkerWeb boxes the upgrade:
- applies the new privacy and header settings to BunkerWeb and the frontend, checks them, and
puts everything back if a check fails; - switches BunkerWeb to the frontend's new private port (8088) only after it sees that port
working.
- applies the new privacy and header settings to BunkerWeb and the frontend, checks them, and
- On Tor-only boxes the upgrade also moves system updates and the clock onto Tor, as above.
It finishes within its time limit. Anything left half-done is finished or undone by a timer
within six hours. - Some protections take effect from the next upgrade after this one, because this upgrade
is still run by v1.19.0's own code: the upgrade's private temporary folder, its own symlink
and npm checks, and its restart checks. - Registered
https://onion address? An operator who registered one should register again
withhttp://: runsudo morphit-ops registeron that server. - Bitcoin per-order addresses and Monero order-bound addresses start only when the maintainer
pins the treasury keys in a later release. That release comes after every instance runs
v1.20.0. Until then, Bitcoin fees work as before and Monero fees use the transaction key.
Branded instances keep their logo, icons and name, and now their colours: the upgrade
re-applies them.Downloads
-
Source code (ZIP)
1 download
-
Source code (TAR.GZ)
0 downloads
- Orders paid in BLURT show on every instance.
-
Morphit v1.19.0 Stable
released this
2026-09-27 01:42:11 +00:00 | 16 commits to main since this releaseMorphit v1.19.0
Instances can now carry their own name and logo. An operator runs one command
and their site says "Sign in to Vigilante Trading" instead of "Sign in to
Morphit", shows their logo in the header, homepage and footer, and uses their
icon for the browser tab and the phone home screen. Nothing is rebuilt, so
visitors' build-integrity check stays green, and every upgrade keeps it. No
protocol or consensus change.Added
-
Your instance, your name and logo. Every Morphit instance runs the same
signed frontend, and until now every one of them looked like morphit.io. An
operator can now brand theirs (use the full path to wherever you copied the
files):sudo morphit-ops branding apply \ --logo /home/you/my-logo.svg --logo-footer /home/you/my-wordmark.svg \ --icon /home/you/my-symbol.svg --name "Vigilante Trading"- The logo replaces the Morphit logo in the header, on the homepage and
in the footer. It is shown at the same height as the Morphit logo and is
never stretched or squashed. The occasional sheen still sweeps across it,
following the new logo's own shape. The red BETA marker goes away once you
supply a logo (you can turn it back on). - The icon becomes the browser-tab icon and the "add to home screen"
icon. The iPhone and iPad launch screens show the new logo. - The name replaces "Morphit" wherever the site names itself: "Sign in
to …", page titles, RSS feed titles, and on the pages Morphit prerenders
(homepage, sign-in, FAQ, guides) also link previews and the home-screen
label, on first paint and for visitors without JavaScript. It works in all
10 languages. Where the text is about the software or the federation ("Run
a Morphit node", "other Morphit instances"), it still says Morphit, and a
small "Runs on Morphit" line under the footer logo links to About this
instance, which shows the site's name and how many files the operator
re-branded.
sudo morphit-ops branding setup(or Branding in thesudo morphit-ops
menu) asks for each file and the name instead. The logo files are kept in
/etc/morphit/branding/and the name inmorphit.config.env, and every
upgrade re-applies them.morphit-ops branding statusshows what is set, and
morphit-ops branding resetbrings back the plain Morphit look. The full
guide isdocs/BRANDING.md. - The logo replaces the Morphit logo in the header, on the homepage and
-
Logo files are checked before they are served. Each SVG is compared with
a list of the parts a plain drawing uses; anything else — scripts, event
handlers, links, embedded web pages, animation, references to other files or
the internet — is refused and nothing changes. The web server also serves
these files under a policy that stops anything in them from running if
someone opens one directly. A site name that passes for the Morphit project
itself (a look-alike of "Morphit", or one of the project's account names) is
refused, like it is for directory names. -
Branding keeps the build-integrity check green. Branding never rebuilds
the frontend. It edits the served files in place and never touches the ones
the on-chain release record covers (the start page, the service worker and
the app code), so visitors never see the tamper warning because of it. The
publishedverify.jsonlists every file the operator re-branded. If
branding applyis interrupted (Ctrl-C, a full disk), the nextapplyor
resetfinishes or undoes it correctly.
Fixed
-
npm's "New major version of npm available!" notice is gone for good. It
kept appearing at the very end of installs and upgrades, telling operators to
runnpm install -g npm@…— advice that can break an install, which pins its
own Node and npm. The earlier fixes switched it off insidemorphit-ops, but
the notice comes from the npm process that startsmorphit-ops(on a
guided install themorphit-opsshortcut runs through npm, and so does
npx morphit-ops), and it prints when that process exits, where no setting
insidemorphit-opscan reach it. Now:- the install carries its own npm setting (
.npmrc) that turns the notice
off for every npm run inside it; - the
morphit-opsshortcut and the MCP deploy turn it off themselves; - every install and upgrade also turns it off for the whole server.
Because the notice comes from the version that starts the upgrade, the
upgrade to this release can still show it one last time; after that it
cannot appear. - the install carries its own npm setting (
-
The server-wide npm setting file is no longer writable by every account.
Saving a setting withnpm config set --location=globalleaves npm's global
settings file world-writable, and servers set up with the guided installer
had such a file. Any account on the server could then have added a setting
that runs its own code the next time root used npm. Installs and upgrades
now write that file themselves and keep it owned by root and not writable by
others, and repair an existing one. -
A failed late upgrade no longer leaves a container frontend serving
nothing. If an upgrade rolled back after the frontend container had been
switched to the new files, the container kept pointing at the removed folder
and every page failed. The rollback now re-attaches it to the restored
install. -
morphit-ops payment-method addaccepts its documented form.--name "…" --description "…" --category …used to save the word "true" in
place of each value unless it was written as--name="…". Both forms now
work, a value may start with a dash ("-5% off"), andblock --reason "…"
and the other commands' value flags behave the same way. -
Two-factor setup labels your account correctly in authenticator apps
when the site's name contains spaces or a colon. -
Upgrade hints now say
sudo morphit-ops …instead ofnpx morphit-ops …,
which could look the tool up on the public npm registry when run outside the
install.
Changed
- The homepage lists ENS instead of Nostr among the ways to reach
Morphit: "Reachable on clearnet, Tor, Lokinet, I2P, ENS and Federated
instance runners." (all 10 languages).
Upgrading
sudo morphit-ops upgrade. Nothing else is needed; an instance that does not
set any branding looks exactly as before.To keep even this one upgrade free of the npm notice, run this once first (it
also makes npm's global settings file root-only):cd ~ && sudo npm config set update-notifier=false --location=global && sudo chmod 644 "$(npm config get globalconfig)"To brand an instance after upgrading, copy the logo files to the server and
run thebranding applycommand above. The phone icons and launch screens are
drawn from your SVG files by a small image converter (librsvg2-bin); if the
server does not have it, the command offers to install it.Downloads
-
Source code (ZIP)
0 downloads
-
Source code (TAR.GZ)
2 downloads
-
-
Morphit v1.18.0 Stable
released this
2026-09-25 22:28:09 +00:00 | 17 commits to main since this releaseMorphit v1.18.0
A major release. Chat between instances no longer waits for the blockchain,
and before release every part of Morphit was reviewed again from the ground
up: that closed a way for one hostile chain node to take over a node during
setup, several ways a Tor-only node could still reveal its home address, and
free fake "verified" listings. The rest of this page gives the details, fix by
fix.Chat between two privacy-only instances now lands in about two seconds instead
of eight, plus fixes for people using Morphit over Tor or I2P, an orderbook
comparison that accused honest instances of hiding orders, and a page that
blurred itself on load. It also closes two ways a Tor-only instance was still
reaching the open internet, one of them through its relay. The upgrade fixes
instances that are already installed, not only new ones. No protocol or
consensus change.Added
-
Chat between two privacy-only instances is now fast enough to hold a
conversation in. If you and the person you are talking to are on different
instances, your message used to travel to them through the blockchain — and
the blockchain's own timings made that hopeless. Sending waits for the message
to be written into a block, which takes three seconds; the other instance then
waits up to two more for its next check of the chain before it even looks. That
is five seconds of waiting before a single Tor or I2P round trip is counted,
and a delivered message pays for three of those. No amount of faster or extra
chain nodes helps, because none of that time is spent talking to them.So instances now hand each other chat messages directly, at the same moment the
message goes to the chain rather than after it. The chain still receives every
message and is still the permanent record — nothing about what Morphit
publishes has changed, and the saved copy of your conversation still comes from
it. What has changed is that the other person no longer waits for it to get
there.The connections between instances are kept open in the background, refreshed
every few minutes. This matters more than it sounds: opening a fresh Tor
circuit or I2P tunnel takes half a minute or more, and if the first message of
a conversation had to pay for that, the one message where the other person has
no reason to be watching their screen would be the slowest one. Keeping the
route ready means the first message is as quick as the rest.An instance accepts a handed-over message only if it carries a valid signature
from the sender's own posting key, checked against the key it holds for that
account — so a message cannot be forged or put in someone else's name, and
this is true no matter who sends it or where they connect from. If the key no
longer matches, it is re-read from the blockchain once (rate-limited), and that
correction is held in memory rather than written to your database. Blocks are
honoured exactly as before, and a first message from a stranger still shows up
in an open conversation without ringing anyone's phone. No message handed over
this way is written to the database; the permanent record still comes from the
chain alone. There is one write, and an earlier draft of these notes said there
was none: when a handed-over message may notify someone who has push turned on,
one row goes into the push queue, keyed on the transaction so it is queued once
however many routes deliver it. That row is a notification, not the message,
and the queue's own janitor retires it.Measured the whole way — one person's browser to the other person's screen,
across two instances, every leg over a slow privacy hop — delivery went from
7.3 seconds, which was the old route's best possible case, to about 1.4.To be precise about what that is: the instances, routes, signatures and
sockets are real, and the per-hop latency is a stated model (900ms for a
privacy hop). It is a measurement of the software under an assumption about
the network, not a measurement of Tor. Real timings depend on your own
circuits, andops/fastchat-latency-probe.shmeasures them on your box and
tells you where you stand. -
Two people on the same instance now get fast chat too — and they were the
slowest case of all. The fast path hands messages to OTHER instances, and an
instance is not its own peer, so when both people were on the same server
neither of them got any of this: the message went to the blockchain and came
back a block later, then waited for the next check of the chain, then a read
to fetch it. Up to 6.8 seconds on a privacy-only instance for two people on
one machine, which is very likely the most common conversation there is. An
instance now delivers to its own users the moment it relays their message,
with the same block-list and notification checks it applies to anything a
peer hands it. To the recipient's inbox: 1,334ms privacy-only, 527ms
clearnet. -
A reply to someone who just messaged you now notifies them. Fast
notification requires evidence the conversation is wanted — the sender is
answering your own live order, or you have written to them before. Both were
read from the permanent record, which runs 45 to 63 seconds behind. That broke
the ordinary marketplace exchange in the half that matters: a buyer messages a
seller about the seller's order and the seller is told at once, but when the
seller REPLIES the order is not the buyer's, and the buyer's own opening
message is not in the permanent record yet. So the person who started the
conversation thirty seconds ago got no notification at all until the chain
caught up. Measured before the fix: the check returned "no".An instance now remembers, briefly, that it just relayed one of its own users'
messages to someone — first-hand knowledge it had all along, minutes before
the permanent record admits it. This widens nothing: it only ever says yes to
a pair where the recipient demonstrably wrote first, which is the same thing
the old check meant, established sooner and from a better source. A message is
only remembered after the network accepts it, so a forged one records nothing.
Someone who has never written to you is as much a stranger as before. -
An instance hands over what it has queued before it shuts down. A restart
used to drop chat deliveries that were waiting to go out to other instances.
They still arrived by the blockchain, so nothing was lost — they were just
slow, for whoever happened to be mid-conversation when an operator restarted.
Shutdown now passes them on first, with a two-second limit so one unreachable
instance cannot hold up a restart. -
The browser's half of the inbox is now tested too. The part of the app
that turns a "new message" ping into a lit badge and a card for a conversation
you have never had before had no test of any kind. It does now, and so does
the agreement between the two halves: the server sends four specific fields,
and the browser lights nothing without the three that name the thread. Rename
one of those on either side and chat badges stop working for everyone —
silently, with no error anywhere,
which is the worst way for something to break. Both sides are now pinned, so
neither can drift without a test failing.The same is now true of the broadcast contract — the thing this release
actually changed — which had no test of any kind either. See "an older browser
tab" below for what that omission was hiding. -
Every combination measured, both directions, to the inbox. Same instance,
clearnet to clearnet, clearnet to privacy, privacy to clearnet, privacy to
privacy — a buyer's first contact and the seller's reply, each timed to the
moment it reaches the other person's inbox rather than an already-open
chatroom. Slowest of all twelve: 1,367ms, against the six-second bar.Each of those timings is now held to a floor as well as the six-second
ceiling. A result faster than the hops that case has to cross means the
message skipped a leg, which is what a test passing for the wrong reason looks
like — so the test now fails on it. Adding that floor found two defects in the
test itself, described under Fixed. -
Sending no longer waits for the blockchain either. Getting the message to
the other person quickly is only half of it: the sender was still watching
their own message sit there. Every send waited for the message to be written
into a block before the page was told it had gone — up to three seconds of
pure waiting, on top of the round trip to their own instance. Chat messages now
return as soon as the network has accepted the message rather than when it has
been sealed into a block. Nothing is given up: the message is still checked and
still rejected if there is anything wrong with it, and it still goes to the
chain exactly as before. Everything that genuinely needs a block number —
orders, transfers, account creation — is unchanged and still waits.On a fast connection this was never the problem; on a slow one it was decisive.
A send that waits for a block crosses six seconds once a round trip passes
about 2.6 seconds, which privacy networks reach regularly. Measured on a
three-second hop, the old behaviour took 6.4 seconds and the new one 3.4.An older browser tab is safe. The page asks for the faster answer rather
than the server deciding on its own, because a tab you left open can be older
than the instance serving it — and a tab from before this release would have
read the new reply as a failure, showing you a red message that had in fact
been delivered, with a retry button that would send it twice. An older tab
simply never asks, gets the old reply, and still benefits from the fast
delivery on the other side. Both directions of that mismatch are now tested. -
A sick blockchain node no longer stops conversations. Because the message
is handed to the other instance before it is sent to the chain, a Blurt node
that has wedged or gone unreachable no longer takes chat down with it. Measured
with a node deliberately stalled for twenty seconds, the message still reached
the recipient in under one. -
A hidden instance no longer throttles its own federation. Requests are
rate-limited per address, and over Tor or I2P every instance in the federation
arrives at the same one — the local privacy daemon — so the whole federation
shared a single allowance meant for one caller. On a privacy-only node so does
every human visitor, since all traffic arrives that way. Measured: the 241st
hand-off in a minute was refused no matter how many different instances sent
them, which would have crippled fast chat on precisely the nodes it was built
for. The allowance is now sized for a real federation, and the actual
protection moved to something that works regardless of address: a fixed-size
queue of pending work. -
A busy instance no longer goes deaf. Checking a message's signature costs
real processor time — about four milliseconds each, measured — and a delivery
can carry dozens. Done the obvious way that work would run in one solid block,
during which the instance answers nobody: no chat updates, no page loads,
nothing. Checking now happens after the sending instance has been answered,
and pauses between messages so everything else keeps running. Measured: the
instance never went quiet for more than 17 milliseconds while checking 64
messages, and answered the sending instance in 2 milliseconds rather than the
330 the checking itself takes.This also removes a subtle leak. An instance that took longer to answer for a
message it cared about than for one it did not would be telling anyone with a
stopwatch whether a particular person reads their messages there. Answering
before doing the work means the reply reveals nothing either way. -
Under real overload, fast chat steps aside instead of falling over. There
is a firm limit on how much pending work an instance will hold. Past it, extra
hand-offs are dropped rather than queued — and dropping is the right answer,
because every one of those messages is already on its way through the
blockchain, which delivers it regardless. An overloaded instance quietly
returns to ordinary blockchain timing instead of accumulating work it cannot
finish in time. Theshedcounter in the health output is the thing to watch. -
Fast chat holds up as the federation grows. Every message is offered to
every instance, so each instance has to keep up with the whole network's chat,
not just its own users'. One message per delivery could not have done that: a
privacy connection carries one exchange at a time, so a single instance would
have topped out at well under one message a second no matter how many users
were waiting. Messages that arrive while a delivery to an instance is already
in progress now travel together in the next one. Nothing is ever held back
waiting for company — a message to an idle instance leaves immediately, which
is the ordinary case — so this costs nothing when things are quiet and does the
work when they are not. Measured: forty messages that would have needed forty
round trips took two, about eighteen times the throughput per instance, with a
lone message still leaving in four milliseconds.
Fixed
Found by an independent review of this release
Everything in this section was written after four reviewers — none of whom had
seen the code being written — went over the new delivery path. It is listed
first because some of it is serious, and because a release that only tells you
what went right is not telling you much.-
Ten requests a second could have stopped an instance accepting anything.
The new peer endpoint shared a rate-limit allowance with the ordinary write
path, because the limiter groups requests by category rather than by limit. Six
hundred peer messages in a minute — no key needed, nothing valid needed — and
every user on that instance would have been refused: no chat, no orders, no
transfers. On a privacy-only instance, where every visitor and every peer
arrives through the same local daemon and therefore looks like one caller, that
is one allowance for the entire world. Federation traffic now has its own. -
Batching was switched off by the size limit it ran into. Grouping messages
is what makes a busy federation affordable, and a full group of long messages
is a quarter of a megabyte against a limit of four kilobytes. Groups form only
when an instance is already busy — so the fast path worked while idle and
refused its own traffic under exactly the load it exists to carry, silently
falling back to blockchain timing. The limit now matches what the endpoint
carries, the sending side keeps itself under it, and an instance that refuses
the size anyway is sent the messages one at a time instead of losing them. -
One database hiccup could have turned the fast path off until the next
message arrived. A single failed lookup abandoned every message waiting to be
checked, with nothing scheduled to come back for them — and they went on
occupying the queue until it was permanently full and refusing everything. One
failure now costs one message. -
An older browser tab would have shown delivered messages as failed. A tab
left open from before this release reads the new, faster reply as malformed:
the message reaches the other person, and the sender sees it in red with a
retry button that sends a second copy. The faster reply is now something the
page asks for, so an older tab never gets it. -
A message could be shown as sent forever without ever reaching the chain.
Waiting for a block used to be the proof it had landed; without that wait,
nothing replaced it, and an accepted-but-dropped message looked delivered
indefinitely to both people. A send that is not confirmed within two and a half
minutes is now marked so and can be resent — and if the real copy turns up
late, the mark clears itself. (The window was two minutes until the last round
of review showed that the slowest legitimate path takes up to 123 seconds.) -
A lost reply could turn a delivered message red. Your instance shows your
own message to you before it answers the send, so a connection that dropped at
the wrong moment stamped "failed" over a message already visible in the
transcript. Retrying sent it twice. -
The endpoint had no ceiling on several things anyone could inflate. How
many signatures one message carries (each costs real work to check), how far in
the future a message may claim to expire (which decides how long a captured one
can be replayed), and how large a group may be. All bounded now, and each bound
has a test that was watched to fail without it. -
A free, untraceable way to light someone's inbox badge. Every other use of
a fast message checked whether the sender was allowed to notify that person;
the live badge did not. That mattered little when every message had to go
through the blockchain — which costs resource credits and leaves a record — and
a great deal once an instance could hand one over directly. The badge now obeys
the same check as everything else, and first contact from the peer route is
rate-limited per sender, per recipient — so a flooder exhausts only their own
channel to that person, and the direct path is not a cheaper way to reach
someone than the chain it runs alongside. Established conversations are not
affected at all, and an open chatroom still receives everything unconditionally. -
A stolen key kept working after its owner replaced it. Posting keys were
recorded once and never updated, which no previous feature depended on. This
one does: the key is the only thing standing between a handed-over message
and your screen. So a key the owner had replaced after it leaked still
verified, which the blockchain itself would refuse. And the owner's own
messages, signed with the new key, stopped verifying.Your instance now records a key change from the same blocks it already reads
account creations from. It also forgets any copy of the old key it was holding
in memory. That second step is not a detail. Without it, the leaked key would
have kept working for up to half an hour, in exactly the case a key change is
for. Tested against real key-change operations and a real database; removing
only the forgetting step fails the leaked-key case.Key changes made before you upgrade are covered too. Recording changes
from now on does nothing for an account whose owner replaced a leaked key
last month: your database still holds the leaked key. So every key recorded
before this release starts out unconfirmed. On its first start, your instance
checks each one against the blockchain and corrects any that changed. Until
an account's key is confirmed, a message handed over for it is checked
against the blockchain first. If the blockchain can't be asked right then,
the message travels by the blockchain as it always did. -
Two defects in this release's own tests. Adding a lower bound to the timing
checks — a result faster than the hops it had to cross is a bug, not good
news — immediately exposed them. The link between instances was being modelled
in one direction only, so every reply figure quoted was short by half a hop;
and the stand-in blockchain gave every message the same identifier, so the
duplicate-suppression correctly discarded the second message in each case while
the test, which only checked that notifications arrived, never noticed. Both
fixed, and the figures in this document are the corrected ones.Worse, the four cross-instance cases were not crossing instances at all: both
stood-up instances shared one in-process event channel, so the test passed with
the entire federation deleted. It now fails 24 of 36 checks with the send side
removed and 24 with the receiving side removed, and both of those deletions are
permanent parts of the test suite. -
The test of the notification check was answering the wrong question. Its
stand-in database returned an order in a shape the real query never produces,
so the check said "no" in every scenario — meaning the headline promise of this
release, that a stranger's first message reaches a seller whose browser is
shut, was never actually tested. It is now, in both directions.
Found by checking the release against what it was for
The feature is meant to be fast between instances of every type, across every
network Morphit runs on. Read strictly, that is a claim about three networks —
Tor, I2P and Lokinet — and it had been tested against one.-
Federated chat did not work at all on an instance without a Tor daemon.
Which is most of them: a fresh install has no Tor, and the setting that points
at one is filled in by default whether or not anything is behind it. So your
instance picked the.onionof every peer that had published one, threw away
the ordinary web address sitting right beside it, and failed to deliver a
single message to those peers — instantly, silently, for as long as that
configuration stood. Nothing errored, your peer count looked healthy, and the
only symptom anyone could report was that chat was "sometimes slow", because
every message quietly fell back to the blockchain's own timing.An instance now keeps every address a peer has published and picks one it
can actually reach, preferring the private ones exactly as before. If a
private route turns out to be unusable, the message goes by another road
within the same send — a few milliseconds later, not a timeout later —
and your instance remembers which network is down so the next message does not
pay to find out again. When the daemon comes back, the next background check
notices and the private route resumes on its own.Two instances that are both private-only, with no ordinary web address between
them, fail over to each other's second private network instead. That is the
case with nothing underneath it, and it is now the case with a test. -
Your instance could mark healthy peers as unreachable when the fault was
yours. If your Tor daemon stopped, your instance went through the federation
directory recording every.onionpeer as unreachable — peers that were up the
whole time. The protection against exactly this was written, and documented,
and could never run, because of a detail of how failures are reported inside
the browser-style request library. On I2P and Lokinet the protection had never
existed at all. All three now work, and the test for it uses real failures from
a real dead connection rather than ones written by hand, because a hand-written
one would have agreed with the bug. -
A connection setting could have quietly undone the whole point of keeping
routes warm. Your instance holds open a connection to each private peer and
refreshes it every three minutes, so a message never waits for a new tunnel to
be built — which on Tor or I2P takes half a minute or more. How long an unused
connection is kept is a separate setting, in a different file, and nothing
checked that it was longer than three minutes.Had it ever been set shorter, every single refresh would have found the
connection already closed and built a new one: the loop that exists to remove
that cost would have been paying it instead, on a timer, forever. Nothing
would have looked wrong — messages still arrive and the refresh still reports
success. Only the speed would have changed, on exactly the networks where it
hurts most.Both settings are now checked against each other, and against the point where
Tor discards an idle circuit at the other end. Separately, the I2P side now
proves it reuses one tunnel across many messages rather than opening a new one
each time — Tor has been checked that way since the start, I2P never had been,
and it is the same setting that keeps the six-second promise on both. -
If your I2P router is set to refuse tunnels, your instance now says so
instead of blaming your peers. Reaching another instance over I2P means
asking your own I2P proxy to open a tunnel. The Java I2P router has a setting
that refuses those outright —i2ptunnel.httpclient.allowInternalSSL=false,
which despite its name blocks ordinary in-network destinations on every port.
i2pd has no such setting and cannot be configured to refuse.When that refusal happened, your proxy was running and answering, so it did
not look like anything being down. It was recorded as the other instance
refusing your message. The result was a climbing failure count pointed at
peers that were perfectly healthy, no mention anywhere of your own router,
and federated chat over I2P simply not working.Your instance now recognises the refusal for what it is and reports it in
/v1/health. It still waits for two different addresses to fail before
blaming your router rather than the destination — a router refusing by policy
refuses every address, so that happens immediately — and the operator guide
explains which field to read when you have only one I2P peer. -
One peer's out-of-date Lokinet address could push your other Lokinet
traffic onto the ordinary web. When a private route fails, your instance
decides whether the fault was its own — and if it was, stops offering that
network to every peer for a minute. On Tor and I2P it can tell: the failure
names your own daemon. On Lokinet it cannot, because the failure is a name
that would not resolve, and a peer whose published.lokiaddress is stale or
mistyped fails in exactly the way your own router being switched off fails.So one peer with an out-of-date address was read as your Lokinet being down.
Every other.lokipeer then lost that route for a minute, and any of them
that also publish an ordinary web address had your messages sent that way
instead — on an instance whose operator chose a private network precisely to
avoid it. Nothing failed, so nothing was reported.Your instance now waits for two different addresses to fail before blaming
its own router, which a router that really is off supplies immediately, since
it fails every address it is given. A route that is proven to work clears the
suspicion, and old evidence expires rather than accumulating. Tor and I2P are
unchanged and still decide on the first failure, because there the failure
says whose it is. -
A censored instance is now found over whichever private network it
published. Your instance retries an unreachable peer over its hidden address
— but it only ever tried.onion. An instance behind a national firewall that
had published only an I2P destination, which is what an operator does where Tor
itself is blocked, was recorded as unreachable and dropped out of the
directory. All four published address types are tried now. -
And
morphit-ops healthnow shows it, which is where you would actually
look. All of the above went onto the health endpoint, and the tool you use
to read that endpoint displayed none of it — it showed the blockchain-tailing
half and stopped. The operator guide, meanwhile, told you to read these fields
with the ops CLI.Running
morphit-ops healthnow gives you a line for federated chat, with the
reason underneath when there is one:Fed. chat: degraded — tor unusable from this box (12 delivered, 40 failed) ↳ your tor is not answering — start it; the next warm-up clears thisForty failures that are not your peers' fault, and the fix on the next line.
An instance older than this release has nothing to report here and the line is
left out rather than shown as zeros, because during an upgrade "0 delivered"
would read as a fault instead of a version difference. -
When chat is slow,
/v1/healthnow tells you whose fault it is. A failure
count sends you looking at the federation when the answer is a daemon on your
own machine. Your instance now reports which of its own networks are down, by
name, and marks each recent failure with whether the message ever left your
box. The reasons were always recorded; until now only the tests could read
them. -
A growing federation would have spent its message fan-out on dead
instances. Every chat message goes to every other instance, so that list is
capped — and which instances made the cut was decided by how recently your
node had last checked on them, which says nothing about whether they are
answering. An instance known to be down, checked a minute ago, outranked a
healthy one checked an hour ago. Worse, a brand-new instance sorted below all
of them, because it had never been checked at all — so the one place whose
users have no established conversations to fall back on was the first to be
left out.Instances are now ranked by health, with censored ones kept among the healthy
(unreachable over the ordinary internet but demonstrably alive on the chain is
the case Morphit is built for, not a sick node). Your node also now says how
many instances the cap left out, so "healthy, reachable, and still slow" has
an answer instead of being a mystery.Nothing is mis-served today — the federation is far smaller than the cap. It
is fixed now because the failure it produces is invisible from the affected
instance, and "we are far from the limit" is how a cliff goes unlit. -
Message ordering does not depend on anybody's clock, and now cannot start
to. A message delivered directly carries no timestamp of its own; the
receiving instance works one out from the transaction. If that had come from
the sender's clock, two operators whose servers disagreed about the time would
have shuffled each other's messages — and every operator would have needed to
keep their clock in sync without ever being told so.It comes from the blockchain's own clock instead, which every instance shares,
so this was never a problem. But the one number the calculation depends on was
written down separately in the browser and in the server with nothing tying
them together, and changing either alone would have quietly misdated every
directly-delivered message — visible only as messages appearing in the wrong
order next to ones that arrived the slow way. There is now a check that fails
if the two ever drift apart. -
A missing database index could switch off every push notification on your
instance, and nothing would have told you. A chat message is notified twice
on purpose — once when your instance sees it, once when the chain makes it
final — and exactly one of those is allowed to reach the phone. The mechanism
is a uniqueness rule in the database that the second one collides with
harmlessly.If that rule is absent, PostgreSQL does not quietly skip the check: it refuses
the insert outright. Your instance catches the refusal so a notification
failure can never take a chat message down with it — which is the right
behaviour, and which means a database missing that one index delivers no
chat and no feedback notifications at all, with no error page, no failing
health check, and one log line per message. Outbid notifications carry on
working, which makes it worse rather than better: push is evidently alive, so
nobody would connect "my phone stopped buzzing for messages" to a schema
problem.This is not hypothetical bookkeeping: the index is created by a migration, and
a database restored from a dump taken with the wrong flags, or hand-repaired,
or migrated by an interrupted run, arrives without it and looks completely
healthy.morphit-ops doctoralready compared your database's tables and columns
against the shipped schema and would tell you about anything missing. It had
never looked at indexes. It does now, and it names the ones you are missing.
Getting that right needed one subtlety — the schema creates a handful of
indexes and later replaces them, so counting those as missing would tell every
healthy operator their database was broken, which is how a real warning gets
trained into background noise. Verified against three live databases: a
healthy one reports clean, one with the index dropped names it, and restoring
it goes clean again. -
On a slower server, your instance now knows how many messages it can take
and still be fast. Every message another instance hands you has its signature
checked before it is shown, and those checks happen one at a time. So anything
waiting in that line is waiting behind all the checks in front of it — which
means the length of the line is really a length of time, and how much time
depends entirely on how quick your hardware is.Your instance used to allow a fixed 500 messages to queue up, whatever kind of
box it was running on. On a fast machine that is about three seconds and fine.
On a modest VPS with the database in a container it can be ten — so your
instance would accept a message, tell the sending instance it had it, and then
deliver it well outside the six seconds this release is built around. That is
worse than politely declining it, because a declined message travels by
blockchain and arrives late in a way nobody was misled about.Your instance now times its own checks and allows exactly as many to queue as
it can still clear in time. A fast box carries more than before; a slow one
carries less and says so.morphit-ops healthshows the figure when it is
below the maximum, along with what a check is costing you:Fed. chat: 3 peer(s) (120 delivered, 8 failed) ↳ received 97 from peers, 61 shed ↳ intake queue held to 252 (18.2ms per check) so pushes still land inside 6s — extra pushes go by chainNothing to configure. If you see that line, the overflow is going by blockchain
exactly as it always did, and a faster disk or a less busy machine is what
would raise the number. -
And the line above it was broken since the last release.
morphit-ops healthwas written to read a field the indexer does not send, so "received N
from peers" never appeared on anybody's terminal. It does now. The two sides
are checked against each other from this release on, so a rename cannot quietly
disconnect them again. -
A captured message could have been re-delivered by flooding your instance.
Your instance remembers every message another instance hands it, so the same
one cannot be shown to you twice. That memory is finite, and it used to make
room by forgetting its oldest entry — which meant anybody could decide when it
forgot, by sending enough of their own messages to push the rest out.The result would have been a real message from a real person appearing in a
conversation a second time, minutes later. Not a forged message: nobody can
write in someone else's name, and the blockchain copy remains the record of
what was actually said. But a duplicate nobody sent, which is unsettling and
looks like something worse than it is.Measured rather than assumed, and the surprising part is which instances were
exposed: the memory holds fifty thousand messages, and the limit on filling it
is how fast your server checks signatures — so a fast server was more
exposed than a slow one, because it could be emptied sooner. On the hardware
this was measured on it took under five minutes.Your instance now refuses new hand-offs rather than forgetting something it
still needs to remember. Those refused messages arrive by blockchain as they
always did. If it happens,morphit-ops healthsays so plainly, because at
any volume it means somebody is trying:↳ 41 push(es) refused to protect replay memory — a flood, or this box is busier than that table is sized forNothing to configure, and the memory recovers by itself.
-
A just-restarted instance is now cautious about how much it takes on. The
limit described above — how many hand-offs your instance queues before
declining — is worked out from how long its own signature checks take. Until
it has done a few, it has to assume something, and it used to assume the speed
of a fast machine with a local database.On anything slower that assumption is optimistic in the dangerous direction:
for the first minute or so after a restart your instance would accept more
than it could deliver in time. Which is precisely when a backlog is waiting —
the messages that arrived while it was down.It now assumes the slow end to begin with and speeds up as it measures itself.
A fast server reaches its true capacity within a few dozen messages; a slow one
never over-promises in the first place. The only visible difference is that a
busy instance may decline a few more hand-offs in the minute after a restart,
and those arrive by blockchain as always.
Found by following each fix to its edges
The last rounds of review took every open question left from earlier rounds and
read the code around it, not just the lines it named. The first two items are
the most serious in this release, and both are about a promise Morphit makes to
operators who run hidden-only.-
A hidden-only instance contacted other instances over the open internet.
An instance set up to use no clearnet at all still looked up and connected
directly to every peer that has a clearnet address, every time it checked the
directory. That hands this box's real IP address to every one of those
peers, which is the exposure hidden-only mode exists to prevent. The safety
net that is meant to refuse any stray clearnet request was being bypassed by
the one piece of code that made its own connections. It now refuses before it
even looks the name up, since the lookup alone reveals which peer you are
asking about. A peer your instance declines to contact is listed as "not
checked", never as unreachable. Demonstrated with a real listener standing in
for the peer: before the fix it was contacted; after, it is not. -
The relay on a Tor-only install used clearnet blockchain nodes. The relay
is the part of Morphit that sends signups and transfers to the blockchain. It
has its own list of blockchain nodes, and the Tor-only install never set it,
so it used the public clearnet list from the box's own address. It also could
not use a Tor or I2P node at all, and would not start without a clearnet
one. The instance still said "Zero use of clearnet internet", because that
claim only asked the indexer.The relay now uses the same Tor and I2P routing and the same fail-closed
safety net as the indexer. A Tor-only install gives it only hidden nodes, and
upgrading does the same for a node that is already installed. The
"zero clearnet" claim now asks the relay as well, and is not made unless the
relay confirms it. A hidden-only relay does not send phone or browser push
notifications, because every push service in existence is a clearnet server.
See Notes for what to configure. -
…and turning push off there had two side effects, both fixed. On a node
where people had subscribed to push before upgrading, the queue of pushes
waiting to go out would have grown for as long as the node ran, because only
the push sender cleared it. It is now cleared by the same rules whether or not
anything is sending. And those people's browsers kept saying "subscribed", or
told them the operator had not turned push on "yet". Now the settings page
says plainly why push is off on this instance, in all ten languages, and stops
claiming a subscription that nothing will deliver to. -
Prices on a hidden-only instance never came from the federation. A
hidden-only instance sets its prices from what its peers report. It had never
received a single report: every request to a peer was sent through a function
that refuses Tor and I2P addresses, and the price checker was off by default.
So it priced everything from its own trades or the fixed minimum, while its
public status said "federated". It now asks each peer at every hidden address
that peer publishes, over I2P, Tor and Lokinet, and always runs on a
hidden-only instance. Clearnet instances are unchanged. -
A message the blockchain never recorded looked delivered. The check that
marks an unconfirmed message almost never ran. Your own instance
shows you your message the moment it accepts it, and that moved the message
past the state the check was looking at. It now covers that case. A resend
inside an order conversation also dropped the order it belonged to, so it was
charged as a message to a stranger and filed as a direct message; resends now
keep it.On the other person's screen, a message that never reaches the blockchain is
no longer shown as if it had. After two and a half minutes it is shown dashed
and dimmed, with a note saying it was never recorded. The PDF export says the
same instead of "pending". If the real copy arrives late, the note clears
itself. A resend replaces the unrecorded copy only when the words are
identical, so a resend cannot swap new words in behind old ones. The two
error messages that were English-only now appear in all ten languages. -
A review and a chat message in one transaction could cost a notification.
Both used the transaction's id to recognise their own duplicates, so the second
one looked like a copy of the first and was silently skipped. Reviews now use
their own key. Chat's key is unchanged, so the protection against being
notified twice for one message still works exactly as before. -
An I2P key handed over as text was saved in a form I2P cannot use. The
key import accepted a key written out as base64 text, checked it, said it was
valid and named the address it hosts. Then it stored the text, not the key
itself. Restored to i2pd, that hosts nothing and logs nothing about why. It now
stores the key itself. Tested against a real i2pd: the restored key hosts the
address the import promised. A file that holds only the public address still
gets refused with nothing saved, as before.If you imported an I2P key as base64 text on 1.17.15, it was stored that
way. Exporting it now writes the binary form i2pd can use, and prints a
note saying so. To store it in that form too, import the exported file again. -
Your instance now checks its own Tor, I2P and Lokinet directly. It used to
find out that one of them was down only when a message failed to go through
it. It now checks at startup and every minute. For Lokinet it asks the name
that always means "this machine", which proves Lokinet itself is working. That
settled a question the instance could not answer before: when a Lokinet peer
cannot be reached, is it that peer or our own Lokinet? Because it could not
tell, a peer whose Lokinet address had died kept its place in the list of
peers each message is sent to, indefinitely. Now it is recorded as failing
and makes way for peers that work.
Found by a final review, after the release was cut
1.18.0 was fully tested and ready to publish when five more reviewers read it,
each taking one part: the receiving side, the sending side and its transports,
the database and key handling, the browser and relay, and the upgrade and
operator scripts. None of them had seen the code written, and each was asked
to look for what the fifteen earlier rounds had missed. They found forty-four
things. Each finding was reproduced or traced before it was fixed. Each code
fix has a test, and each test was first run against the unfixed code and seen
to fail. What they found, most serious first:-
Any chat message ever sent could be replayed as new. Every chat
transaction, signature included, is public on the chain. An instance checks
a handed-over message's expiry time, but only when the time was written the
usual way. The same time wrapped in a list skipped the check, and the
signature still verified. Anyone, with no key, could take an old message
between two people and hand it to an instance again, as often as every
twelve minutes. It arrived stamped as new: at the bottom of the open
chatroom, with the unread badge lit and possibly a phone notification. An
old "pay to this address" message could become the newest message in a
trade. An instance now accepts only a transaction in exactly the shape the
chain writes. It rebuilds a clean copy from the checked fields and uses that
copy for everything after, not the one it was sent. -
A posting key the owner had disowned could keep working. Your instance
checks handed-over messages against the posting key it holds for the
sender. Two kinds of account change did not update that key. One emptied the
posting keys, which is a natural way to stop a leaked key. The other needed
two keys to sign together. Either way the old single key stayed trusted for
fast chat after the chain stopped accepting it. A key is now trusted only
when it can sign alone, and any change to an account's posting authority
updates the stored key, clearing it if no single key qualifies. -
Keys were confirmed against a single chain node. This release confirms
every posting key recorded before it, and that confirmation believed the
first node that answered. A node that was out of date could confirm a key
the owner had already replaced, and it would stay confirmed. Keys are now
confirmed only when two nodes agree. Four related gaps are closed with it:- a key missing from a node's answer is treated as no answer, not as "no
key"; - a failed confirmation is retried in the background, starting after a
minute and backing off to half an hour, instead of waiting for the next
restart; - a database restored from a snapshot has every key marked unconfirmed, so
this node checks them itself instead of trusting the publisher's marks; - while this node's indexer is more than 200 blocks behind the chain, fast
chat asks the chain instead of trusting the stored key.
- a key missing from a node's answer is treated as no answer, not as "no
-
One instance could stop your instance checking its peers. When a peer
on Tor, I2P or Lokinet answered with an error and a large page, the
connection was never closed and the probe waited for it forever. From then
on no peer was checked again until a restart, so the directory and fast-chat
rankings froze. An error answer's body is now discarded, and closing the
connection has a time limit. -
One registration could switch I2P off for fast chat, everywhere. An
instance decides its own I2P is down when two different addresses fail in a
way that points at its own router. Both addresses could belong to one peer,
which can publish two. So a single registration with two dead I2P names made
every instance that tried them drop I2P for a minute at a time, moving other
peers' messages to Tor or the clearnet. It now takes two different
instances. The same review found that peers listed without a check (a
clearnet peer seen from a hidden-only node, for example) were ranked with
the best-checked peers for fast chat. They now rank below every peer that
was actually checked. -
Tor-only servers were asking their internet provider about Lokinet.
Lokinet has no proxy, so a.lokiname is looked up with the system's DNS.
On a server without Lokinet, which is every server Morphit installs, that
DNS is the internet provider's. The indexer asked it forlocalhost.loki
once a minute, and for every peer's.lokiaddress on each warm-up and
send. That tells the provider the server runs Morphit. Lokinet is now off
unless you publish a Lokinet address or setMORPHIT_INDEXER_LOKINET=on.
The same review closed three more lookups a tor-only server should never
make:- with its Tor or I2P proxy switched off, the indexer handed
.onionand
.i2pnames to the ordinary connection, whose first step is a DNS lookup;
they are now refused; - a
.onionname of the wrong length was not treated as hidden, and went to
DNS; it is now refused; morphit-ops doctorfetched the node's own.onionaddress directly
before trying the local port, which sent the node's own onion name to the
provider's DNS; it now goes straight to the local port for a hidden
address.
- with its Tor or I2P proxy switched off, the indexer handed
-
A trading partner could put words on your screen next to a blockchain
proof they did not match. Your browser merges two copies of the same
message: the one delivered fast, and the one read from the chain. It matched
them on a tag chosen by the sender, and never compared what they said. A
sender could have one message delivered fast and never recorded, then record
a different message under the same tag. Your screen, and the PDF export,
then showed the first message's words with the second message's "Blockchain
proof". Two copies are now merged only when their encrypted contents are
identical. -
Leaving a chat lost the "not sent — tap to retry" state. A message the
network accepted but that never reached the chain is marked failed after a
while and offered for resending. That state lived only while the
conversation stayed open. Leave and come back, and the message either
restarted its wait or quietly disappeared. While the tab stays open, it is
now kept until the message is confirmed or resent. (In the setting that keeps
no copy of your own messages there is nothing to restore, so nothing
changes there.) And before marking anything failed, the page now
asks the chain for the transaction. A slow indexer used to make a message
already on the chain look lost, and resending it put it there twice. -
On a Tor-only relay you could not remove your push subscription. Such a
relay sends no web push, and it refused every push request, including
"unsubscribe". The stored link between your account and your device's push
address stayed, with nothing ever pruning it. Unsubscribing now works
whether or not the relay sends push, and the settings page does it for you
when it learns push is off there. -
A message the chain had already accepted could be reported as rejected.
If a chain node took a transaction but the answer timed out, the next node
answered "duplicate", and that was reported as a rejection, which invites a
second copy. For chat, "duplicate" now counts as sent. -
One slow chain lookup could hold up every incoming message. When a
sender's key needed re-reading from the chain, the single worker that checks
incoming messages waited for it, for tens of seconds if the chain nodes were
slow, while more messages were accepted behind it. Those checks now run
beside the queue, at most sixteen at once and fifteen seconds each. A
message that is not checked in time goes by the chain as usual. -
Smaller things on the receiving side. One malformed entry turned a whole
batch into an error and dropped the good entries after it; it now costs only
that entry. Queued messages kept whatever extra data the sender attached, up
to about 128 MB for a full queue; only the checked copy is kept now. Two
deliveries of the same message arriving together could both announce it;
the second is now suppressed before either waits on anything. -
Smaller things on the sending side. A warm-up read a peer's reply with
no size limit; it now stops at 64 KB. A connection to a peer was closed by
the peer's web server after 75 seconds idle, but renewed every three minutes,
so every renewal paid for a new tunnel. The frontend now keeps idle
connections for five minutes and says so in its replies. Shutdown now has a
time limit even with a warm-up in progress. The Tor connection code could
report one failure twice. A failure while greeting your own proxy was blamed
on the peer. A proxy that closed early was waited on for the full twenty
seconds. A proxy reply of an unexpected length could leave stray bytes in
the connection. And bytes that arrived with the proxy's reply could be lost. -
morphit-ops doctorpassed a broken push index. If an attempt to create
the push queue's unique index failed, Postgres left an index with the right
name that it does not use. Doctor checked only the name, so it reported a
healthy schema while every chat and feedback push failed to queue. It now
checks that the index is valid and unique, and the repair steps in
OPERATIONS.md drop a broken index before rebuilding it. -
A very old database could not upgrade. Migration 38 builds an index on
a column that is only added after all migrations have run. A database old
enough not to have that column stopped at migration 38 on every boot. The
migration now adds the column first if it is missing. -
The latency probe could report
PASSfor a peer it never reached. The
script you run after this release to measure real Tor and I2P timings
counted an error page from your own I2P proxy as a fast answer. It also
measured I2P on a different path from the indexer's, could run its three
"cold" Tor samples on one existing circuit, read settings the indexer does
not use, and, given a clearnet address, connected to it directly from your
server. All fixed; see "Measuring real fast-chat latency to a peer" in
OPERATIONS.md for what it does now. -
The relay fix for Tor-only nodes now proves itself. On a node that uses
no clearnet, the upgrade moves the relay onto hidden services only. It used
to announce "The relay now reaches the chain over hidden services only" as
soon as it had edited the file, before any restart and without checking. It
now restarts the relay and reads the relay's own health report. If the relay
does not come back, it restores the file and restarts the relay on its old
settings. It also runs first among the upgrade's self-repairs, each of which
can now fail without stopping the others. -
Automatic IPFS release pinning pinned nothing. The pinner, the IPNS
rebroadcast, their setup script and the desktop upgrade notice all asked for
the release on port 8088, where nothing listens. The indexer is on 8081.
Every run failed quietly and the timer looked healthy. They now use 8081,
and the pinner and rebroadcast also try the indexer's Docker bridge
addresses. -
The release monitor could never report a release. Its service blocked
memory permissions Node needs to run. It looked for its tools from the
wrong directory and went to the npm registry for them. When it did report,
the version fields came out empty. And on a hidden-only node the check
downloaded the whole release over Tor to learn one version number. All
fixed: it uses the install's own tools, and a hidden-only node reads the
version from its own indexer. UPGRADING.md also named an Ansible role for
it that does not exist; it now gives the real steps. -
Two old repair scripts could undo newer work.
ops/apply-relay-fix.sh
andops/apply-federation-fix.shpatched the indexer's code on a live
server until a fix shipped. Both fixes shipped long ago, but the scripts
still ship with every release. Run today, the first would replace a function
with its old version and drop the rule that keeps a hidden-only node from
looking up its relay's public name. Both now see that their fix is already
installed and stop, changing nothing and restarting nothing.
Three smaller findings were looked at and deliberately left for later. They are
recorded in the audit with the reasoning:- the chat hand-off to a clearnet peer does not use the probe's check against
private addresses. Certificate checking stops anything being delivered to
one, so the exposure is a connection attempt; - an
https://hidden-service address is dialled as plain HTTP on port 80; - a cached key correction is dropped a few milliseconds before the database
change that replaces it is committed.
Found by the v1.18.0 deep-deep
This release was renumbered from 1.17.16 to 1.18.0 because of how much it
changes. Before publishing it, six more reviewers went over the whole system,
not only this release's changes. Each took one area:- the fast-chat path;
- keys, the database and the upgrade path;
- every outbound connection and the transports;
- the browser and the relay;
- installing, upgrading and the release pipeline;
- the marketplace itself, attacked as a whole.
They worked through each area threat by threat (spoofing, tampering,
repudiation, information disclosure, denial of service, elevation of
privilege), then attacked it the way a hostile peer, operator, counterparty or
RPC node would. About sixty findings came out. All are fixed except the three
named at the end of this section. Each fix has a test that was run against the
unfixed code and seen to fail first. The most serious, first:-
A single hostile chain node could take over a new node during fast
sync. A new node can start from a published database snapshot instead of
replaying the chain. It found the snapshot by asking ONE chain node for
@morphit's history, checked no signature, and fed the downloaded file to
the database tool, which also runs shell commands written into the file. One
dishonest node among the twenty public ones could therefore run commands as
root on any node being set up. Now:- the snapshot record must be agreed by two independent node operators;
- it must be signed by @morphit's pinned key;
- the file is refused if it contains any database command outside its data;
- the restore runs as one transaction, so a failed restore leaves the
existing database untouched; - any database code the snapshot adds that Morphit itself does not create
is removed.
-
Published snapshots contained people's push subscriptions. The relay
shares the indexer's database, and the export copied all of it: every
browser push subscription, which links an account to a device's push
address, went out in a file published on IPFS. So did the relay's own
payout queue. Snapshots now carry only what the chain can rebuild, and a
restore wipes those tables if an older snapshot still carries them. -
Registering a crafted address found out where Tor-only nodes are. A node
that uses no clearnet decided whether an address was "local" by how its
name started. So an instance registered ashttps://10.something.attacker
was looked up in the internet provider's DNS and connected to from the
node's home address, every time a user sent a chat message. Only real IP
addresses count as local now. Registration refuses names made to look like
one, and a hidden-only node never gives a peer a clearnet address at all. -
Tor-only nodes still used the clearnet in several places:
- the
morphit-opsmenu checked for new versions and the relay's balance
over the clearnet; registerand payment-method changes were broadcast through clearnet
chain nodes, putting the node's onion address and home IP in the same
request;- the first-online helper tested clearnet nodes every five minutes;
- the IPFS daemon joined the public IPFS network from the home address and
announced itself as a host of Morphit releases, which anyone could list; - each upgrade's seeding step downloaded a file from git.agorise.net.
All of these now go through the node's own indexer or its hidden
transports. IPFS runs with no public network at all, sending no telemetry
and making no DNS lookups, and it still serves releases over the node's
onion and I2P addresses. Existing nodes are switched over by the upgrade,
which then checks that IPFS came back. - the
-
"Two nodes agree" could mean one operator. Keys and other trusted
answers must be agreed by two chain nodes. Every privacy-network node is
listed twice, once as an onion and once as I2P, so one operator answering
on both counted as two. Agreement now counts operators, not addresses. The
chat fast path's key re-check, which asked a single node, now asks for
agreement too. -
A local program could decide what an upgrade installs.
morphit-ops upgraderuns as root and took the release's fingerprint and download
location from whatever answered first on the indexer's port. Any program on
the machine that got there first chose what root would install.- The upgrade now reads the node's settings from its own root-owned
configuration. - It asks only the indexer address configured there, and checks that the
process answering really is the Morphit indexer service. - A node with a non-standard setup can set
MORPHIT_UPGRADE_TRUST_LOCAL_INDEXER=1to skip only that last check.
- The upgrade now reads the node's settings from its own root-owned
-
A mirror could downgrade a node. Any older signed release was accepted
as an "upgrade". Now:- an upgrade must be strictly newer (use
--allow-downgradeto go back on
purpose); - the unpacked release must say it is the version that was asked for;
- a known release fingerprint must match even when a signature checks out;
- a signature that is present but wrong refuses the upgrade.
The release pipeline also pins its one remaining unpinned build step to an
exact commit, and checks the tarball, fingerprint and signature again before
publishing. - an upgrade must be strictly newer (use
-
The relay's per-address signup limits could be skipped with one header.
Each signup costs the relay about 102 BLURT. The relay believed whatever
address a visitor wrote in a request header, so one person could look like
a new visitor on every request. That got around every per-address limit, up
to the daily ceiling of about 5,100 BLURT. The public firewall trusted the
same header from anyone. The relay and the AI-agent endpoint now use only
the address the proxy itself saw, and the proxies no longer pass the
visitor's claim along. Existing BunkerWeb installs are corrected by the
upgrade, which checks the running firewall afterwards. -
Anyone could get free "verified" Bitcoin-fee listings. When the block
explorers reported a fee transaction as not existing, the order waited as
"pending" instead of "unpaid". Two accounts, one of them the poster, could
then vouch for it and make it verified, as often as they liked. Now:- "not found" by the explorers means unpaid;
- the poster cannot vouch for their own order;
- vouchers flagged as linked to the poster don't count;
- the indexer re-checks pending and vouched-for orders with the explorers,
whose answer always wins.
-
Someone else's Bitcoin fee payment could be claimed first. The fee
address is public, so a bot watching Bitcoin's pending transactions can
claim a payment before the person who made it posts their order. Before,
the real payer's order then silently disappeared. Now it appears, marked as
a fee someone else already claimed. Tying each payment to the person who
made it needs a change to how Bitcoin fees are paid, and it is being
designed for a later release. Until then, post the order right after paying,
or pay the fee in BLURT. -
Privacy-only nodes could be told wrong prices. A privacy-only node takes
its price from the median of its peers' prices, and a peer is free to set
up. Enough fake peers could move the median anywhere: users would be quoted
a listing fee a hundred times too small (and lose it as "underpaid") or ten
times too large. Now:- each operator counts once;
- the result must stay within 15% of the fee price pinned on chain;
- the page never quotes outside that range.
-
Fast chat could be jammed or misused:
- the send endpoint forwarded messages the chain would reject, however
large, to every peer before the chain had seen them; - a quick-notification shortcut skipped the check that a message's order
tag is real; - junk signatures could use up the key re-check allowance for everyone;
- one sender could fill the replay memory and lock everyone else out for
minutes; - messages that arrived while a key was being re-checked were dropped;
- a sender-chosen time decided whether an order still counted as live.
Each is fixed: only what a peer would accept is forwarded, one signer can
only use its own share of the memory and the queue, and gates use the time
a message actually arrived. - the send endpoint forwarded messages the chain would reject, however
-
In the browser:
- a profile picture could cover the whole page with its own drawing. That
could be a fake "payment verified" banner in the middle of a trade.
Pictures are now shown as images, which cannot escape their frame; - signing out left notifications for that account arriving on that
browser; - the idle lock left an open conversation readable;
- a message that reached the chain but that indexers refused looked
delivered forever. It now says so; - the chain lookup could wait forever.
- a profile picture could cover the whole page with its own drawing. That
-
Reputation and the directory:
- completed trades counted even when no listing fee was paid;
- the free first-purchase listing could be cited in a review and trigger
the relay's welcome bonus; - whoever registered a web address first owned it in the directory, so a
squatter could get the real operator marked as an impostor. Ownership now
follows the account the address itself names; - look-alike operator tags such as
m0rphitormorphit-iowere accepted.
The network, themorphit-ops registercommand and the registration page
now refuse them.
-
Operator tools:
- the snapshot check always reported a false "quarantined";
- a restore under a different database user failed after dropping the
database; - chain nodes could redirect the indexer and relay, and send replies of any
size; - the relay fix for Tor-only nodes could undo itself on a slow Tor start;
- a rollback did not restore the relay's settings;
- exporting a privacy key could write into a file another user had
prepared; - release-mirror file names were not checked;
- the I2P proxy closing mid-handshake could jam I2P delivery until a
restart; - the zero-clearnet claim ignored the Matrix alert bot;
- the AI-agent order search always came back empty;
- the release monitor's failure hint pointed at the wrong place on
privacy-only nodes.
Left for a later release, on purpose:
- tying Bitcoin fee payments to the payer (above);
- a fee or rate limit on operator registrations;
- a reputation requirement for price-reporting peers.
The first upgrade to 1.18.0 is still run by the previous version's upgrade
program, so a few of these protections take effect from the next upgrade on.
OPERATIONS.md §25b lists which.Found by the release's first CI run
- A snapshot export could leave out rows and still report success. The
export writes one table in two steps: the database dump, then that table's
shared rows added by a second tool. If the second tool failed, its failure was
swallowed and the snapshot was published without those rows. Every step now
has to succeed, or the export stops and says so. A new test runs the export
with each database tool broken in turn and expects it to refuse. - "Your psql is too old" was the answer to three different problems.
Restoring a snapshot needs a recentpsql(August 2025 or later). When psql
was not installed at all, or could not reach the database, the restore still
said it was too old. It now says which of the three it is, so the fix it
suggests is the right one. It still changes nothing in all three cases, and it
never prints the database address (which can hold the password). - The test machine itself had an old psql. Its restore tests died at that
check before they read the snapshot. Some of them looked like passes, because
"refused, and nothing changed" is also what those tests expect from a hostile
snapshot. The test machine now installs the current PostgreSQL 16 client tools
from PostgreSQL's own package repository. Their signing key is checked
against its published fingerprint before anything is installed. Before any
test runs, the machine also proves that psql accepts the safety mode the
restore relies on. A check on the test setup keeps all of that in place, and a
new test fails outright on any machine whose psql lacks it.
Also fixed
-
Starting a chat now works on a privacy-only instance. Opening a
conversation with someone for the first time checks their chat key against the
blockchain before trusting anything your instance says about it. Your browser
gave that check fifteen seconds. Your instance is allowed a full minute for the
same lookup, because reaching the chain over Tor or I2P means building a
circuit or a tunnel first — so the browser kept giving up while the server it
was waiting on was still perfectly on schedule. Worse, "no answer yet" was
treated as "the blockchain says this person has no key", which is what Morphit
reports when a key looks tampered with. So a slow connection told you your own
instance might be fabricating data, and advised you to go and use a different
one — the least useful advice possible if you are using a privacy-only instance
because the others are blocked where you are. The wait now allows for the
slower route, and a connection that does not complete is reported as exactly
that: try again in a moment. -
The rest of the site works there too. That timeout was not the only one
sized for an ordinary connection. The chain lookup every message send waits
on, the account-name check, the profile fetch behind display names and
avatars, the fee figures, account creation, the release check and the version
poll were all capped at between five and thirty seconds — often less than a
fresh Tor or I2P connection spends simply getting established. Each then
reported the same underlying timeout as its own unrelated problem: a key that
couldn't be verified, an instance that looked unreachable, avatars replaced by
patterns, fees quietly falling back to defaults. Rather than adjust them one
at a time, requests that cross a privacy network are now given the time that
route needs, everywhere, automatically. Ordinary instances are unchanged and
no slower. -
A stalled connection now fails instead of hanging forever. Timeouts
covered only the moment of connecting — once a reply began arriving, the rest
of it had no time limit at all. A connection that died halfway through, which
is an everyday occurrence on Tor and I2P, left the page waiting with no error
and nothing to retry: a spinner that never stopped, or a message stuck as
sending. The limit now covers the whole exchange. -
Comparing two instances no longer invents differences. The comparison page
fetches each instance's orders and shows what one has and the other does not,
so you can tell whether orders are being hidden from you. But each instance
returns at most a hundred orders, newest first — and a busy instance's hundred
therefore reaches less far back in time. A few genuinely new orders at the top
push an equal number off the bottom, and those pushed-out orders, sitting on
both instances quite happily, were listed as missing from one of them. They
always looked old, because the bottom of the list is where old orders are. The
comparison is now limited to the range both instances could fully report on,
matched the same way the orders themselves are ordered so that nothing inside
that range is skipped, says plainly when it had to narrow the range, and
states its conclusion outright instead of leaving you to read three numbers.
Orders genuinely missing from one instance are still reported — including one
sitting right at the edge of the range, which is where a hidden order would be
easiest to miss. -
The Settings page no longer loads blurred. The language filter opened its
menu whenever the field received focus, and an open menu dims and blurs the
page behind it. Firefox restores focus to wherever it was when you reload — so
once you had used that field in a tab, every later visit re-opened the menu and
blurred the page before you had touched anything. Clicking anywhere cleared it,
which made it look like a rendering fault rather than a real one. Menus now
open when you press, type, or use the arrow keys, and never on focus alone. Two
other filters had the same fault and are fixed with it. -
Privacy-network nodes are no longer offered to browsers that cannot use
them. A secure page is not permitted to fetch anything over plain HTTP, and
the Tor addresses Morphit tried first are plain HTTP — so on a normal instance
every visitor's browser started by making two requests it was always going to
refuse, and logged an error for each. They are no longer offered where they
cannot work. Visitors on Tor are still pointed at the instance's own.onion
address, where the privacy path applies properly and no ordinary connection is
made at all.
Notes
-
No protocol/consensus change. Everything here is operator-facing or in the
browser; nothing about what Morphit publishes to the chain has changed. The
direct instance-to-instance chat delivery is an addition alongside the chain,
not a replacement for it. -
One thing to configure — and
morphit-ops doctornow checks it for you.
/v1/federationcarries a batch of chat messages from a peer and needs a
larger body limit than the read default:MORPHIT_INDEXER_MAX_FEDERATION_BODY_BYTES
(256 KB, documented inops/env/indexer.env.example) and a matching
client_max_body_sizeon that location in your reverse proxy, or the proxy
rejects the batch before the indexer ever sees it.This matters most if you are upgrading, because your existing proxy config
is by definition the old one. The shipped configs inops/nginx/have the new
block; a config you copied and edited months ago does not. And the failure is
invisible in every way that matters — nothing errors, single messages keep
working, and chat only goes slow when your instance is BUSY, which is when you
are least likely to be reading logs.So
morphit-ops doctornow sends an 8 KB batch-shaped body to your own public
origin — above the 4 KB read default, far below the federation cap — and tells
you whether it got through. The body is deliberately not a real transaction, so
nothing is delivered and nothing is stored; the indexer refusing it on its
contents is the PASS, because that proves the bytes arrived. It goes through the public origin
deliberately: probing localhost would skip the proxy and report all-clear on
exactly the box that has the problem. On a privacy-only instance whose own
address the host cannot resolve, it says it could not check and gives you the
command to run by hand, rather than guessing. -
Database migrations v60 and v61 run by themselves on upgrade:
- v60 only corrects the description stored on one column
(push_pending.source_trx_id), which said something untrue; - v61 adds one column with a default (
accounts.posting_key_reconciled).
No existing data is rewritten and no table is rebuilt.
- v60 only corrects the description stored on one column
-
After upgrading, the indexer confirms every stored posting key against the
blockchain once, in the background. It reads 100 accounts per request.
When it finishes, the startup log showsposting_key_backfill_donewith a
reconciledcount. Until then, chat from an account whose key hasn't been
confirmed yet may take the slower blockchain route. -
The relay has three new settings, all with working defaults:
MORPHIT_RELAY_HIDDEN_RPC_ENDPOINTS(the public Tor and I2P blockchain nodes,
the same list the indexer uses),MORPHIT_RELAY_TOR_SOCKS(127.0.0.1:9050)
andMORPHIT_RELAY_I2P_HTTP_PROXY(127.0.0.1:4444). Documented in
ops/env/relay.env.example.- If your indexer already uses no clearnet,
morphit-ops upgradefixes the
relay for you. If your relay had no endpoint list of its own (which is
what every Tor-only install had), the upgrade gives it none on clearnet.
It also gives it the indexer's hidden endpoints and proxies, and says so.
It writes a marked block at the end of/etc/morphit/relay.env(or
/opt/morphit/morphit.envif there is norelay.env), which you can
remove to undo it. - If you set the relay's list yourself, the upgrade leaves it alone. It
warns you instead and names the line to change:MORPHIT_RELAY_BLURT_RPC=
(empty). - A hidden-only relay sends no push notifications, because every push
service is a clearnet server. It logspush_disabled_hidden_onlyat startup
so that is never a surprise. Subscriptions your users took earlier are kept,
in case push comes back. Meanwhile their queued pushes are expired and
cleared on the usual schedule (push_queue_janitorin the relay log). - The relay now reports
hidden_onlyon its/v1/health. The instance's
"Zero use of clearnet internet" claim requires that to be true. If the relay
has never answered, the claim is not made. If it answered and then went
down, the last answer stands, because it describes configuration. morphit-ops edit→ RPC now updates the relay's list along with the
indexer's. It used to change only the indexer's.
- If your indexer already uses no clearnet,
-
A hidden-only indexer now always runs the peer price checker, whatever
its setting says. Without it, a hidden-only instance has no federated prices. -
Whether your own Tor, I2P and Lokinet are up is in the local health output
underfastpath→federationDiagnostics→localTransports.nullmeans
you do not run that one.falsemeans it did not answer, and that network
shows innetworksDown. -
For anyone working on the code:
npm run lintat the top of the
repository is now a real check, and it runs with the tests. It fails on any
new or changed file that is not formatted. Files that were unformatted before
are listed inscripts/prettier-ratchet-baseline.txt; that list only ever
shrinks. Running prettier anywhere in the repository now uses the project's
own style. Before, it used prettier's defaults outsideapps/. -
Nothing else to configure for fast chat. Instances discover each other through
the directory they already share, and use whatever Tor or I2P proxy the
instance is already configured with. An instance running an older version simply does not
receive hand-offs, and its users fall back to chain delivery as before. -
To see whether it is working on your own node:
curl -s localhost:3000/v1/health -H 'X-Morphit-Local-Health: 1'and look
underfastpath→federationfor how many peers are known, how many routes
are warm, how many hand-offs have been delivered or failed, and how many
travelled together; and underfastpath→federationIntakefor what is
waiting to be checked and whether anything is being dropped. -
Notifications to a phone or a closed browser were already checked and are
unaffected: that queue is drained every two seconds, well inside the target. -
A message is no longer delivered twice. It could previously arrive once from
the fast path and again when the instance read that block from the chain a few
seconds later. Nobody saw double, because the app already collapsed the pair —
but it was wasted work and double the traffic on the connections where that
costs most. Failures are
logged with the reason, not just a count. -
Both users having a fast VPN does not change the arithmetic above, and it is
worth knowing why: an instance with no clearnet address has no clearnet address
for anybody. Their browsers still reach it over Tor or I2P. A good connection
makes those circuits healthier and faster — it does not skip them — so the
three privacy hops a message crosses are the thing to measure, which is what
the probe script does. -
Nothing needs reconfiguring. The longer waits apply automatically, and only to
requests that actually travel over Tor or I2P. -
If you saw a red "chat key looks tampered with" warning on a
.onionor.i2p
instance, it was almost certainly this bug rather than a real tamper signal.
That warning now means what it says. -
This release was reviewed by fifteen independent passes before it shipped —
none of which had seen the code being written — and a good deal of what is
under "Fixed" came out of them, including two defects in this release's own
tests. The full record, including what those reviews checked and found sound,
is indocs/AUDIT-v1.18.0-FASTCHAT-DEEP-DEEP.md. The design decisions are in
docs/adr/0052-federated-fast-chat-delivery.md. -
The fixes were reviewed too. The first review's remediation added a lot of
new code, and nobody but its author had seen it — which is the same condition
that produced the first round. So it got the same treatment, and that second
pass found two more serious bugs, both introduced BY the fixes: a self-exclusion
that would have cut the canonical instance out of every community instance's
peer list, and an anti-spam cap keyed on the victim rather than the sender, so
one hostile account could have denied a real buyer's first contact. Both are in
docs/AUDIT-v1.18.0-FASTCHAT-DEEP-DEEP.mdunder "Round two", along with one
fix that was made and then deliberately reverted. -
The safety rule this whole feature rests on is now checked against a real
database, not by reading the code. A chat message pushed between instances
is shown to you, never stored — the blockchain remains the only record of
what was said. That is what makes it acceptable to display a message before
the chain has confirmed it: the worst a forged push can do is show something
that quietly fails to appear a minute later.Until now that rule was verified by scanning the source for anything that
writes. Useful, but not the same thing: it proves no write was typed in those
files, where the rule says nothing anywhere should change. Those came apart
once already during this release, when a fix corrected a stale key and saved
it — spotted by reading, not by a test.There is now a test that photographs every table, pushes a real message
through the real code against a real database, and checks that the message
arrived and that not one row anywhere is different. It does the same for a
forged push, because a stranger who cannot prove who they are should not be
able to cause the smallest write. Both were confirmed to catch a deliberately
planted one. -
Review notifications are now held to the same rule, and were not being
checked at all. A review notifies you twice by design for exactly the same
reason a chat message does — once when your instance sees it, once when the
chain makes it final — and the same mechanism collapses the pair. That
mechanism had never been run against a database. It works, and now there is a
test saying so, including the interleaving that broke the chat version in
v1.5.5.The reason it went unchecked is worth saying plainly: the test written for the
chat half carried a note claiming review notifications were single-path and
needed no such protection. That was simply wrong, and a wrong note beside a
passing test is what stops the next person looking. -
And so is the one-message-one-notification rule, which has been broken in
production before. In v1.5.5 a notification was deleted the moment it was
sent, so when the slower of the two paths arrived a minute later there was
nothing left for it to collide with and your phone buzzed twice. The fix was
never to the uniqueness rule — that was always right — but to how long the
record survives, and that is a property no amount of reading the code can
check. It is now driven against a real database, including the exact
interleaving that broke it: notify, deliver, then notify again.The first version of that test passed against the old bug, which is worth
saying out loud. It counted rows, and the count is one either way — one
surviving record when it works, one freshly-created record when it does not.
It now counts what is still waiting to be delivered, which is what the phone
actually reacts to, and replaying the v1.5.5 behaviour fails it. -
Validation. All 15 workspaces typecheck clean, and svelte-check reports
861 files with no errors and no warnings. 2,700 unit tests pass, plus
237 integration tests against a real PostgreSQL. That suite was once
recorded as runnable only on release hardware; it turns out to run here. The
smoke battery (704 runners, 22,011 scenarios) was run three times
end to end with identical results. It now includes a lint and formatting gate
that actually runs.All 18 harnesses pass, 358 checks between them. Eight are MUTATION
harnesses, and the number worth quoting is theirs: 216 deliberate bugs
introduced into the source, 216 caught. Two of the execution harnesses also
plant bugs of their own (8 more, all caught), so the total is 224. The
seventeenth harness is the first to need a real database, because what it
guards is SQL and a migration. The eighteenth, added in the final review,
runs the latency probe against stub proxies and a stub peer.(At the cut these figures were 2,466 unit and 159 integration tests, 17
harnesses, 325 checks and 197 mutations. The two reviews that followed added
the rest. The mutation count stayed at 216 through the deep-deep: its fixes
moved 18 mutations' targets, and each was re-aimed at where its property now
lives and seen caught again, rather than dropped.)Two earlier drafts of this note got that number wrong, and both corrections
are left visible rather than tidied away, because a number nobody can
reproduce is worse than no number. The first said "167 deliberate bugs" —
that was the total check count, which includes the nine execution harnesses
that verify behaviour without mutating anything. The second said 102, which
was not the count of anything: the harnesses held 96 mutations at that point.
The figure above was taken by running all eighteen and counting the
mutations each one reported, not by searching the harness sources. Counting
the sources has been wrong every time it was tried.Every mutation added in this release was watched to fail against correct code
first, because a test never seen to fail is not a test — and five of them
SURVIVED on their first attempt, which is exactly what they are for. Two were
real gaps in the second round's tests. Two more were gaps in the smoke that
covered the transport work, where unit tests had the property and the smoke
did not — and a unit test elsewhere is not an answer, because a harness's job
is to prove the SMOKE is a test. The fifth was the new transport harness
reporting five mutations as unguarded when in truth they had never been
applied; that one is written up in the audit, because a mutation harness that
can be wrong about whether its mutations reached the code is a harness whose
green means nothing.The last rounds added two more of each kind:
- A survivor. Flipping the new migration's default passed every test,
because the test database is built from the full schema and the migration
never ran there. An upgrading node gets the column only from the migration,
so a case now runs the real migration on a database taken back to before
it, and the flipped default is caught. - A mutation that became equivalent. The I2P import mutant stopped
meaning anything once export learned to repair keys stored the old way. The
exported bytes were right either way, so that check now keys on the repair
notice instead.
- A survivor. Flipping the new migration's default passed every test,
Downloads
-
Source code (ZIP)
0 downloads
-
Source code (TAR.GZ)
0 downloads
-
-
Morphit v1.17.15 Stable
released this
2026-09-18 01:19:52 +00:00 | 19 commits to main since this releaseMorphit v1.17.15
Makes adding a privacy-network address safe to do without guessing. No protocol
or consensus change.Fixed
-
You can now see which addresses a registration will publish, before you sign
it. Publishing your instance to the federation is permanent, and peers reach
you at whatever those addresses say — but the confirmation screen showed only
your origin, name, contact and tag. It never showed the addresses themselves.
That is exactly how one instance came to announce a privacy address its own
router had stopped hosting: every peer's attempt to reach it that way failed,
and the one moment someone could have noticed showed nothing. All of them are
now listed for you to check first. -
Your instance reads its own privacy address from the right setting. It
previously took the first address-shaped value found anywhere in the
configuration file. On an instance that also lists other nodes' privacy
addresses — which most do — that could pick up someone else's and then report
your correct configuration as wrong, indefinitely. It now reads the setting
that holds your address. -
Importing a privacy-network key now checks that it is the right key. It was
accepted on sight. Morphit now works out which address the key actually
belongs to and shows it, so you can confirm it matches the address you
publish — and warns you plainly if it does not. A key file exported as text
(base64) is now accepted as-is rather than refused. -
And it tells you if you have the address rather than the key. The long
string you publish as your address looks a lot like a key file and is easy to
grab by mistake. It cannot be used to host anything: your server could not
prove the address is its own, so the site would simply never load there, with
no error to explain why. Morphit now recognises it, explains the difference,
refuses the import, and tells you what the real key file looks like.
Notes
- No protocol/consensus change. Everything here is operator-facing.
- Adding a vanity name (
something.i2p) alongside your long address is safe: the
long one stays the address of record, since a vanity name only resolves for
users whose router knows the registry it is listed in.
Downloads
-
Source code (ZIP)
0 downloads
-
Source code (TAR.GZ)
2 downloads
-
-
Morphit v1.17.14 Stable
released this
2026-09-17 18:21:24 +00:00 | 20 commits to main since this releaseMorphit v1.17.14
Three fixes to what an upgrade tells you and how long it interrupts your site.
No protocol or consensus change.Fixed
- Your site is restarted once per upgrade instead of twice. The web front end
was rebuilt and restarted a second time moments after the first, every upgrade,
even when nothing about it had changed — a short outage for no reason. The
second pass exists for a real case (an older installer that does not rebuild),
so it now asks the running site what settings it is actually using and does
nothing when they already match. - The warrant-canary reminder now tells you the truth for your setup. It said
the canary "republishes on its own at the next scheduled weekly refresh". That
is only true on an instance that runs that refresh itself. If you sign your
canary on another computer, nothing on the server republishes anything — and
believing otherwise would let it go stale after 14 days and show your visitors
a false tamper warning. The reminder now checks which setup you have and says
what actually applies. - A privacy-only instance no longer gets two contradictory messages. One said
a check was skipped because the instance is reached through Tor and I2P; the
next told it to configure an ordinary web address it correctly does not have.
Notes
- No protocol/consensus change. Everything here is operator-facing.
- If a recent upgrade warned that your privacy address was "advertised wrong",
that came from the previous version doing the check. It settles once every
instance is on this release.
Downloads
-
Source code (ZIP)
1 download
-
Source code (TAR.GZ)
5 downloads
- Your site is restarted once per upgrade instead of twice. The web front end
-
Morphit v1.17.13 Stable
released this
2026-09-17 06:31:21 +00:00 | 21 commits to main since this releaseMorphit v1.17.13
Stops an upgrade interrupting operators who sign their warrant canary on a
separate computer. No protocol or consensus change.Fixed
- An upgrade no longer stops to ask about your warrant canary if you already
have one. If you sign your canary on another machine and upload it, a
redeploy clears the copy on the server until you upload the next one. The
upgrade only remembered as far back as the previous install, so if you ever
skipped an upload, the next upgrade concluded you had never had a canary and
offered to create one on the server — which would have made a second signing
key competing with your real one, and stopped an unattended upgrade waiting
for an answer. Your instance now remembers permanently that you have a canary,
and also checks whether your live site is serving one. A brand-new instance
with no canary at all is still offered one, as before.
Notes
- No protocol/consensus change. Everything here is operator-facing.
- If a recent upgrade asked you about setting up a canary and you sign yours
elsewhere, answering "no" was correct. After this release it will not ask again.
Downloads
-
Source code (ZIP)
2 downloads
-
Source code (TAR.GZ)
6 downloads
- An upgrade no longer stops to ask about your warrant canary if you already