Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion crates/buzz-db/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -898,7 +898,7 @@ impl Db {
.fetch_one(&mut *tx)
.await?;

if owned_count >= relay_members::MAX_COMMUNITIES_PER_OWNER {
if owned_count >= relay_members::max_communities_per_owner() {
tx.rollback().await?;
return Ok(CreateCommunityWithOwnerResult::LimitReached);
}
Expand Down
74 changes: 69 additions & 5 deletions crates/buzz-db/src/relay_members.rs
Original file line number Diff line number Diff line change
Expand Up @@ -373,10 +373,30 @@ pub enum TransferResult {
LimitReached,
}

/// Maximum number of communities a single pubkey can own. Enforced at the
/// relay layer — the authoritative layer — so that concurrent transfers or
/// transfer-vs-create races cannot both pass a preflight count.
pub const MAX_COMMUNITIES_PER_OWNER: i64 = 3;
/// Default maximum number of communities a single pubkey can own.
const DEFAULT_MAX_COMMUNITIES_PER_OWNER: i64 = 3;

/// Parse and validate the effective per-owner community limit.
///
/// Accepts a raw string value (e.g., from an env var). Returns the parsed
/// positive integer, or `DEFAULT_MAX_COMMUNITIES_PER_OWNER` if the input is
/// absent, unparseable, or non-positive.
pub fn effective_owner_limit(raw: Option<&str>) -> i64 {
raw.and_then(|s| s.parse::<i64>().ok())
.filter(|&n| n > 0)
.unwrap_or(DEFAULT_MAX_COMMUNITIES_PER_OWNER)
}

/// Maximum number of communities a single pubkey can own. Operator-tunable via
/// `BUZZ_MAX_COMMUNITIES_PER_OWNER`; falls back to 3 when the variable is
/// absent, non-numeric, or non-positive so existing deployments are unaffected.
pub fn max_communities_per_owner() -> i64 {
effective_owner_limit(
std::env::var("BUZZ_MAX_COMMUNITIES_PER_OWNER")
.ok()
.as_deref(),
)
}

/// Stable advisory-lock key for serializing ownership-granting operations
/// (transfer + create) per recipient pubkey. Uses FNV-1a over the hex pubkey
Expand Down Expand Up @@ -472,7 +492,7 @@ pub async fn transfer_ownership(
.fetch_one(&mut *tx)
.await?;

if owned_count >= MAX_COMMUNITIES_PER_OWNER {
if owned_count >= max_communities_per_owner() {
tx.rollback().await?;
return Ok(TransferResult::LimitReached);
}
Expand Down Expand Up @@ -949,4 +969,48 @@ mod tests {
"owner"
);
}

// ---- unit tests for effective_owner_limit (no DB required) ----

/// Missing env var falls back to the default of 3.
#[test]
fn owner_limit_default_when_absent() {
assert_eq!(
effective_owner_limit(None),
DEFAULT_MAX_COMMUNITIES_PER_OWNER
);
}

/// A non-numeric value falls back to the default.
#[test]
fn owner_limit_default_on_invalid_string() {
assert_eq!(
effective_owner_limit(Some("abc")),
DEFAULT_MAX_COMMUNITIES_PER_OWNER
);
}

/// Zero is not a positive integer — falls back to the default.
#[test]
fn owner_limit_default_on_zero() {
assert_eq!(
effective_owner_limit(Some("0")),
DEFAULT_MAX_COMMUNITIES_PER_OWNER
);
}

/// A negative value falls back to the default.
#[test]
fn owner_limit_default_on_negative() {
assert_eq!(
effective_owner_limit(Some("-5")),
DEFAULT_MAX_COMMUNITIES_PER_OWNER
);
}

/// A valid positive integer is accepted as the effective limit.
#[test]
fn owner_limit_positive_override() {
assert_eq!(effective_owner_limit(Some("100")), 100);
}
}