mirror of
https://github.com/serai-dex/serai.git
synced 2025-04-16 11:11:56 +00:00
Work on testing the Router
Completes the `Executed` enum in the router. Adds an `Escape` struct. Both are needed for testing purposes. Documents the gas constants in intent and reasoning. Adds modernized tests around key rotation and the escape hatch. Also updates the rest of the codebase which had accumulated errors.
This commit is contained in:
parent
6508957cbc
commit
669b8b776b
13 changed files with 765 additions and 355 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -9446,6 +9446,7 @@ dependencies = [
|
|||
"alloy-sol-macro",
|
||||
"alloy-sol-types",
|
||||
"alloy-transport",
|
||||
"serai-processor-ethereum-primitives",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
|
|
|
@ -27,4 +27,7 @@ alloy-transport = { version = "0.9", default-features = false }
|
|||
alloy-simple-request-transport = { path = "../../../networks/ethereum/alloy-simple-request-transport", default-features = false }
|
||||
alloy-provider = { version = "0.9", default-features = false }
|
||||
|
||||
ethereum-primitives = { package = "serai-processor-ethereum-primitives", path = "../primitives", default-features = false }
|
||||
|
||||
# TODO futures-util = { version = "0.3", default-features = false, features = ["std"] }
|
||||
tokio = { version = "1", default-features = false, features = ["rt"] }
|
||||
|
|
|
@ -13,6 +13,8 @@ use alloy_transport::{TransportErrorKind, RpcError};
|
|||
use alloy_simple_request_transport::SimpleRequest;
|
||||
use alloy_provider::{Provider, RootProvider};
|
||||
|
||||
use ethereum_primitives::LogIndex;
|
||||
|
||||
use tokio::task::JoinSet;
|
||||
|
||||
#[rustfmt::skip]
|
||||
|
@ -31,9 +33,11 @@ pub use abi::IERC20::Transfer;
|
|||
#[derive(Clone, Debug)]
|
||||
pub struct TopLevelTransfer {
|
||||
/// The ID of the event for this transfer.
|
||||
pub id: ([u8; 32], u64),
|
||||
pub id: LogIndex,
|
||||
/// The hash of the transaction which caused this transfer.
|
||||
pub transaction_hash: [u8; 32],
|
||||
/// The address which made the transfer.
|
||||
pub from: [u8; 20],
|
||||
pub from: Address,
|
||||
/// The amount transferred.
|
||||
pub amount: U256,
|
||||
/// The data appended after the call itself.
|
||||
|
@ -52,12 +56,12 @@ impl Erc20 {
|
|||
/// Match a transaction for its top-level transfer to the specified address (if one exists).
|
||||
pub async fn match_top_level_transfer(
|
||||
provider: impl AsRef<RootProvider<SimpleRequest>>,
|
||||
transaction_id: B256,
|
||||
transaction_hash: B256,
|
||||
to: Address,
|
||||
) -> Result<Option<TopLevelTransfer>, RpcError<TransportErrorKind>> {
|
||||
// Fetch the transaction
|
||||
let transaction =
|
||||
provider.as_ref().get_transaction_by_hash(transaction_id).await?.ok_or_else(|| {
|
||||
provider.as_ref().get_transaction_by_hash(transaction_hash).await?.ok_or_else(|| {
|
||||
TransportErrorKind::Custom(
|
||||
"node didn't have the transaction which emitted a log it had".to_string().into(),
|
||||
)
|
||||
|
@ -81,7 +85,7 @@ impl Erc20 {
|
|||
|
||||
// Fetch the transaction's logs
|
||||
let receipt =
|
||||
provider.as_ref().get_transaction_receipt(transaction_id).await?.ok_or_else(|| {
|
||||
provider.as_ref().get_transaction_receipt(transaction_hash).await?.ok_or_else(|| {
|
||||
TransportErrorKind::Custom(
|
||||
"node didn't have receipt for a transaction we were matching for a top-level transfer"
|
||||
.to_string()
|
||||
|
@ -102,6 +106,9 @@ impl Erc20 {
|
|||
continue;
|
||||
}
|
||||
|
||||
let block_hash = log.block_hash.ok_or_else(|| {
|
||||
TransportErrorKind::Custom("log didn't have its block hash set".to_string().into())
|
||||
})?;
|
||||
let log_index = log.log_index.ok_or_else(|| {
|
||||
TransportErrorKind::Custom("log didn't have its index set".to_string().into())
|
||||
})?;
|
||||
|
@ -125,8 +132,9 @@ impl Erc20 {
|
|||
let data = transaction.inner.input().as_ref()[encoded.len() ..].to_vec();
|
||||
|
||||
return Ok(Some(TopLevelTransfer {
|
||||
id: (*transaction_id, log_index),
|
||||
from: *log.from.0,
|
||||
id: LogIndex { block_hash: *block_hash, index_within_block: log_index },
|
||||
transaction_hash: *transaction_hash,
|
||||
from: log.from,
|
||||
amount: log.value,
|
||||
data,
|
||||
}));
|
||||
|
|
|
@ -8,12 +8,15 @@ use borsh::{BorshSerialize, BorshDeserialize};
|
|||
|
||||
use group::ff::PrimeField;
|
||||
|
||||
use alloy_core::primitives::{hex::FromHex, Address, U256, Bytes, TxKind};
|
||||
use alloy_core::primitives::{
|
||||
hex::{self, FromHex},
|
||||
Address, U256, Bytes, TxKind,
|
||||
};
|
||||
use alloy_sol_types::{SolValue, SolConstructor, SolCall, SolEvent};
|
||||
|
||||
use alloy_consensus::TxLegacy;
|
||||
|
||||
use alloy_rpc_types_eth::{TransactionRequest, TransactionInput, BlockId, Filter};
|
||||
use alloy_rpc_types_eth::{BlockId, Log, Filter, TransactionInput, TransactionRequest};
|
||||
use alloy_transport::{TransportErrorKind, RpcError};
|
||||
use alloy_simple_request_transport::SimpleRequest;
|
||||
use alloy_provider::{Provider, RootProvider};
|
||||
|
@ -51,8 +54,9 @@ mod abi {
|
|||
pub use super::_router_abi::Router::constructorCall;
|
||||
}
|
||||
use abi::{
|
||||
SeraiKeyUpdated as SeraiKeyUpdatedEvent, InInstruction as InInstructionEvent,
|
||||
Executed as ExecutedEvent,
|
||||
NextSeraiKeySet as NextSeraiKeySetEvent, SeraiKeyUpdated as SeraiKeyUpdatedEvent,
|
||||
InInstruction as InInstructionEvent, Batch as BatchEvent, EscapeHatch as EscapeHatchEvent,
|
||||
Escaped as EscapedEvent,
|
||||
};
|
||||
|
||||
#[cfg(test)]
|
||||
|
@ -81,12 +85,20 @@ pub enum Coin {
|
|||
Address,
|
||||
),
|
||||
}
|
||||
|
||||
impl Coin {
|
||||
fn address(&self) -> Address {
|
||||
match self {
|
||||
Coin::Ether => [0; 20].into(),
|
||||
Coin::Erc20(address) => *address,
|
||||
impl From<Coin> for Address {
|
||||
fn from(coin: Coin) -> Address {
|
||||
match coin {
|
||||
Coin::Ether => Address::ZERO,
|
||||
Coin::Erc20(address) => address,
|
||||
}
|
||||
}
|
||||
}
|
||||
impl From<Address> for Coin {
|
||||
fn from(address: Address) -> Coin {
|
||||
if address == Address::ZERO {
|
||||
Coin::Ether
|
||||
} else {
|
||||
Coin::Erc20(address)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -96,6 +108,8 @@ impl Coin {
|
|||
pub struct InInstruction {
|
||||
/// The ID for this `InInstruction`.
|
||||
pub id: LogIndex,
|
||||
/// The hash of the transaction which caused this.
|
||||
pub transaction_hash: [u8; 32],
|
||||
/// The address which transferred these coins to Serai.
|
||||
#[borsh(
|
||||
serialize_with = "ethereum_primitives::serialize_address",
|
||||
|
@ -126,6 +140,8 @@ impl From<&[(SeraiAddress, U256)]> for OutInstructions {
|
|||
#[allow(non_snake_case)]
|
||||
let (destinationType, destination) = match address {
|
||||
SeraiAddress::Address(address) => {
|
||||
// Per the documentation, `DestinationType::Address`'s value is an ABI-encoded
|
||||
// address
|
||||
(abi::DestinationType::Address, (Address::from(address)).abi_encode())
|
||||
}
|
||||
SeraiAddress::Contract(contract) => (
|
||||
|
@ -147,41 +163,90 @@ impl From<&[(SeraiAddress, U256)]> for OutInstructions {
|
|||
/// An action which was executed by the Router.
|
||||
#[derive(Clone, PartialEq, Eq, Debug, BorshSerialize, BorshDeserialize)]
|
||||
pub enum Executed {
|
||||
/// New key was set.
|
||||
SetKey {
|
||||
/// Next key was set.
|
||||
NextSeraiKeySet {
|
||||
/// The nonce this was done with.
|
||||
nonce: u64,
|
||||
/// The key set.
|
||||
key: [u8; 32],
|
||||
},
|
||||
/// Executed Batch.
|
||||
/// The next key was updated to.
|
||||
SeraiKeyUpdated {
|
||||
/// The nonce this was done with.
|
||||
nonce: u64,
|
||||
/// The key set.
|
||||
key: [u8; 32],
|
||||
},
|
||||
/// Executed batch of `OutInstruction`s.
|
||||
Batch {
|
||||
/// The nonce this was done with.
|
||||
nonce: u64,
|
||||
/// The hash of the signed message for the Batch executed.
|
||||
message_hash: [u8; 32],
|
||||
},
|
||||
/// The escape hatch was set.
|
||||
EscapeHatch {
|
||||
/// The nonce this was done with.
|
||||
nonce: u64,
|
||||
/// The address set to escape to.
|
||||
#[borsh(
|
||||
serialize_with = "ethereum_primitives::serialize_address",
|
||||
deserialize_with = "ethereum_primitives::deserialize_address"
|
||||
)]
|
||||
escape_to: Address,
|
||||
},
|
||||
}
|
||||
|
||||
impl Executed {
|
||||
/// The nonce consumed by this executed event.
|
||||
///
|
||||
/// This is a `u64` despite the contract defining the nonce as a `u256`. Since the nonce is
|
||||
/// incremental, the u64 will never be exhausted.
|
||||
pub fn nonce(&self) -> u64 {
|
||||
match self {
|
||||
Executed::SetKey { nonce, .. } | Executed::Batch { nonce, .. } => *nonce,
|
||||
Executed::NextSeraiKeySet { nonce, .. } |
|
||||
Executed::SeraiKeyUpdated { nonce, .. } |
|
||||
Executed::Batch { nonce, .. } |
|
||||
Executed::EscapeHatch { nonce, .. } => *nonce,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// An Escape from the Router.
|
||||
#[derive(Clone, PartialEq, Eq, Debug, BorshSerialize, BorshDeserialize)]
|
||||
pub struct Escape {
|
||||
/// The coin escaped.
|
||||
pub coin: Coin,
|
||||
/// The amount escaped.
|
||||
#[borsh(
|
||||
serialize_with = "ethereum_primitives::serialize_u256",
|
||||
deserialize_with = "ethereum_primitives::deserialize_u256"
|
||||
)]
|
||||
pub amount: U256,
|
||||
}
|
||||
|
||||
/// A view of the Router for Serai.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Router(Arc<RootProvider<SimpleRequest>>, Address);
|
||||
pub struct Router {
|
||||
provider: Arc<RootProvider<SimpleRequest>>,
|
||||
address: Address,
|
||||
}
|
||||
impl Router {
|
||||
const DEPLOYMENT_GAS: u64 = 1_000_000;
|
||||
const CONFIRM_NEXT_SERAI_KEY_GAS: u64 = 58_000;
|
||||
const UPDATE_SERAI_KEY_GAS: u64 = 61_000;
|
||||
/*
|
||||
The gas limits to use for transactions.
|
||||
|
||||
These are expected to be constant as a distributed group signs the transactions invoking these
|
||||
calls. Having the gas be constant prevents needing to run a protocol to determine what gas to
|
||||
use.
|
||||
|
||||
These gas limits may break if/when gas opcodes undergo repricing. In that case, this library is
|
||||
expected to be modified with these made parameters. The caller would then be expected to pass
|
||||
the correct set of prices for the network they're operating on.
|
||||
*/
|
||||
const CONFIRM_NEXT_SERAI_KEY_GAS: u64 = 57_736;
|
||||
const UPDATE_SERAI_KEY_GAS: u64 = 60_045;
|
||||
const EXECUTE_BASE_GAS: u64 = 48_000;
|
||||
const ESCAPE_HATCH_GAS: u64 = 58_000;
|
||||
const ESCAPE_GAS: u64 = 200_000;
|
||||
const ESCAPE_HATCH_GAS: u64 = 61_238;
|
||||
|
||||
fn code() -> Vec<u8> {
|
||||
const BYTECODE: &[u8] =
|
||||
|
@ -198,11 +263,10 @@ impl Router {
|
|||
|
||||
/// Obtain the transaction to deploy this contract.
|
||||
///
|
||||
/// This transaction assumes the `Deployer` has already been deployed.
|
||||
/// This transaction assumes the `Deployer` has already been deployed. The gas limit and gas
|
||||
/// price are not set and are left to the caller.
|
||||
pub fn deployment_tx(initial_serai_key: &PublicKey) -> TxLegacy {
|
||||
let mut tx = Deployer::deploy_tx(Self::init_code(initial_serai_key));
|
||||
tx.gas_limit = Self::DEPLOYMENT_GAS * 120 / 100;
|
||||
tx
|
||||
Deployer::deploy_tx(Self::init_code(initial_serai_key))
|
||||
}
|
||||
|
||||
/// Create a new view of the Router.
|
||||
|
@ -216,25 +280,25 @@ impl Router {
|
|||
let Some(deployer) = Deployer::new(provider.clone()).await? else {
|
||||
return Ok(None);
|
||||
};
|
||||
let Some(deployment) = deployer
|
||||
let Some(address) = deployer
|
||||
.find_deployment(ethereum_primitives::keccak256(Self::init_code(initial_serai_key)))
|
||||
.await?
|
||||
else {
|
||||
return Ok(None);
|
||||
};
|
||||
Ok(Some(Self(provider, deployment)))
|
||||
Ok(Some(Self { provider, address }))
|
||||
}
|
||||
|
||||
/// The address of the router.
|
||||
pub fn address(&self) -> Address {
|
||||
self.1
|
||||
self.address
|
||||
}
|
||||
|
||||
/// Get the message to be signed in order to confirm the next key for Serai.
|
||||
pub fn confirm_next_serai_key_message(nonce: u64) -> Vec<u8> {
|
||||
pub fn confirm_next_serai_key_message(chain_id: U256, nonce: u64) -> Vec<u8> {
|
||||
abi::confirmNextSeraiKeyCall::new((abi::Signature {
|
||||
c: U256::try_from(nonce).unwrap().into(),
|
||||
s: U256::ZERO.into(),
|
||||
c: chain_id.into(),
|
||||
s: U256::try_from(nonce).unwrap().into(),
|
||||
},))
|
||||
.abi_encode()
|
||||
}
|
||||
|
@ -242,7 +306,7 @@ impl Router {
|
|||
/// Construct a transaction to confirm the next key representing Serai.
|
||||
pub fn confirm_next_serai_key(&self, sig: &Signature) -> TxLegacy {
|
||||
TxLegacy {
|
||||
to: TxKind::Call(self.1),
|
||||
to: TxKind::Call(self.address),
|
||||
input: abi::confirmNextSeraiKeyCall::new((abi::Signature::from(sig),)).abi_encode().into(),
|
||||
gas_limit: Self::CONFIRM_NEXT_SERAI_KEY_GAS * 120 / 100,
|
||||
..Default::default()
|
||||
|
@ -250,9 +314,9 @@ impl Router {
|
|||
}
|
||||
|
||||
/// Get the message to be signed in order to update the key for Serai.
|
||||
pub fn update_serai_key_message(nonce: u64, key: &PublicKey) -> Vec<u8> {
|
||||
pub fn update_serai_key_message(chain_id: U256, nonce: u64, key: &PublicKey) -> Vec<u8> {
|
||||
abi::updateSeraiKeyCall::new((
|
||||
abi::Signature { c: U256::try_from(nonce).unwrap().into(), s: U256::ZERO.into() },
|
||||
abi::Signature { c: chain_id.into(), s: U256::try_from(nonce).unwrap().into() },
|
||||
key.eth_repr().into(),
|
||||
))
|
||||
.abi_encode()
|
||||
|
@ -261,7 +325,7 @@ impl Router {
|
|||
/// Construct a transaction to update the key representing Serai.
|
||||
pub fn update_serai_key(&self, public_key: &PublicKey, sig: &Signature) -> TxLegacy {
|
||||
TxLegacy {
|
||||
to: TxKind::Call(self.1),
|
||||
to: TxKind::Call(self.address),
|
||||
input: abi::updateSeraiKeyCall::new((
|
||||
abi::Signature::from(sig),
|
||||
public_key.eth_repr().into(),
|
||||
|
@ -274,10 +338,16 @@ impl Router {
|
|||
}
|
||||
|
||||
/// Get the message to be signed in order to execute a series of `OutInstruction`s.
|
||||
pub fn execute_message(nonce: u64, coin: Coin, fee: U256, outs: OutInstructions) -> Vec<u8> {
|
||||
pub fn execute_message(
|
||||
chain_id: U256,
|
||||
nonce: u64,
|
||||
coin: Coin,
|
||||
fee: U256,
|
||||
outs: OutInstructions,
|
||||
) -> Vec<u8> {
|
||||
abi::executeCall::new((
|
||||
abi::Signature { c: U256::try_from(nonce).unwrap().into(), s: U256::ZERO.into() },
|
||||
coin.address(),
|
||||
abi::Signature { c: chain_id.into(), s: U256::try_from(nonce).unwrap().into() },
|
||||
Address::from(coin),
|
||||
fee,
|
||||
outs.0,
|
||||
))
|
||||
|
@ -289,8 +359,8 @@ impl Router {
|
|||
// TODO
|
||||
let gas_limit = Self::EXECUTE_BASE_GAS + outs.0.iter().map(|_| 200_000 + 10_000).sum::<u64>();
|
||||
TxLegacy {
|
||||
to: TxKind::Call(self.1),
|
||||
input: abi::executeCall::new((abi::Signature::from(sig), coin.address(), fee, outs.0))
|
||||
to: TxKind::Call(self.address),
|
||||
input: abi::executeCall::new((abi::Signature::from(sig), Address::from(coin), fee, outs.0))
|
||||
.abi_encode()
|
||||
.into(),
|
||||
gas_limit: gas_limit * 120 / 100,
|
||||
|
@ -299,9 +369,9 @@ impl Router {
|
|||
}
|
||||
|
||||
/// Get the message to be signed in order to trigger the escape hatch.
|
||||
pub fn escape_hatch_message(nonce: u64, escape_to: Address) -> Vec<u8> {
|
||||
pub fn escape_hatch_message(chain_id: U256, nonce: u64, escape_to: Address) -> Vec<u8> {
|
||||
abi::escapeHatchCall::new((
|
||||
abi::Signature { c: U256::try_from(nonce).unwrap().into(), s: U256::ZERO.into() },
|
||||
abi::Signature { c: chain_id.into(), s: U256::try_from(nonce).unwrap().into() },
|
||||
escape_to,
|
||||
))
|
||||
.abi_encode()
|
||||
|
@ -310,7 +380,7 @@ impl Router {
|
|||
/// Construct a transaction to trigger the escape hatch.
|
||||
pub fn escape_hatch(&self, escape_to: Address, sig: &Signature) -> TxLegacy {
|
||||
TxLegacy {
|
||||
to: TxKind::Call(self.1),
|
||||
to: TxKind::Call(self.address),
|
||||
input: abi::escapeHatchCall::new((abi::Signature::from(sig), escape_to)).abi_encode().into(),
|
||||
gas_limit: Self::ESCAPE_HATCH_GAS * 120 / 100,
|
||||
..Default::default()
|
||||
|
@ -318,11 +388,10 @@ impl Router {
|
|||
}
|
||||
|
||||
/// Construct a transaction to escape coins via the escape hatch.
|
||||
pub fn escape(&self, coin: Address) -> TxLegacy {
|
||||
pub fn escape(&self, coin: Coin) -> TxLegacy {
|
||||
TxLegacy {
|
||||
to: TxKind::Call(self.1),
|
||||
input: abi::escapeCall::new((coin,)).abi_encode().into(),
|
||||
gas_limit: Self::ESCAPE_GAS,
|
||||
to: TxKind::Call(self.address),
|
||||
input: abi::escapeCall::new((Address::from(coin),)).abi_encode().into(),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
@ -334,9 +403,10 @@ impl Router {
|
|||
allowed_tokens: &HashSet<Address>,
|
||||
) -> Result<Vec<InInstruction>, RpcError<TransportErrorKind>> {
|
||||
// The InInstruction events for this block
|
||||
let filter = Filter::new().from_block(block).to_block(block).address(self.1);
|
||||
let filter = Filter::new().from_block(block).to_block(block).address(self.address);
|
||||
let filter = filter.event_signature(InInstructionEvent::SIGNATURE_HASH);
|
||||
let logs = self.0.get_logs(&filter).await?;
|
||||
let mut logs = self.provider.get_logs(&filter).await?;
|
||||
logs.sort_by_key(|log| (log.block_number, log.log_index));
|
||||
|
||||
/*
|
||||
We check that for all InInstructions for ERC20s emitted, a corresponding transfer occurred.
|
||||
|
@ -348,7 +418,7 @@ impl Router {
|
|||
let mut in_instructions = vec![];
|
||||
for log in logs {
|
||||
// Double check the address which emitted this log
|
||||
if log.address() != self.1 {
|
||||
if log.address() != self.address {
|
||||
Err(TransportErrorKind::Custom(
|
||||
"node returned a log from a different address than requested".to_string().into(),
|
||||
))?;
|
||||
|
@ -366,7 +436,7 @@ impl Router {
|
|||
})?,
|
||||
};
|
||||
|
||||
let tx_hash = log.transaction_hash.ok_or_else(|| {
|
||||
let transaction_hash = log.transaction_hash.ok_or_else(|| {
|
||||
TransportErrorKind::Custom("log didn't have its transaction hash set".to_string().into())
|
||||
})?;
|
||||
|
||||
|
@ -380,21 +450,19 @@ impl Router {
|
|||
.inner
|
||||
.data;
|
||||
|
||||
let coin = if log.coin.0 == [0; 20] {
|
||||
Coin::Ether
|
||||
} else {
|
||||
let token = log.coin;
|
||||
|
||||
let coin = Coin::from(log.coin);
|
||||
if let Coin::Erc20(token) = coin {
|
||||
if !allowed_tokens.contains(&token) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Get all logs for this TX
|
||||
let receipt = self.0.get_transaction_receipt(tx_hash).await?.ok_or_else(|| {
|
||||
TransportErrorKind::Custom(
|
||||
"node didn't have the receipt for a transaction it had".to_string().into(),
|
||||
)
|
||||
})?;
|
||||
let receipt =
|
||||
self.provider.get_transaction_receipt(transaction_hash).await?.ok_or_else(|| {
|
||||
TransportErrorKind::Custom(
|
||||
"node didn't have the receipt for a transaction it had".to_string().into(),
|
||||
)
|
||||
})?;
|
||||
let tx_logs = receipt.inner.logs();
|
||||
|
||||
/*
|
||||
|
@ -402,9 +470,11 @@ impl Router {
|
|||
Accordingly, when looking for the matching transfer, disregard the top-level transfer (if
|
||||
one exists).
|
||||
*/
|
||||
if let Some(matched) = Erc20::match_top_level_transfer(&self.0, tx_hash, self.1).await? {
|
||||
if let Some(matched) =
|
||||
Erc20::match_top_level_transfer(&self.provider, transaction_hash, self.address).await?
|
||||
{
|
||||
// Mark this log index as used so it isn't used again
|
||||
transfer_check.insert(matched.id.1);
|
||||
transfer_check.insert(matched.id.index_within_block);
|
||||
}
|
||||
|
||||
// Find a matching transfer log
|
||||
|
@ -432,7 +502,7 @@ impl Router {
|
|||
}
|
||||
let Ok(transfer) = Transfer::decode_log(&tx_log.inner.clone(), true) else { continue };
|
||||
// Check if this is a transfer to us for the expected amount
|
||||
if (transfer.to == self.1) && (transfer.value == log.amount) {
|
||||
if (transfer.to == self.address) && (transfer.value == log.amount) {
|
||||
transfer_check.insert(log_index);
|
||||
found_transfer = true;
|
||||
break;
|
||||
|
@ -447,12 +517,11 @@ impl Router {
|
|||
"ERC20 InInstruction with no matching transfer log".to_string().into(),
|
||||
))?;
|
||||
}
|
||||
|
||||
Coin::Erc20(token)
|
||||
};
|
||||
|
||||
in_instructions.push(InInstruction {
|
||||
id,
|
||||
transaction_hash: *transaction_hash,
|
||||
from: log.from,
|
||||
coin,
|
||||
amount: log.amount,
|
||||
|
@ -464,74 +533,123 @@ impl Router {
|
|||
}
|
||||
|
||||
/// Fetch the executed actions from this block.
|
||||
pub async fn executed(&self, block: u64) -> Result<Vec<Executed>, RpcError<TransportErrorKind>> {
|
||||
pub async fn executed(
|
||||
&self,
|
||||
from_block: u64,
|
||||
to_block: u64,
|
||||
) -> Result<Vec<Executed>, RpcError<TransportErrorKind>> {
|
||||
fn decode<E: SolEvent>(log: &Log) -> Result<E, RpcError<TransportErrorKind>> {
|
||||
Ok(
|
||||
log
|
||||
.log_decode::<E>()
|
||||
.map_err(|e| {
|
||||
TransportErrorKind::Custom(
|
||||
format!("filtered to event yet couldn't decode log: {e:?}").into(),
|
||||
)
|
||||
})?
|
||||
.inner
|
||||
.data,
|
||||
)
|
||||
}
|
||||
|
||||
let filter = Filter::new().from_block(from_block).to_block(to_block).address(self.address);
|
||||
let mut logs = self.provider.get_logs(&filter).await?;
|
||||
logs.sort_by_key(|log| (log.block_number, log.log_index));
|
||||
|
||||
let mut res = vec![];
|
||||
for log in logs {
|
||||
// Double check the address which emitted this log
|
||||
if log.address() != self.address {
|
||||
Err(TransportErrorKind::Custom(
|
||||
"node returned a log from a different address than requested".to_string().into(),
|
||||
))?;
|
||||
}
|
||||
|
||||
{
|
||||
let filter = Filter::new().from_block(block).to_block(block).address(self.1);
|
||||
let filter = filter.event_signature(SeraiKeyUpdatedEvent::SIGNATURE_HASH);
|
||||
let logs = self.0.get_logs(&filter).await?;
|
||||
|
||||
for log in logs {
|
||||
// Double check the address which emitted this log
|
||||
if log.address() != self.1 {
|
||||
Err(TransportErrorKind::Custom(
|
||||
"node returned a log from a different address than requested".to_string().into(),
|
||||
))?;
|
||||
match log.topics().first() {
|
||||
Some(&NextSeraiKeySetEvent::SIGNATURE_HASH) => {
|
||||
let event = decode::<NextSeraiKeySetEvent>(&log)?;
|
||||
res.push(Executed::NextSeraiKeySet {
|
||||
nonce: event.nonce.try_into().map_err(|e| {
|
||||
TransportErrorKind::Custom(format!("failed to convert nonce to u64: {e:?}").into())
|
||||
})?,
|
||||
key: event.key.into(),
|
||||
});
|
||||
}
|
||||
|
||||
let log = log
|
||||
.log_decode::<SeraiKeyUpdatedEvent>()
|
||||
.map_err(|e| {
|
||||
TransportErrorKind::Custom(
|
||||
format!("filtered to SeraiKeyUpdatedEvent yet couldn't decode log: {e:?}").into(),
|
||||
)
|
||||
})?
|
||||
.inner
|
||||
.data;
|
||||
|
||||
res.push(Executed::SetKey {
|
||||
nonce: log.nonce.try_into().map_err(|e| {
|
||||
TransportErrorKind::Custom(format!("failed to convert nonce to u64: {e:?}").into())
|
||||
})?,
|
||||
key: log.key.into(),
|
||||
});
|
||||
Some(&SeraiKeyUpdatedEvent::SIGNATURE_HASH) => {
|
||||
let event = decode::<SeraiKeyUpdatedEvent>(&log)?;
|
||||
res.push(Executed::SeraiKeyUpdated {
|
||||
nonce: event.nonce.try_into().map_err(|e| {
|
||||
TransportErrorKind::Custom(format!("failed to convert nonce to u64: {e:?}").into())
|
||||
})?,
|
||||
key: event.key.into(),
|
||||
});
|
||||
}
|
||||
Some(&BatchEvent::SIGNATURE_HASH) => {
|
||||
let event = decode::<BatchEvent>(&log)?;
|
||||
res.push(Executed::Batch {
|
||||
nonce: event.nonce.try_into().map_err(|e| {
|
||||
TransportErrorKind::Custom(format!("failed to convert nonce to u64: {e:?}").into())
|
||||
})?,
|
||||
message_hash: event.messageHash.into(),
|
||||
});
|
||||
}
|
||||
Some(&EscapeHatchEvent::SIGNATURE_HASH) => {
|
||||
let event = decode::<EscapeHatchEvent>(&log)?;
|
||||
res.push(Executed::EscapeHatch {
|
||||
nonce: event.nonce.try_into().map_err(|e| {
|
||||
TransportErrorKind::Custom(format!("failed to convert nonce to u64: {e:?}").into())
|
||||
})?,
|
||||
escape_to: event.escapeTo,
|
||||
});
|
||||
}
|
||||
Some(&InInstructionEvent::SIGNATURE_HASH | &EscapedEvent::SIGNATURE_HASH) => {}
|
||||
unrecognized => Err(TransportErrorKind::Custom(
|
||||
format!("unrecognized event yielded by the Router: {:?}", unrecognized.map(hex::encode))
|
||||
.into(),
|
||||
))?,
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
let filter = Filter::new().from_block(block).to_block(block).address(self.1);
|
||||
let filter = filter.event_signature(ExecutedEvent::SIGNATURE_HASH);
|
||||
let logs = self.0.get_logs(&filter).await?;
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
for log in logs {
|
||||
// Double check the address which emitted this log
|
||||
if log.address() != self.1 {
|
||||
Err(TransportErrorKind::Custom(
|
||||
"node returned a log from a different address than requested".to_string().into(),
|
||||
))?;
|
||||
}
|
||||
/// Fetch the `Escape`s from the smart contract through the escape hatch.
|
||||
pub async fn escapes(
|
||||
&self,
|
||||
from_block: u64,
|
||||
to_block: u64,
|
||||
) -> Result<Vec<Escape>, RpcError<TransportErrorKind>> {
|
||||
let filter = Filter::new().from_block(from_block).to_block(to_block).address(self.address);
|
||||
let mut logs =
|
||||
self.provider.get_logs(&filter.event_signature(EscapedEvent::SIGNATURE_HASH)).await?;
|
||||
logs.sort_by_key(|log| (log.block_number, log.log_index));
|
||||
|
||||
let log = log
|
||||
.log_decode::<ExecutedEvent>()
|
||||
.map_err(|e| {
|
||||
TransportErrorKind::Custom(
|
||||
format!("filtered to ExecutedEvent yet couldn't decode log: {e:?}").into(),
|
||||
)
|
||||
})?
|
||||
.inner
|
||||
.data;
|
||||
|
||||
res.push(Executed::Batch {
|
||||
nonce: log.nonce.try_into().map_err(|e| {
|
||||
TransportErrorKind::Custom(format!("failed to convert nonce to u64: {e:?}").into())
|
||||
})?,
|
||||
message_hash: log.messageHash.into(),
|
||||
});
|
||||
let mut res = vec![];
|
||||
for log in logs {
|
||||
// Double check the address which emitted this log
|
||||
if log.address() != self.address {
|
||||
Err(TransportErrorKind::Custom(
|
||||
"node returned a log from a different address than requested".to_string().into(),
|
||||
))?;
|
||||
}
|
||||
// Double check the topic
|
||||
if log.topics().first() != Some(&EscapedEvent::SIGNATURE_HASH) {
|
||||
Err(TransportErrorKind::Custom(
|
||||
"node returned a log for a different topic than filtered to".to_string().into(),
|
||||
))?;
|
||||
}
|
||||
}
|
||||
|
||||
res.sort_by_key(Executed::nonce);
|
||||
let log = log
|
||||
.log_decode::<EscapedEvent>()
|
||||
.map_err(|e| {
|
||||
TransportErrorKind::Custom(
|
||||
format!("filtered to event yet couldn't decode log: {e:?}").into(),
|
||||
)
|
||||
})?
|
||||
.inner
|
||||
.data;
|
||||
res.push(Escape { coin: Coin::from(log.coin), amount: log.amount });
|
||||
}
|
||||
|
||||
Ok(res)
|
||||
}
|
||||
|
@ -541,8 +659,9 @@ impl Router {
|
|||
block: BlockId,
|
||||
call: Vec<u8>,
|
||||
) -> Result<Option<PublicKey>, RpcError<TransportErrorKind>> {
|
||||
let call = TransactionRequest::default().to(self.1).input(TransactionInput::new(call.into()));
|
||||
let bytes = self.0.call(&call).block(block).await?;
|
||||
let call =
|
||||
TransactionRequest::default().to(self.address).input(TransactionInput::new(call.into()));
|
||||
let bytes = self.provider.call(&call).block(block).await?;
|
||||
// This is fine as both key calls share a return type
|
||||
let res = abi::nextSeraiKeyCall::abi_decode_returns(&bytes, true)
|
||||
.map_err(|e| TransportErrorKind::Custom(format!("failed to decode key: {e:?}").into()))?;
|
||||
|
@ -575,9 +694,9 @@ impl Router {
|
|||
/// Fetch the nonce of the next action to execute
|
||||
pub async fn next_nonce(&self, block: BlockId) -> Result<u64, RpcError<TransportErrorKind>> {
|
||||
let call = TransactionRequest::default()
|
||||
.to(self.1)
|
||||
.to(self.address)
|
||||
.input(TransactionInput::new(abi::nextNonceCall::new(()).abi_encode().into()));
|
||||
let bytes = self.0.call(&call).block(block).await?;
|
||||
let bytes = self.provider.call(&call).block(block).await?;
|
||||
let res = abi::nextNonceCall::abi_decode_returns(&bytes, true)
|
||||
.map_err(|e| TransportErrorKind::Custom(format!("failed to decode nonce: {e:?}").into()))?;
|
||||
Ok(u64::try_from(res._0).map_err(|_| {
|
||||
|
@ -586,14 +705,17 @@ impl Router {
|
|||
}
|
||||
|
||||
/// Fetch the address the escape hatch was set to
|
||||
pub async fn escaped_to(&self, block: BlockId) -> Result<Address, RpcError<TransportErrorKind>> {
|
||||
pub async fn escaped_to(
|
||||
&self,
|
||||
block: BlockId,
|
||||
) -> Result<Option<Address>, RpcError<TransportErrorKind>> {
|
||||
let call = TransactionRequest::default()
|
||||
.to(self.1)
|
||||
.to(self.address)
|
||||
.input(TransactionInput::new(abi::escapedToCall::new(()).abi_encode().into()));
|
||||
let bytes = self.0.call(&call).block(block).await?;
|
||||
let bytes = self.provider.call(&call).block(block).await?;
|
||||
let res = abi::escapedToCall::abi_decode_returns(&bytes, true).map_err(|e| {
|
||||
TransportErrorKind::Custom(format!("failed to decode the address escaped to: {e:?}").into())
|
||||
})?;
|
||||
Ok(res._0)
|
||||
Ok(if res._0 == Address([0; 20].into()) { None } else { Some(res._0) })
|
||||
}
|
||||
}
|
||||
|
|
21
processor/ethereum/router/src/tests/constants.rs
Normal file
21
processor/ethereum/router/src/tests/constants.rs
Normal file
|
@ -0,0 +1,21 @@
|
|||
use alloy_sol_types::SolCall;
|
||||
|
||||
#[test]
|
||||
fn selector_collisions() {
|
||||
assert_eq!(
|
||||
crate::_irouter_abi::IRouter::confirmNextSeraiKeyCall::SELECTOR,
|
||||
crate::_router_abi::Router::confirmNextSeraiKey34AC53ACCall::SELECTOR
|
||||
);
|
||||
assert_eq!(
|
||||
crate::_irouter_abi::IRouter::updateSeraiKeyCall::SELECTOR,
|
||||
crate::_router_abi::Router::updateSeraiKey5A8542A2Call::SELECTOR
|
||||
);
|
||||
assert_eq!(
|
||||
crate::_irouter_abi::IRouter::executeCall::SELECTOR,
|
||||
crate::_router_abi::Router::execute4DE42904Call::SELECTOR
|
||||
);
|
||||
assert_eq!(
|
||||
crate::_irouter_abi::IRouter::escapeHatchCall::SELECTOR,
|
||||
crate::_router_abi::Router::escapeHatchDCDD91CCCall::SELECTOR
|
||||
);
|
||||
}
|
|
@ -10,10 +10,11 @@ use alloy_sol_types::SolCall;
|
|||
|
||||
use alloy_consensus::TxLegacy;
|
||||
|
||||
use alloy_rpc_types_eth::{BlockNumberOrTag, TransactionReceipt};
|
||||
#[rustfmt::skip]
|
||||
use alloy_rpc_types_eth::{BlockNumberOrTag, TransactionInput, TransactionRequest, TransactionReceipt};
|
||||
use alloy_simple_request_transport::SimpleRequest;
|
||||
use alloy_rpc_client::ClientBuilder;
|
||||
use alloy_provider::RootProvider;
|
||||
use alloy_provider::{Provider, RootProvider};
|
||||
|
||||
use alloy_node_bindings::{Anvil, AnvilInstance};
|
||||
|
||||
|
@ -21,39 +22,14 @@ use ethereum_primitives::LogIndex;
|
|||
use ethereum_schnorr::{PublicKey, Signature};
|
||||
use ethereum_deployer::Deployer;
|
||||
|
||||
use crate::{Coin, OutInstructions, Router};
|
||||
use crate::{
|
||||
_irouter_abi::IRouterWithoutCollisions::{
|
||||
self as IRouter, IRouterWithoutCollisionsErrors as IRouterErrors,
|
||||
},
|
||||
Coin, OutInstructions, Router, Executed, Escape,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn execute_reentrancy_guard() {
|
||||
let hash = alloy_core::primitives::keccak256(b"ReentrancyGuard Router.execute");
|
||||
assert_eq!(
|
||||
alloy_core::primitives::hex::encode(
|
||||
(U256::from_be_slice(hash.as_ref()) - U256::from(1u8)).to_be_bytes::<32>()
|
||||
),
|
||||
// Constant from the Router contract
|
||||
"cf124a063de1614fedbd6b47187f98bf8873a1ae83da5c179a5881162f5b2401",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn selector_collisions() {
|
||||
assert_eq!(
|
||||
crate::_irouter_abi::IRouter::confirmNextSeraiKeyCall::SELECTOR,
|
||||
crate::_router_abi::Router::confirmNextSeraiKey34AC53ACCall::SELECTOR
|
||||
);
|
||||
assert_eq!(
|
||||
crate::_irouter_abi::IRouter::updateSeraiKeyCall::SELECTOR,
|
||||
crate::_router_abi::Router::updateSeraiKey5A8542A2Call::SELECTOR
|
||||
);
|
||||
assert_eq!(
|
||||
crate::_irouter_abi::IRouter::executeCall::SELECTOR,
|
||||
crate::_router_abi::Router::execute4DE42904Call::SELECTOR
|
||||
);
|
||||
assert_eq!(
|
||||
crate::_irouter_abi::IRouter::escapeHatchCall::SELECTOR,
|
||||
crate::_router_abi::Router::escapeHatchDCDD91CCCall::SELECTOR
|
||||
);
|
||||
}
|
||||
mod constants;
|
||||
|
||||
pub(crate) fn test_key() -> (Scalar, PublicKey) {
|
||||
loop {
|
||||
|
@ -65,111 +41,418 @@ pub(crate) fn test_key() -> (Scalar, PublicKey) {
|
|||
}
|
||||
}
|
||||
|
||||
async fn setup_test(
|
||||
) -> (AnvilInstance, Arc<RootProvider<SimpleRequest>>, Router, (Scalar, PublicKey)) {
|
||||
let anvil = Anvil::new().spawn();
|
||||
fn sign(key: (Scalar, PublicKey), msg: &[u8]) -> Signature {
|
||||
let nonce = Scalar::random(&mut OsRng);
|
||||
let c = Signature::challenge(ProjectivePoint::GENERATOR * nonce, &key.1, msg);
|
||||
let s = nonce + (c * key.0);
|
||||
Signature::new(c, s).unwrap()
|
||||
}
|
||||
|
||||
let provider = Arc::new(RootProvider::new(
|
||||
ClientBuilder::default().transport(SimpleRequest::new(anvil.endpoint()), true),
|
||||
));
|
||||
/// Calculate the gas used by a transaction if none of its calldata's bytes were zero
|
||||
struct CalldataAgnosticGas;
|
||||
impl CalldataAgnosticGas {
|
||||
fn calculate(tx: &TxLegacy, mut gas_used: u64) -> u64 {
|
||||
const ZERO_BYTE_GAS_COST: u64 = 4;
|
||||
const NON_ZERO_BYTE_GAS_COST: u64 = 16;
|
||||
for b in &tx.input {
|
||||
if *b == 0 {
|
||||
gas_used += NON_ZERO_BYTE_GAS_COST - ZERO_BYTE_GAS_COST;
|
||||
}
|
||||
}
|
||||
gas_used
|
||||
}
|
||||
}
|
||||
|
||||
let (private_key, public_key) = test_key();
|
||||
assert!(Router::new(provider.clone(), &public_key).await.unwrap().is_none());
|
||||
struct RouterState {
|
||||
next_key: Option<(Scalar, PublicKey)>,
|
||||
key: Option<(Scalar, PublicKey)>,
|
||||
next_nonce: u64,
|
||||
escaped_to: Option<Address>,
|
||||
}
|
||||
|
||||
// Deploy the Deployer
|
||||
let receipt = ethereum_test_primitives::publish_tx(&provider, Deployer::deployment_tx()).await;
|
||||
assert!(receipt.status());
|
||||
struct Test {
|
||||
#[allow(unused)]
|
||||
anvil: AnvilInstance,
|
||||
provider: Arc<RootProvider<SimpleRequest>>,
|
||||
chain_id: U256,
|
||||
router: Router,
|
||||
state: RouterState,
|
||||
}
|
||||
|
||||
// Get the TX to deploy the Router
|
||||
let mut tx = Router::deployment_tx(&public_key);
|
||||
// Set a gas price (100 gwei)
|
||||
tx.gas_price = 100_000_000_000;
|
||||
// Sign it
|
||||
let tx = ethereum_primitives::deterministically_sign(tx);
|
||||
// Publish it
|
||||
let receipt = ethereum_test_primitives::publish_tx(&provider, tx).await;
|
||||
assert!(receipt.status());
|
||||
assert_eq!(Router::DEPLOYMENT_GAS, ((receipt.gas_used + 1000) / 1000) * 1000);
|
||||
impl Test {
|
||||
async fn verify_state(&self) {
|
||||
assert_eq!(
|
||||
self.router.next_key(BlockNumberOrTag::Latest.into()).await.unwrap(),
|
||||
self.state.next_key.map(|key| key.1)
|
||||
);
|
||||
assert_eq!(
|
||||
self.router.key(BlockNumberOrTag::Latest.into()).await.unwrap(),
|
||||
self.state.key.map(|key| key.1)
|
||||
);
|
||||
assert_eq!(
|
||||
self.router.next_nonce(BlockNumberOrTag::Latest.into()).await.unwrap(),
|
||||
self.state.next_nonce
|
||||
);
|
||||
assert_eq!(
|
||||
self.router.escaped_to(BlockNumberOrTag::Latest.into()).await.unwrap(),
|
||||
self.state.escaped_to,
|
||||
);
|
||||
}
|
||||
|
||||
let router = Router::new(provider.clone(), &public_key).await.unwrap().unwrap();
|
||||
async fn new() -> Self {
|
||||
// The following is explicitly only evaluated against the cancun network upgrade at this time
|
||||
let anvil = Anvil::new().arg("--hardfork").arg("cancun").spawn();
|
||||
|
||||
(anvil, provider, router, (private_key, public_key))
|
||||
let provider = Arc::new(RootProvider::new(
|
||||
ClientBuilder::default().transport(SimpleRequest::new(anvil.endpoint()), true),
|
||||
));
|
||||
let chain_id = U256::from(provider.get_chain_id().await.unwrap());
|
||||
|
||||
let (private_key, public_key) = test_key();
|
||||
assert!(Router::new(provider.clone(), &public_key).await.unwrap().is_none());
|
||||
|
||||
// Deploy the Deployer
|
||||
let receipt = ethereum_test_primitives::publish_tx(&provider, Deployer::deployment_tx()).await;
|
||||
assert!(receipt.status());
|
||||
|
||||
let mut tx = Router::deployment_tx(&public_key);
|
||||
tx.gas_limit = 1_100_000;
|
||||
tx.gas_price = 100_000_000_000;
|
||||
let tx = ethereum_primitives::deterministically_sign(tx);
|
||||
let receipt = ethereum_test_primitives::publish_tx(&provider, tx).await;
|
||||
assert!(receipt.status());
|
||||
|
||||
let router = Router::new(provider.clone(), &public_key).await.unwrap().unwrap();
|
||||
let state = RouterState {
|
||||
next_key: Some((private_key, public_key)),
|
||||
key: None,
|
||||
// Nonce 0 should've been consumed by setting the next key to the key initialized with
|
||||
next_nonce: 1,
|
||||
escaped_to: None,
|
||||
};
|
||||
|
||||
// Confirm nonce 0 was used as such
|
||||
{
|
||||
let block = receipt.block_number.unwrap();
|
||||
let executed = router.executed(block, block).await.unwrap();
|
||||
assert_eq!(executed.len(), 1);
|
||||
assert_eq!(executed[0], Executed::NextSeraiKeySet { nonce: 0, key: public_key.eth_repr() });
|
||||
}
|
||||
|
||||
let res = Test { anvil, provider, chain_id, router, state };
|
||||
res.verify_state().await;
|
||||
res
|
||||
}
|
||||
|
||||
async fn call_and_decode_err(&self, tx: TxLegacy) -> IRouterErrors {
|
||||
let call = TransactionRequest::default()
|
||||
.to(self.router.address())
|
||||
.input(TransactionInput::new(tx.input));
|
||||
let call_err = self.provider.call(&call).await.unwrap_err();
|
||||
call_err.as_error_resp().unwrap().as_decoded_error::<IRouterErrors>(true).unwrap()
|
||||
}
|
||||
|
||||
fn confirm_next_serai_key_tx(&self) -> TxLegacy {
|
||||
let msg = Router::confirm_next_serai_key_message(self.chain_id, self.state.next_nonce);
|
||||
let sig = sign(self.state.next_key.unwrap(), &msg);
|
||||
|
||||
self.router.confirm_next_serai_key(&sig)
|
||||
}
|
||||
|
||||
async fn confirm_next_serai_key(&mut self) {
|
||||
let mut tx = self.confirm_next_serai_key_tx();
|
||||
tx.gas_price = 100_000_000_000;
|
||||
let tx = ethereum_primitives::deterministically_sign(tx);
|
||||
let receipt = ethereum_test_primitives::publish_tx(&self.provider, tx.clone()).await;
|
||||
assert!(receipt.status());
|
||||
if self.state.key.is_none() {
|
||||
assert_eq!(
|
||||
CalldataAgnosticGas::calculate(tx.tx(), receipt.gas_used),
|
||||
Router::CONFIRM_NEXT_SERAI_KEY_GAS,
|
||||
);
|
||||
} else {
|
||||
assert!(
|
||||
CalldataAgnosticGas::calculate(tx.tx(), receipt.gas_used) <
|
||||
Router::CONFIRM_NEXT_SERAI_KEY_GAS
|
||||
);
|
||||
}
|
||||
|
||||
{
|
||||
let block = receipt.block_number.unwrap();
|
||||
let executed = self.router.executed(block, block).await.unwrap();
|
||||
assert_eq!(executed.len(), 1);
|
||||
assert_eq!(
|
||||
executed[0],
|
||||
Executed::SeraiKeyUpdated {
|
||||
nonce: self.state.next_nonce,
|
||||
key: self.state.next_key.unwrap().1.eth_repr()
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
self.state.next_nonce += 1;
|
||||
self.state.key = self.state.next_key;
|
||||
self.state.next_key = None;
|
||||
self.verify_state().await;
|
||||
}
|
||||
|
||||
fn update_serai_key_tx(&self) -> ((Scalar, PublicKey), TxLegacy) {
|
||||
let next_key = test_key();
|
||||
|
||||
let msg = Router::update_serai_key_message(self.chain_id, self.state.next_nonce, &next_key.1);
|
||||
let sig = sign(self.state.key.unwrap(), &msg);
|
||||
|
||||
(next_key, self.router.update_serai_key(&next_key.1, &sig))
|
||||
}
|
||||
|
||||
async fn update_serai_key(&mut self) {
|
||||
let (next_key, mut tx) = self.update_serai_key_tx();
|
||||
tx.gas_price = 100_000_000_000;
|
||||
let tx = ethereum_primitives::deterministically_sign(tx);
|
||||
let receipt = ethereum_test_primitives::publish_tx(&self.provider, tx.clone()).await;
|
||||
assert!(receipt.status());
|
||||
assert_eq!(
|
||||
CalldataAgnosticGas::calculate(tx.tx(), receipt.gas_used),
|
||||
Router::UPDATE_SERAI_KEY_GAS,
|
||||
);
|
||||
|
||||
{
|
||||
let block = receipt.block_number.unwrap();
|
||||
let executed = self.router.executed(block, block).await.unwrap();
|
||||
assert_eq!(executed.len(), 1);
|
||||
assert_eq!(
|
||||
executed[0],
|
||||
Executed::NextSeraiKeySet { nonce: self.state.next_nonce, key: next_key.1.eth_repr() }
|
||||
);
|
||||
}
|
||||
|
||||
self.state.next_nonce += 1;
|
||||
self.state.next_key = Some(next_key);
|
||||
self.verify_state().await;
|
||||
}
|
||||
|
||||
fn escape_hatch_tx(&self, escape_to: Address) -> TxLegacy {
|
||||
let msg = Router::escape_hatch_message(self.chain_id, self.state.next_nonce, escape_to);
|
||||
let sig = sign(self.state.key.unwrap(), &msg);
|
||||
self.router.escape_hatch(escape_to, &sig)
|
||||
}
|
||||
|
||||
async fn escape_hatch(&mut self) {
|
||||
let mut escape_to = [0; 20];
|
||||
OsRng.fill_bytes(&mut escape_to);
|
||||
let escape_to = Address(escape_to.into());
|
||||
|
||||
// Set the code of the address to escape to so it isn't flagged as a non-contract
|
||||
let () = self.provider.raw_request("anvil_setCode".into(), (escape_to, [0])).await.unwrap();
|
||||
|
||||
let mut tx = self.escape_hatch_tx(escape_to);
|
||||
tx.gas_price = 100_000_000_000;
|
||||
let tx = ethereum_primitives::deterministically_sign(tx);
|
||||
let receipt = ethereum_test_primitives::publish_tx(&self.provider, tx.clone()).await;
|
||||
assert!(receipt.status());
|
||||
assert_eq!(CalldataAgnosticGas::calculate(tx.tx(), receipt.gas_used), Router::ESCAPE_HATCH_GAS);
|
||||
|
||||
{
|
||||
let block = receipt.block_number.unwrap();
|
||||
let executed = self.router.executed(block, block).await.unwrap();
|
||||
assert_eq!(executed.len(), 1);
|
||||
assert_eq!(executed[0], Executed::EscapeHatch { nonce: self.state.next_nonce, escape_to });
|
||||
}
|
||||
|
||||
self.state.next_nonce += 1;
|
||||
self.state.escaped_to = Some(escape_to);
|
||||
self.verify_state().await;
|
||||
}
|
||||
|
||||
fn escape_tx(&self, coin: Coin) -> TxLegacy {
|
||||
let mut tx = self.router.escape(coin);
|
||||
tx.gas_limit = 100_000;
|
||||
tx.gas_price = 100_000_000_000;
|
||||
tx
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_constructor() {
|
||||
let (_anvil, _provider, router, key) = setup_test().await;
|
||||
assert_eq!(router.next_key(BlockNumberOrTag::Latest.into()).await.unwrap(), Some(key.1));
|
||||
assert_eq!(router.key(BlockNumberOrTag::Latest.into()).await.unwrap(), None);
|
||||
assert_eq!(router.next_nonce(BlockNumberOrTag::Latest.into()).await.unwrap(), 1);
|
||||
assert_eq!(
|
||||
router.escaped_to(BlockNumberOrTag::Latest.into()).await.unwrap(),
|
||||
Address::from([0; 20])
|
||||
);
|
||||
}
|
||||
|
||||
async fn confirm_next_serai_key(
|
||||
provider: &Arc<RootProvider<SimpleRequest>>,
|
||||
router: &Router,
|
||||
nonce: u64,
|
||||
key: (Scalar, PublicKey),
|
||||
) -> TransactionReceipt {
|
||||
let msg = Router::confirm_next_serai_key_message(nonce);
|
||||
|
||||
let nonce = Scalar::random(&mut OsRng);
|
||||
let c = Signature::challenge(ProjectivePoint::GENERATOR * nonce, &key.1, &msg);
|
||||
let s = nonce + (c * key.0);
|
||||
|
||||
let sig = Signature::new(c, s).unwrap();
|
||||
|
||||
let mut tx = router.confirm_next_serai_key(&sig);
|
||||
tx.gas_price = 100_000_000_000;
|
||||
let tx = ethereum_primitives::deterministically_sign(tx);
|
||||
let receipt = ethereum_test_primitives::publish_tx(provider, tx).await;
|
||||
assert!(receipt.status());
|
||||
assert_eq!(Router::CONFIRM_NEXT_SERAI_KEY_GAS, ((receipt.gas_used + 1000) / 1000) * 1000);
|
||||
receipt
|
||||
// `Test::new` internalizes all checks on initial state
|
||||
Test::new().await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_confirm_next_serai_key() {
|
||||
let (_anvil, provider, router, key) = setup_test().await;
|
||||
|
||||
assert_eq!(router.next_key(BlockNumberOrTag::Latest.into()).await.unwrap(), Some(key.1));
|
||||
assert_eq!(router.key(BlockNumberOrTag::Latest.into()).await.unwrap(), None);
|
||||
assert_eq!(router.next_nonce(BlockNumberOrTag::Latest.into()).await.unwrap(), 1);
|
||||
|
||||
let receipt = confirm_next_serai_key(&provider, &router, 1, key).await;
|
||||
|
||||
assert_eq!(router.next_key(receipt.block_hash.unwrap().into()).await.unwrap(), None);
|
||||
assert_eq!(router.key(receipt.block_hash.unwrap().into()).await.unwrap(), Some(key.1));
|
||||
assert_eq!(router.next_nonce(receipt.block_hash.unwrap().into()).await.unwrap(), 2);
|
||||
let mut test = Test::new().await;
|
||||
// TODO: Check all calls fail at this time, including inInstruction
|
||||
test.confirm_next_serai_key().await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_update_serai_key() {
|
||||
let (_anvil, provider, router, key) = setup_test().await;
|
||||
confirm_next_serai_key(&provider, &router, 1, key).await;
|
||||
let mut test = Test::new().await;
|
||||
test.confirm_next_serai_key().await;
|
||||
test.update_serai_key().await;
|
||||
|
||||
let update_to = test_key().1;
|
||||
let msg = Router::update_serai_key_message(2, &update_to);
|
||||
// Once we update to a new key, we should, of course, be able to continue to rotate keys
|
||||
test.confirm_next_serai_key().await;
|
||||
}
|
||||
|
||||
let nonce = Scalar::random(&mut OsRng);
|
||||
let c = Signature::challenge(ProjectivePoint::GENERATOR * nonce, &key.1, &msg);
|
||||
let s = nonce + (c * key.0);
|
||||
#[tokio::test]
|
||||
async fn test_eth_in_instruction() {
|
||||
todo!("TODO")
|
||||
}
|
||||
|
||||
let sig = Signature::new(c, s).unwrap();
|
||||
#[tokio::test]
|
||||
async fn test_erc20_in_instruction() {
|
||||
todo!("TODO")
|
||||
}
|
||||
|
||||
let mut tx = router.update_serai_key(&update_to, &sig);
|
||||
tx.gas_price = 100_000_000_000;
|
||||
let tx = ethereum_primitives::deterministically_sign(tx);
|
||||
let receipt = ethereum_test_primitives::publish_tx(&provider, tx).await;
|
||||
assert!(receipt.status());
|
||||
assert_eq!(Router::UPDATE_SERAI_KEY_GAS, ((receipt.gas_used + 1000) / 1000) * 1000);
|
||||
#[tokio::test]
|
||||
async fn test_eth_address_out_instruction() {
|
||||
todo!("TODO")
|
||||
}
|
||||
|
||||
assert_eq!(router.key(receipt.block_hash.unwrap().into()).await.unwrap(), Some(key.1));
|
||||
assert_eq!(router.next_key(receipt.block_hash.unwrap().into()).await.unwrap(), Some(update_to));
|
||||
assert_eq!(router.next_nonce(receipt.block_hash.unwrap().into()).await.unwrap(), 3);
|
||||
#[tokio::test]
|
||||
async fn test_erc20_address_out_instruction() {
|
||||
todo!("TODO")
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_eth_code_out_instruction() {
|
||||
todo!("TODO")
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_erc20_code_out_instruction() {
|
||||
todo!("TODO")
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_escape_hatch() {
|
||||
let mut test = Test::new().await;
|
||||
test.confirm_next_serai_key().await;
|
||||
|
||||
// Queue another key so the below test cases can run
|
||||
test.update_serai_key().await;
|
||||
|
||||
{
|
||||
// The zero address should be invalid to escape to
|
||||
assert!(matches!(
|
||||
test.call_and_decode_err(test.escape_hatch_tx([0; 20].into())).await,
|
||||
IRouterErrors::InvalidEscapeAddress(IRouter::InvalidEscapeAddress {})
|
||||
));
|
||||
// Empty addresses should be invalid to escape to
|
||||
assert!(matches!(
|
||||
test.call_and_decode_err(test.escape_hatch_tx([1; 20].into())).await,
|
||||
IRouterErrors::EscapeAddressWasNotAContract(IRouter::EscapeAddressWasNotAContract {})
|
||||
));
|
||||
// Non-empty addresses without code should be invalid to escape to
|
||||
let tx = ethereum_primitives::deterministically_sign(TxLegacy {
|
||||
to: Address([1; 20].into()).into(),
|
||||
gas_limit: 21_000,
|
||||
gas_price: 100_000_000_000u128,
|
||||
value: U256::from(1),
|
||||
..Default::default()
|
||||
});
|
||||
let receipt = ethereum_test_primitives::publish_tx(&test.provider, tx.clone()).await;
|
||||
assert!(receipt.status());
|
||||
assert!(matches!(
|
||||
test.call_and_decode_err(test.escape_hatch_tx([1; 20].into())).await,
|
||||
IRouterErrors::EscapeAddressWasNotAContract(IRouter::EscapeAddressWasNotAContract {})
|
||||
));
|
||||
|
||||
// Escaping at this point in time should fail
|
||||
assert!(matches!(
|
||||
test.call_and_decode_err(test.router.escape(Coin::Ether)).await,
|
||||
IRouterErrors::EscapeHatchNotInvoked(IRouter::EscapeHatchNotInvoked {})
|
||||
));
|
||||
}
|
||||
|
||||
// Invoke the escape hatch
|
||||
test.escape_hatch().await;
|
||||
|
||||
// Now that the escape hatch has been invoked, all of the following calls should fail
|
||||
{
|
||||
assert!(matches!(
|
||||
test.call_and_decode_err(test.update_serai_key_tx().1).await,
|
||||
IRouterErrors::EscapeHatchInvoked(IRouter::EscapeHatchInvoked {})
|
||||
));
|
||||
assert!(matches!(
|
||||
test.call_and_decode_err(test.confirm_next_serai_key_tx()).await,
|
||||
IRouterErrors::EscapeHatchInvoked(IRouter::EscapeHatchInvoked {})
|
||||
));
|
||||
// TODO inInstruction
|
||||
// TODO execute
|
||||
// We reject further attempts to update the escape hatch to prevent the last key from being
|
||||
// able to switch from the honest escape hatch to siphoning via a malicious escape hatch (such
|
||||
// as after the validators represented unstake)
|
||||
assert!(matches!(
|
||||
test.call_and_decode_err(test.escape_hatch_tx(test.state.escaped_to.unwrap())).await,
|
||||
IRouterErrors::EscapeHatchInvoked(IRouter::EscapeHatchInvoked {})
|
||||
));
|
||||
}
|
||||
|
||||
// Check the escape fn itself
|
||||
|
||||
// ETH
|
||||
{
|
||||
let () = test
|
||||
.provider
|
||||
.raw_request("anvil_setBalance".into(), (test.router.address(), 1))
|
||||
.await
|
||||
.unwrap();
|
||||
let tx = ethereum_primitives::deterministically_sign(test.escape_tx(Coin::Ether));
|
||||
let receipt = ethereum_test_primitives::publish_tx(&test.provider, tx.clone()).await;
|
||||
assert!(receipt.status());
|
||||
|
||||
let block = receipt.block_number.unwrap();
|
||||
assert_eq!(
|
||||
test.router.escapes(block, block).await.unwrap(),
|
||||
vec![Escape { coin: Coin::Ether, amount: U256::from(1) }],
|
||||
);
|
||||
|
||||
assert!(test.provider.get_balance(test.router.address()).await.unwrap() == U256::from(0));
|
||||
assert!(
|
||||
test.provider.get_balance(test.state.escaped_to.unwrap()).await.unwrap() == U256::from(1)
|
||||
);
|
||||
}
|
||||
|
||||
// TODO ERC20 escape
|
||||
}
|
||||
|
||||
/*
|
||||
event InInstruction(
|
||||
address indexed from, address indexed coin, uint256 amount, bytes instruction
|
||||
);
|
||||
event Batch(uint256 indexed nonce, bytes32 indexed messageHash, bytes results);
|
||||
error InvalidSeraiKey();
|
||||
error InvalidSignature();
|
||||
error AmountMismatchesMsgValue();
|
||||
error TransferFromFailed();
|
||||
error Reentered();
|
||||
error EscapeFailed();
|
||||
function executeArbitraryCode(bytes memory code) external payable;
|
||||
struct Signature {
|
||||
bytes32 c;
|
||||
bytes32 s;
|
||||
}
|
||||
enum DestinationType {
|
||||
Address,
|
||||
Code
|
||||
}
|
||||
struct CodeDestination {
|
||||
uint32 gasLimit;
|
||||
bytes code;
|
||||
}
|
||||
struct OutInstruction {
|
||||
DestinationType destinationType;
|
||||
bytes destination;
|
||||
uint256 amount;
|
||||
}
|
||||
function execute(
|
||||
Signature calldata signature,
|
||||
address coin,
|
||||
uint256 fee,
|
||||
OutInstruction[] calldata outs
|
||||
) external;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
|
@ -189,7 +472,7 @@ async fn test_eth_in_instruction() {
|
|||
gas_limit: 1_000_000,
|
||||
to: TxKind::Call(router.address()),
|
||||
value: amount,
|
||||
input: crate::abi::inInstructionCall::new((
|
||||
input: crate::_irouter_abi::inInstructionCall::new((
|
||||
[0; 20].into(),
|
||||
amount,
|
||||
in_instruction.clone().into(),
|
||||
|
@ -227,11 +510,6 @@ async fn test_eth_in_instruction() {
|
|||
assert_eq!(parsed_in_instructions[0].data, in_instruction);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_erc20_in_instruction() {
|
||||
todo!("TODO")
|
||||
}
|
||||
|
||||
async fn publish_outs(
|
||||
provider: &RootProvider<SimpleRequest>,
|
||||
router: &Router,
|
||||
|
@ -275,68 +553,4 @@ async fn test_eth_address_out_instruction() {
|
|||
|
||||
assert_eq!(router.next_nonce(receipt.block_hash.unwrap().into()).await.unwrap(), 3);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_erc20_address_out_instruction() {
|
||||
todo!("TODO")
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_eth_code_out_instruction() {
|
||||
todo!("TODO")
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_erc20_code_out_instruction() {
|
||||
todo!("TODO")
|
||||
}
|
||||
|
||||
async fn escape_hatch(
|
||||
provider: &Arc<RootProvider<SimpleRequest>>,
|
||||
router: &Router,
|
||||
nonce: u64,
|
||||
key: (Scalar, PublicKey),
|
||||
escape_to: Address,
|
||||
) -> TransactionReceipt {
|
||||
let msg = Router::escape_hatch_message(nonce, escape_to);
|
||||
|
||||
let nonce = Scalar::random(&mut OsRng);
|
||||
let c = Signature::challenge(ProjectivePoint::GENERATOR * nonce, &key.1, &msg);
|
||||
let s = nonce + (c * key.0);
|
||||
|
||||
let sig = Signature::new(c, s).unwrap();
|
||||
|
||||
let mut tx = router.escape_hatch(escape_to, &sig);
|
||||
tx.gas_price = 100_000_000_000;
|
||||
let tx = ethereum_primitives::deterministically_sign(tx);
|
||||
let receipt = ethereum_test_primitives::publish_tx(provider, tx).await;
|
||||
assert!(receipt.status());
|
||||
assert_eq!(Router::ESCAPE_HATCH_GAS, ((receipt.gas_used + 1000) / 1000) * 1000);
|
||||
receipt
|
||||
}
|
||||
|
||||
async fn escape(
|
||||
provider: &Arc<RootProvider<SimpleRequest>>,
|
||||
router: &Router,
|
||||
coin: Coin,
|
||||
) -> TransactionReceipt {
|
||||
let mut tx = router.escape(coin.address());
|
||||
tx.gas_price = 100_000_000_000;
|
||||
let tx = ethereum_primitives::deterministically_sign(tx);
|
||||
let receipt = ethereum_test_primitives::publish_tx(provider, tx).await;
|
||||
assert!(receipt.status());
|
||||
receipt
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_escape_hatch() {
|
||||
let (_anvil, provider, router, key) = setup_test().await;
|
||||
confirm_next_serai_key(&provider, &router, 1, key).await;
|
||||
let escape_to: Address = {
|
||||
let mut escape_to = [0; 20];
|
||||
OsRng.fill_bytes(&mut escape_to);
|
||||
escape_to.into()
|
||||
};
|
||||
escape_hatch(&provider, &router, 2, key, escape_to).await;
|
||||
escape(&provider, &router, Coin::Ether).await;
|
||||
}
|
||||
*/
|
||||
|
|
|
@ -6,11 +6,13 @@
|
|||
static ALLOCATOR: zalloc::ZeroizingAlloc<std::alloc::System> =
|
||||
zalloc::ZeroizingAlloc(std::alloc::System);
|
||||
|
||||
use core::time::Duration;
|
||||
use std::sync::Arc;
|
||||
|
||||
use alloy_core::primitives::U256;
|
||||
use alloy_simple_request_transport::SimpleRequest;
|
||||
use alloy_rpc_client::ClientBuilder;
|
||||
use alloy_provider::RootProvider;
|
||||
use alloy_provider::{Provider, RootProvider};
|
||||
|
||||
use serai_client::validator_sets::primitives::Session;
|
||||
|
||||
|
@ -62,10 +64,26 @@ async fn main() {
|
|||
ClientBuilder::default().transport(SimpleRequest::new(bin::url()), true),
|
||||
));
|
||||
|
||||
let chain_id = {
|
||||
let mut delay = Duration::from_secs(5);
|
||||
loop {
|
||||
match provider.get_chain_id().await {
|
||||
Ok(chain_id) => break chain_id,
|
||||
Err(e) => {
|
||||
log::error!("failed to fetch the chain ID on boot: {e:?}");
|
||||
tokio::time::sleep(delay).await;
|
||||
delay = (delay + Duration::from_secs(5)).max(Duration::from_secs(120));
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
bin::main_loop::<SetInitialKey, _, KeyGenParams, _>(
|
||||
db.clone(),
|
||||
Rpc { db: db.clone(), provider: provider.clone() },
|
||||
Scheduler::<bin::Db>::new(SmartContract),
|
||||
Scheduler::<bin::Db>::new(SmartContract {
|
||||
chain_id: U256::from_le_slice(&chain_id.to_le_bytes()),
|
||||
}),
|
||||
TransactionPublisher::new(db, provider, {
|
||||
let relayer_hostname = env::var("ETHEREUM_RELAYER_HOSTNAME")
|
||||
.expect("ethereum relayer hostname wasn't specified")
|
||||
|
|
|
@ -99,6 +99,7 @@ impl primitives::Block for FullEpoch {
|
|||
let Some(expected) =
|
||||
eventualities.active_eventualities.remove(executed.nonce().to_le_bytes().as_slice())
|
||||
else {
|
||||
// TODO: Why is this a continue, not an assert?
|
||||
continue;
|
||||
};
|
||||
assert_eq!(
|
||||
|
|
|
@ -81,8 +81,8 @@ impl ReceivedOutput<<Secp256k1 as Ciphersuite>::G, Address> for Output {
|
|||
match self {
|
||||
Output::Output { key: _, instruction } => {
|
||||
let mut id = [0; 40];
|
||||
id[.. 32].copy_from_slice(&instruction.id.0);
|
||||
id[32 ..].copy_from_slice(&instruction.id.1.to_le_bytes());
|
||||
id[.. 32].copy_from_slice(&instruction.id.block_hash);
|
||||
id[32 ..].copy_from_slice(&instruction.id.index_within_block.to_le_bytes());
|
||||
OutputId(id)
|
||||
}
|
||||
// Yet upon Eventuality completions, we report a Change output to ensure synchrony per the
|
||||
|
@ -97,7 +97,7 @@ impl ReceivedOutput<<Secp256k1 as Ciphersuite>::G, Address> for Output {
|
|||
|
||||
fn transaction_id(&self) -> Self::TransactionId {
|
||||
match self {
|
||||
Output::Output { key: _, instruction } => instruction.id.0,
|
||||
Output::Output { key: _, instruction } => instruction.transaction_hash,
|
||||
Output::Eventuality { key: _, nonce } => {
|
||||
let mut id = [0; 32];
|
||||
id[.. 8].copy_from_slice(&nonce.to_le_bytes());
|
||||
|
@ -114,7 +114,7 @@ impl ReceivedOutput<<Secp256k1 as Ciphersuite>::G, Address> for Output {
|
|||
|
||||
fn presumed_origin(&self) -> Option<Address> {
|
||||
match self {
|
||||
Output::Output { key: _, instruction } => Some(Address::from(instruction.from)),
|
||||
Output::Output { key: _, instruction } => Some(Address::Address(*instruction.from.0)),
|
||||
Output::Eventuality { .. } => None,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,8 +17,8 @@ use crate::{output::OutputId, machine::ClonableTransctionMachine};
|
|||
|
||||
#[derive(Clone, PartialEq, Debug)]
|
||||
pub(crate) enum Action {
|
||||
SetKey { nonce: u64, key: PublicKey },
|
||||
Batch { nonce: u64, coin: Coin, fee: U256, outs: Vec<(Address, U256)> },
|
||||
SetKey { chain_id: U256, nonce: u64, key: PublicKey },
|
||||
Batch { chain_id: U256, nonce: u64, coin: Coin, fee: U256, outs: Vec<(Address, U256)> },
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq, Eq, Debug)]
|
||||
|
@ -33,17 +33,25 @@ impl Action {
|
|||
|
||||
pub(crate) fn message(&self) -> Vec<u8> {
|
||||
match self {
|
||||
Action::SetKey { nonce, key } => Router::update_serai_key_message(*nonce, key),
|
||||
Action::Batch { nonce, coin, fee, outs } => {
|
||||
Router::execute_message(*nonce, *coin, *fee, OutInstructions::from(outs.as_ref()))
|
||||
Action::SetKey { chain_id, nonce, key } => {
|
||||
Router::update_serai_key_message(*chain_id, *nonce, key)
|
||||
}
|
||||
Action::Batch { chain_id, nonce, coin, fee, outs } => Router::execute_message(
|
||||
*chain_id,
|
||||
*nonce,
|
||||
*coin,
|
||||
*fee,
|
||||
OutInstructions::from(outs.as_ref()),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn eventuality(&self) -> Eventuality {
|
||||
Eventuality(match self {
|
||||
Self::SetKey { nonce, key } => Executed::SetKey { nonce: *nonce, key: key.eth_repr() },
|
||||
Self::Batch { nonce, .. } => {
|
||||
Self::SetKey { chain_id: _, nonce, key } => {
|
||||
Executed::NextSeraiKeySet { nonce: *nonce, key: key.eth_repr() }
|
||||
}
|
||||
Self::Batch { chain_id: _, nonce, .. } => {
|
||||
Executed::Batch { nonce: *nonce, message_hash: keccak256(self.message()) }
|
||||
}
|
||||
})
|
||||
|
@ -77,6 +85,10 @@ impl SignableTransaction for Action {
|
|||
Err(io::Error::other("unrecognized Action type"))?;
|
||||
}
|
||||
|
||||
let mut chain_id = [0; 32];
|
||||
reader.read_exact(&mut chain_id)?;
|
||||
let chain_id = U256::from_be_bytes(chain_id);
|
||||
|
||||
let mut nonce = [0; 8];
|
||||
reader.read_exact(&mut nonce)?;
|
||||
let nonce = u64::from_le_bytes(nonce);
|
||||
|
@ -88,10 +100,10 @@ impl SignableTransaction for Action {
|
|||
let key =
|
||||
PublicKey::from_eth_repr(key).ok_or_else(|| io::Error::other("invalid key in Action"))?;
|
||||
|
||||
Action::SetKey { nonce, key }
|
||||
Action::SetKey { chain_id, nonce, key }
|
||||
}
|
||||
1 => {
|
||||
let coin = Coin::read(reader)?;
|
||||
let coin = borsh::from_reader(reader)?;
|
||||
|
||||
let mut fee = [0; 32];
|
||||
reader.read_exact(&mut fee)?;
|
||||
|
@ -111,22 +123,24 @@ impl SignableTransaction for Action {
|
|||
|
||||
outs.push((address, amount));
|
||||
}
|
||||
Action::Batch { nonce, coin, fee, outs }
|
||||
Action::Batch { chain_id, nonce, coin, fee, outs }
|
||||
}
|
||||
_ => unreachable!(),
|
||||
})
|
||||
}
|
||||
fn write(&self, writer: &mut impl io::Write) -> io::Result<()> {
|
||||
match self {
|
||||
Self::SetKey { nonce, key } => {
|
||||
Self::SetKey { chain_id, nonce, key } => {
|
||||
writer.write_all(&[0])?;
|
||||
writer.write_all(&chain_id.to_be_bytes::<32>())?;
|
||||
writer.write_all(&nonce.to_le_bytes())?;
|
||||
writer.write_all(&key.eth_repr())
|
||||
}
|
||||
Self::Batch { nonce, coin, fee, outs } => {
|
||||
Self::Batch { chain_id, nonce, coin, fee, outs } => {
|
||||
writer.write_all(&[1])?;
|
||||
writer.write_all(&chain_id.to_be_bytes::<32>())?;
|
||||
writer.write_all(&nonce.to_le_bytes())?;
|
||||
coin.write(writer)?;
|
||||
borsh::BorshSerialize::serialize(coin, writer)?;
|
||||
writer.write_all(&fee.as_le_bytes())?;
|
||||
writer.write_all(&u32::try_from(outs.len()).unwrap().to_le_bytes())?;
|
||||
for (address, amount) in outs {
|
||||
|
@ -167,9 +181,9 @@ impl primitives::Eventuality for Eventuality {
|
|||
}
|
||||
|
||||
fn read(reader: &mut impl io::Read) -> io::Result<Self> {
|
||||
Executed::read(reader).map(Self)
|
||||
Ok(Self(borsh::from_reader(reader)?))
|
||||
}
|
||||
fn write(&self, writer: &mut impl io::Write) -> io::Result<()> {
|
||||
self.0.write(writer)
|
||||
borsh::BorshSerialize::serialize(&self.0, writer)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -88,8 +88,8 @@ impl<D: Db> signers::TransactionPublisher<Transaction> for TransactionPublisher<
|
|||
let nonce = tx.0.nonce();
|
||||
// Convert from an Action (an internal representation of a signable event) to a TxLegacy
|
||||
let tx = match tx.0 {
|
||||
Action::SetKey { nonce: _, key } => router.update_serai_key(&key, &tx.1),
|
||||
Action::Batch { nonce: _, coin, fee, outs } => {
|
||||
Action::SetKey { chain_id: _, nonce: _, key } => router.update_serai_key(&key, &tx.1),
|
||||
Action::Batch { chain_id: _, nonce: _, coin, fee, outs } => {
|
||||
router.execute(coin, fee, OutInstructions::from(outs.as_ref()), &tx.1)
|
||||
}
|
||||
};
|
||||
|
|
|
@ -165,12 +165,14 @@ impl<D: Db> ScannerFeed for Rpc<D> {
|
|||
let mut instructions = router.in_instructions(block.number, &HashSet::from(TOKENS)).await?;
|
||||
|
||||
for token in TOKENS {
|
||||
for TopLevelTransfer { id, from, amount, data } in Erc20::new(provider.clone(), **token)
|
||||
.top_level_transfers(block.number, router.address())
|
||||
.await?
|
||||
for TopLevelTransfer { id, transaction_hash, from, amount, data } in
|
||||
Erc20::new(provider.clone(), **token)
|
||||
.top_level_transfers(block.number, router.address())
|
||||
.await?
|
||||
{
|
||||
instructions.push(EthereumInInstruction {
|
||||
id,
|
||||
transaction_hash,
|
||||
from,
|
||||
coin: EthereumCoin::Erc20(token),
|
||||
amount,
|
||||
|
@ -179,7 +181,7 @@ impl<D: Db> ScannerFeed for Rpc<D> {
|
|||
}
|
||||
}
|
||||
|
||||
let executed = router.executed(block.number).await?;
|
||||
let executed = router.executed(block.number, block.number).await?;
|
||||
|
||||
Ok((instructions, executed))
|
||||
}
|
||||
|
|
|
@ -36,7 +36,9 @@ fn balance_to_ethereum_amount(balance: Balance) -> U256 {
|
|||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct SmartContract;
|
||||
pub(crate) struct SmartContract {
|
||||
pub(crate) chain_id: U256,
|
||||
}
|
||||
impl<D: Db> smart_contract_scheduler::SmartContract<Rpc<D>> for SmartContract {
|
||||
type SignableTransaction = Action;
|
||||
|
||||
|
@ -46,8 +48,11 @@ impl<D: Db> smart_contract_scheduler::SmartContract<Rpc<D>> for SmartContract {
|
|||
_retiring_key: KeyFor<Rpc<D>>,
|
||||
new_key: KeyFor<Rpc<D>>,
|
||||
) -> (Self::SignableTransaction, EventualityFor<Rpc<D>>) {
|
||||
let action =
|
||||
Action::SetKey { nonce, key: PublicKey::new(new_key).expect("rotating to an invald key") };
|
||||
let action = Action::SetKey {
|
||||
chain_id: self.chain_id,
|
||||
nonce,
|
||||
key: PublicKey::new(new_key).expect("rotating to an invald key"),
|
||||
};
|
||||
(action.clone(), action.eventuality())
|
||||
}
|
||||
|
||||
|
@ -133,6 +138,7 @@ impl<D: Db> smart_contract_scheduler::SmartContract<Rpc<D>> for SmartContract {
|
|||
}
|
||||
|
||||
res.push(Action::Batch {
|
||||
chain_id: self.chain_id,
|
||||
nonce,
|
||||
coin: coin_to_ethereum_coin(coin),
|
||||
fee: U256::try_from(total_gas).unwrap() * fee_per_gas,
|
||||
|
|
Loading…
Reference in a new issue