diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 85d6ab8a06..b1703f9618 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -312,10 +312,12 @@ also revokes that session. On open relays, an owner who was already a nonmember at admission does not become a membership requirement. Moderation timeouts are not read by this worker because they restrict writes, not an established socket. All membership, ownership, and ban predicates include the bound -community. Admission reads the captured owner's membership from the writer -when the delegation check did not already establish it; that bounded point -lookup records the baseline needed to distinguish a later membership removal -from pre-existing open-relay nonmembership. +community. When the delegation check did not already establish the captured +owner's membership, admission makes a bounded writer-authoritative membership +read to record whether that owner was already a relay member. This adds a writer +dependency to admission; a failed point read refuses the socket with an +authorization-unavailable AUTH response. The baseline distinguishes a later +membership removal from pre-existing open-relay nonmembership. Each batch snapshot is the revalidation ordering point. A removal or ban committed before that snapshot closes the session; a re-add or unban committed diff --git a/crates/buzz-relay/src/handlers/auth.rs b/crates/buzz-relay/src/handlers/auth.rs index 15203689e2..b43bfbad3a 100644 --- a/crates/buzz-relay/src/handlers/auth.rs +++ b/crates/buzz-relay/src/handlers/auth.rs @@ -233,6 +233,16 @@ pub(crate) struct FinalAdmissionCheck { pub(crate) owner_was_relay_member_at_admission: bool, } +fn admission_db_error_class(error: &buzz_db::DbError) -> &'static str { + match error { + buzz_db::DbError::Sqlx(sqlx::Error::PoolTimedOut) => "pool_timeout", + buzz_db::DbError::Sqlx(sqlx::Error::PoolClosed) => "pool_closed", + buzz_db::DbError::Sqlx(sqlx::Error::Io(_)) => "io_error", + error if error.is_statement_cancelled() => "statement_cancelled", + _ => "database_error", + } +} + /// Final ban and relay-membership verdict for an authenticating socket. /// /// Callers bind the socket to `pubkey` first (the registry a ban's or @@ -294,8 +304,13 @@ pub(crate) async fn final_admission_check( .await { Ok(is_member) => is_member, - Err(_) => { - warn!("captured-owner membership snapshot failed at admission; denying"); + Err(error) => { + warn!( + community = %community, + pubkey = %pubkey.to_hex(), + error_class = admission_db_error_class(&error), + "captured-owner membership writer check failed at admission; denying" + ); final_check.denial = Some(AdmissionDenial { metric: "owner_membership_check_error", reason: "error: internal error checking relay membership",