Skip to content

NIP-IA: failed kind:13535 snapshot publish is unrepairable — changed=false early return skips publish_nipia_archival_list on every retry #4617

Description

@alanshurafa

Describe the bug

If the relay-signed kind:13535 archived-identities snapshot fails to publish during a NIP-IA archive request, that snapshot is permanently stale for the affected identity — and no number of retries by the client can repair it.

The failure chain:

  1. handle_identity_archive_event calls db.archive(...), which inserts with ON CONFLICT (community_id, pubkey) DO NOTHING and returns changed = rows_affected > 0 (
    let result = sqlx::query(
    "INSERT INTO archived_identities \
    (community_id, pubkey, consent_path, actor, reason, replaced_by, request_event_id) \
    VALUES ($1, $2, $3, $4, $5, $6, $7) \
    ON CONFLICT (community_id, pubkey) DO NOTHING",
    )
    .bind(community_id.as_uuid())
    .bind(pubkey)
    .bind(consent_path)
    .bind(actor)
    .bind(reason)
    .bind(replaced_by)
    .bind(request_event_id)
    .execute(pool)
    .await?;
    Ok(result.rows_affected() > 0)
    }
    ).
  2. On the first (successful) archive, the kind:8002 delta and kind:13535 snapshot publishes are attempted, but their errors are only warn!-logged (
    if let Err(e) = publish_delta {
    warn!(error = %e, "failed to publish NIP-IA delta");
    }
    if let Err(e) = publish_nipia_archival_list(tenant, state).await {
    warn!(error = %e, "failed to publish NIP-IA archival list");
    }
    ). The handler still returns Ok(()).
  3. Any subsequent archive request for the same identity hits changed == false and returns early — before publish_nipia_archival_list is ever reached (
    if !changed {
    return Ok(());
    }
    ).

So the one code path that could republish the snapshot is gated behind a DB state transition that, by design, can only happen once. Retrying the archive request is an idempotent no-op: the DB row exists, the snapshot stays wrong, and the client sees success every time.

Steps to reproduce

Reproduced live on a hosted relay on 2026-08-03:

  1. Archive an identity at a moment when the snapshot publish fails transiently (in our case the relay logged failed to publish NIP-IA archival list; the archived_identities row was committed).
  2. Observe the divergence: identity 51c6e299… had a DB row (and is_archived returned true), but was absent from the authoritative kind:13535 snapshot (buzz agents archived verified against the raw event).
  3. Send the same kind:9035 archive request again — repeatedly. Every attempt is accepted (OK true), and none of them republish the snapshot.
  4. The only client-side repair we found was a full unarchive → archive cycle: two changed=true transitions, which forces the delta + snapshot publishes to run again.

Expected behavior

The kind:13535 snapshot should be repairable without destructively cycling the archive state. Two possible fixes (either alone would resolve it):

  • Republish on idempotent requests: when changed == false, skip the delta (there was no state transition to describe) but still call publish_nipia_archival_list. The snapshot is a pure function of the archived_identities table, so republishing is always safe, and it makes "retry the request" an actual repair path.
  • Reconcile on read: derive/verify the snapshot from the DB when it's served, so a lost publish self-heals.

Secondary issue: side-effect failures are invisible to the requesting client. The request event itself is accepted by ingest (

if is_identity_archive_request_kind(kind_u32) {
crate::handlers::identity_archive::handle_identity_archive_event(tenant, state, &event)
.await
.map_err(|e| IngestError::Rejected(format!("invalid: {e}")))?;
}
), so the client gets OK true even when the delta or snapshot publish failed — the errors exist only in relay logs. A client has no signal that the archive it just performed isn't reflected in the authoritative list. Worth considering whether publish failures should surface in the OK message (or at least be queryable), though that may deserve its own issue.

Related but distinct: #3848 covers snapshots lost to same-second created_at collisions between successive publishes. This issue is about a snapshot publish that failed outright and can never be retried because the retry path is gated on changed.

Version and platform

  • Buzz version: main @ ac4fa13 (also observed on a hosted relay, 2026-08-03)
  • OS: server-side (relay); client on Windows 11

Logs / additional context

Relay-side log lines for the failed publish (only visible server-side):

WARN failed to publish NIP-IA archival list

Client-side, every retry of the archive request returned:

["OK","<event-id>",true,""]

with no change to the kind:13535 snapshot.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions