fix(marmot): return newest KeyPackage by created_at
KeyPackageFetcher.fetchKeyPackage used client.fetchFirst, which closes the subscription on the first event a relay sends. For kind:443 (KeyPackage) that's wrong after a rotation: the publisher does not replace the prior event (kind:443 is not addressable), so relays may still hold the old KP and whichever one wins the race gets returned. Switch to fetchAll — drain every matching event until EOSE, then pick the one with the highest created_at. Keeps freshly-rotated bundles reachable and matches whitenoise/mdk semantics. Unskips interop test 16 (wn rotates, amy discovers). The inverse path (test 13) already worked because `wn keys check` looks up via the addressable kind:10051 index instead of kind:443 directly. https://claude.ai/code/session_01Unm6uLHGLj9UcBY7hWfJVW
This commit is contained in:
+15
-5
@@ -23,7 +23,7 @@ package com.vitorpamplona.quartz.marmot.mip00KeyPackages
|
|||||||
import com.vitorpamplona.quartz.marmot.MarmotFilters
|
import com.vitorpamplona.quartz.marmot.MarmotFilters
|
||||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||||
import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient
|
import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient
|
||||||
import com.vitorpamplona.quartz.nip01Core.relay.client.accessories.fetchFirst
|
import com.vitorpamplona.quartz.nip01Core.relay.client.accessories.fetchAll
|
||||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -60,8 +60,16 @@ object KeyPackageFetcher {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* One-shot fetch of a user's KeyPackage across [relays], returning the first
|
* Drain every kind:443 KeyPackage event for [targetPubKey] across [relays]
|
||||||
* matching event or `null` if none of the relays yielded one before timeout.
|
* and return the most recently published one (highest `created_at`), or
|
||||||
|
* `null` if nothing arrived before the timeout.
|
||||||
|
*
|
||||||
|
* Returning the newest is important after a KeyPackage rotation: MIP-00
|
||||||
|
* does not use addressable replacement for kind:443, so a relay may still
|
||||||
|
* hold the prior KP alongside the new one. `fetchFirst` would race the
|
||||||
|
* relays and pick whichever replied first, which would frequently be the
|
||||||
|
* older event. Draining to EOSE and selecting by `created_at` matches
|
||||||
|
* MDK/whitenoise semantics and keeps freshly-rotated bundles reachable.
|
||||||
*/
|
*/
|
||||||
suspend fun fetchKeyPackage(
|
suspend fun fetchKeyPackage(
|
||||||
client: INostrClient,
|
client: INostrClient,
|
||||||
@@ -71,8 +79,10 @@ object KeyPackageFetcher {
|
|||||||
): KeyPackageEvent? {
|
): KeyPackageEvent? {
|
||||||
if (relays.isEmpty()) return null
|
if (relays.isEmpty()) return null
|
||||||
val filter = MarmotFilters.keyPackagesByAuthor(targetPubKey)
|
val filter = MarmotFilters.keyPackagesByAuthor(targetPubKey)
|
||||||
val event = client.fetchFirst(filters = relays.associateWith { listOf(filter) }, timeoutMs = timeoutMs)
|
val events = client.fetchAll(filters = relays.associateWith { listOf(filter) }, timeoutMs = timeoutMs)
|
||||||
return event as? KeyPackageEvent
|
// fetchAll returns events sorted by created_at DESC, so the first
|
||||||
|
// KeyPackageEvent is the most recent one any relay had.
|
||||||
|
return events.firstNotNullOfOrNull { it as? KeyPackageEvent }
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -238,14 +238,37 @@ test_16_wn_keypackage_rotation() {
|
|||||||
banner "Test 16 — wn rotates KeyPackage; amy discovers new KP"
|
banner "Test 16 — wn rotates KeyPackage; amy discovers new KP"
|
||||||
local id="16 wn keypackage rotation"
|
local id="16 wn keypackage rotation"
|
||||||
|
|
||||||
# amy's KeyPackageFetcher.fetchKeyPackage calls client.fetchFirst,
|
# KeyPackageFetcher now drains to EOSE and returns the event with the
|
||||||
# which returns the first matching event a relay sends — nostr-rs-relay
|
# highest created_at, so a freshly-rotated KP is reliably preferred
|
||||||
# typically serves kind:443 events in storage order, not created_at
|
# over an older one still held on some relays. This test verifies
|
||||||
# order, so after a rotation amy may keep seeing the older event_id
|
# the wn->amy direction of that behaviour.
|
||||||
# depending on which arrives first. Making this test deterministic
|
|
||||||
# requires a "fetch latest by created_at" KeyPackage fetcher; until
|
# Capture the KP amy currently sees for B (the one B originally published).
|
||||||
# then the check flaps. The inverse direction (test 13, amy rotates
|
local before
|
||||||
# and wn sees via `wn keys check` which is an addressable index) is
|
before=$(amy_json marmot key-package check "$B_NPUB" 2>/dev/null \
|
||||||
# the reliable one.
|
| jq -r '.event_id // empty')
|
||||||
record_result "$id" skip "pending createdAt-sorted KeyPackage fetch path"
|
if [[ -z "$before" ]]; then
|
||||||
|
record_result "$id" fail "no prior KP visible to amy for B"; return
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Ask B to rotate. `wn keys publish` writes a new kind:443 with a fresh
|
||||||
|
# created_at; the old event may or may not be evicted depending on the
|
||||||
|
# relay's retention policy, so both may coexist for a while.
|
||||||
|
wn_b keys publish >/dev/null 2>&1 || {
|
||||||
|
record_result "$id" fail "wn_b keys publish failed"; return
|
||||||
|
}
|
||||||
|
|
||||||
|
local deadline=$(( $(date +%s) + 60 )) after=""
|
||||||
|
while [[ $(date +%s) -lt $deadline ]]; do
|
||||||
|
after=$(amy_json marmot key-package check "$B_NPUB" 2>/dev/null \
|
||||||
|
| jq -r '.event_id // empty')
|
||||||
|
[[ -n "$after" && "$after" != "$before" ]] && break
|
||||||
|
sleep 3
|
||||||
|
done
|
||||||
|
if [[ -n "$after" && "$after" != "$before" ]]; then
|
||||||
|
info "amy saw KP rotation: ${before:0:8}… → ${after:0:8}…"
|
||||||
|
record_result "$id" pass
|
||||||
|
else
|
||||||
|
record_result "$id" fail "amy kept seeing the pre-rotation KP"
|
||||||
|
fi
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user