Skip to main content

cast/cmd/
find_block.rs

1use crate::cmd::rpc_provider;
2use alloy_provider::Provider;
3use clap::Parser;
4use eyre::Result;
5use foundry_cli::{json::print_scalar, opts::RpcOpts};
6use futures::join;
7
8/// CLI arguments for `cast find-block`.
9#[derive(Clone, Debug, Parser)]
10pub struct FindBlockArgs {
11    /// The UNIX timestamp to search for, in seconds.
12    timestamp: u64,
13
14    #[command(flatten)]
15    rpc: RpcOpts,
16}
17
18fn interpolate_block(
19    low_block: u64,
20    low_timestamp: u64,
21    high_block: u64,
22    high_timestamp: u64,
23    target_timestamp: u64,
24) -> u64 {
25    let block_range = high_block - low_block;
26    let midpoint = high_block - block_range / 2;
27    if high_timestamp <= low_timestamp {
28        return midpoint;
29    }
30
31    let timestamp_offset = target_timestamp - low_timestamp;
32    let timestamp_range = high_timestamp - low_timestamp;
33    let block_offset =
34        u128::from(timestamp_offset) * u128::from(block_range) / u128::from(timestamp_range);
35    (low_block + block_offset as u64).clamp(low_block + 1, high_block - 1)
36}
37
38impl FindBlockArgs {
39    pub async fn run(self) -> Result<()> {
40        let Self { timestamp: ts_target, rpc } = self;
41        let provider = rpc_provider(&rpc)?;
42
43        let last_block_num = provider.get_block_number().await?;
44        let (ts_block_latest, ts_block_1) =
45            join!(timestamp(&provider, last_block_num), timestamp(&provider, 1));
46        let ts_block_latest = ts_block_latest?;
47        let ts_block_1 = ts_block_1?;
48
49        let block_num = if ts_block_latest < ts_target {
50            // If the most recent block's timestamp is below the target, return it
51            last_block_num
52        } else if ts_block_1 > ts_target {
53            // If the target timestamp is below block 1's timestamp, return that
54            1
55        } else {
56            // Otherwise, find the block that is closest to the timestamp
57            let mut low_block = 1_u64; // block 0 has a timestamp of 0: https://github.com/ethereum/go-ethereum/issues/17042#issuecomment-559414137
58            let mut low_timestamp = ts_block_1;
59            let mut high_block = last_block_num;
60            let mut high_timestamp = ts_block_latest;
61            // Limit interpolation to the range's binary search depth so irregular chains retain
62            // logarithmic worst-case behavior.
63            let mut interpolation_budget =
64                u64::BITS - last_block_num.saturating_sub(1).leading_zeros();
65            loop {
66                let block_range = high_block - low_block;
67                if block_range == 0 {
68                    break low_block;
69                }
70                if block_range == 1 {
71                    // Round to the higher block when the timestamp is equidistant.
72                    let high_diff = high_timestamp - ts_target;
73                    let low_diff = ts_target - low_timestamp;
74                    break if low_diff < high_diff { low_block } else { high_block };
75                }
76
77                let midpoint = high_block - block_range / 2;
78                let next_block = if interpolation_budget == 0 {
79                    midpoint
80                } else {
81                    interpolation_budget -= 1;
82                    interpolate_block(
83                        low_block,
84                        low_timestamp,
85                        high_block,
86                        high_timestamp,
87                        ts_target,
88                    )
89                };
90                let next_timestamp = timestamp(&provider, next_block).await?;
91
92                if next_timestamp == ts_target {
93                    break next_block;
94                }
95                if next_timestamp < ts_target {
96                    low_block = next_block;
97                    low_timestamp = next_timestamp;
98                } else {
99                    high_block = next_block;
100                    high_timestamp = next_timestamp;
101                }
102            }
103        };
104        print_scalar(block_num)?;
105
106        Ok(())
107    }
108}
109
110async fn timestamp(
111    provider: &impl Provider<alloy_network::AnyNetwork>,
112    number: u64,
113) -> Result<u64> {
114    Ok(provider
115        .get_block_by_number(number.into())
116        .await?
117        .ok_or_else(|| eyre::eyre!("block {number} not found"))?
118        .header
119        .timestamp)
120}
121
122#[cfg(test)]
123mod tests {
124    use super::interpolate_block;
125
126    #[test]
127    fn interpolates_block_from_timestamps() {
128        assert_eq!(interpolate_block(1, 100, 11, 200, 150), 6);
129        // Stays inside the search bounds.
130        assert_eq!(interpolate_block(1, 100, 11, 200, 100), 2);
131        assert_eq!(interpolate_block(1, 100, 11, 200, 200), 10);
132        // Does not overflow.
133        assert_eq!(interpolate_block(0, 0, u64::MAX, u64::MAX, u64::MAX / 2), u64::MAX / 2);
134        // Falls back to the midpoint for equal timestamps.
135        assert_eq!(interpolate_block(1, 100, 10, 100, 100), 6);
136    }
137}