Skip to main content

Module blacklist_watcher

Module blacklist_watcher 

Source
Expand description

Blacklist compliance watcher (ADR 011 § Content Takedown).

Every paid-delivery node runs this task after resolving the mandatory blockchain.content_blacklist_address. It keeps the local blob store compliant with ContentBlacklist: it evicts any blob whose hash is blacklisted in scope for this operator (global ∪ current-region ∪ ripening-prev-region). The scope decision is the contract’s isHashBlacklistedForOperator view, so region packing and the ADR 030 ripening math never leave the chain.

Enumerated deny-set, re-scoped. The set of blacklisted entries is read straight from ContentBlacklist’s enumeration views at one pinned block on boot, rather than replayed from HashBlacklisted logs (#1497). Two views feed two sets:

  • Hash entries. getScopeRegions(operator) gives the one-to-three region keys in scope right now; each region’s set is paged from blacklistedHashes / blacklistedHashCount. Entries are keyed by (region, hash) — the contract’s own key (_hashEntries[region][hash]) — so a same-hash entry under another region survives a HashRemoved for the first. Every seen-but-not-yet-evicted entry is retained in known and re-scoped on a periodic (on_tick_complete) pass, because a hash can become live + in scope with no HashBlacklisted event, via an operator region/ripening change (CapacityBond.updateRegion), which emits nothing on this contract at all.
  • Address union. blacklistedAddresses / blacklistedAddressCount enumerate the origin ∪ operator deny set. Each address is liveness-filtered by the contract’s own isOriginBlacklisted(a) || isOperatorBlacklisted(a) disjunction (the same one OriginAssignment._syncAddr evaluates), which keeps voted-out OPERATORS (addOperator sets only the operator mapping) and live origins while dropping emergency origins past their auto-expiry. The survivors seed crate::content_deny::ContentDenylist, which the delivery path consults to refuse any StreamRequest funded by a blacklisted address.

No durable projection, no scan cursor. Both sets are enumerable on chain, so a restart rebuilds them by reading the chain rather than by reloading a persisted mirror or replaying from a deploy-block floor. The live tail is seeded at the enumeration snapshot block (CursorStart::Seeded, no persist) — there is no historical scan on any boot, warm or cold. This is the same shape the origin directory and slash watchers already use.

The pinned-block reads are load-bearing, not tidiness: the enumeration views remove by swap-and-pop, so a page and the count it is checked against MUST be read at one block height, or a concurrent removal can move an unread element into an already-read slot and skip it. A seen != count mismatch aborts the snapshot rather than seating a partial set.

The live tail follows both event families on the shared resumable_watcher eth_getLogs poller: HashBlacklisted/HashRemoved for the hash set and OriginBlacklistUpdated + OperatorBlacklisted/OperatorBlacklistCleared for the address union (both routed to the one deny-set, mirroring OriginAssignment’s union). It is a low-latency signal to re-read; the periodic re-enumeration is the backstop that catches anything the tail missed (a reorg at the tip, or a region/ripening transition into a never-enumerated region).

Enforcement is two writes per hash, in this order: the deny records that the refusal is governance-sourced, then the eviction reclaims the bytes.

decdn_cache::CacheEngine::evict durably records the takedown (survives restart via evicted.log) and cascades to every serving surface through decdn_cache::CacheEngine::refuses: the DHT republisher drops the hash on its next tick, the probe handler stops signing has_blob: true, and the client handler never re-pull-fills it. evict is sticky and works on absent hashes, so a hash blacklisted while the node was offline (and not yet held) is still pre-blocked.

The deny is what evicted.log cannot express: why. Every one of these refusals signs the one class Declined, so no client learns which list a hash is on (ADR 011 §StreamRequest Response); the deny keeps the cause on the operator’s per-reason metric. The governance deny-set the delivery path reads (CacheEngine::set_chain_denied_one) is rebuilt each boot by re-checking every enumerated hash through the same isHashBlacklistedForOperator liveness the tail uses, so a lapsed entry is not enforced.

A fail-closed readiness gate (InitialSyncGate) keeps every ALPN listener shut until the first enumeration + enforcement pass completes: a node refuses to serve until blacklist enforcement is live.

Resilience. The re-scope + re-enumeration pass runs on the operator’s rescan cadence (checked at the end of every poll tick), pulled forward to the poll cadence whenever a live re-check fails — an enforcement failure must not wait out the full cadence while the blob stays slashably servable; every RPC read is bounded by a per-call timeout; the re-scope is interruptible by shutdown (checked between hashes) so a large backlog cannot overrun the runtime shutdown deadline. Serving a blacklisted hash is slashable (SlashJudge.submitBlacklistChallenge), so prompt eviction is the node’s only local protection.