2024-09-14 07:54:18 -04:00
|
|
|
use core::future::Future;
|
2024-09-19 02:41:07 -04:00
|
|
|
use std::{sync::Arc, collections::HashSet};
|
2024-09-14 07:54:18 -04:00
|
|
|
|
2024-09-19 02:41:07 -04:00
|
|
|
use alloy_core::primitives::B256;
|
2025-04-12 08:09:09 -04:00
|
|
|
use alloy_rpc_types_eth::{Header, BlockNumberOrTag};
|
2024-09-18 18:35:31 -04:00
|
|
|
use alloy_transport::{RpcError, TransportErrorKind};
|
2024-09-18 00:54:20 -04:00
|
|
|
use alloy_provider::{Provider, RootProvider};
|
2024-09-14 07:54:18 -04:00
|
|
|
|
2025-01-30 03:14:24 -05:00
|
|
|
use serai_client::primitives::{ExternalNetworkId, ExternalCoin, Amount};
|
2024-09-14 07:54:18 -04:00
|
|
|
|
2024-09-20 02:06:35 -04:00
|
|
|
use tokio::task::JoinSet;
|
|
|
|
|
|
2024-09-19 02:41:07 -04:00
|
|
|
use serai_db::Db;
|
|
|
|
|
|
2024-09-14 07:54:18 -04:00
|
|
|
use scanner::ScannerFeed;
|
|
|
|
|
|
2024-09-19 02:41:07 -04:00
|
|
|
use ethereum_schnorr::PublicKey;
|
2025-01-24 05:34:49 -05:00
|
|
|
use ethereum_router::{InInstruction as EthereumInInstruction, Executed, Router};
|
2024-09-19 02:41:07 -04:00
|
|
|
|
|
|
|
|
use crate::{
|
2024-09-20 00:55:03 -04:00
|
|
|
TOKENS, ETHER_DUST, DAI_DUST, InitialSeraiKey,
|
2024-09-19 02:41:07 -04:00
|
|
|
block::{Epoch, FullEpoch},
|
|
|
|
|
};
|
2024-09-14 07:54:18 -04:00
|
|
|
|
|
|
|
|
#[derive(Clone)]
|
2024-09-19 02:41:07 -04:00
|
|
|
pub(crate) struct Rpc<D: Db> {
|
|
|
|
|
pub(crate) db: D,
|
2025-04-12 08:09:09 -04:00
|
|
|
pub(crate) provider: Arc<RootProvider>,
|
2024-09-14 07:54:18 -04:00
|
|
|
}
|
|
|
|
|
|
2024-09-19 02:41:07 -04:00
|
|
|
impl<D: Db> ScannerFeed for Rpc<D> {
|
2025-01-30 03:14:24 -05:00
|
|
|
const NETWORK: ExternalNetworkId = ExternalNetworkId::Ethereum;
|
2024-09-14 07:54:18 -04:00
|
|
|
|
|
|
|
|
// We only need one confirmation as Ethereum properly finalizes
|
|
|
|
|
const CONFIRMATIONS: u64 = 1;
|
|
|
|
|
// The window length should be roughly an hour
|
|
|
|
|
const WINDOW_LENGTH: u64 = 10;
|
|
|
|
|
|
|
|
|
|
const TEN_MINUTES: u64 = 2;
|
|
|
|
|
|
|
|
|
|
type Block = FullEpoch;
|
|
|
|
|
|
2024-09-18 18:35:31 -04:00
|
|
|
type EphemeralError = RpcError<TransportErrorKind>;
|
2024-09-14 07:54:18 -04:00
|
|
|
|
|
|
|
|
fn latest_finalized_block_number(
|
|
|
|
|
&self,
|
|
|
|
|
) -> impl Send + Future<Output = Result<u64, Self::EphemeralError>> {
|
|
|
|
|
async move {
|
|
|
|
|
let actual_number = self
|
|
|
|
|
.provider
|
2025-04-12 08:09:09 -04:00
|
|
|
.get_block(BlockNumberOrTag::Finalized.into())
|
2024-09-18 18:35:31 -04:00
|
|
|
.await?
|
|
|
|
|
.ok_or_else(|| {
|
|
|
|
|
TransportErrorKind::Custom("there was no finalized block".to_string().into())
|
|
|
|
|
})?
|
2024-09-14 07:54:18 -04:00
|
|
|
.header
|
|
|
|
|
.number;
|
|
|
|
|
// Error if there hasn't been a full epoch yet
|
|
|
|
|
if actual_number < 32 {
|
2024-09-18 18:35:31 -04:00
|
|
|
Err(TransportErrorKind::Custom(
|
|
|
|
|
"there has not been a completed epoch yet".to_string().into(),
|
|
|
|
|
))?
|
2024-09-14 07:54:18 -04:00
|
|
|
}
|
|
|
|
|
// The divison by 32 returns the amount of completed epochs
|
|
|
|
|
// Converting from amount of completed epochs to the latest completed epoch requires
|
|
|
|
|
// subtracting 1
|
|
|
|
|
let latest_full_epoch = (actual_number / 32) - 1;
|
|
|
|
|
Ok(latest_full_epoch)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn time_of_block(
|
|
|
|
|
&self,
|
|
|
|
|
number: u64,
|
|
|
|
|
) -> impl Send + Future<Output = Result<u64, Self::EphemeralError>> {
|
2024-09-19 02:41:07 -04:00
|
|
|
async move {
|
|
|
|
|
let header = self
|
|
|
|
|
.provider
|
2025-04-12 08:09:09 -04:00
|
|
|
.get_block(BlockNumberOrTag::Number(number).into())
|
2024-09-19 02:41:07 -04:00
|
|
|
.await?
|
|
|
|
|
.ok_or_else(|| {
|
|
|
|
|
TransportErrorKind::Custom(
|
|
|
|
|
"asked for time of a block our node doesn't have".to_string().into(),
|
|
|
|
|
)
|
|
|
|
|
})?
|
|
|
|
|
.header;
|
|
|
|
|
// This is monotonic ever since the merge
|
|
|
|
|
// https://github.com/ethereum/consensus-specs/blob/4afe39822c9ad9747e0f5635cca117c18441ec1b
|
|
|
|
|
// /specs/bellatrix/beacon-chain.md?plain=1#L393-L394
|
|
|
|
|
Ok(header.timestamp)
|
|
|
|
|
}
|
2024-09-14 07:54:18 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn unchecked_block_header_by_number(
|
|
|
|
|
&self,
|
|
|
|
|
number: u64,
|
|
|
|
|
) -> impl Send
|
|
|
|
|
+ Future<Output = Result<<Self::Block as primitives::Block>::Header, Self::EphemeralError>>
|
|
|
|
|
{
|
|
|
|
|
async move {
|
|
|
|
|
let start = number * 32;
|
|
|
|
|
let prior_end_hash = if start == 0 {
|
|
|
|
|
[0; 32]
|
|
|
|
|
} else {
|
|
|
|
|
self
|
|
|
|
|
.provider
|
2025-04-12 08:09:09 -04:00
|
|
|
.get_block((start - 1).into())
|
2024-09-18 18:35:31 -04:00
|
|
|
.await?
|
2024-09-14 07:54:18 -04:00
|
|
|
.ok_or_else(|| {
|
2024-09-18 18:35:31 -04:00
|
|
|
TransportErrorKind::Custom(
|
|
|
|
|
format!("ethereum node didn't have requested block: {number:?}. was the node reset?")
|
|
|
|
|
.into(),
|
|
|
|
|
)
|
2024-09-14 07:54:18 -04:00
|
|
|
})?
|
|
|
|
|
.header
|
|
|
|
|
.hash
|
|
|
|
|
.into()
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let end_header = self
|
|
|
|
|
.provider
|
2025-04-12 08:09:09 -04:00
|
|
|
.get_block((start + 31).into())
|
2024-09-18 18:35:31 -04:00
|
|
|
.await?
|
2024-09-14 07:54:18 -04:00
|
|
|
.ok_or_else(|| {
|
2024-09-18 18:35:31 -04:00
|
|
|
TransportErrorKind::Custom(
|
|
|
|
|
format!("ethereum node didn't have requested block: {number:?}. was the node reset?")
|
|
|
|
|
.into(),
|
|
|
|
|
)
|
2024-09-14 07:54:18 -04:00
|
|
|
})?
|
|
|
|
|
.header;
|
|
|
|
|
|
|
|
|
|
let end_hash = end_header.hash.into();
|
|
|
|
|
|
2024-09-19 02:46:32 -04:00
|
|
|
Ok(Epoch { prior_end_hash, end_hash })
|
2024-09-14 07:54:18 -04:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn unchecked_block_by_number(
|
|
|
|
|
&self,
|
|
|
|
|
number: u64,
|
|
|
|
|
) -> impl Send + Future<Output = Result<Self::Block, Self::EphemeralError>> {
|
|
|
|
|
async move {
|
2024-09-19 02:41:07 -04:00
|
|
|
let epoch = self.unchecked_block_header_by_number(number).await?;
|
|
|
|
|
|
|
|
|
|
let Some(router) = Router::new(
|
|
|
|
|
self.provider.clone(),
|
|
|
|
|
&PublicKey::new(
|
|
|
|
|
InitialSeraiKey::get(&self.db).expect("fetching a block yet never confirmed a key").0,
|
|
|
|
|
)
|
|
|
|
|
.expect("initial key used by Serai wasn't representable on Ethereum"),
|
|
|
|
|
)
|
|
|
|
|
.await?
|
|
|
|
|
else {
|
2024-09-20 02:06:35 -04:00
|
|
|
Err(TransportErrorKind::Custom("router wasn't deployed on-chain yet".to_string().into()))?
|
2024-09-19 02:41:07 -04:00
|
|
|
};
|
|
|
|
|
|
2024-09-20 02:06:35 -04:00
|
|
|
async fn sync_block(
|
|
|
|
|
router: Router,
|
|
|
|
|
block: Header,
|
|
|
|
|
) -> Result<(Vec<EthereumInInstruction>, Vec<Executed>), RpcError<TransportErrorKind>> {
|
2025-01-24 05:34:49 -05:00
|
|
|
let instructions = router
|
2025-01-24 07:44:47 -05:00
|
|
|
.in_instructions_unordered(block.number ..= block.number, &HashSet::from(TOKENS))
|
2025-01-23 06:10:18 -05:00
|
|
|
.await?;
|
2024-09-20 02:06:35 -04:00
|
|
|
|
2025-01-24 07:44:47 -05:00
|
|
|
let executed = router.executed(block.number ..= block.number).await?;
|
2024-09-20 02:06:35 -04:00
|
|
|
|
|
|
|
|
Ok((instructions, executed))
|
|
|
|
|
}
|
2024-09-20 01:24:28 -04:00
|
|
|
|
2024-09-20 02:06:35 -04:00
|
|
|
// We use JoinSet here to minimize the latency of the variety of requests we make. For each
|
|
|
|
|
// JoinError that may occur, we unwrap it as no underlying tasks should panic
|
|
|
|
|
let mut join_set = JoinSet::new();
|
2024-09-19 02:41:07 -04:00
|
|
|
let mut to_check = epoch.end_hash;
|
2024-09-20 02:06:35 -04:00
|
|
|
// TODO: This makes 32 sequential requests. We should run them in parallel using block
|
|
|
|
|
// nunbers
|
2024-09-19 02:41:07 -04:00
|
|
|
while to_check != epoch.prior_end_hash {
|
|
|
|
|
let to_check_block = self
|
|
|
|
|
.provider
|
2025-04-12 08:09:09 -04:00
|
|
|
.get_block(B256::from(to_check).into())
|
2024-09-19 02:41:07 -04:00
|
|
|
.await?
|
|
|
|
|
.ok_or_else(|| {
|
|
|
|
|
TransportErrorKind::Custom(
|
|
|
|
|
format!(
|
|
|
|
|
"ethereum node didn't have requested block: {}. was the node reset?",
|
|
|
|
|
hex::encode(to_check)
|
|
|
|
|
)
|
|
|
|
|
.into(),
|
|
|
|
|
)
|
|
|
|
|
})?
|
|
|
|
|
.header;
|
|
|
|
|
|
2024-09-20 02:06:35 -04:00
|
|
|
// Update the next block to check
|
|
|
|
|
to_check = *to_check_block.parent_hash;
|
2024-09-19 02:41:07 -04:00
|
|
|
|
2024-09-20 02:06:35 -04:00
|
|
|
// Spawn a task to sync this block
|
2025-01-24 05:34:49 -05:00
|
|
|
join_set.spawn(sync_block(router.clone(), to_check_block));
|
2024-09-20 02:06:35 -04:00
|
|
|
}
|
2024-09-19 02:41:07 -04:00
|
|
|
|
2024-09-20 02:06:35 -04:00
|
|
|
let mut instructions = vec![];
|
|
|
|
|
let mut executed = vec![];
|
|
|
|
|
while let Some(instructions_and_executed) = join_set.join_next().await {
|
|
|
|
|
let (mut these_instructions, mut these_executed) = instructions_and_executed.unwrap()?;
|
|
|
|
|
instructions.append(&mut these_instructions);
|
|
|
|
|
executed.append(&mut these_executed);
|
2024-09-19 02:41:07 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Ok(FullEpoch { epoch, instructions, executed })
|
2024-09-14 07:54:18 -04:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-01-30 03:14:24 -05:00
|
|
|
fn dust(coin: ExternalCoin) -> Amount {
|
|
|
|
|
assert_eq!(coin.network(), ExternalNetworkId::Ethereum);
|
2024-09-19 02:41:07 -04:00
|
|
|
match coin {
|
2025-01-30 03:14:24 -05:00
|
|
|
ExternalCoin::Ether => ETHER_DUST,
|
|
|
|
|
ExternalCoin::Dai => DAI_DUST,
|
2024-09-19 02:41:07 -04:00
|
|
|
_ => unreachable!(),
|
|
|
|
|
}
|
2024-09-14 07:54:18 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn cost_to_aggregate(
|
|
|
|
|
&self,
|
2025-01-30 03:14:24 -05:00
|
|
|
coin: ExternalCoin,
|
2024-09-14 07:54:18 -04:00
|
|
|
_reference_block: &Self::Block,
|
|
|
|
|
) -> impl Send + Future<Output = Result<Amount, Self::EphemeralError>> {
|
|
|
|
|
async move {
|
2025-01-30 03:14:24 -05:00
|
|
|
assert_eq!(coin.network(), ExternalNetworkId::Ethereum);
|
2024-09-19 02:41:07 -04:00
|
|
|
// There is no cost to aggregate as we receive to an account
|
2024-09-14 07:54:18 -04:00
|
|
|
Ok(Amount(0))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|