use std::collections::{HashMap, HashSet};
use std::future::Future;
use std::sync::{Arc, Mutex};
use std::time::Duration;
use alloy::eips::BlockId;
use alloy::primitives::{Address, B256, U256};
use alloy::providers::Provider;
use alloy::rpc::types::Log;
use alloy::sol_types::SolEvent;
use anyhow::{Context as _, Result};
use decdn_cache::{CacheEngine, Hash};
use decdn_common::redact::sanitize_err_chain;
use decdn_incentive::content_blacklist::ContentBlacklist;
use decdn_incentive::content_blacklist::ContentBlacklist::{
HashBlacklisted, HashRemoved, OperatorBlacklistCleared, OperatorBlacklisted,
OriginBlacklistUpdated,
};
use tokio::sync::oneshot;
use tokio::time::Instant;
use tokio_util::sync::CancellationToken;
use tracing::{debug, info, warn};
use crate::chain_events::boot_retry::{BootFault, BootRetry};
use crate::chain_events::multiplexed_poller::{Route, SinkSource};
use crate::chain_events::resumable_watcher::{CursorStart, LogSink, clear_cadence_on_recovery};
use crate::chain_events::shared_head::{HeadSource, snapshot_block};
use crate::chain_events::timed;
use crate::chain_freshness::ChainFreshness;
use crate::content_deny::ContentDenylist;
use crate::metrics::{Metrics, metric_hook};
use crate::warming_allowance::WarmingAllowance;
const BLACKLIST_ENUM_PAGE_SIZE: u64 = 100;
pub(crate) type InitialSyncResult = std::result::Result<(), String>;
#[derive(Clone)]
struct InitialSyncGate(Arc<Mutex<Option<oneshot::Sender<InitialSyncResult>>>>);
impl InitialSyncGate {
fn new(sender: oneshot::Sender<InitialSyncResult>) -> Self {
Self(Arc::new(Mutex::new(Some(sender))))
}
fn signal(&self, result: InitialSyncResult) {
let sender = self
.0
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.take();
if let Some(sender) = sender {
let _ = sender.send(result);
}
}
}
trait BlacklistChainReads: Send + Sync {
fn snapshot_block(&self) -> impl Future<Output = Result<u64>> + Send;
fn scope_regions(
&self,
operator: Address,
at: u64,
) -> impl Future<Output = Result<Vec<B256>>> + Send;
fn blacklisted_hash_count(
&self,
region: B256,
at: u64,
) -> impl Future<Output = Result<U256>> + Send;
fn blacklisted_hashes(
&self,
region: B256,
offset: U256,
limit: U256,
at: u64,
) -> impl Future<Output = Result<Vec<B256>>> + Send;
fn blacklisted_address_count(&self, at: u64) -> impl Future<Output = Result<U256>> + Send;
fn blacklisted_addresses(
&self,
offset: U256,
limit: U256,
at: u64,
) -> impl Future<Output = Result<Vec<Address>>> + Send;
fn is_origin_blacklisted(
&self,
addr: Address,
at: u64,
) -> impl Future<Output = Result<bool>> + Send;
fn is_operator_blacklisted(
&self,
addr: Address,
at: u64,
) -> impl Future<Output = Result<bool>> + Send;
}
#[derive(Clone)]
struct ContractReads<P: Provider + Clone> {
contract: ContentBlacklist::ContentBlacklistInstance<P>,
head: Arc<dyn HeadSource>,
}
impl<P: Provider + Clone> BlacklistChainReads for ContractReads<P> {
async fn snapshot_block(&self) -> Result<u64> {
snapshot_block(
self.contract.provider(),
&*self.head,
*self.contract.address(),
)
.await
}
async fn scope_regions(&self, operator: Address, at: u64) -> Result<Vec<B256>> {
timed(
None,
"getScopeRegions",
self.contract
.getScopeRegions(operator)
.block(BlockId::Number(at.into()))
.call(),
)
.await
}
async fn blacklisted_hash_count(&self, region: B256, at: u64) -> Result<U256> {
timed(
None,
"blacklistedHashCount",
self.contract
.blacklistedHashCount(region)
.block(BlockId::Number(at.into()))
.call(),
)
.await
}
async fn blacklisted_hashes(
&self,
region: B256,
offset: U256,
limit: U256,
at: u64,
) -> Result<Vec<B256>> {
timed(
None,
"blacklistedHashes",
self.contract
.blacklistedHashes(region, offset, limit)
.block(BlockId::Number(at.into()))
.call(),
)
.await
}
async fn blacklisted_address_count(&self, at: u64) -> Result<U256> {
timed(
None,
"blacklistedAddressCount",
self.contract
.blacklistedAddressCount()
.block(BlockId::Number(at.into()))
.call(),
)
.await
}
async fn blacklisted_addresses(
&self,
offset: U256,
limit: U256,
at: u64,
) -> Result<Vec<Address>> {
timed(
None,
"blacklistedAddresses",
self.contract
.blacklistedAddresses(offset, limit)
.block(BlockId::Number(at.into()))
.call(),
)
.await
}
async fn is_origin_blacklisted(&self, addr: Address, at: u64) -> Result<bool> {
timed(
None,
"isOriginBlacklisted",
self.contract
.isOriginBlacklisted(addr)
.block(BlockId::Number(at.into()))
.call(),
)
.await
}
async fn is_operator_blacklisted(&self, addr: Address, at: u64) -> Result<bool> {
timed(
None,
"isOperatorBlacklisted",
self.contract
.isOperatorBlacklisted(addr)
.block(BlockId::Number(at.into()))
.call(),
)
.await
}
}
struct BootstrapSnapshot {
block: u64,
origins: HashSet<Address>,
known: HashSet<(B256, Hash)>,
}
async fn bootstrap_snapshot<R: BlacklistChainReads>(
reads: &R,
operator: Address,
) -> Result<BootstrapSnapshot> {
let block = reads
.snapshot_block()
.await
.context("read the ContentBlacklist enumeration snapshot block")?;
let origins = enumerate_address_union(reads, operator, block).await?;
let known = enumerate_known(reads, operator, block).await?;
Ok(BootstrapSnapshot {
block,
origins,
known,
})
}
async fn enumerate_address_union<R: BlacklistChainReads>(
reads: &R,
_operator: Address,
at: u64,
) -> Result<HashSet<Address>> {
let count = reads
.blacklisted_address_count(at)
.await
.context("blacklistedAddressCount")?;
let mut raw: HashSet<Address> = HashSet::new();
let mut offset = U256::ZERO;
let page = U256::from(BLACKLIST_ENUM_PAGE_SIZE);
while offset < count {
let batch = reads
.blacklisted_addresses(offset, page, at)
.await
.with_context(|| format!("blacklistedAddresses(offset={offset}, limit={page})"))?;
if batch.is_empty() {
break;
}
offset = offset.saturating_add(U256::from(batch.len()));
raw.extend(batch);
}
let seen = U256::from(raw.len());
anyhow::ensure!(
seen == count,
"address deny-set enumeration read {seen} of {count} entries at block {at}; the pinned \
set was read inconsistently (a mid-page swap-and-pop removal, or an inconsistent/reorged \
RPC view of this block), so the snapshot would be missing an address — aborting rather \
than seating a partial set"
);
let mut live: HashSet<Address> = HashSet::with_capacity(raw.len());
for addr in raw {
let denied = reads
.is_origin_blacklisted(addr, at)
.await
.with_context(|| format!("isOriginBlacklisted({addr})"))?
|| reads
.is_operator_blacklisted(addr, at)
.await
.with_context(|| format!("isOperatorBlacklisted({addr})"))?;
if denied {
live.insert(addr);
}
}
Ok(live)
}
async fn enumerate_region_hashes<R: BlacklistChainReads>(
reads: &R,
region: B256,
at: u64,
) -> Result<HashSet<Hash>> {
let count = reads
.blacklisted_hash_count(region, at)
.await
.with_context(|| format!("blacklistedHashCount({region})"))?;
let mut hashes: HashSet<Hash> = HashSet::new();
let mut offset = U256::ZERO;
let page = U256::from(BLACKLIST_ENUM_PAGE_SIZE);
while offset < count {
let batch = reads
.blacklisted_hashes(region, offset, page, at)
.await
.with_context(|| {
format!("blacklistedHashes(region={region}, offset={offset}, limit={page})")
})?;
if batch.is_empty() {
break;
}
offset = offset.saturating_add(U256::from(batch.len()));
hashes.extend(batch.into_iter().map(|h| Hash::from_bytes(h.0)));
}
let seen = U256::from(hashes.len());
anyhow::ensure!(
seen == count,
"region {region} hash enumeration read {seen} of {count} entries at block {at}; the \
pinned set was read inconsistently (a mid-page swap-and-pop removal, or an \
inconsistent/reorged RPC view of this block), so the snapshot would be missing a hash — \
aborting rather than seating a partial set"
);
Ok(hashes)
}
async fn enumerate_known<R: BlacklistChainReads>(
reads: &R,
operator: Address,
at: u64,
) -> Result<HashSet<(B256, Hash)>> {
let regions = reads
.scope_regions(operator, at)
.await
.with_context(|| format!("getScopeRegions({operator})"))?;
let mut known = HashSet::new();
for region in regions {
for hash in enumerate_region_hashes(reads, region, at).await? {
known.insert((region, hash));
}
}
Ok(known)
}
struct WatcherState {
known: HashSet<(B256, Hash)>,
denylist: Arc<ContentDenylist>,
origin_changes: HashMap<Address, (u64, bool)>,
removed_entries: HashMap<(B256, Hash), u64>,
}
impl WatcherState {
fn add_entry(&mut self, region: B256, hash: Hash) {
self.known.insert((region, hash));
}
fn fold_reenumeration(&mut self, snapshot: BootstrapSnapshot) {
let at = snapshot.block;
self.origin_changes.retain(|_, (block, _)| *block > at);
self.removed_entries.retain(|_, block| *block > at);
let mut origins = snapshot.origins;
for (origin, (_, blacklisted)) in &self.origin_changes {
if *blacklisted {
origins.insert(*origin);
} else {
origins.remove(origin);
}
}
let removed = &self.removed_entries;
self.known.extend(
snapshot
.known
.into_iter()
.filter(|entry| !removed.contains_key(entry)),
);
self.denylist.set_chain_origins(origins);
}
fn remove_entry(&mut self, region: B256, hash: Hash, block: u64) {
self.known.remove(&(region, hash));
self.removed_entries.insert((region, hash), block);
}
fn drop_hash(&mut self, hash: Hash) {
self.known.retain(|(_, known_hash)| *known_hash != hash);
}
fn distinct_hashes(&self) -> Vec<Hash> {
let unique: HashSet<Hash> = self.known.iter().map(|(_, hash)| *hash).collect();
unique.into_iter().collect()
}
fn set_origin(&mut self, origin: Address, blacklisted: bool, block: u64) {
self.denylist.apply_chain_origin(origin, blacklisted);
self.origin_changes.insert(origin, (block, blacklisted));
}
}
struct BlacklistSink<P: Provider + Clone> {
contract: ContentBlacklist::ContentBlacklistInstance<P>,
reads: ContractReads<P>,
operator: Address,
cache: CacheEngine,
warming: Arc<WarmingAllowance>,
state: WatcherState,
shutdown: CancellationToken,
rescan_interval: Duration,
last_rescan: Option<Instant>,
metrics: Arc<Metrics>,
}
impl<P: Provider + Clone> LogSink for BlacklistSink<P> {
async fn apply(&mut self, log: Log) -> Result<()> {
let failed = handle_log(
&self.contract,
self.operator,
&self.cache,
&self.warming,
&mut self.state,
log,
)
.await?;
if failed {
self.last_rescan = None;
}
Ok(())
}
async fn on_tick_complete(&mut self) -> Result<()> {
let due = self
.last_rescan
.is_none_or(|at| at.elapsed() >= self.rescan_interval);
if due {
match bootstrap_snapshot(&self.reads, self.operator).await {
Ok(snapshot) => self.state.fold_reenumeration(snapshot),
Err(err) => {
self.metrics.blacklist_reenumeration_failure();
warn!(
error = %sanitize_err_chain(&err),
"blacklist watcher: periodic re-enumeration failed; keeping the current \
deny-set (the live tail is still the primary path)"
);
}
}
}
if due {
let RescanOutcome { clean, failed, .. } = rescan(
&self.contract,
self.operator,
&self.cache,
&self.warming,
&mut self.state,
&self.shutdown,
OnScopeFailure::Continue,
)
.await;
if failed > 0 {
self.metrics.blacklist_enforcement_failure(failed);
}
self.last_rescan = clean.then(Instant::now);
}
Ok(())
}
fn on_recovered(&mut self) {
clear_cadence_on_recovery(&mut self.last_rescan, self.rescan_interval);
}
}
struct RescanOutcome {
clean: bool,
failed: u64,
decisive_failure: Option<RecheckFailure>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum OnScopeFailure {
Continue,
Stop,
}
async fn rescan<P>(
contract: &ContentBlacklist::ContentBlacklistInstance<P>,
operator: Address,
cache: &CacheEngine,
warming: &Arc<WarmingAllowance>,
state: &mut WatcherState,
shutdown: &CancellationToken,
on_scope_failure: OnScopeFailure,
) -> RescanOutcome
where
P: Provider + Clone,
{
let snapshot = state.distinct_hashes();
let mut evicted = 0usize;
let mut failed = 0u64;
let mut decisive_failure: Option<RecheckFailure> = None;
let mut clean = true;
for hash in snapshot {
if shutdown.is_cancelled() {
return RescanOutcome {
clean: false,
failed,
decisive_failure,
};
}
match recheck(contract, operator, cache, warming, state, hash).await {
Recheck::Evicted => evicted = evicted.saturating_add(1),
Recheck::NoAction => {}
Recheck::Failed(cause) => {
clean = false;
failed = failed.saturating_add(1);
let is_scope = matches!(cause, RecheckFailure::Scope(_));
let replaces = match &decisive_failure {
None => true,
Some(RecheckFailure::Scope(_)) => !is_scope,
Some(RecheckFailure::Evict(_)) => false,
};
if replaces {
decisive_failure = Some(cause);
}
if is_scope && on_scope_failure == OnScopeFailure::Stop {
break;
}
}
}
}
if evicted > 0 {
info!(evicted, "blacklist watcher evicted blacklisted blobs");
}
if failed > 0 {
warn!(
unenforced = failed,
"blacklist re-scope could not enforce every entry"
);
}
RescanOutcome {
clean,
failed,
decisive_failure,
}
}
async fn handle_log<P>(
contract: &ContentBlacklist::ContentBlacklistInstance<P>,
operator: Address,
cache: &CacheEngine,
warming: &Arc<WarmingAllowance>,
state: &mut WatcherState,
log: Log,
) -> Result<bool>
where
P: Provider + Clone,
{
match log.topic0() {
Some(topic) if *topic == HashBlacklisted::SIGNATURE_HASH => Ok(on_blacklisted_log(
contract, operator, cache, warming, state, &log,
)
.await
.is_failed()),
Some(topic) if *topic == HashRemoved::SIGNATURE_HASH => {
on_removed_log(contract, operator, cache, state, &log).await;
Ok(false)
}
Some(topic) if *topic == OriginBlacklistUpdated::SIGNATURE_HASH => {
on_origin_log(state, &log)?;
Ok(false)
}
Some(topic) if *topic == OperatorBlacklisted::SIGNATURE_HASH => {
on_operator_log(state, &log, true)?;
Ok(false)
}
Some(topic) if *topic == OperatorBlacklistCleared::SIGNATURE_HASH => {
on_operator_log(state, &log, false)?;
Ok(false)
}
_ => Ok(false),
}
}
fn on_operator_log(state: &mut WatcherState, log: &Log, blacklisted: bool) -> Result<()> {
let operator = if blacklisted {
match OperatorBlacklisted::decode_log_data(&log.inner.data) {
Ok(event) => event.operator,
Err(err) => return Err(undecodable_origin_log(&err, log, "OperatorBlacklisted")),
}
} else {
match OperatorBlacklistCleared::decode_log_data(&log.inner.data) {
Ok(event) => event.operator,
Err(err) => {
return Err(undecodable_origin_log(
&err,
log,
"OperatorBlacklistCleared",
));
}
}
};
state.set_origin(operator, blacklisted, log_block(log));
debug!(%operator, blacklisted, "blacklist watcher: operator blacklist updated");
Ok(())
}
fn log_block(log: &Log) -> u64 {
log.block_number.unwrap_or(u64::MAX)
}
fn undecodable_origin_log(err: &alloy::sol_types::Error, log: &Log, event: &str) -> anyhow::Error {
anyhow::anyhow!("{err}").context(format!(
"undecodable {event} log at block {:?} tx {:?}; refusing to advance the scan cursor \
past an unreadable takedown event",
log.block_number, log.transaction_hash
))
}
fn on_origin_log(state: &mut WatcherState, log: &Log) -> Result<()> {
let event = match OriginBlacklistUpdated::decode_log_data(&log.inner.data) {
Ok(event) => event,
Err(err) => return Err(undecodable_origin_log(&err, log, "OriginBlacklistUpdated")),
};
state.set_origin(event.origin, event.blacklisted, log_block(log));
debug!(
origin = %event.origin,
blacklisted = event.blacklisted,
"blacklist watcher: origin blacklist updated"
);
Ok(())
}
async fn on_blacklisted_log<P>(
contract: &ContentBlacklist::ContentBlacklistInstance<P>,
operator: Address,
cache: &CacheEngine,
warming: &Arc<WarmingAllowance>,
state: &mut WatcherState,
log: &Log,
) -> Recheck
where
P: Provider + Clone,
{
match HashBlacklisted::decode_log_data(&log.inner.data) {
Ok(event) => {
let hash = Hash::from_bytes(event.hash.0);
state.add_entry(event.region, hash);
recheck(contract, operator, cache, warming, state, hash).await
}
Err(err) => {
warn!(error = %err, "blacklist watcher: undecodable HashBlacklisted log");
Recheck::NoAction
}
}
}
async fn on_removed_log<P: Provider + Clone>(
contract: &ContentBlacklist::ContentBlacklistInstance<P>,
operator: Address,
cache: &CacheEngine,
state: &mut WatcherState,
log: &Log,
) {
match HashRemoved::decode_log_data(&log.inner.data) {
Ok(event) => {
let hash = Hash::from_bytes(event.hash.0);
state.remove_entry(event.region, hash, log_block(log));
if cache.is_chain_denied(hash) {
lift_deny_if_out_of_scope(contract, operator, cache, hash).await;
}
debug!(
region = %event.region,
%hash,
"blacklist entry removed on-chain (local eviction stays sticky)"
);
}
Err(err) => warn!(error = %err, "blacklist watcher: undecodable HashRemoved log"),
}
}
async fn lift_deny_if_out_of_scope<P: Provider + Clone>(
contract: &ContentBlacklist::ContentBlacklistInstance<P>,
operator: Address,
cache: &CacheEngine,
hash: Hash,
) {
match scope_check(contract, operator, hash).await {
Ok(false) => undeny_hash(cache, hash),
Ok(true) => {}
Err(err) => warn!(
%hash,
error = %sanitize_err_chain(&err),
"blacklist watcher: isHashBlacklistedForOperator failed after HashRemoved; \
keeping the hash governance-denied"
),
}
}
#[derive(Debug)]
enum Recheck {
Evicted,
NoAction,
Failed(RecheckFailure),
}
impl Recheck {
const fn is_failed(&self) -> bool {
matches!(self, Self::Failed(_))
}
}
#[derive(Debug)]
enum RecheckFailure {
Scope(anyhow::Error),
Evict(anyhow::Error),
}
fn deny_hash(cache: &CacheEngine, hash: Hash) {
cache.set_chain_denied_one(hash, true);
}
fn undeny_hash(cache: &CacheEngine, hash: Hash) {
cache.set_chain_denied_one(hash, false);
}
async fn recheck<P>(
contract: &ContentBlacklist::ContentBlacklistInstance<P>,
operator: Address,
cache: &CacheEngine,
warming: &Arc<WarmingAllowance>,
state: &mut WatcherState,
hash: Hash,
) -> Recheck
where
P: Provider + Clone,
{
if cache.is_evicted(hash) {
if !cache.is_chain_denied(hash) {
match scope_check(contract, operator, hash).await {
Ok(true) => deny_hash(cache, hash),
Ok(false) => {}
Err(err) => warn!(
%hash,
error = %sanitize_err_chain(&err),
"blacklist watcher: isHashBlacklistedForOperator failed for an evicted \
hash; its governance deny waits for the next re-enumeration"
),
}
}
state.drop_hash(hash);
return Recheck::NoAction;
}
match scope_check(contract, operator, hash).await {
Ok(true) => {
deny_hash(cache, hash);
match evict(cache, warming, hash).await {
Ok(()) => {
state.drop_hash(hash);
Recheck::Evicted
}
Err(err) => Recheck::Failed(RecheckFailure::Evict(err)),
}
}
Ok(false) => Recheck::NoAction,
Err(err) => {
warn!(
%hash,
error = %sanitize_err_chain(&err),
"blacklist watcher: isHashBlacklistedForOperator failed; keeping for re-scope"
);
Recheck::Failed(RecheckFailure::Scope(err))
}
}
}
async fn scope_check<P>(
contract: &ContentBlacklist::ContentBlacklistInstance<P>,
operator: Address,
hash: Hash,
) -> Result<bool>
where
P: Provider + Clone,
{
let hash_key = B256::from(*hash.as_bytes());
timed(
None,
"isHashBlacklistedForOperator",
contract
.isHashBlacklistedForOperator(hash_key, operator)
.call(),
)
.await
.with_context(|| format!("isHashBlacklistedForOperator({hash})"))
}
async fn evict(cache: &CacheEngine, warming: &Arc<WarmingAllowance>, hash: Hash) -> Result<()> {
match cache.evict(hash).await {
Ok(()) => {
info!(%hash, "evicted blacklisted blob (ADR 011 compliance)");
warming.forget(hash);
Ok(())
}
Err(err) => {
let err = anyhow::Error::new(err).context(format!("evict {hash}"));
warn!(%hash, error = %sanitize_err_chain(&err), "blacklist watcher: evict failed; will retry");
Err(err)
}
}
}
#[allow(clippy::too_many_arguments)]
async fn enumerate_and_enforce<P>(
reads: &ContractReads<P>,
contract: &ContentBlacklist::ContentBlacklistInstance<P>,
operator: Address,
cache: &CacheEngine,
warming: &Arc<WarmingAllowance>,
denylist: &Arc<ContentDenylist>,
metrics: &Arc<Metrics>,
shutdown: &CancellationToken,
) -> Result<BootEnforced>
where
P: Provider + Clone,
{
let snapshot = bootstrap_snapshot(reads, operator).await?;
denylist.set_chain_origins(snapshot.origins.clone());
let mut state = WatcherState {
known: snapshot.known,
denylist: Arc::clone(denylist),
origin_changes: HashMap::new(),
removed_entries: HashMap::new(),
};
let RescanOutcome {
clean,
failed,
decisive_failure,
} = rescan(
contract,
operator,
cache,
warming,
&mut state,
shutdown,
OnScopeFailure::Stop,
)
.await;
if failed > 0 {
metrics.blacklist_enforcement_failure(failed);
}
if !clean {
let cause = match decisive_failure {
Some(RecheckFailure::Scope(err)) => err,
Some(RecheckFailure::Evict(err)) => BootFault(format!("{err:#}")).into(),
None => anyhow::anyhow!("enforcement pass was cancelled"),
};
return Err(cause.context(format!(
"initial ContentBlacklist enforcement could not enforce every entry ({failed} failed)"
)));
}
Ok(BootEnforced {
block: snapshot.block,
origin_count: snapshot.origins.len(),
state,
})
}
struct BootEnforced {
block: u64,
origin_count: usize,
state: WatcherState,
}
#[allow(clippy::too_many_arguments)]
pub(crate) async fn bootstrap<P>(
provider: P,
contract_addr: Address,
operator: Address,
cache: CacheEngine,
warming: Arc<WarmingAllowance>,
head: Arc<dyn HeadSource>,
rescan_interval: Duration,
initial_sync_tx: oneshot::Sender<InitialSyncResult>,
metrics: &Arc<Metrics>,
denylist: Arc<ContentDenylist>,
chain_freshness: ChainFreshness,
boot: &BootRetry,
) -> Result<Route>
where
P: Provider + Clone + 'static,
{
let contract = ContentBlacklist::new(contract_addr, provider.clone());
let reads = ContractReads {
contract: contract.clone(),
head: Arc::clone(&head),
};
let initial_sync = InitialSyncGate::new(initial_sync_tx);
let boot_shutdown = CancellationToken::new();
let attempt = boot
.run("ContentBlacklist boot snapshot", || {
enumerate_and_enforce(
&reads,
&contract,
operator,
&cache,
&warming,
&denylist,
metrics,
&boot_shutdown,
)
})
.await;
let BootEnforced {
block: snapshot_block,
origin_count,
state,
} = match attempt {
Ok(done) => done,
Err(err) => {
initial_sync.signal(Err(format!(
"initial ContentBlacklist sync failed: {}",
sanitize_err_chain(&err)
)));
return Err(err).context("enumerate and enforce the ContentBlacklist boot snapshot");
}
};
chain_freshness.stamp();
initial_sync.signal(Ok(()));
info!(
%contract_addr,
%operator,
snapshot_block,
known_hashes = state.known.len(),
origins = origin_count,
"blacklist compliance watcher enumerated its boot snapshot"
);
let sink_metrics = Arc::clone(metrics);
let sink_factory: SinkSource = SinkSource::Factory(Box::new(move |shutdown| {
Box::new(BlacklistSink {
contract,
reads,
operator,
cache,
warming,
state,
shutdown: shutdown.clone(),
rescan_interval: rescan_interval.max(Duration::from_secs(1)),
last_rescan: Some(Instant::now()),
metrics: sink_metrics,
}) as Box<dyn crate::chain_events::multiplexed_poller::ErasedSink>
}));
Ok(Route {
addresses: vec![contract_addr],
topic0s: blacklist_route_topic0s(),
start: blacklist_cursor_start(snapshot_block),
sink: sink_factory,
label: "blacklist",
on_established: Some(metric_hook(
metrics,
Metrics::blacklist_watcher_cycle_established,
)),
on_backoff: Some(metric_hook(
metrics,
Metrics::blacklist_watcher_backoff_started,
)),
on_tick_success: Some({
let metrics = Arc::clone(metrics);
let freshness = chain_freshness.clone();
Box::new(move || {
metrics.blacklist_watcher_tick();
freshness.stamp();
})
}),
on_task_panic: Some(metric_hook(
metrics,
Metrics::blacklist_watcher_task_panicked,
)),
})
}
fn blacklist_route_topic0s() -> Vec<B256> {
vec![
HashBlacklisted::SIGNATURE_HASH,
HashRemoved::SIGNATURE_HASH,
OriginBlacklistUpdated::SIGNATURE_HASH,
OperatorBlacklisted::SIGNATURE_HASH,
OperatorBlacklistCleared::SIGNATURE_HASH,
]
}
const fn blacklist_cursor_start(snapshot_block: u64) -> CursorStart {
CursorStart::Seeded { at: snapshot_block }
}
#[cfg(test)]
#[allow(
clippy::panic,
clippy::unwrap_used,
clippy::expect_used,
clippy::indexing_slicing
)]
mod tests;