diff --git a/crates/blackbeard-api/src/ingest.rs b/crates/blackbeard-api/src/ingest.rs index 4f2a576..b09e54a 100644 --- a/crates/blackbeard-api/src/ingest.rs +++ b/crates/blackbeard-api/src/ingest.rs @@ -36,14 +36,30 @@ use crate::state::{ChainRuntime, PendingAttribution}; /// dropped head while an ever-growing queue would only hide the problem. const HEAD_BUFFER: usize = 64; -/// How long to wait for the telemetry feed to report a block before deciding -/// who reported it first. +/// How long to wait before first asking the feed who reported a block. /// /// Reports arrive from nodes all over the network over a couple of seconds; /// judging at the moment the block is seen would count only the fastest-peered /// nodes and systematically favour them. const ATTRIBUTION_SETTLE: Duration = Duration::from_secs(8); +/// How often to ask again while the feed has not yet reported the block. +const ATTRIBUTION_RETRY: Duration = Duration::from_secs(5); + +/// How long to keep asking before recording an abstention. +/// +/// **This is not the same quantity as [`ATTRIBUTION_SETTLE`], and conflating +/// them cost an afternoon.** Settle is how long reports take to spread across +/// the network once the feed has the block; this is how far behind the chain +/// the feed itself runs. On Planck the second is near zero and one look at 8 s +/// worked. On mainnet the feed sits ~57 blocks back — roughly a minute at a +/// one-second block time, because ~190 nodes reporting every second is an order +/// of magnitude more traffic than Planck ever produced — so every lookup asked +/// before the answer existed, every observation became an abstention, and no +/// author was ever named. Three minutes is comfortably past the observed lag +/// without holding blocks whose reports are never coming. +const ATTRIBUTION_GIVE_UP: Duration = Duration::from_secs(180); + /// Interval between difficulty and sync-state polls. const POLL_INTERVAL: Duration = Duration::from_secs(4); @@ -321,10 +337,12 @@ async fn record( .pending_attributions .lock() .unwrap_or_else(|e| e.into_inner()); + let now = tokio::time::Instant::now(); pending.push_back(PendingAttribution { hash, miner, - due: tokio::time::Instant::now() + ATTRIBUTION_SETTLE, + due: now + ATTRIBUTION_SETTLE, + give_up: now + ATTRIBUTION_GIVE_UP, }); } } @@ -513,11 +531,36 @@ fn resolve_attributions(chain: &Arc) { if due.is_empty() { return; } - let mut inner = chain.write(); - for p in due { - inner - .attributor - .observe(&p.miner, chain.telemetry.first_import(&p.hash)); + + // Resolve what the feed can answer for; put the rest back to be asked again. + // Re-queued entries keep the queue sorted by `due` because every retry adds + // the same interval, which is what lets the drain above stop at the first + // entry that is not yet due. + let mut retry = Vec::new(); + { + let mut inner = chain.write(); + for p in due { + match chain.telemetry.first_import(&p.hash) { + Some(report) => inner.attributor.observe(&p.miner, Some(report)), + // Nothing yet. An abstention recorded now would be a claim the + // feed never made — it has simply not reached this block. + None if now < p.give_up => retry.push(PendingAttribution { + due: now + ATTRIBUTION_RETRY, + ..p + }), + None => inner.attributor.observe(&p.miner, None), + } + } + } + + if !retry.is_empty() { + let mut pending = chain + .pending_attributions + .lock() + .unwrap_or_else(|e| e.into_inner()); + for p in retry { + pending.push_back(p); + } } } diff --git a/crates/blackbeard-api/src/state.rs b/crates/blackbeard-api/src/state.rs index 0efa505..2923c4e 100644 --- a/crates/blackbeard-api/src/state.rs +++ b/crates/blackbeard-api/src/state.rs @@ -114,8 +114,15 @@ pub struct PendingAttribution { pub hash: String, /// Its author. pub miner: blackbeard_entities::MinerId, - /// When the feed has had long enough to report. + /// When to next ask the feed who reported this block first. pub due: tokio::time::Instant, + /// When to stop asking and record an abstention. + /// + /// The feed runs behind the chain by however much it is behind — measured + /// on mainnet at ~57 blocks, about a minute — so a single look at a fixed + /// offset asks before the answer exists and misses every time. This is + /// retried until the feed catches up or the block is too old to matter. + pub give_up: tokio::time::Instant, } /// One chain, live.