feat(xmtp_mls): verify our own key package is live on the network - #3954
feat(xmtp_mls): verify our own key package is live on the network#3954insipx wants to merge 1 commit into
Conversation
|
Claude finished @insipx's task —— View job PR Review - Key Package Liveness CheckReview Date: 2026-08-07T23:46:09Z This is a well-engineered solution to a critical reliability problem. The implementation shows careful attention to edge cases, failure modes, and defensive design. The code quality is excellent with comprehensive test coverage and thorough documentation. ✅ Strengths1. Design Quality
2. Edge Case Handling (crates/xmtp_mls/src/worker/key_package_maintenance.rs)
3. Clock Skew Defense (key_package_maintenance.rs:297-314, identity.rs:277-288)
4. Test Coverage 🔍 Observations (not issues)1. Documented Known Limitation - Concurrent TaskRunners (design doc §5a, lines 350-390)
2. TaskRunner Coupling (design doc §5a, lines 391-397) 3. Network Probe Writes Acknowledged (design doc §9) 🎯 Suggestions (optional improvements)1. Consider Metric/Tracing for
These are rare failure modes that indicate infrastructure problems worth tracking separately from normal "offline client" inconclusive outcomes. 2. Migration Reversibility (down.sql) ✅ Security Assessment
✅ Performance Assessment
✅ Code Quality
SummaryApprove with confidence. This PR solves a real production incident with a well-designed, thoroughly tested, and carefully documented solution. The author has clearly thought through failure modes, documented known limitations honestly, and made sound engineering tradeoffs. The CI fix in commit The only suggestions above are optional observability improvements; the code is production-ready as-is. |
ApprovabilityVerdict: Needs human review This PR introduces a new key-package liveness watchdog feature that adds daily network probes and automatic repair mechanisms for MLS key management. While well-documented and tested with strong author ownership, the significant new runtime behavior affecting core cryptographic key handling warrants human review. You can customize Macroscope's approvability policy. Learn more. |
Key-package rotation is driven purely by a local deadline column (`identity.next_key_package_rotation_ns`). Nothing ever verified that a usable key package is actually published. When that column is wrong the failure is silent and terminal: - the published key package expires and is never rotated; - anyone adding the installation records it in `failed_installations` instead of emitting an Add proposal, so it is a group member with no MLS leaf; - with no leaf it receives no welcome, so the existing welcome-driven rotation nudge never fires either; - the client sees no local error. It syncs, sends messages, and creates groups. Closed loop, no exit, no logs. A dev-network installation sat unreachable for weeks this way, and its only symptom was a missing INFO line. Add `KpLiveness`: a recurring TaskRunner singleton that probes the network for this installation's own key package and queues a rotation when it is absent, unverifiable, or close to expiry. It is seeded next to `KpRotation` and `KpDeletion`, so the first client build after upgrade checks at once. That is what rescues installations that are already broken. The watchdog is deliberately a separate task, not a branch inside `KpRotation`. It needs its own schedule (daily, against rotation's 30 days), its own retry budget (a network probe fails often; rotation must not share its backoff), and its own pull-in target. A watchdog that shares a schedule and a retry budget with its subject fails with its subject. Details the design gets right, and the reasons: - "Absent" has two wire shapes. A short response is `MismatchedKeyPackages`, but the network actually returns a present-but-empty payload. Both mean absent. - The response is matched to the request by position, so a backend defect can return a valid key package for another installation. The probe compares the leaf signature key, and reports `Inconclusive`, because an upload cannot repair a mapping defect and rotating would churn the whole fleet. - Only "asked for 1, got 0" proves absence. Any other count is a backend defect. - The throttle column is never trusted blindly. A future stamp, from clock skew or corruption, would disable the watchdog for months. That is the same failure this check exists to catch. - Startup reconciliation pulls the task in unconditionally, because a clock jump can also strand the task row where the handler's clamp cannot reach it. - Probe failures return a deadline, never `Err`, so an offline client does not drive the task into the TaskRunner's backoff. Also log the rotation deadline at INFO on both branches. A silent "not due" branch is what hid the incident. Requires xmtp/proto#342 (merged as f53e21af), which `proto_version` now pins. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
f33e167 to
8cf9617
Compare
CI fix pushed (
|
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #3954 +/- ##
==========================================
- Coverage 86.02% 85.96% -0.06%
==========================================
Files 417 417
Lines 68092 68565 +473
==========================================
+ Hits 58578 58944 +366
- Misses 9514 9621 +107 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
CI status on
|
The problem
Key-package rotation is driven purely by a local deadline column
(
identity.next_key_package_rotation_ns).rotate_if_neededreadsis_identity_needs_rotation(), and if the column says "not due" it returnsOk(false)and does nothing. Nothing in the client ever verifies that a usable keypackage is actually published on the network.
When that column is wrong, the failure is silent, total, and self-sustaining:
not_afterand becomes unusable.fetch_key_packagesfor that installation returns nothing.failed_installationsinstead of emittingan
Addproposal. It is listed as a group member with no MLS leaf.nudge (
queue_key_rotation) never fires either.groups against the groups where it still has valid leaves.
Closed loop, no exit, no logs.
A dev-network installation reached exactly this state and stayed unreachable for weeks.
Across seven client builds in a 31-hour window, with rotation roughly two months overdue,
the
"Start rotating keys and uploading the new key package"INFO line never appearedonce. Its absence was the only externally visible symptom. A comparison client on the
same build and network rotated normally, so the rotation machinery itself was fine.
The systemic gap: local rotation state is never reconciled against network reality.
What this adds
KpLiveness— a recurring TaskRunner singleton that probes the network for thisinstallation's own key package on a ~24h throttle, and queues a rotation when it is
absent, unverifiable, or close to expiry.
It is seeded next to
KpRotation/KpDeletion.create_or_ignore_taskkeeps an existingrow's deadline, but a fresh seed is due immediately — so the first client build after
upgrade checks at once, which is what rescues installations that are already broken.
Repair reuses
queue_key_rotation, the same already-tested path the welcome nudge uses.Liveness never rotates inline.
Why a separate task, not a branch inside
KpRotationFolding the probe into the existing
KpRotationarm was evaluated and rejected:KpRotation's deadline follows the rotation column(~30 days). Sharing a row means multiplexing two schedules onto one column — and
deriving the watchdog's cadence from the very column whose corruption it detects.
a failing probe drives rotation into exponential backoff, and vice versa.
PullInDeadlinetargets a task bydata_hash. With one row there is no wayto say "verify now" as distinct from "rotate now".
A watchdog that shares a schedule and a retry budget with its subject fails with its subject.
Details worth reviewing
Several of these came out of an adversarial review pass and are the non-obvious parts:
MismatchedKeyPackages, but thenetwork actually returns a present-but-empty payload — exactly what
xdbg query fetch-key-packagesshowed for the affected installation. Both mean absent.This is why the probe calls the API directly instead of
MlsStore, which collapses anempty payload into a generic verification error.
can return a perfectly valid key package belonging to someone else. It verifies, it has
a healthy lifetime, and we would record ourselves as reachable while staying unaddable —
rebuilding the exact loop this exists to break. The probe compares the leaf signature key.
A mismatch is
Inconclusive, not unhealthy: an upload cannot repair a mapping defect,and rotating would make one backend incident churn the whole fleet every interval.
defect, classified
Inconclusivefor the same reason.write time, or corruption — would suppress the watchdog for months. That is byte-for-byte
the failure this check exists to catch, so the fix must not reproduce it.
onto the task row, and the dispatcher skips future-dated tasks — so the handler's clamp
becomes unreachable because the handler never runs. Startup reconciliation therefore
enqueues the liveness pull-in unconditionally.
Err, so an offline client cannot drive thistask into the TaskRunner's backoff. Genuine storage errors still propagate for the
DB-reconnect contract.
rotate_if_needednow logs at INFO on both branches. The silent"not due" branch is what hid the incident.
A commit-driven trigger (our own installation appearing in
failed_installations) wasbuilt and then removed: it wrote durable task rows from unvalidated remote input,
before
expected_diff_matches_commitand the credential checks — and it is near-unreachableanyway, since an installation in
failed_installationshas no leaf and cannot decrypt thecommit naming it.
Known limitations, including the pre-existing unclaimed-TaskRunner race and its real
destructive worst case, are documented in §5a of the design doc rather than glossed.
Testing
crates/xmtp_mls/src/worker/key_package_maintenance.rs. Classification is extracted intopure functions (
now_nsis a parameter, not a hidden clock) and asserted directly overevery wire shape — no network, no override — so they cannot go false-green if the probe body
changes. A live-backend test additionally pins real backend behavior; that is the test that
found the empty-payload shape.
Covers: healthy → no rotation; absent → rotation queued and nudged; throttle blocks a
repeat probe; nudges coalesce to one pull-in; future stamp does not disable the check;
a clock-skew-stranded row is confirmed non-dispatchable then rescued by reconciliation;
every
next_liveness_deadlineboundary; every response shape including a valid key packagefor a different installation.
just lint-rust— cleancargo nextest run -p xmtp_mls -p xmtp_db— 936 passed (should_reconnectisknown-flaky and retried green)
just wasm check— passesDependency
Requires xmtp/proto#342, already merged as
f53e21af.crates/xmtp_proto/proto_versionpins that rev and the generated Rust was regeneratedthrough
dev/gen_protos.sh— not hand-edited. The regenerated diff covers only the newKpLivenessmessage and its oneof arm.Design doc
docs/superpowers/specs/2026-08-07-kp-liveness-check-design.md🤖 Generated with Claude Code
Note
Add key package liveness check to verify own key package is live on the network
KpLivenesstask that periodically probes the network to verify the installation's own key package is present, valid, and not near expiry.Healthy,Absent,Unverifiable,ExpiringSoon, orInconclusive; actionable outcomes (Absent,Unverifiable,ExpiringSoon) queue a key package rotation.key_package_liveness_checked_at_nscolumn in the identity table, with constants for check interval, retry interval, and minimum remaining lifetime.KpRotationdispatches now also nudge the liveness task unconditionally.KpLivenessproto variant and DB migration for the liveness timestamp column.Macroscope summarized 8cf9617.