From 19187d2c3072c339b23887956c6b38632e3a9bb6 Mon Sep 17 00:00:00 2001 From: Luke Parker Date: Thu, 9 Nov 2023 05:17:34 -0500 Subject: [PATCH] Implement calculation of monotonic network times for Bitcoin and Monero --- processor/src/main.rs | 7 +++- processor/src/networks/bitcoin.rs | 32 +++++++++++--- processor/src/networks/mod.rs | 7 +++- processor/src/networks/monero.rs | 46 +++++++++++++++++++-- tests/full-stack/src/tests/mint_and_burn.rs | 16 +++++-- 5 files changed, 93 insertions(+), 15 deletions(-) diff --git a/processor/src/main.rs b/processor/src/main.rs index 103888e6..e1aa7b54 100644 --- a/processor/src/main.rs +++ b/processor/src/main.rs @@ -244,6 +244,9 @@ async fn handle_coordinator_msg( // assert!(substrate_mutable.existing.as_ref().is_none()); // Wait until a network's block's time exceeds Serai's time + // These time calls are extremely expensive for what they do, yet they only run when + // confirming the first key pair, before any network activity has occurred, so they + // should be fine // If the latest block number is 10, then the block indexed by 1 has 10 confirms // 10 + 1 - 10 = 1 @@ -251,7 +254,7 @@ async fn handle_coordinator_msg( while { block_i = (get_latest_block_number(network).await + 1).saturating_sub(N::CONFIRMATIONS); - get_block(network, block_i).await.time() < context.serai_time + get_block(network, block_i).await.time(network).await < context.serai_time } { info!( "serai confirmed the first key pair for a set. {} {}", @@ -267,7 +270,7 @@ async fn handle_coordinator_msg( // which... should be impossible // Yet a prevented panic is a prevented panic while (earliest > 0) && - (get_block(network, earliest - 1).await.time() >= context.serai_time) + (get_block(network, earliest - 1).await.time(network).await >= context.serai_time) { earliest -= 1; } diff --git a/processor/src/networks/bitcoin.rs b/processor/src/networks/bitcoin.rs index d363e126..688823ad 100644 --- a/processor/src/networks/bitcoin.rs +++ b/processor/src/networks/bitcoin.rs @@ -256,6 +256,7 @@ impl SignableTransactionTrait for SignableTransaction { } } +#[async_trait] impl BlockTrait for Block { type Id = [u8; 32]; fn id(&self) -> Self::Id { @@ -270,10 +271,31 @@ impl BlockTrait for Block { hash } - // TODO: Don't use this block's time, use the network time at this block - // TODO: Confirm network time is monotonic, enabling its usage here - fn time(&self) -> u64 { - self.header.time.into() + 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] } } @@ -325,7 +347,7 @@ impl Bitcoin { let mut res = Rpc::new(url.clone()).await; while let Err(e) = res { log::error!("couldn't connect to Bitcoin node: {e:?}"); - tokio::time::sleep(Duration::from_secs(5)).await; + sleep(Duration::from_secs(5)).await; res = Rpc::new(url.clone()).await; } Bitcoin { rpc: res.unwrap() } diff --git a/processor/src/networks/mod.rs b/processor/src/networks/mod.rs index 0122f241..c0ad9e8a 100644 --- a/processor/src/networks/mod.rs +++ b/processor/src/networks/mod.rs @@ -181,13 +181,16 @@ impl Default for EventualitiesTracker { } } +#[async_trait] pub trait Block: Send + Sync + Sized + Clone + Debug { // This is currently bounded to being 32 bytes. type Id: 'static + Id; fn id(&self) -> Self::Id; fn parent(&self) -> Self::Id; - // The monotonic network time at this block. - fn time(&self) -> u64; + /// The monotonic network time at this block. + /// + /// This call is presumed to be expensive and should only be called sparingly. + async fn time(&self, rpc: &N) -> u64; } // The post-fee value of an expected branch. diff --git a/processor/src/networks/monero.rs b/processor/src/networks/monero.rs index dca7392b..30dc392f 100644 --- a/processor/src/networks/monero.rs +++ b/processor/src/networks/monero.rs @@ -156,6 +156,7 @@ impl SignableTransactionTrait for SignableTransaction { } } +#[async_trait] impl BlockTrait for Block { type Id = [u8; 32]; fn id(&self) -> Self::Id { @@ -166,9 +167,48 @@ impl BlockTrait for Block { self.header.previous } - // TODO: Check Monero enforces this to be monotonic and sane - fn time(&self) -> u64 { - self.header.timestamp + async fn time(&self, rpc: &Monero) -> u64 { + // Constant from Monero + const BLOCKCHAIN_TIMESTAMP_CHECK_WINDOW: u64 = 60; + + // If Monero doesn't have enough blocks to build a window, it doesn't define a network time + if (u64::try_from(self.number()).unwrap() + 1) < BLOCKCHAIN_TIMESTAMP_CHECK_WINDOW { + // Use the block number as the time + return self.number().try_into().unwrap(); + } + + let mut timestamps = vec![self.header.timestamp]; + let mut parent = self.parent(); + while u64::try_from(timestamps.len()).unwrap() < BLOCKCHAIN_TIMESTAMP_CHECK_WINDOW { + 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(parent_block.header.timestamp); + parent = parent_block.parent(); + + if parent_block.number() == 0 { + break; + } + } + timestamps.sort(); + + // Because 60 has two medians, Monero's epee picks the in-between value, calculated by the + // following formula (from the "get_mid" function) + let n = timestamps.len() / 2; + let a = timestamps[n - 1]; + let b = timestamps[n]; + #[rustfmt::skip] // Enables Ctrl+F'ing for everything after the `= ` + let res = (a/2) + (b/2) + ((a - 2*(a/2)) + (b - 2*(b/2)))/2; + // Techniaslly, res may be 1 if all prior blocks had a timestamp by 0, which would break + // monotonicity with our above definition of height as time + // Ensure monotonicity by increasing this value by the window size + res + BLOCKCHAIN_TIMESTAMP_CHECK_WINDOW } } diff --git a/tests/full-stack/src/tests/mint_and_burn.rs b/tests/full-stack/src/tests/mint_and_burn.rs index 1277f6fa..8221228d 100644 --- a/tests/full-stack/src/tests/mint_and_burn.rs +++ b/tests/full-stack/src/tests/mint_and_burn.rs @@ -1,5 +1,5 @@ use std::{ - sync::{Arc, Mutex}, + sync::{OnceLock, Arc, Mutex}, time::{Duration, Instant}, collections::HashSet, }; @@ -40,6 +40,11 @@ async fn mint_and_burn_test() { producer: &mut usize, count: usize, ) { + static MINE_BLOCKS_CALL: OnceLock> = OnceLock::new(); + + // Only let one instance of this function run at a time + let _lock = MINE_BLOCKS_CALL.get_or_init(|| tokio::sync::Mutex::new(())).lock().await; + // Pick a block producer via a round robin let producer_handles = &handles[*producer]; *producer += 1; @@ -177,8 +182,8 @@ async fn mint_and_burn_test() { // Bound execution to 60m keep_mining && (Instant::now().duration_since(start) < Duration::from_secs(60 * 60)) } { - // Mine a block every 5s - tokio::time::sleep(Duration::from_secs(5)).await; + // Mine a block every 3s + tokio::time::sleep(Duration::from_secs(3)).await; mine_blocks(&handles, &ops, &mut producer, 1).await; } }) @@ -228,6 +233,11 @@ async fn mint_and_burn_test() { (key_pair(false, NetworkId::Bitcoin).await, key_pair(true, NetworkId::Monero).await) }; + // Because the initial keys only become active when the network's time matches the Serai + // time, the Serai time is real yet the network time may be significantly delayed due to + // potentially being a median, mine a bunch of blocks now + mine_blocks(&handles, &ops, &mut 0, 100).await; + // Create a Serai address to receive the sriBTC/sriXMR to let (serai_pair, serai_addr) = { let mut name = [0; 4];