• v1.21.0 0248892e7f

    Morphit v1.21.0
    All checks were successful
    morphit-ci / Supply-chain audit gate (every advisory triaged, none fixable in range) (push) Successful in 41s
    morphit-ci / TypeScript typecheck (sweep all workspaces) (push) Successful in 1m25s
    morphit-ci / apps/web svelte-check (svelte-kit sync + svelte-aware tsc) (push) Successful in 57s
    morphit-ci / Integration tests (real Postgres 16) (push) Successful in 9m14s
    morphit-ci / ansible-lint (playbook quality gate) (push) Successful in 57s
    morphit-ci / Smoke suite (run-smokes.sh, triple-pulse) (push) Successful in 1h12m35s
    morphit-release / Build + publish release tarball (push) Successful in 35m56s
    Stable

    Ghost released this 2026-10-05 20:43:23 +00:00 | 3 commits to main since this release

    Signed by agorise
    GPG key ID: 53524E1F1017EB9C

    Morphit 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 upgrade as usual (see "Every node" below for the
    new --questions and --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.org only (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 upgrade tries again", that is now true even when you are already on
    the newest release.
    A plain sudo morphit-ops upgrade on 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-only and --json
    still only look.

    sudo morphit-ops upgrade --heals does 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:

    1. Move the bot to a homeserver on the node or a .onion one: sudo morphit-ops matrix setup.
    2. Run sudo rm /etc/morphit/matrix-bot.tor-only-decision.
    3. 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 with npm 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.asc
    

    Copy both to the node, logging in the way you normally do. Use scp -O, not plain scp:

    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:

    1. 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
      
    2. Run the upgrade from the bundle. --yes answers 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 --yes
      

      You 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). Answer y.
      • Without --yes, a box that has never had a warrant canary may also ask "Set one up now, right here on this
        box? …".
        Answer n. 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.
    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
      
    4. Read what ran after the services restarted:

      sudo cat /var/log/morphit/after-upgrade-heal.log
      

      The 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.

    5. 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
      
    6. 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 .asc again 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 .asc does.
    • 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 run sudo 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.
    • 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 nginx deny
        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 had BLACKLIST_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.
    • 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=never in
        /etc/update-manager/release-upgrades). To move to a new Ubuntu release later, set it back to Prompt=lts and
        run sudo 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 a vendor/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 --heals on 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 upgrade checks 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/static back to
      root (it already left apps/web/build alone).

    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_KEY secret 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.sh on 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
  • v1.20.3 d7e5957593

    agorise released this 2026-10-02 04:48:05 +00:00 | 7 commits to main since this release

    Morphit 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 upgrade on each server. Nothing needs doing by hand.

    Downloads
  • v1.20.2 bb06920ce5

    agorise released this 2026-10-02 00:51:51 +00:00 | 8 commits to main since this release

    Morphit 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.json or /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.

    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 health shows it as "Block check".

    Upgrading

    Run sudo morphit-ops upgrade on 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
  • v1.20.1 9223ccd131

    agorise released this 2026-10-01 03:16:50 +00:00 | 9 commits to main since this release

    Morphit 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".
    • 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 status shows the result under "Web proxy (BunkerWeb)".
    • 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 a SIGNUPS_DISABLED file 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/relay now 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 upgrade on 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 with sudo 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
  • v1.20.0 1997acdfc6

    agorise released this 2026-09-30 18:02:27 +00:00 | 10 commits to main since this release

    Morphit 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 upgrade publishes 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.
    • 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 btc shows the gap limit to set in the
        treasury wallet.
    • 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.
    • Monero block explorers updated.
      • localmonero.co/blocks now 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, write raw-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 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.
    • 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.
    • 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.
    • 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.
        The SIGNUPS_DISABLED switch 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 relay refuses a sudden account-creation fee spike (more than 1.5× the configured fee)
      instead of paying it.
      • New error codes: relay_fee_spike and broadcast_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.
    • Typing 12,50 into 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.
    • 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.
    • 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.
    • 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-ops lookups 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 doctor no 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 --version shows the real version.
      • branding status asks 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.

    Upgrading

    Run sudo morphit-ops upgrade on 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 register on that server.
      • Check the result with sudo morphit-ops status → "Fees account (federation)".
    • 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.
    • 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
      with http://: run sudo morphit-ops register on 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
  • v1.19.0 2c6b6797a6

    agorise released this 2026-09-27 01:42:11 +00:00 | 16 commits to main since this release

    Morphit 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 the sudo morphit-ops
      menu) asks for each file and the name instead. The logo files are kept in
      /etc/morphit/branding/ and the name in morphit.config.env, and every
      upgrade re-applies them. morphit-ops branding status shows what is set, and
      morphit-ops branding reset brings back the plain Morphit look. The full
      guide is docs/BRANDING.md.

    • 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
      published verify.json lists every file the operator re-branded. If
      branding apply is interrupted (Ctrl-C, a full disk), the next apply or
      reset finishes 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
      run npm install -g npm@… — advice that can break an install, which pins its
      own Node and npm. The earlier fixes switched it off inside morphit-ops, but
      the notice comes from the npm process that starts morphit-ops (on a
      guided install the morphit-ops shortcut runs through npm, and so does
      npx morphit-ops), and it prints when that process exits, where no setting
      inside morphit-ops can reach it. Now:

      • the install carries its own npm setting (.npmrc) that turns the notice
        off for every npm run inside it;
      • the morphit-ops shortcut 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 server-wide npm setting file is no longer writable by every account.
      Saving a setting with npm config set --location=global leaves 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 add accepts 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"), and block --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 of npx 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 the branding apply command 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
  • v1.18.0 1f917067dc

    agorise released this 2026-09-25 22:28:09 +00:00 | 17 commits to main since this release

    Morphit 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, and ops/fastchat-latency-probe.sh measures 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. The shed counter 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 .onion of 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 .onion peer 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 .loki address 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 .loki peer 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 health now 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 health now 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 this
      

      Forty 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/health now 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 doctor already 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 health shows 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 chain
      

      Nothing 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 health was 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 health says 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 for
      

      Nothing 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.
    • 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 .loki name 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 for localhost.loki
      once a minute, and for every peer's .loki address 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 set MORPHIT_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 .onion and
        .i2p names to the ordinary connection, whose first step is a DNS lookup;
        they are now refused;
      • a .onion name of the wrong length was not treated as hidden, and went to
        DNS; it is now refused;
      • morphit-ops doctor fetched the node's own .onion address 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.
    • 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 doctor passed 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 PASS for 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
      and ops/apply-federation-fix.sh patched 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 as https://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-ops menu checked for new versions and the relay's balance
        over the clearnet;
      • register and 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.

    • "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 upgrade runs 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=1 to skip only that last check.
    • A mirror could downgrade a node. Any older signed release was accepted
      as an "upgrade". Now:

      • an upgrade must be strictly newer (use --allow-downgrade to 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.

    • 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.

    • 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.
    • 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 m0rphit or morphit-io were accepted.
        The network, the morphit-ops register command 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 recent psql (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 doctor now checks it for you.
      /v1/federation carries 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 in ops/env/indexer.env.example) and a matching
      client_max_body_size on 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 in ops/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 doctor now 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.

    • 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 shows posting_key_backfill_done with a
      reconciled count. 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)
      and MORPHIT_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 upgrade fixes 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.env if there is no relay.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 logs push_disabled_hidden_only at 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_janitor in the relay log).
      • The relay now reports hidden_only on 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.
    • 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
      under fastpath → federationDiagnostics → localTransports. null means
      you do not run that one. false means it did not answer, and that network
      shows in networksDown.

    • For anyone working on the code: npm run lint at 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 in scripts/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 outside apps/.

    • 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
      under fastpath → federation for 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 under fastpath → federationIntake for 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 .onion or .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 in docs/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.md under "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.
    Downloads
  • v1.17.15 d969a42f83

    agorise released this 2026-09-18 01:19:52 +00:00 | 19 commits to main since this release

    Morphit 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
  • v1.17.14 b2636be7b8

    agorise released this 2026-09-17 18:21:24 +00:00 | 20 commits to main since this release

    Morphit 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
  • v1.17.13 89bcb9be1a

    agorise released this 2026-09-17 06:31:21 +00:00 | 21 commits to main since this release

    Morphit 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