mirror of
https://github.com/serai-dex/serai.git
synced 2025-12-13 22:49:25 +00:00
Outline the Ethereum processor
This was only half-finished to begin with, unfortunately...
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
/*
|
||||
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
|
||||
#![doc = include_str!("../README.md")]
|
||||
#![deny(missing_docs)]
|
||||
@@ -59,240 +60,6 @@ use crate::{
|
||||
},
|
||||
};
|
||||
|
||||
#[cfg(not(test))]
|
||||
const DAI: [u8; 20] =
|
||||
match const_hex::const_decode_to_array(b"0x6B175474E89094C44Da98b954EedeAC495271d0F") {
|
||||
Ok(res) => res,
|
||||
Err(_) => panic!("invalid non-test DAI hex address"),
|
||||
};
|
||||
#[cfg(test)] // TODO
|
||||
const DAI: [u8; 20] =
|
||||
match const_hex::const_decode_to_array(b"0000000000000000000000000000000000000000") {
|
||||
Ok(res) => res,
|
||||
Err(_) => panic!("invalid test DAI hex address"),
|
||||
};
|
||||
|
||||
fn coin_to_serai_coin(coin: &EthereumCoin) -> Option<Coin> {
|
||||
match coin {
|
||||
EthereumCoin::Ether => Some(Coin::Ether),
|
||||
EthereumCoin::Erc20(token) => {
|
||||
if *token == DAI {
|
||||
return Some(Coin::Dai);
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn amount_to_serai_amount(coin: Coin, amount: U256) -> Amount {
|
||||
assert_eq!(coin.network(), NetworkId::Ethereum);
|
||||
assert_eq!(coin.decimals(), 8);
|
||||
// Remove 10 decimals so we go from 18 decimals to 8 decimals
|
||||
let divisor = U256::from(10_000_000_000u64);
|
||||
// This is valid up to 184b, which is assumed for the coins allowed
|
||||
Amount(u64::try_from(amount / divisor).unwrap())
|
||||
}
|
||||
|
||||
fn balance_to_ethereum_amount(balance: Balance) -> U256 {
|
||||
assert_eq!(balance.coin.network(), NetworkId::Ethereum);
|
||||
assert_eq!(balance.coin.decimals(), 8);
|
||||
// Restore 10 decimals so we go from 8 decimals to 18 decimals
|
||||
let factor = U256::from(10_000_000_000u64);
|
||||
U256::from(balance.amount.0) * factor
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
||||
pub struct Address(pub [u8; 20]);
|
||||
impl TryFrom<Vec<u8>> for Address {
|
||||
type Error = ();
|
||||
fn try_from(bytes: Vec<u8>) -> Result<Address, ()> {
|
||||
if bytes.len() != 20 {
|
||||
Err(())?;
|
||||
}
|
||||
let mut res = [0; 20];
|
||||
res.copy_from_slice(&bytes);
|
||||
Ok(Address(res))
|
||||
}
|
||||
}
|
||||
impl TryInto<Vec<u8>> for Address {
|
||||
type Error = ();
|
||||
fn try_into(self) -> Result<Vec<u8>, ()> {
|
||||
Ok(self.0.to_vec())
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Address {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
ethereum_serai::alloy::primitives::Address::from(self.0).fmt(f)
|
||||
}
|
||||
}
|
||||
|
||||
impl SignableTransaction for RouterCommand {
|
||||
fn fee(&self) -> u64 {
|
||||
// Return a fee of 0 as we'll handle amortization on our end
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<D: Db> TransactionTrait<Ethereum<D>> for Transaction {
|
||||
type Id = [u8; 32];
|
||||
fn id(&self) -> Self::Id {
|
||||
self.hash.0
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
async fn fee(&self, _network: &Ethereum<D>) -> u64 {
|
||||
// Return a fee of 0 as we'll handle amortization on our end
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
// We use 32-block Epochs to represent blocks.
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
||||
pub struct Epoch {
|
||||
// The hash of the block which ended the prior Epoch.
|
||||
prior_end_hash: [u8; 32],
|
||||
// The first block number within this Epoch.
|
||||
start: u64,
|
||||
// The hash of the last block within this Epoch.
|
||||
end_hash: [u8; 32],
|
||||
// The monotonic time for this Epoch.
|
||||
time: u64,
|
||||
}
|
||||
|
||||
impl Epoch {
|
||||
fn end(&self) -> u64 {
|
||||
self.start + 31
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<D: Db> Block<Ethereum<D>> for Epoch {
|
||||
type Id = [u8; 32];
|
||||
fn id(&self) -> [u8; 32] {
|
||||
self.end_hash
|
||||
}
|
||||
fn parent(&self) -> [u8; 32] {
|
||||
self.prior_end_hash
|
||||
}
|
||||
async fn time(&self, _: &Ethereum<D>) -> u64 {
|
||||
self.time
|
||||
}
|
||||
}
|
||||
|
||||
impl<D: Db> Output<Ethereum<D>> for EthereumInInstruction {
|
||||
type Id = [u8; 32];
|
||||
|
||||
fn kind(&self) -> OutputType {
|
||||
OutputType::External
|
||||
}
|
||||
|
||||
fn id(&self) -> Self::Id {
|
||||
let mut id = [0; 40];
|
||||
id[.. 32].copy_from_slice(&self.id.0);
|
||||
id[32 ..].copy_from_slice(&self.id.1.to_le_bytes());
|
||||
*ethereum_serai::alloy::primitives::keccak256(id)
|
||||
}
|
||||
fn tx_id(&self) -> [u8; 32] {
|
||||
self.id.0
|
||||
}
|
||||
fn key(&self) -> <Secp256k1 as Ciphersuite>::G {
|
||||
self.key_at_end_of_block
|
||||
}
|
||||
|
||||
fn presumed_origin(&self) -> Option<Address> {
|
||||
Some(Address(self.from))
|
||||
}
|
||||
|
||||
fn balance(&self) -> Balance {
|
||||
let coin = coin_to_serai_coin(&self.coin).unwrap_or_else(|| {
|
||||
panic!(
|
||||
"requesting coin for an EthereumInInstruction with a coin {}",
|
||||
"we don't handle. this never should have been yielded"
|
||||
)
|
||||
});
|
||||
Balance { coin, amount: amount_to_serai_amount(coin, self.amount) }
|
||||
}
|
||||
fn data(&self) -> &[u8] {
|
||||
&self.data
|
||||
}
|
||||
|
||||
fn write<W: io::Write>(&self, writer: &mut W) -> io::Result<()> {
|
||||
EthereumInInstruction::write(self, writer)
|
||||
}
|
||||
fn read<R: io::Read>(reader: &mut R) -> io::Result<Self> {
|
||||
EthereumInInstruction::read(reader)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq, Eq, Debug)]
|
||||
pub struct Claim {
|
||||
signature: [u8; 64],
|
||||
}
|
||||
impl AsRef<[u8]> for Claim {
|
||||
fn as_ref(&self) -> &[u8] {
|
||||
&self.signature
|
||||
}
|
||||
}
|
||||
impl AsMut<[u8]> for Claim {
|
||||
fn as_mut(&mut self) -> &mut [u8] {
|
||||
&mut self.signature
|
||||
}
|
||||
}
|
||||
impl Default for Claim {
|
||||
fn default() -> Self {
|
||||
Self { signature: [0; 64] }
|
||||
}
|
||||
}
|
||||
impl From<&Signature> for Claim {
|
||||
fn from(sig: &Signature) -> Self {
|
||||
Self { signature: sig.to_bytes() }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq, Eq, Debug)]
|
||||
pub struct Eventuality(PublicKey, RouterCommand);
|
||||
impl EventualityTrait for Eventuality {
|
||||
type Claim = Claim;
|
||||
type Completion = SignedRouterCommand;
|
||||
|
||||
fn lookup(&self) -> Vec<u8> {
|
||||
match self.1 {
|
||||
RouterCommand::UpdateSeraiKey { nonce, .. } | RouterCommand::Execute { nonce, .. } => {
|
||||
nonce.as_le_bytes().to_vec()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn read<R: io::Read>(reader: &mut R) -> io::Result<Self> {
|
||||
let point = Secp256k1::read_G(reader)?;
|
||||
let command = RouterCommand::read(reader)?;
|
||||
Ok(Eventuality(
|
||||
PublicKey::new(point).ok_or(io::Error::other("unusable key within Eventuality"))?,
|
||||
command,
|
||||
))
|
||||
}
|
||||
fn serialize(&self) -> Vec<u8> {
|
||||
let mut res = vec![];
|
||||
res.extend(self.0.point().to_bytes().as_slice());
|
||||
self.1.write(&mut res).unwrap();
|
||||
res
|
||||
}
|
||||
|
||||
fn claim(completion: &Self::Completion) -> Self::Claim {
|
||||
Claim::from(completion.signature())
|
||||
}
|
||||
fn serialize_completion(completion: &Self::Completion) -> Vec<u8> {
|
||||
let mut res = vec![];
|
||||
completion.write(&mut res).unwrap();
|
||||
res
|
||||
}
|
||||
fn read_completion<R: io::Read>(reader: &mut R) -> io::Result<Self::Completion> {
|
||||
SignedRouterCommand::read(reader)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Ethereum<D: Db> {
|
||||
// This DB is solely used to access the first key generated, as needed to determine the Router's
|
||||
@@ -305,20 +72,6 @@ pub struct Ethereum<D: Db> {
|
||||
deployer: Deployer,
|
||||
router: Arc<RwLock<Option<Router>>>,
|
||||
}
|
||||
impl<D: Db> PartialEq for Ethereum<D> {
|
||||
fn eq(&self, _other: &Ethereum<D>) -> bool {
|
||||
true
|
||||
}
|
||||
}
|
||||
impl<D: Db> fmt::Debug for Ethereum<D> {
|
||||
fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
fmt
|
||||
.debug_struct("Ethereum")
|
||||
.field("deployer", &self.deployer)
|
||||
.field("router", &self.router)
|
||||
.finish_non_exhaustive()
|
||||
}
|
||||
}
|
||||
impl<D: Db> Ethereum<D> {
|
||||
pub async fn new(db: D, daemon_url: String, relayer_url: String) -> Self {
|
||||
let provider = Arc::new(RootProvider::new(
|
||||
@@ -384,110 +137,10 @@ impl<D: Db> Ethereum<D> {
|
||||
|
||||
#[async_trait]
|
||||
impl<D: Db> Network for Ethereum<D> {
|
||||
type Curve = Secp256k1;
|
||||
|
||||
type Transaction = Transaction;
|
||||
type Block = Epoch;
|
||||
|
||||
type Output = EthereumInInstruction;
|
||||
type SignableTransaction = RouterCommand;
|
||||
type Eventuality = Eventuality;
|
||||
type TransactionMachine = RouterCommandMachine;
|
||||
|
||||
type Scheduler = Scheduler<Self>;
|
||||
|
||||
type Address = Address;
|
||||
|
||||
const NETWORK: NetworkId = NetworkId::Ethereum;
|
||||
const ID: &'static str = "Ethereum";
|
||||
const ESTIMATED_BLOCK_TIME_IN_SECONDS: usize = 32 * 12;
|
||||
const CONFIRMATIONS: usize = 1;
|
||||
|
||||
const DUST: u64 = 0; // TODO
|
||||
|
||||
const COST_TO_AGGREGATE: u64 = 0;
|
||||
|
||||
// TODO: usize::max, with a merkle tree in the router
|
||||
const MAX_OUTPUTS: usize = 256;
|
||||
|
||||
fn tweak_keys(keys: &mut ThresholdKeys<Self::Curve>) {
|
||||
while PublicKey::new(keys.group_key()).is_none() {
|
||||
*keys = keys.offset(<Secp256k1 as Ciphersuite>::F::ONE);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
async fn external_address(&self, _key: <Secp256k1 as Ciphersuite>::G) -> Address {
|
||||
Address(self.router().await.as_ref().unwrap().address())
|
||||
}
|
||||
|
||||
fn branch_address(_key: <Secp256k1 as Ciphersuite>::G) -> Option<Address> {
|
||||
None
|
||||
}
|
||||
|
||||
fn change_address(_key: <Secp256k1 as Ciphersuite>::G) -> Option<Address> {
|
||||
None
|
||||
}
|
||||
|
||||
fn forward_address(_key: <Secp256k1 as Ciphersuite>::G) -> Option<Address> {
|
||||
None
|
||||
}
|
||||
|
||||
async fn get_latest_block_number(&self) -> Result<usize, NetworkError> {
|
||||
let actual_number = self
|
||||
.provider
|
||||
.get_block(BlockNumberOrTag::Finalized.into(), BlockTransactionsKind::Hashes)
|
||||
.await
|
||||
.map_err(|_| NetworkError::ConnectionError)?
|
||||
.ok_or(NetworkError::ConnectionError)?
|
||||
.header
|
||||
.number;
|
||||
// Error if there hasn't been a full epoch yet
|
||||
if actual_number < 32 {
|
||||
Err(NetworkError::ConnectionError)?
|
||||
}
|
||||
// If this is 33, the division will return 1, yet 1 is the epoch in progress
|
||||
let latest_full_epoch = (actual_number / 32).saturating_sub(1);
|
||||
Ok(latest_full_epoch.try_into().unwrap())
|
||||
}
|
||||
|
||||
async fn get_block(&self, number: usize) -> Result<Self::Block, NetworkError> {
|
||||
let latest_finalized = self.get_latest_block_number().await?;
|
||||
if number > latest_finalized {
|
||||
Err(NetworkError::ConnectionError)?
|
||||
}
|
||||
|
||||
let start = number * 32;
|
||||
let prior_end_hash = if start == 0 {
|
||||
[0; 32]
|
||||
} else {
|
||||
self
|
||||
.provider
|
||||
.get_block(u64::try_from(start - 1).unwrap().into(), BlockTransactionsKind::Hashes)
|
||||
.await
|
||||
.ok()
|
||||
.flatten()
|
||||
.ok_or(NetworkError::ConnectionError)?
|
||||
.header
|
||||
.hash
|
||||
.into()
|
||||
};
|
||||
|
||||
let end_header = self
|
||||
.provider
|
||||
.get_block(u64::try_from(start + 31).unwrap().into(), BlockTransactionsKind::Hashes)
|
||||
.await
|
||||
.ok()
|
||||
.flatten()
|
||||
.ok_or(NetworkError::ConnectionError)?
|
||||
.header;
|
||||
|
||||
let end_hash = end_header.hash.into();
|
||||
let time = end_header.timestamp;
|
||||
|
||||
Ok(Epoch { prior_end_hash, start: start.try_into().unwrap(), end_hash, time })
|
||||
}
|
||||
|
||||
async fn get_outputs(
|
||||
&self,
|
||||
block: &Self::Block,
|
||||
@@ -627,97 +280,6 @@ impl<D: Db> Network for Ethereum<D> {
|
||||
res
|
||||
}
|
||||
|
||||
async fn needed_fee(
|
||||
&self,
|
||||
_block_number: usize,
|
||||
inputs: &[Self::Output],
|
||||
_payments: &[Payment<Self>],
|
||||
_change: &Option<Self::Address>,
|
||||
) -> Result<Option<u64>, NetworkError> {
|
||||
assert_eq!(inputs.len(), 0);
|
||||
// Claim no fee is needed so we can perform amortization ourselves
|
||||
Ok(Some(0))
|
||||
}
|
||||
|
||||
async fn signable_transaction(
|
||||
&self,
|
||||
_block_number: usize,
|
||||
_plan_id: &[u8; 32],
|
||||
key: <Self::Curve as Ciphersuite>::G,
|
||||
inputs: &[Self::Output],
|
||||
payments: &[Payment<Self>],
|
||||
change: &Option<Self::Address>,
|
||||
scheduler_addendum: &<Self::Scheduler as SchedulerTrait<Self>>::Addendum,
|
||||
) -> Result<Option<(Self::SignableTransaction, Self::Eventuality)>, NetworkError> {
|
||||
assert_eq!(inputs.len(), 0);
|
||||
assert!(change.is_none());
|
||||
let chain_id = self.provider.get_chain_id().await.map_err(|_| NetworkError::ConnectionError)?;
|
||||
|
||||
// TODO: Perform fee amortization (in scheduler?
|
||||
// TODO: Make this function internal and have needed_fee properly return None as expected?
|
||||
// TODO: signable_transaction is written as cannot return None if needed_fee returns Some
|
||||
// TODO: Why can this return None at all if it isn't allowed to return None?
|
||||
|
||||
let command = match scheduler_addendum {
|
||||
Addendum::Nonce(nonce) => RouterCommand::Execute {
|
||||
chain_id: U256::try_from(chain_id).unwrap(),
|
||||
nonce: U256::try_from(*nonce).unwrap(),
|
||||
outs: payments
|
||||
.iter()
|
||||
.filter_map(|payment| {
|
||||
Some(OutInstruction {
|
||||
target: if let Some(data) = payment.data.as_ref() {
|
||||
// This introspects the Call serialization format, expecting the first 20 bytes to
|
||||
// be the address
|
||||
// This avoids wasting the 20-bytes allocated within address
|
||||
let full_data = [payment.address.0.as_slice(), data].concat();
|
||||
let mut reader = full_data.as_slice();
|
||||
|
||||
let mut calls = vec![];
|
||||
while !reader.is_empty() {
|
||||
calls.push(Call::read(&mut reader).ok()?)
|
||||
}
|
||||
// The above must have executed at least once since reader contains the address
|
||||
assert_eq!(calls[0].to, payment.address.0);
|
||||
|
||||
OutInstructionTarget::Calls(calls)
|
||||
} else {
|
||||
OutInstructionTarget::Direct(payment.address.0)
|
||||
},
|
||||
value: {
|
||||
assert_eq!(payment.balance.coin, Coin::Ether); // TODO
|
||||
balance_to_ethereum_amount(payment.balance)
|
||||
},
|
||||
})
|
||||
})
|
||||
.collect(),
|
||||
},
|
||||
Addendum::RotateTo { nonce, new_key } => {
|
||||
assert!(payments.is_empty());
|
||||
RouterCommand::UpdateSeraiKey {
|
||||
chain_id: U256::try_from(chain_id).unwrap(),
|
||||
nonce: U256::try_from(*nonce).unwrap(),
|
||||
key: PublicKey::new(*new_key).expect("new key wasn't a valid ETH public key"),
|
||||
}
|
||||
}
|
||||
};
|
||||
Ok(Some((
|
||||
command.clone(),
|
||||
Eventuality(PublicKey::new(key).expect("key wasn't a valid ETH public key"), command),
|
||||
)))
|
||||
}
|
||||
|
||||
async fn attempt_sign(
|
||||
&self,
|
||||
keys: ThresholdKeys<Self::Curve>,
|
||||
transaction: Self::SignableTransaction,
|
||||
) -> Result<Self::TransactionMachine, NetworkError> {
|
||||
Ok(
|
||||
RouterCommandMachine::new(keys, transaction)
|
||||
.expect("keys weren't usable to sign router commands"),
|
||||
)
|
||||
}
|
||||
|
||||
async fn publish_completion(
|
||||
&self,
|
||||
completion: &<Self::Eventuality as EventualityTrait>::Completion,
|
||||
@@ -725,32 +287,6 @@ impl<D: Db> Network for Ethereum<D> {
|
||||
// Publish this to the dedicated TX server for a solver to actually publish
|
||||
#[cfg(not(test))]
|
||||
{
|
||||
let mut msg = vec![];
|
||||
match completion.command() {
|
||||
RouterCommand::UpdateSeraiKey { nonce, .. } | RouterCommand::Execute { nonce, .. } => {
|
||||
msg.extend(&u32::try_from(nonce).unwrap().to_le_bytes());
|
||||
}
|
||||
}
|
||||
completion.write(&mut msg).unwrap();
|
||||
|
||||
let Ok(mut socket) = TcpStream::connect(&self.relayer_url).await else {
|
||||
log::warn!("couldn't connect to the relayer server");
|
||||
Err(NetworkError::ConnectionError)?
|
||||
};
|
||||
let Ok(()) = socket.write_all(&u32::try_from(msg.len()).unwrap().to_le_bytes()).await else {
|
||||
log::warn!("couldn't send the message's len to the relayer server");
|
||||
Err(NetworkError::ConnectionError)?
|
||||
};
|
||||
let Ok(()) = socket.write_all(&msg).await else {
|
||||
log::warn!("couldn't write the message to the relayer server");
|
||||
Err(NetworkError::ConnectionError)?
|
||||
};
|
||||
if socket.read_u8().await.ok() != Some(1) {
|
||||
log::warn!("didn't get the ack from the relayer server");
|
||||
Err(NetworkError::ConnectionError)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Publish this using a dummy account we fund with magic RPC commands
|
||||
@@ -938,3 +474,4 @@ impl<D: Db> Network for Ethereum<D> {
|
||||
self.get_block(self.get_latest_block_number().await.unwrap()).await.unwrap()
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user