Skip to content

Commit

Permalink
adds a binary search to get block by timestamp
Browse files Browse the repository at this point in the history
  • Loading branch information
montekki committed Oct 5, 2023
1 parent 49762a3 commit df749c6
Show file tree
Hide file tree
Showing 4 changed files with 143 additions and 3 deletions.
2 changes: 1 addition & 1 deletion client/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,4 @@ tokio = "1.32.0"

[dev-dependencies]
hex = "0.4"
pretty_assertions = "1"
pretty_assertions = "1.4.0"
13 changes: 11 additions & 2 deletions client/src/error.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
use ethers::{
abi::EncodePackedError, contract::ContractError, prelude::Middleware, providers::ProviderError,
types::H256,
abi::EncodePackedError,
contract::ContractError,
prelude::Middleware,
providers::ProviderError,
types::{TimeError, H256},
};

#[derive(Debug, thiserror::Error)]
Expand All @@ -18,6 +21,9 @@ pub enum Error {
#[error(transparent)]
EncodePackedError(#[from] EncodePackedError),

#[error(transparent)]
TimeError(#[from] TimeError),

#[error("Contract error {0}")]
ContractError(String),

Expand All @@ -44,6 +50,9 @@ pub enum Error {

#[error("Message not RLP bytes encoded: {0}")]
MessageNotRlpBytes(String),

#[error("Block has no number, parent block hash is {0:?}")]
BlockHasNoNumber(H256),
}

impl<M: Middleware> From<ContractError<M>> for Error {
Expand Down
65 changes: 65 additions & 0 deletions client/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ pub use error::{Error, Result};

use async_trait::async_trait;
use auto_impl::auto_impl;
use ethers::types::BlockNumber;
use ethers::{
abi::{AbiDecode, AbiEncode, ParamType, RawLog, Token},
contract::{EthCall, EthEvent, EthLogDecode},
Expand Down Expand Up @@ -553,6 +554,70 @@ where
}
}

/// Get the block number by timestamp
///
/// # Arguments
/// * `at_date_time`: Look for the block at this datetime
/// * `start_from_block`: Will perform search starting from this block,
/// if `None` is specified then searches from block 1.
/// * `middleware`: The client to perform requests to RPC with.
pub async fn get_block_number_by_timestamp<M: Middleware>(
at_date: chrono::DateTime<chrono::offset::Utc>,
start_from_block: Option<U64>,
middleware: M,
) -> Result<Option<U64>> {
let start_from_block = start_from_block.unwrap_or(1_u64.into());

let right_block = match middleware
.get_block(BlockNumber::Latest)
.await
.map_err(|e| Error::Middleware(format!("{e}")))?
{
Some(r) => r,
None => return Ok(None),
};

let mut right = right_block
.number
.ok_or(Error::BlockHasNoNumber(right_block.parent_hash))?;

let mut left = start_from_block;

if at_date > right_block.time()? {
return Ok(None);
}

let mut middle = left + (right - left) / 2;

while left < right {
middle = left + (right - left) / 2;

let middle_block = middleware
.get_block(BlockNumber::Number(middle))
.await
.map_err(|e| Error::Middleware(format!("{e}")))?
.ok_or(Error::BlockHasNoNumber(right_block.parent_hash))?;

let middle_block_timestamp = middle_block.time()?;

let signed_duration_since_requested_timestamp =
middle_block_timestamp.signed_duration_since(at_date);

let num_milliseconds = signed_duration_since_requested_timestamp.num_milliseconds();

// look within the 500ms margin to the right of the given date.
if (0..500).contains(&num_milliseconds) {
return Ok(Some(middle));
} else if num_milliseconds.is_positive() {
right = middle;
} else {
left = middle;
}
}

Ok(Some(middle))
}

fn get_l1_bridge_burn_message_keccak(
burn: &BridgeBurnFilter,
l1_receiver: Address,
Expand Down
66 changes: 66 additions & 0 deletions client/tests/request_blocks_by_timestamp.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
use std::sync::Arc;

use chrono::{prelude::*, Datelike, TimeZone, Utc};
use ethers::providers::{Http, Provider};
use pretty_assertions::assert_eq;

#[tokio::test]
async fn request_first_block_unlimited() {
let provider_l2 = Provider::<Http>::try_from("https://mainnet.era.zksync.io").unwrap();
let client_l2 = Arc::new(provider_l2);

let date = "2023-10-5T12:00:00Z".parse::<DateTime<Utc>>().unwrap();

let previous_midnight = Utc
.with_ymd_and_hms(date.year(), date.month(), date.day(), 0, 0, 0)
.unwrap();

let res = client::get_block_number_by_timestamp(previous_midnight, None, client_l2)
.await
.unwrap()
.unwrap();

// First block on 2023-10-5 https://explorer.zksync.io/block/15560287
assert_eq!(res, 15560287.into());
}

#[tokio::test]
async fn request_first_block_limited() {
let provider_l2 = Provider::<Http>::try_from("https://mainnet.era.zksync.io").unwrap();
let client_l2 = Arc::new(provider_l2);

let date = "2023-10-5T12:00:00Z".parse::<DateTime<Utc>>().unwrap();

let previous_midnight = Utc
.with_ymd_and_hms(date.year(), date.month(), date.day(), 0, 0, 0)
.unwrap();

let res =
client::get_block_number_by_timestamp(previous_midnight, Some(15000000.into()), client_l2)
.await
.unwrap()
.unwrap();

// First block on 2023-10-5 https://explorer.zksync.io/block/15560287
assert_eq!(res, 15560287.into());
}

#[tokio::test]
async fn request_from_the_future() {
let provider_l2 = Provider::<Http>::try_from("https://mainnet.era.zksync.io").unwrap();
let client_l2 = Arc::new(provider_l2);

let date = Utc::now();

let previous_midnight = Utc
.with_ymd_and_hms(date.year(), date.month(), date.day() + 1, 0, 0, 0)
.unwrap();

let res =
client::get_block_number_by_timestamp(previous_midnight, Some(15000000.into()), client_l2)
.await
.unwrap();

// No first block in the next day
assert_eq!(res, None);
}

0 comments on commit df749c6

Please sign in to comment.