Don't track deployment block in the Router

This technically has a TOCTOU where we sync an Epoch's metadata (signifying we
did sync to that point), then check if the Router was deployed, yet at that
very moment the node resets to genesis. By ensuring the Router is deployed, we
avoid this (and don't need to track the deployment block in-contract).

Also uses a JoinSet to sync the 32 blocks in parallel.
This commit is contained in:
Luke Parker 2024-09-20 02:06:35 -04:00
parent 7e4c59a0a3
commit 554c5778e4
3 changed files with 50 additions and 64 deletions

View file

@ -7,9 +7,6 @@ import "Schnorr.sol";
// _ is used as a prefix for internal functions and smart-contract-scoped variables // _ is used as a prefix for internal functions and smart-contract-scoped variables
contract Router { contract Router {
// The block at which this contract was deployed.
uint256 private _deploymentBlock;
// Nonce is incremented for each command executed, preventing replays // Nonce is incremented for each command executed, preventing replays
uint256 private _nonce; uint256 private _nonce;
@ -66,8 +63,6 @@ contract Router {
} }
constructor(bytes32 initialSeraiKey) _updateSeraiKeyAtEndOfFn(0, initialSeraiKey) { constructor(bytes32 initialSeraiKey) _updateSeraiKeyAtEndOfFn(0, initialSeraiKey) {
_deploymentBlock = block.number;
// We consumed nonce 0 when setting the initial Serai key // We consumed nonce 0 when setting the initial Serai key
_nonce = 1; _nonce = 1;
// Nonces are incremented by 1 upon account creation, prior to any code execution, per EIP-161 // Nonces are incremented by 1 upon account creation, prior to any code execution, per EIP-161
@ -235,10 +230,6 @@ contract Router {
return _nonce; return _nonce;
} }
function deploymentBlock() external view returns (uint256) {
return _deploymentBlock;
}
function smartContractNonce() external view returns (uint256) { function smartContractNonce() external view returns (uint256) {
return _smartContractNonce; return _smartContractNonce;
} }

View file

@ -11,7 +11,7 @@ use alloy_consensus::TxLegacy;
use alloy_sol_types::{SolValue, SolConstructor, SolCall, SolEvent}; use alloy_sol_types::{SolValue, SolConstructor, SolCall, SolEvent};
use alloy_rpc_types_eth::{TransactionInput, TransactionRequest, Filter}; use alloy_rpc_types_eth::Filter;
use alloy_transport::{TransportErrorKind, RpcError}; use alloy_transport::{TransportErrorKind, RpcError};
use alloy_simple_request_transport::SimpleRequest; use alloy_simple_request_transport::SimpleRequest;
use alloy_provider::{Provider, RootProvider}; use alloy_provider::{Provider, RootProvider};
@ -296,23 +296,6 @@ impl Router {
self.1 self.1
} }
/// Fetch the block this contract was deployed at.
pub async fn deployment_block(&self) -> Result<u64, RpcError<TransportErrorKind>> {
let call = TransactionRequest::default()
.to(self.address())
.input(TransactionInput::new(abi::deploymentBlockCall::new(()).abi_encode().into()));
let bytes = self.0.call(&call).await?;
let deployment_block = abi::deploymentBlockCall::abi_decode_returns(&bytes, true)
.map_err(|e| {
TransportErrorKind::Custom(
format!("node returned a non-u256 for function returning u256: {e:?}").into(),
)
})?
._0;
Ok(deployment_block.try_into().unwrap())
}
/// Get the message to be signed in order to update the key for Serai. /// Get the message to be signed in order to update the key for Serai.
pub fn update_serai_key_message(chain_id: U256, nonce: u64, key: &PublicKey) -> Vec<u8> { pub fn update_serai_key_message(chain_id: U256, nonce: u64, key: &PublicKey) -> Vec<u8> {
( (

View file

@ -2,20 +2,23 @@ use core::future::Future;
use std::{sync::Arc, collections::HashSet}; use std::{sync::Arc, collections::HashSet};
use alloy_core::primitives::B256; use alloy_core::primitives::B256;
use alloy_rpc_types_eth::{BlockTransactionsKind, BlockNumberOrTag}; use alloy_rpc_types_eth::{Header, BlockTransactionsKind, BlockNumberOrTag};
use alloy_transport::{RpcError, TransportErrorKind}; use alloy_transport::{RpcError, TransportErrorKind};
use alloy_simple_request_transport::SimpleRequest; use alloy_simple_request_transport::SimpleRequest;
use alloy_provider::{Provider, RootProvider}; use alloy_provider::{Provider, RootProvider};
use serai_client::primitives::{NetworkId, Coin, Amount}; use serai_client::primitives::{NetworkId, Coin, Amount};
use tokio::task::JoinSet;
use serai_db::Db; use serai_db::Db;
use scanner::ScannerFeed; use scanner::ScannerFeed;
use ethereum_schnorr::PublicKey; use ethereum_schnorr::PublicKey;
use ethereum_erc20::{TopLevelTransfer, Erc20}; use ethereum_erc20::{TopLevelTransfer, Erc20};
use ethereum_router::{Coin as EthereumCoin, InInstruction as EthereumInInstruction, Router}; #[rustfmt::skip]
use ethereum_router::{Coin as EthereumCoin, InInstruction as EthereumInInstruction, Executed, Router};
use crate::{ use crate::{
TOKENS, ETHER_DUST, DAI_DUST, InitialSeraiKey, TOKENS, ETHER_DUST, DAI_DUST, InitialSeraiKey,
@ -141,8 +144,6 @@ impl<D: Db> ScannerFeed for Rpc<D> {
) -> impl Send + Future<Output = Result<Self::Block, Self::EphemeralError>> { ) -> impl Send + Future<Output = Result<Self::Block, Self::EphemeralError>> {
async move { async move {
let epoch = self.unchecked_block_header_by_number(number).await?; let epoch = self.unchecked_block_header_by_number(number).await?;
let mut instructions = vec![];
let mut executed = vec![];
let Some(router) = Router::new( let Some(router) = Router::new(
self.provider.clone(), self.provider.clone(),
@ -153,16 +154,42 @@ impl<D: Db> ScannerFeed for Rpc<D> {
) )
.await? .await?
else { else {
// The Router wasn't deployed yet so we cannot have any on-chain interactions Err(TransportErrorKind::Custom("router wasn't deployed on-chain yet".to_string().into()))?
// If the Router has been deployed by the block we've synced to, it won't have any events
// for these blocks anways, so this doesn't risk a consensus split
return Ok(FullEpoch { epoch, instructions, executed });
}; };
let router_deployment_block = router.deployment_block().await?; async fn sync_block(
provider: Arc<RootProvider<SimpleRequest>>,
router: Router,
block: Header,
) -> Result<(Vec<EthereumInInstruction>, Vec<Executed>), RpcError<TransportErrorKind>> {
let mut instructions = router.in_instructions(block.number, &HashSet::from(TOKENS)).await?;
// TODO: Use a LocalSet and handle all these in parallel for token in TOKENS {
for TopLevelTransfer { id, from, amount, data } in Erc20::new(provider.clone(), token)
.top_level_transfers(block.number, router.address())
.await?
{
instructions.push(EthereumInInstruction {
id,
from,
coin: EthereumCoin::Erc20(token),
amount,
data,
});
}
}
let executed = router.executed(block.number).await?;
Ok((instructions, executed))
}
// 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();
let mut to_check = epoch.end_hash; let mut to_check = epoch.end_hash;
// TODO: This makes 32 sequential requests. We should run them in parallel using block
// nunbers
while to_check != epoch.prior_end_hash { while to_check != epoch.prior_end_hash {
let to_check_block = self let to_check_block = self
.provider .provider
@ -179,34 +206,19 @@ impl<D: Db> ScannerFeed for Rpc<D> {
})? })?
.header; .header;
// If this is before the Router was deployed, move on // Update the next block to check
if to_check_block.number < router_deployment_block {
// This is sa
break;
}
instructions.append(
&mut router.in_instructions(to_check_block.number, &HashSet::from(TOKENS)).await?,
);
for token in TOKENS {
for TopLevelTransfer { id, from, amount, data } in
Erc20::new(self.provider.clone(), token)
.top_level_transfers(to_check_block.number, router.address())
.await?
{
instructions.push(EthereumInInstruction {
id,
from,
coin: EthereumCoin::Erc20(token),
amount,
data,
});
}
}
executed.append(&mut router.executed(to_check_block.number).await?);
to_check = *to_check_block.parent_hash; to_check = *to_check_block.parent_hash;
// Spawn a task to sync this block
join_set.spawn(sync_block(self.provider.clone(), router.clone(), to_check_block));
}
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);
} }
Ok(FullEpoch { epoch, instructions, executed }) Ok(FullEpoch { epoch, instructions, executed })