Second commit on this branch (first added the per-sender cap + accept_replica
primitive). This commit wires the actual cross-node propagation:
Outbound (sender side)
----------------------
* New ``DMRelay._replicate_envelope_to_peers_async()`` — fire-and-forget
thread that POSTs the envelope to every authenticated relay peer via
the same per-peer HMAC pattern gate-message replication uses (#256
``X-Peer-Url`` + ``X-Peer-HMAC`` headers, ``resolve_peer_key_for_url``).
* ``deposit()`` now calls the replication helper after a successful
local accept. Per-peer errors are swallowed — slow Tor peers must not
block the sender's UX, and the recipient polling from a healthy peer
works fine even if some peers are down.
* Metrics: dm_replication_push_ok / _rejected / _error.
Inbound (receiving side)
------------------------
* New endpoint ``POST /api/mesh/dm/replicate-envelope`` in
routers/mesh_peer_sync.py.
* Same HMAC auth gate (``_verify_peer_push_hmac``) as the existing
infonet/gate peer-push endpoints. Unauthenticated requests get 403.
* Body cap of 64 KB (DM envelope is bounded by MESH_DM_MAX_MSG_BYTES).
* Calls DMRelay.accept_replica which enforces the per-sender cap as a
network rule — hostile sender's relay can hold extras locally but
honest peers reject them on inbound replication.
End-to-end flow now works
-------------------------
1. Alice's node accepts a deposit to Bob's mailbox (local cap check).
2. Alice's node spawns a background thread that POSTs the envelope
to MESH_RELAY_PEERS with per-peer HMAC.
3. Each peer's /api/mesh/dm/replicate-envelope verifies the HMAC and
calls accept_replica, which re-enforces the per-sender cap.
4. Bob (offline at the time of send) eventually logs into ANY node
in MESH_RELAY_PEERS, his existing pollDmMailboxes pulls from
the local mailbox there, finds Alice's envelope, decrypts.
Tests
-----
backend/tests/test_dm_replicate_envelope_endpoint.py — 4 tests:
TestReplicateEndpointAuth:
- rejects requests without peer HMAC (403)
- rejects requests with WRONG peer HMAC (403) — confirms the
HMAC is actually verified, not just present
- rejects oversize bodies (>64 KB) with 400/413
TestReplicateEndpointRegistered:
- static check that POST /api/mesh/dm/replicate-envelope is
registered on app.routes — catches future refactor that
drops the router include
All 38 backend tests touching the new code paths still pass:
test_dm_relay_per_sender_cap.py (14)
test_dm_replicate_envelope_endpoint.py (4)
test_no_new_duplicate_routes.py (1) — new route is unique
test_per_peer_secret_resolver.py (19) — HMAC primitive unaffected
What's still ahead (PR-3+)
--------------------------
* ack propagation: when recipient pulls a message on node X, peers Y/Z
should prune their copies to free the sender's quota network-wide.
Without this, the sender's quota frees only on the node the recipient
actually polled — other peers still see N pending until TTL expiry.
Workable but suboptimal. PR-3 will add a /api/mesh/dm/ack endpoint
with the same HMAC pattern.
* recipient pull-from-peers: today the recipient's poll only hits
their own node's relay. If they log into a peer they didn't deposit
with, they need a way to fetch envelopes from other peers in
MESH_RELAY_PEERS. Today this works as long as the recipient's
current node is one of the peers Alice's node pushed to — which is
true in a fully-meshed deployment but not guaranteed for partial
meshes. PR-4 if telemetry shows this matters.
Foundation work for cross-node DM mailbox replication. Adds the network
rule that makes the replication safe to ship next, plus the primitives
the outbound replication PR will call.
The rule
--------
A single sender can have at most N UNACKED messages parked in a single
recipient's mailbox at any one time. Default N=2, tunable via
``MESH_DM_PENDING_PER_SENDER_LIMIT``. Once the recipient pulls (acks) a
message, the sender's quota for that (sender, recipient) pair frees up.
Network rule, not local rule
----------------------------
The cap is enforced TWICE:
1. ``DMRelay.deposit(...)`` — local check on the sender's own node.
Refuses to spool the (N+1)th message before it can be replicated.
2. ``DMRelay.accept_replica(...)`` — replication-acceptance check on
every receiving peer. Refuses to accept an inbound replica that
would put the local mailbox over the cap.
The second half is what makes the rule a NETWORK rule. A hostile sender
could patch out the deposit check on their own relay and continue to
spool extras locally — but those extras can never propagate, because
every honest peer enforces the same cap on the way in. A recipient who
polls from honest peers therefore never sees more than N pending from
any one sender, regardless of how many spam attempts the hostile
sender's relay accepted.
New API surface on ``DMRelay``
------------------------------
_per_sender_pending_limit() — reads MESH_DM_PENDING_PER_SENDER_LIMIT
_per_sender_pending_count(...) — counts unacked from a sender for a mailbox
accept_replica(envelope=...) — peer-push receive entry point
envelope_for_replication(...) — helper to extract a wire-form envelope
``accept_replica`` is idempotent on duplicate ``msg_id`` (replication
round-trips and multi-path delivery don't double-spool).
``envelope_for_replication`` exposes the exact shape ``accept_replica``
expects, so the follow-up PR (outbound replication wiring) just has to
fetch the envelope and POST it to authenticated peer URLs with the
existing per-peer HMAC pattern from #256.
Why this is PR-1 of two
-----------------------
The full cross-node mailbox replication needs three pieces:
A. cap enforcement on deposit (in this PR)
B. cap enforcement on replica acceptance (in this PR)
C. outbound: push envelope to MESH_RELAY_PEERS after deposit (NEXT PR)
(A) + (B) shipped together close the cap-bypass attack surface BEFORE
(C) introduces the actual cross-node propagation. Shipping them in the
other order would briefly let extras propagate during the window between
"outbound push lands" and "accept_replica cap lands."
Tests
-----
backend/tests/test_dm_relay_per_sender_cap.py — 14 tests:
TestDepositCap:
- first 2 deposits succeed (UX baseline)
- 3rd from same sender rejected with friendly message
- different senders have independent quotas
- different recipients have independent quotas
- ack frees the quota (after recipient pulls, sender can deposit again)
- cap is env-tunable
TestAcceptReplicaCap:
- replica accepted under cap
- idempotent on duplicate msg_id (no double-spool, no rejection)
- rejected at cap with structured ``cap_violation`` marker so
sender's relay can stop retrying
- per-sender, not per-mailbox: different sender_block_ref passes
even when another sender at the same mailbox is capped
- malformed envelope shapes rejected without crash
TestEnvelopeForReplication:
- returns the envelope for stored messages
- returns None for unknown msg_id
- round-trips through accept_replica end-to-end (proves the wire
shape matches across the two sides)
Ship the v0.9.79 runtime refresh with transport lane isolation, Infonet secure-message address management, MeshChat MQTT controls, selected asset trail behavior, telemetry panel refinements, onboarding updates, and desktop/package metadata alignment.
Also ignore local graphify work products so analysis folders do not leak into future commits.
Gate messages now propagate via the Infonet hashchain as encrypted blobs — every node syncs them
through normal chain sync while only Gate members with MLS keys can decrypt. Added mesh reputation
system, peer push workers, voluntary Wormhole opt-in for node participation, fork recovery,
killwormhole scripts, obfuscated terminology, and hardened the self-updater to protect encryption
keys and chain state during updates.
New features: Shodan search, train tracking, Sentinel Hub imagery, 8 new intelligence layers,
CCTV expansion to 11,000+ cameras across 6 countries, Mesh Terminal CLI, prediction markets,
desktop-shell scaffold, and comprehensive mesh test suite (215 frontend + backend tests passing).
Community contributors: @wa1id, @AlborzNazari, @adust09, @Xpirix, @imqdcr, @csysp, @suranyami,
@chr0n1x, @johan-martensson, @singularfailure, @smithbh, @OrfeoTerkuci, @deuza, @tm-const,
@Elhard1, @ttulttul