Skip to content

Commit 4ed1afe

Browse files
authored
chain/ethereum: Fix dropped block trigger when once and polling filters match same block (#6530)
* chain/ethereum: Fix dropped trigger when once and polling filters match same block blocks_matching_polling_intervals used find_map over polling_intervals, which short-circuits after the first matching entry. Since a single (start_block, interval) tuple satisfies at most one of the once/polling conditions, a block that should emit both Start and End only got one. HashSet iteration order made which trigger survived non-deterministic across process restarts, causing PoI divergence. Replace with two independent any() scans so both conditions are evaluated across all entries, matching parse_block_triggers on the BlockFinality::NonFinal (reorg-window) path. * chain/ethereum: Extract block_trigger_types_from_intervals helper Both blocks_matching_polling_intervals (JSON-RPC path) and parse_block_triggers (Firehose path) now compute the same once/polling match logic. Pull it into a shared pure function so the two paths cannot drift out of sync. * chain/ethereum: Add tests for block_trigger_types_from_intervals Cover the determinism-critical cases the new helper guards against: once-only, polling-only, once + polling sharing a start_block, cross-datasource collision where a polling schedule lands on another datasource's once block, and the empty/no-match cases.
1 parent 6848b54 commit 4ed1afe

1 file changed

Lines changed: 154 additions & 56 deletions

File tree

chain/ethereum/src/ethereum_adapter.rs

Lines changed: 154 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -1016,31 +1016,16 @@ impl EthereumAdapter {
10161016
+ '_,
10171017
>,
10181018
> {
1019-
// Create a HashMap of block numbers to Vec<EthereumBlockTriggerType>
1019+
// Create a HashMap of block numbers to Vec<EthereumBlockTriggerType>.
10201020
let matching_blocks = (from..=to)
10211021
.filter_map(|block_number| {
1022-
filter
1023-
.polling_intervals
1024-
.iter()
1025-
.find_map(|(start_block, interval)| {
1026-
let has_once_trigger = (*interval == 0) && (block_number == *start_block);
1027-
let has_polling_trigger = block_number >= *start_block
1028-
&& *interval > 0
1029-
&& ((block_number - start_block) % *interval) == 0;
1030-
1031-
if has_once_trigger || has_polling_trigger {
1032-
let mut triggers = Vec::new();
1033-
if has_once_trigger {
1034-
triggers.push(EthereumBlockTriggerType::Start);
1035-
}
1036-
if has_polling_trigger {
1037-
triggers.push(EthereumBlockTriggerType::End);
1038-
}
1039-
Some((block_number, triggers))
1040-
} else {
1041-
None
1042-
}
1043-
})
1022+
let triggers =
1023+
block_trigger_types_from_intervals(block_number, &filter.polling_intervals);
1024+
if triggers.is_empty() {
1025+
None
1026+
} else {
1027+
Some((block_number, triggers))
1028+
}
10441029
})
10451030
.collect::<HashMap<_, _>>();
10461031

@@ -2009,6 +1994,36 @@ pub(crate) fn parse_call_triggers(
20091994
}
20101995
}
20111996

1997+
/// For a given `block_number`, return the block trigger types that fire
1998+
/// based on the rules in `polling_intervals`. Each entry is `(start_block,
1999+
/// interval)` where `interval == 0` encodes a `once` rule and `interval > 0`
2000+
/// encodes a `polling every interval` rule. Both rule kinds can fire at the
2001+
/// same block (e.g. a once and polling rule sharing a `start_block`), so the
2002+
/// returned Vec may contain `Start`, `End`, both, or neither.
2003+
pub(crate) fn block_trigger_types_from_intervals(
2004+
block_number: i32,
2005+
polling_intervals: &HashSet<(i32, i32)>,
2006+
) -> Vec<EthereumBlockTriggerType> {
2007+
let has_once_trigger = polling_intervals
2008+
.iter()
2009+
.any(|(start_block, interval)| *interval == 0 && block_number == *start_block);
2010+
2011+
let has_polling_trigger = polling_intervals.iter().any(|(start_block, interval)| {
2012+
*interval > 0
2013+
&& block_number >= *start_block
2014+
&& (block_number - start_block) % *interval == 0
2015+
});
2016+
2017+
let mut triggers = Vec::new();
2018+
if has_once_trigger {
2019+
triggers.push(EthereumBlockTriggerType::Start);
2020+
}
2021+
if has_polling_trigger {
2022+
triggers.push(EthereumBlockTriggerType::End);
2023+
}
2024+
triggers
2025+
}
2026+
20122027
/// This method does not parse block triggers with `once` filters.
20132028
/// This is because it is to be run before any other triggers are run.
20142029
/// So we have `parse_initialization_triggers` for that.
@@ -2050,38 +2065,12 @@ pub(crate) fn parse_block_triggers(
20502065
EthereumBlockTriggerType::End,
20512066
));
20522067
} else if !block_filter.polling_intervals.is_empty() {
2053-
let has_polling_trigger =
2054-
&block_filter
2055-
.polling_intervals
2056-
.iter()
2057-
.any(|(start_block, interval)| match interval {
2058-
0 => false,
2059-
_ => {
2060-
block_number >= *start_block
2061-
&& (block_number - *start_block) % *interval == 0
2062-
}
2063-
});
2064-
2065-
let has_once_trigger =
2066-
&block_filter
2067-
.polling_intervals
2068-
.iter()
2069-
.any(|(start_block, interval)| match interval {
2070-
0 => block_number == *start_block,
2071-
_ => false,
2072-
});
2073-
2074-
if *has_once_trigger {
2075-
triggers.push(EthereumTrigger::Block(
2076-
block_ptr3.clone(),
2077-
EthereumBlockTriggerType::Start,
2078-
));
2079-
}
2080-
2081-
if *has_polling_trigger {
2068+
for trigger_type in
2069+
block_trigger_types_from_intervals(block_number, &block_filter.polling_intervals)
2070+
{
20822071
triggers.push(EthereumTrigger::Block(
2083-
block_ptr3,
2084-
EthereumBlockTriggerType::End,
2072+
block_ptr3.cheap_clone(),
2073+
trigger_type,
20852074
));
20862075
}
20872076
}
@@ -2704,8 +2693,8 @@ mod tests {
27042693
use crate::trigger::{EthereumBlockTriggerType, EthereumTrigger};
27052694

27062695
use super::{
2707-
EthereumBlock, EthereumBlockFilter, EthereumBlockWithCalls, check_block_receipt_support,
2708-
parse_block_triggers,
2696+
EthereumBlock, EthereumBlockFilter, EthereumBlockWithCalls,
2697+
block_trigger_types_from_intervals, check_block_receipt_support, parse_block_triggers,
27092698
};
27102699
use graph::blockchain::BlockPtr;
27112700
use graph::components::ethereum::AnyNetworkBare;
@@ -2926,6 +2915,115 @@ mod tests {
29262915
);
29272916
}
29282917

2918+
#[test]
2919+
fn block_trigger_types_once_only() {
2920+
let intervals = HashSet::from_iter(vec![(100, 0)]);
2921+
2922+
assert_eq!(
2923+
vec![EthereumBlockTriggerType::Start],
2924+
block_trigger_types_from_intervals(100, &intervals),
2925+
"once rule fires Start at start_block"
2926+
);
2927+
assert_eq!(
2928+
Vec::<EthereumBlockTriggerType>::new(),
2929+
block_trigger_types_from_intervals(99, &intervals),
2930+
"once rule does not fire before start_block"
2931+
);
2932+
assert_eq!(
2933+
Vec::<EthereumBlockTriggerType>::new(),
2934+
block_trigger_types_from_intervals(101, &intervals),
2935+
"once rule does not fire after start_block"
2936+
);
2937+
}
2938+
2939+
#[test]
2940+
fn block_trigger_types_polling_only() {
2941+
let intervals = HashSet::from_iter(vec![(100, 10)]);
2942+
2943+
assert_eq!(
2944+
vec![EthereumBlockTriggerType::End],
2945+
block_trigger_types_from_intervals(100, &intervals),
2946+
"polling rule fires End at start_block"
2947+
);
2948+
assert_eq!(
2949+
vec![EthereumBlockTriggerType::End],
2950+
block_trigger_types_from_intervals(110, &intervals),
2951+
"polling rule fires End at start_block + interval"
2952+
);
2953+
assert_eq!(
2954+
Vec::<EthereumBlockTriggerType>::new(),
2955+
block_trigger_types_from_intervals(105, &intervals),
2956+
"polling rule does not fire off-interval"
2957+
);
2958+
assert_eq!(
2959+
Vec::<EthereumBlockTriggerType>::new(),
2960+
block_trigger_types_from_intervals(90, &intervals),
2961+
"polling rule does not fire before start_block"
2962+
);
2963+
}
2964+
2965+
#[test]
2966+
fn block_trigger_types_once_and_polling_same_start_block() {
2967+
// A single data source with both a `once` handler and a `polling` handler
2968+
// contributes both entries at its start_block. Both must fire at start_block.
2969+
let intervals = HashSet::from_iter(vec![(100, 0), (100, 10)]);
2970+
2971+
assert_eq!(
2972+
vec![
2973+
EthereumBlockTriggerType::Start,
2974+
EthereumBlockTriggerType::End,
2975+
],
2976+
block_trigger_types_from_intervals(100, &intervals),
2977+
"both Start and End should fire when once and polling rules share start_block"
2978+
);
2979+
assert_eq!(
2980+
vec![EthereumBlockTriggerType::End],
2981+
block_trigger_types_from_intervals(110, &intervals),
2982+
"only polling fires at later interval matches"
2983+
);
2984+
}
2985+
2986+
#[test]
2987+
fn block_trigger_types_cross_datasource_collision() {
2988+
// Two data sources: DS-A with once at 100, DS-B with polling every 10 from 50.
2989+
// Block 100 satisfies DS-A's once rule and also (100-50) % 10 == 0, so both fire.
2990+
let intervals = HashSet::from_iter(vec![(100, 0), (50, 10)]);
2991+
2992+
assert_eq!(
2993+
vec![
2994+
EthereumBlockTriggerType::Start,
2995+
EthereumBlockTriggerType::End,
2996+
],
2997+
block_trigger_types_from_intervals(100, &intervals),
2998+
"both triggers fire when a once rule and an unrelated polling rule collide"
2999+
);
3000+
assert_eq!(
3001+
vec![EthereumBlockTriggerType::End],
3002+
block_trigger_types_from_intervals(60, &intervals),
3003+
"only polling fires at a block where only the polling rule matches"
3004+
);
3005+
}
3006+
3007+
#[test]
3008+
fn block_trigger_types_no_match() {
3009+
let intervals = HashSet::from_iter(vec![(100, 0), (100, 10)]);
3010+
assert_eq!(
3011+
Vec::<EthereumBlockTriggerType>::new(),
3012+
block_trigger_types_from_intervals(99, &intervals),
3013+
"no triggers when block matches neither rule"
3014+
);
3015+
}
3016+
3017+
#[test]
3018+
fn block_trigger_types_empty_intervals() {
3019+
let intervals: HashSet<(i32, i32)> = HashSet::new();
3020+
assert_eq!(
3021+
Vec::<EthereumBlockTriggerType>::new(),
3022+
block_trigger_types_from_intervals(100, &intervals),
3023+
"empty intervals yields no triggers"
3024+
);
3025+
}
3026+
29293027
fn address(id: u64) -> Address {
29303028
Address::left_padding_from(&id.to_be_bytes())
29313029
}

0 commit comments

Comments
 (0)