Skip to main content

cast/cmd/
find_block.rs

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