mirror of
https://github.com/serai-dex/serai.git
synced 2024-12-22 19:49:22 +00:00
Implement calculation of monotonic network times for Bitcoin and Monero
This commit is contained in:
parent
43ae6794db
commit
19187d2c30
5 changed files with 93 additions and 15 deletions
|
@ -244,6 +244,9 @@ async fn handle_coordinator_msg<D: Db, N: Network, Co: Coordinator>(
|
||||||
// assert!(substrate_mutable.existing.as_ref().is_none());
|
// assert!(substrate_mutable.existing.as_ref().is_none());
|
||||||
|
|
||||||
// Wait until a network's block's time exceeds Serai's time
|
// 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
|
// If the latest block number is 10, then the block indexed by 1 has 10 confirms
|
||||||
// 10 + 1 - 10 = 1
|
// 10 + 1 - 10 = 1
|
||||||
|
@ -251,7 +254,7 @@ async fn handle_coordinator_msg<D: Db, N: Network, Co: Coordinator>(
|
||||||
while {
|
while {
|
||||||
block_i =
|
block_i =
|
||||||
(get_latest_block_number(network).await + 1).saturating_sub(N::CONFIRMATIONS);
|
(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!(
|
info!(
|
||||||
"serai confirmed the first key pair for a set. {} {}",
|
"serai confirmed the first key pair for a set. {} {}",
|
||||||
|
@ -267,7 +270,7 @@ async fn handle_coordinator_msg<D: Db, N: Network, Co: Coordinator>(
|
||||||
// which... should be impossible
|
// which... should be impossible
|
||||||
// Yet a prevented panic is a prevented panic
|
// Yet a prevented panic is a prevented panic
|
||||||
while (earliest > 0) &&
|
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;
|
earliest -= 1;
|
||||||
}
|
}
|
||||||
|
|
|
@ -256,6 +256,7 @@ impl SignableTransactionTrait for SignableTransaction {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
impl BlockTrait<Bitcoin> for Block {
|
impl BlockTrait<Bitcoin> for Block {
|
||||||
type Id = [u8; 32];
|
type Id = [u8; 32];
|
||||||
fn id(&self) -> Self::Id {
|
fn id(&self) -> Self::Id {
|
||||||
|
@ -270,10 +271,31 @@ impl BlockTrait<Bitcoin> for Block {
|
||||||
hash
|
hash
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Don't use this block's time, use the network time at this block
|
async fn time(&self, rpc: &Bitcoin) -> u64 {
|
||||||
// TODO: Confirm network time is monotonic, enabling its usage here
|
// Use the network median time defined in BIP-0113 since the in-block time isn't guaranteed to
|
||||||
fn time(&self) -> u64 {
|
// be monotonic
|
||||||
self.header.time.into()
|
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;
|
let mut res = Rpc::new(url.clone()).await;
|
||||||
while let Err(e) = res {
|
while let Err(e) = res {
|
||||||
log::error!("couldn't connect to Bitcoin node: {e:?}");
|
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;
|
res = Rpc::new(url.clone()).await;
|
||||||
}
|
}
|
||||||
Bitcoin { rpc: res.unwrap() }
|
Bitcoin { rpc: res.unwrap() }
|
||||||
|
|
|
@ -181,13 +181,16 @@ impl<E: Eventuality> Default for EventualitiesTracker<E> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
pub trait Block<N: Network>: Send + Sync + Sized + Clone + Debug {
|
pub trait Block<N: Network>: Send + Sync + Sized + Clone + Debug {
|
||||||
// This is currently bounded to being 32 bytes.
|
// This is currently bounded to being 32 bytes.
|
||||||
type Id: 'static + Id;
|
type Id: 'static + Id;
|
||||||
fn id(&self) -> Self::Id;
|
fn id(&self) -> Self::Id;
|
||||||
fn parent(&self) -> Self::Id;
|
fn parent(&self) -> Self::Id;
|
||||||
// The monotonic network time at this block.
|
/// The monotonic network time at this block.
|
||||||
fn time(&self) -> u64;
|
///
|
||||||
|
/// 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.
|
// The post-fee value of an expected branch.
|
||||||
|
|
|
@ -156,6 +156,7 @@ impl SignableTransactionTrait for SignableTransaction {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
impl BlockTrait<Monero> for Block {
|
impl BlockTrait<Monero> for Block {
|
||||||
type Id = [u8; 32];
|
type Id = [u8; 32];
|
||||||
fn id(&self) -> Self::Id {
|
fn id(&self) -> Self::Id {
|
||||||
|
@ -166,9 +167,48 @@ impl BlockTrait<Monero> for Block {
|
||||||
self.header.previous
|
self.header.previous
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Check Monero enforces this to be monotonic and sane
|
async fn time(&self, rpc: &Monero) -> u64 {
|
||||||
fn time(&self) -> u64 {
|
// Constant from Monero
|
||||||
self.header.timestamp
|
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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
use std::{
|
use std::{
|
||||||
sync::{Arc, Mutex},
|
sync::{OnceLock, Arc, Mutex},
|
||||||
time::{Duration, Instant},
|
time::{Duration, Instant},
|
||||||
collections::HashSet,
|
collections::HashSet,
|
||||||
};
|
};
|
||||||
|
@ -40,6 +40,11 @@ async fn mint_and_burn_test() {
|
||||||
producer: &mut usize,
|
producer: &mut usize,
|
||||||
count: usize,
|
count: usize,
|
||||||
) {
|
) {
|
||||||
|
static MINE_BLOCKS_CALL: OnceLock<tokio::sync::Mutex<()>> = 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
|
// Pick a block producer via a round robin
|
||||||
let producer_handles = &handles[*producer];
|
let producer_handles = &handles[*producer];
|
||||||
*producer += 1;
|
*producer += 1;
|
||||||
|
@ -177,8 +182,8 @@ async fn mint_and_burn_test() {
|
||||||
// Bound execution to 60m
|
// Bound execution to 60m
|
||||||
keep_mining && (Instant::now().duration_since(start) < Duration::from_secs(60 * 60))
|
keep_mining && (Instant::now().duration_since(start) < Duration::from_secs(60 * 60))
|
||||||
} {
|
} {
|
||||||
// Mine a block every 5s
|
// Mine a block every 3s
|
||||||
tokio::time::sleep(Duration::from_secs(5)).await;
|
tokio::time::sleep(Duration::from_secs(3)).await;
|
||||||
mine_blocks(&handles, &ops, &mut producer, 1).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)
|
(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
|
// Create a Serai address to receive the sriBTC/sriXMR to
|
||||||
let (serai_pair, serai_addr) = {
|
let (serai_pair, serai_addr) = {
|
||||||
let mut name = [0; 4];
|
let mut name = [0; 4];
|
||||||
|
|
Loading…
Reference in a new issue