Add binary search to find the block to start scanning from

This commit is contained in:
Luke Parker
2024-09-11 11:59:15 -04:00
parent 3ac0265f07
commit fcd5fb85df
4 changed files with 113 additions and 193 deletions

View File

@@ -6,11 +6,16 @@
static ALLOCATOR: zalloc::ZeroizingAlloc<std::alloc::System> =
zalloc::ZeroizingAlloc(std::alloc::System);
use core::cmp::Ordering;
use ciphersuite::Ciphersuite;
use serai_client::validator_sets::primitives::Session;
use serai_db::{DbTxn, Db};
use ::primitives::EncodableG;
use ::key_gen::KeyGenParams as KeyGenParamsTrait;
use scanner::{ScannerFeed, Scanner};
mod primitives;
pub(crate) use crate::primitives::*;
@@ -38,6 +43,56 @@ pub(crate) fn hash_bytes(hash: bitcoin_serai::bitcoin::hashes::sha256d::Hash) ->
res
}
async fn first_block_after_time<S: ScannerFeed>(feed: &S, serai_time: u64) -> u64 {
async fn first_block_after_time_iteration<S: ScannerFeed>(
feed: &S,
serai_time: u64,
) -> Result<Option<u64>, S::EphemeralError> {
let latest = feed.latest_finalized_block_number().await?;
let latest_time = feed.time_of_block(latest).await?;
if latest_time < serai_time {
tokio::time::sleep(core::time::Duration::from_secs(serai_time - latest_time)).await;
return Ok(None);
}
// A finalized block has a time greater than or equal to the time we want to start at
// Find the first such block with a binary search
// start_search and end_search are inclusive
let mut start_search = 0;
let mut end_search = latest;
while start_search != end_search {
// This on purposely chooses the earlier block in the case two blocks are both in the middle
let to_check = start_search + ((end_search - start_search) / 2);
let block_time = feed.time_of_block(to_check).await?;
match block_time.cmp(&serai_time) {
Ordering::Less => {
start_search = to_check + 1;
assert!(start_search <= end_search);
}
Ordering::Equal | Ordering::Greater => {
// This holds true since we pick the earlier block upon an even search distance
// If it didn't, this would cause an infinite loop
assert!(to_check < end_search);
end_search = to_check;
}
}
}
Ok(Some(start_search))
}
loop {
match first_block_after_time_iteration(feed, serai_time).await {
Ok(Some(block)) => return block,
Ok(None) => {
log::info!("waiting for block to activate at (a block with timestamp >= {serai_time})");
}
Err(e) => {
log::error!("couldn't find the first block Serai should scan due to an RPC error: {e:?}");
}
}
tokio::time::sleep(core::time::Duration::from_secs(5)).await;
}
}
/// Fetch the next message from the Coordinator.
///
/// This message is guaranteed to have never been handled before, where handling is defined as
@@ -52,11 +107,13 @@ async fn send_message(_msg: messages::ProcessorMessage) {
async fn coordinator_loop<D: Db>(
mut db: D,
feed: Rpc<D>,
mut key_gen: ::key_gen::KeyGen<KeyGenParams>,
mut signers: signers::Signers<D, Rpc<D>, Scheduler<D>, Rpc<D>>,
mut scanner: Option<scanner::Scanner<Rpc<D>>>,
) {
loop {
let db_clone = db.clone();
let mut txn = db.txn();
let msg = next_message(&mut txn).await;
let mut txn = Some(txn);
@@ -120,9 +177,13 @@ async fn coordinator_loop<D: Db>(
<<KeyGenParams as ::key_gen::KeyGenParams>::ExternalNetworkCurve as Ciphersuite>::G,
>::set(txn, session, &key);
// This isn't cheap yet only happens for the very first set of keys
if scanner.is_none() {
todo!("TODO")
// This is presumed extremely expensive, potentially blocking for several minutes, yet
// only happens for the very first set of keys
if session == Session(0) {
assert!(scanner.is_none());
let start_block = first_block_after_time(&feed, serai_time).await;
scanner =
Some(Scanner::new::<Scheduler<D>>(db_clone, feed.clone(), start_block, key.0).await);
}
}
messages::substrate::CoordinatorMessage::SlashesReported { session } => {
@@ -241,36 +302,6 @@ impl TransactionTrait<Bitcoin> for Transaction {
}
}
#[async_trait]
impl BlockTrait<Bitcoin> for Block {
async fn time(&self, rpc: &Bitcoin) -> u64 {
// Use the network median time defined in BIP-0113 since the in-block time isn't guaranteed to
// be monotonic
let mut timestamps = vec![u64::from(self.header.time)];
let mut parent = self.parent();
// BIP-0113 uses a median of the prior 11 blocks
while timestamps.len() < 11 {
let mut parent_block;
while {
parent_block = rpc.rpc.get_block(&parent).await;
parent_block.is_err()
} {
log::error!("couldn't get parent block when trying to get block time: {parent_block:?}");
sleep(Duration::from_secs(5)).await;
}
let parent_block = parent_block.unwrap();
timestamps.push(u64::from(parent_block.header.time));
parent = parent_block.parent();
if parent == [0; 32] {
break;
}
}
timestamps.sort();
timestamps[timestamps.len() / 2]
}
}
impl Bitcoin {
pub(crate) async fn new(url: String) -> Bitcoin {
let mut res = Rpc::new(url.clone()).await;

View File

@@ -34,6 +34,50 @@ impl<D: Db> ScannerFeed for Rpc<D> {
db::LatestBlockToYieldAsFinalized::get(&self.db).ok_or(RpcError::ConnectionError)
}
async fn time_of_block(&self, number: u64) -> Result<u64, Self::EphemeralError> {
let number = usize::try_from(number).unwrap();
/*
The block time isn't guaranteed to be monotonic. It is guaranteed to be greater than the
median time of prior blocks, as detailed in BIP-0113 (a BIP which used that fact to improve
CLTV). This creates a monotonic median time which we use as the block time.
*/
// This implements `GetMedianTimePast`
let median = {
const MEDIAN_TIMESPAN: usize = 11;
let mut timestamps = Vec::with_capacity(MEDIAN_TIMESPAN);
for i in number.saturating_sub(MEDIAN_TIMESPAN) .. number {
timestamps.push(self.rpc.get_block(&self.rpc.get_block_hash(i).await?).await?.header.time);
}
timestamps.sort();
timestamps[timestamps.len() / 2]
};
/*
This block's timestamp is guaranteed to be greater than this median:
https://github.com/bitcoin/bitcoin/blob/0725a374941355349bb4bc8a79dad1affb27d3b9
/src/validation.cpp#L4182-L4184
This does not guarantee the median always increases however. Take the following trivial
example, as the window is initially built:
0 block has time 0 // Prior blocks: []
1 block has time 1 // Prior blocks: [0]
2 block has time 2 // Prior blocks: [0, 1]
3 block has time 2 // Prior blocks: [0, 1, 2]
These two blocks have the same time (both greater than the median of their prior blocks) and
the same median.
The median will never decrease however. The values pushed onto the window will always be
greater than the median. If a value greater than the median is popped, the median will remain
the same (due to the counterbalance of the pushed value). If a value less than the median is
popped, the median will increase (either to another instance of the same value, yet one
closer to the end of the repeating sequence, or to a higher value).
*/
Ok(median.into())
}
async fn unchecked_block_header_by_number(
&self,
number: u64,