feat(relay): RelayInfo::build static-input fence (conformance NIP-11 row)

The conformance obligation for the NIP-11 surface: RelayInfo::build must
not grow unscoped DB/search/audit inputs, so an unauthenticated NIP-11
read can never become a cross-community enumeration oracle.

Binds RelayInfo::build to its exact allowed signature via a const fn
pointer. Adding a &Db / &AppState / search / audit input makes the
function-pointer type stop matching and breaks the build at the fence —
a silent cross-tenant leak becomes a hard compile error, deny-lint style.

Adversarially proven: injecting a &AppState param into build() produces
error[E0308] mismatched types at the fence const (plus E0061 at the call
sites); reverted to confirm the fence, not the call sites alone, is the
guard. buzz-relay package 374 green at --test-threads=1.

Co-authored-by: Tyler Longwell <tlongwell@block.xyz>
Signed-off-by: Tyler Longwell <tlongwell@block.xyz>
This commit is contained in:
npub1qyvc0c5kl4gqv2fd97fsk46tu378sqgy35vc83rvgfwne90sel7s0ed67d
2026-06-26 16:39:22 -04:00
parent 96f3842429
commit 61d9e0e3c8

View File

@@ -180,6 +180,25 @@ pub(crate) fn nip11_facts(state: &crate::state::AppState) -> (Option<String>, bo
(relay_self, advertise_nip43)
}
/// Multi-tenant conformance static-input fence (surface row "NIP-11 relay info
/// and relay `self`").
///
/// The conformance obligation: `RelayInfo::build` "must not grow unscoped
/// DB/search/audit inputs", so an unauthenticated NIP-11 read can never become
/// an enumeration oracle for other communities. Today `build` takes only static
/// and scalar inputs — the per-deployment facts arrive pre-derived through
/// [`nip11_facts`], which reads config and the relay keypair only.
///
/// This const binds `RelayInfo::build` to its **exact** allowed signature. The
/// moment someone adds a `&Db`, `&AppState`, a search handle, an audit handle,
/// or any other unscoped input, the function pointer's type stops matching and
/// **this file fails to compile** — turning a silent cross-tenant leak into a
/// hard build break, the same way a deny-lint would. If you must change this
/// signature, you are changing the conformance contract: update the conformance
/// doc and prove the new input is host-scoped, not unscoped, first.
const _RELAY_INFO_BUILD_STATIC_INPUT_FENCE: fn(Option<&str>, bool, usize) -> RelayInfo =
RelayInfo::build;
#[cfg(test)]
mod tests {
use super::*;