From 7761798a78fdb04114a1e8a01a17aa9b05429de1 Mon Sep 17 00:00:00 2001 From: Luke Parker Date: Sat, 14 Sep 2024 07:54:18 -0400 Subject: [PATCH] Outline the Ethereum processor This was only half-finished to begin with, unfortunately... --- Cargo.lock | 8 +- networks/ethereum/src/crypto.rs | 15 +- processor/ethereum/Cargo.toml | 5 +- processor/ethereum/src/key_gen.rs | 25 + processor/ethereum/src/lib.rs | 467 +----------------- processor/ethereum/src/main.rs | 65 +++ processor/ethereum/src/primitives/block.rs | 71 +++ processor/ethereum/src/primitives/mod.rs | 3 + processor/ethereum/src/primitives/output.rs | 123 +++++ .../ethereum/src/primitives/transaction.rs | 117 +++++ processor/ethereum/src/publisher.rs | 60 +++ processor/ethereum/src/rpc.rs | 135 +++++ processor/ethereum/src/scheduler.rs | 90 ++++ processor/monero/Cargo.toml | 5 - processor/scheduler/smart-contract/Cargo.toml | 2 - processor/scheduler/smart-contract/src/lib.rs | 88 ++-- substrate/client/Cargo.toml | 1 + substrate/client/src/networks/ethereum.rs | 51 ++ substrate/client/src/networks/mod.rs | 3 + 19 files changed, 810 insertions(+), 524 deletions(-) create mode 100644 processor/ethereum/src/key_gen.rs create mode 100644 processor/ethereum/src/main.rs create mode 100644 processor/ethereum/src/primitives/block.rs create mode 100644 processor/ethereum/src/primitives/mod.rs create mode 100644 processor/ethereum/src/primitives/output.rs create mode 100644 processor/ethereum/src/primitives/transaction.rs create mode 100644 processor/ethereum/src/publisher.rs create mode 100644 processor/ethereum/src/rpc.rs create mode 100644 processor/ethereum/src/scheduler.rs create mode 100644 substrate/client/src/networks/ethereum.rs diff --git a/Cargo.lock b/Cargo.lock index 147cc295..e98a8f34 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8351,9 +8351,9 @@ version = "0.1.0" dependencies = [ "borsh", "ciphersuite", + "const-hex", "dkg", "ethereum-serai", - "flexible-transcript", "hex", "k256", "log", @@ -8362,6 +8362,7 @@ dependencies = [ "rand_core", "serai-client", "serai-db", + "serai-env", "serai-processor-bin", "serai-processor-key-gen", "serai-processor-primitives", @@ -8522,11 +8523,8 @@ version = "0.1.0" dependencies = [ "borsh", "ciphersuite", - "curve25519-dalek", "dalek-ff-group", "dkg", - "flexible-transcript", - "hex", "log", "modular-frost", "monero-simple-request-rpc", @@ -8535,7 +8533,6 @@ dependencies = [ "rand_chacha", "rand_core", "serai-client", - "serai-db", "serai-processor-bin", "serai-processor-key-gen", "serai-processor-primitives", @@ -8796,7 +8793,6 @@ dependencies = [ "group", "parity-scale-codec", "serai-db", - "serai-primitives", "serai-processor-primitives", "serai-processor-scanner", "serai-processor-scheduler-primitives", diff --git a/networks/ethereum/src/crypto.rs b/networks/ethereum/src/crypto.rs index 6ea6a0b0..326343d8 100644 --- a/networks/ethereum/src/crypto.rs +++ b/networks/ethereum/src/crypto.rs @@ -1,10 +1,12 @@ use group::ff::PrimeField; use k256::{ - elliptic_curve::{ops::Reduce, point::AffineCoordinates, sec1::ToEncodedPoint}, - ProjectivePoint, Scalar, U256 as KU256, + elliptic_curve::{ + ops::Reduce, + point::{AffineCoordinates, DecompressPoint}, + sec1::ToEncodedPoint, + }, + AffinePoint, ProjectivePoint, Scalar, U256 as KU256, }; -#[cfg(test)] -use k256::{elliptic_curve::point::DecompressPoint, AffinePoint}; use frost::{ algorithm::{Hram, SchnorrSignature}, @@ -99,12 +101,11 @@ impl PublicKey { self.A } - pub(crate) fn eth_repr(&self) -> [u8; 32] { + pub fn eth_repr(&self) -> [u8; 32] { self.px.to_repr().into() } - #[cfg(test)] - pub(crate) fn from_eth_repr(repr: [u8; 32]) -> Option { + pub fn from_eth_repr(repr: [u8; 32]) -> Option { #[allow(non_snake_case)] let A = Option::::from(AffinePoint::decompress(&repr.into(), 0.into()))?.into(); Option::from(Scalar::from_repr(repr.into())).map(|px| PublicKey { A, px }) diff --git a/processor/ethereum/Cargo.toml b/processor/ethereum/Cargo.toml index ede9c71b..12f56d72 100644 --- a/processor/ethereum/Cargo.toml +++ b/processor/ethereum/Cargo.toml @@ -19,11 +19,11 @@ workspace = true [dependencies] rand_core = { version = "0.6", default-features = false } +const-hex = { version = "1", default-features = false, features = ["std"] } hex = { version = "0.4", default-features = false, features = ["std"] } scale = { package = "parity-scale-codec", version = "3", default-features = false, features = ["std"] } borsh = { version = "1", default-features = false, features = ["std", "derive", "de_strict_order"] } -transcript = { package = "flexible-transcript", path = "../../crypto/transcript", default-features = false, features = ["std", "recommended"] } ciphersuite = { path = "../../crypto/ciphersuite", default-features = false, features = ["std", "secp256k1"] } dkg = { path = "../../crypto/dkg", default-features = false, features = ["std", "evrf-secp256k1"] } frost = { package = "modular-frost", path = "../../crypto/frost", default-features = false } @@ -31,12 +31,13 @@ frost = { package = "modular-frost", path = "../../crypto/frost", default-featur k256 = { version = "^0.13.1", default-features = false, features = ["std"] } ethereum-serai = { path = "../../networks/ethereum", default-features = false, optional = true } -serai-client = { path = "../../substrate/client", default-features = false, features = ["bitcoin"] } +serai-client = { path = "../../substrate/client", default-features = false, features = ["ethereum"] } zalloc = { path = "../../common/zalloc" } log = { version = "0.4", default-features = false, features = ["std"] } tokio = { version = "1", default-features = false, features = ["rt-multi-thread", "sync", "time", "macros"] } +serai-env = { path = "../../common/env" } serai-db = { path = "../../common/db" } key-gen = { package = "serai-processor-key-gen", path = "../key-gen" } diff --git a/processor/ethereum/src/key_gen.rs b/processor/ethereum/src/key_gen.rs new file mode 100644 index 00000000..73b7c1e1 --- /dev/null +++ b/processor/ethereum/src/key_gen.rs @@ -0,0 +1,25 @@ +use ciphersuite::{Ciphersuite, Secp256k1}; +use dkg::ThresholdKeys; + +use ethereum_serai::crypto::PublicKey; + +pub(crate) struct KeyGenParams; +impl key_gen::KeyGenParams for KeyGenParams { + const ID: &'static str = "Ethereum"; + + type ExternalNetworkCiphersuite = Secp256k1; + + fn tweak_keys(keys: &mut ThresholdKeys) { + while PublicKey::new(keys.group_key()).is_none() { + *keys = keys.offset(::F::ONE); + } + } + + fn encode_key(key: ::G) -> Vec { + PublicKey::new(key).unwrap().eth_repr().to_vec() + } + + fn decode_key(key: &[u8]) -> Option<::G> { + PublicKey::from_eth_repr(key.try_into().ok()?).map(|key| key.point()) + } +} diff --git a/processor/ethereum/src/lib.rs b/processor/ethereum/src/lib.rs index 99d04203..a8f55c79 100644 --- a/processor/ethereum/src/lib.rs +++ b/processor/ethereum/src/lib.rs @@ -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 { - 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> for Address { - type Error = (); - fn try_from(bytes: Vec) -> Result { - if bytes.len() != 20 { - Err(())?; - } - let mut res = [0; 20]; - res.copy_from_slice(&bytes); - Ok(Address(res)) - } -} -impl TryInto> for Address { - type Error = (); - fn try_into(self) -> Result, ()> { - 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 TransactionTrait> for Transaction { - type Id = [u8; 32]; - fn id(&self) -> Self::Id { - self.hash.0 - } - - #[cfg(test)] - async fn fee(&self, _network: &Ethereum) -> 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 Block> 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) -> u64 { - self.time - } -} - -impl Output> 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) -> ::G { - self.key_at_end_of_block - } - - fn presumed_origin(&self) -> Option
{ - 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(&self, writer: &mut W) -> io::Result<()> { - EthereumInInstruction::write(self, writer) - } - fn read(reader: &mut R) -> io::Result { - 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 { - match self.1 { - RouterCommand::UpdateSeraiKey { nonce, .. } | RouterCommand::Execute { nonce, .. } => { - nonce.as_le_bytes().to_vec() - } - } - } - - fn read(reader: &mut R) -> io::Result { - 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 { - 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 { - let mut res = vec![]; - completion.write(&mut res).unwrap(); - res - } - fn read_completion(reader: &mut R) -> io::Result { - SignedRouterCommand::read(reader) - } -} - #[derive(Clone)] pub struct Ethereum { // 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 { deployer: Deployer, router: Arc>>, } -impl PartialEq for Ethereum { - fn eq(&self, _other: &Ethereum) -> bool { - true - } -} -impl fmt::Debug for Ethereum { - 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 Ethereum { pub async fn new(db: D, daemon_url: String, relayer_url: String) -> Self { let provider = Arc::new(RootProvider::new( @@ -384,110 +137,10 @@ impl Ethereum { #[async_trait] impl Network for Ethereum { - type Curve = Secp256k1; - - type Transaction = Transaction; - type Block = Epoch; - - type Output = EthereumInInstruction; - type SignableTransaction = RouterCommand; - type Eventuality = Eventuality; - type TransactionMachine = RouterCommandMachine; - - type Scheduler = Scheduler; - - 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) { - while PublicKey::new(keys.group_key()).is_none() { - *keys = keys.offset(::F::ONE); - } - } - - #[cfg(test)] - async fn external_address(&self, _key: ::G) -> Address { - Address(self.router().await.as_ref().unwrap().address()) - } - - fn branch_address(_key: ::G) -> Option
{ - None - } - - fn change_address(_key: ::G) -> Option
{ - None - } - - fn forward_address(_key: ::G) -> Option
{ - None - } - - async fn get_latest_block_number(&self) -> Result { - 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 { - 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 Network for Ethereum { res } - async fn needed_fee( - &self, - _block_number: usize, - inputs: &[Self::Output], - _payments: &[Payment], - _change: &Option, - ) -> Result, 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: ::G, - inputs: &[Self::Output], - payments: &[Payment], - change: &Option, - scheduler_addendum: &>::Addendum, - ) -> Result, 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, - transaction: Self::SignableTransaction, - ) -> Result { - Ok( - RouterCommandMachine::new(keys, transaction) - .expect("keys weren't usable to sign router commands"), - ) - } - async fn publish_completion( &self, completion: &::Completion, @@ -725,32 +287,6 @@ impl Network for Ethereum { // 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 Network for Ethereum { self.get_block(self.get_latest_block_number().await.unwrap()).await.unwrap() } } +*/ diff --git a/processor/ethereum/src/main.rs b/processor/ethereum/src/main.rs new file mode 100644 index 00000000..e4ec3701 --- /dev/null +++ b/processor/ethereum/src/main.rs @@ -0,0 +1,65 @@ +#![cfg_attr(docsrs, feature(doc_auto_cfg))] +#![doc = include_str!("../README.md")] +#![deny(missing_docs)] + +#[global_allocator] +static ALLOCATOR: zalloc::ZeroizingAlloc = + zalloc::ZeroizingAlloc(std::alloc::System); + +use std::sync::Arc; + +use ethereum_serai::alloy::{ + primitives::U256, + simple_request_transport::SimpleRequest, + rpc_client::ClientBuilder, + provider::{Provider, RootProvider}, +}; + +use serai_env as env; + +mod primitives; +pub(crate) use crate::primitives::*; + +mod key_gen; +use crate::key_gen::KeyGenParams; +mod rpc; +use rpc::Rpc; +mod scheduler; +use scheduler::{SmartContract, Scheduler}; +mod publisher; +use publisher::TransactionPublisher; + +#[tokio::main] +async fn main() { + let db = bin::init(); + let feed = { + let provider = Arc::new(RootProvider::new( + ClientBuilder::default().transport(SimpleRequest::new(bin::url()), true), + )); + Rpc { provider } + }; + let chain_id = loop { + match feed.provider.get_chain_id().await { + Ok(chain_id) => break U256::try_from(chain_id).unwrap(), + Err(e) => { + log::error!("couldn't connect to the Ethereum node for the chain ID: {e:?}"); + tokio::time::sleep(core::time::Duration::from_secs(5)).await; + } + } + }; + + bin::main_loop::<_, KeyGenParams, _>( + db, + feed.clone(), + Scheduler::new(SmartContract { chain_id }), + TransactionPublisher::new({ + let relayer_hostname = env::var("ETHEREUM_RELAYER_HOSTNAME") + .expect("ethereum relayer hostname wasn't specified") + .to_string(); + let relayer_port = + env::var("ETHEREUM_RELAYER_PORT").expect("ethereum relayer port wasn't specified"); + relayer_hostname + ":" + &relayer_port + }), + ) + .await; +} diff --git a/processor/ethereum/src/primitives/block.rs b/processor/ethereum/src/primitives/block.rs new file mode 100644 index 00000000..e947e851 --- /dev/null +++ b/processor/ethereum/src/primitives/block.rs @@ -0,0 +1,71 @@ +use std::collections::HashMap; + +use ciphersuite::{Ciphersuite, Secp256k1}; + +use serai_client::networks::ethereum::Address; + +use primitives::{ReceivedOutput, EventualityTracker}; +use crate::{output::Output, transaction::Eventuality}; + +// We interpret 32-block Epochs as singular blocks. +// There's no reason for further accuracy when these will all finalize at the same time. +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub(crate) struct Epoch { + // The hash of the block which ended the prior Epoch. + pub(crate) prior_end_hash: [u8; 32], + // The first block number within this Epoch. + pub(crate) start: u64, + // The hash of the last block within this Epoch. + pub(crate) end_hash: [u8; 32], + // The monotonic time for this Epoch. + pub(crate) time: u64, +} + +impl Epoch { + // The block number of the last block within this epoch. + fn end(&self) -> u64 { + self.start + 31 + } +} + +impl primitives::BlockHeader for Epoch { + fn id(&self) -> [u8; 32] { + self.end_hash + } + fn parent(&self) -> [u8; 32] { + self.prior_end_hash + } +} + +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub(crate) struct FullEpoch { + epoch: Epoch, +} + +impl primitives::Block for FullEpoch { + type Header = Epoch; + + type Key = ::G; + type Address = Address; + type Output = Output; + type Eventuality = Eventuality; + + fn id(&self) -> [u8; 32] { + self.epoch.end_hash + } + + fn scan_for_outputs_unordered(&self, key: Self::Key) -> Vec { + todo!("TODO") + } + + #[allow(clippy::type_complexity)] + fn check_for_eventuality_resolutions( + &self, + eventualities: &mut EventualityTracker, + ) -> HashMap< + >::TransactionId, + Self::Eventuality, + > { + todo!("TODO") + } +} diff --git a/processor/ethereum/src/primitives/mod.rs b/processor/ethereum/src/primitives/mod.rs new file mode 100644 index 00000000..fba52dd9 --- /dev/null +++ b/processor/ethereum/src/primitives/mod.rs @@ -0,0 +1,3 @@ +pub(crate) mod output; +pub(crate) mod transaction; +pub(crate) mod block; diff --git a/processor/ethereum/src/primitives/output.rs b/processor/ethereum/src/primitives/output.rs new file mode 100644 index 00000000..fcafae75 --- /dev/null +++ b/processor/ethereum/src/primitives/output.rs @@ -0,0 +1,123 @@ +use std::io; + +use ciphersuite::{Ciphersuite, Secp256k1}; + +use ethereum_serai::{ + alloy::primitives::U256, + router::{Coin as EthereumCoin, InInstruction as EthereumInInstruction}, +}; + +use scale::{Encode, Decode}; +use borsh::{BorshSerialize, BorshDeserialize}; + +use serai_client::{ + primitives::{NetworkId, Coin, Amount, Balance}, + networks::ethereum::Address, +}; + +use primitives::{OutputType, ReceivedOutput}; + +#[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 { + 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()) +} + +#[derive( + Clone, Copy, PartialEq, Eq, Hash, Debug, Encode, Decode, BorshSerialize, BorshDeserialize, +)] +pub(crate) struct OutputId(pub(crate) [u8; 40]); +impl Default for OutputId { + fn default() -> Self { + Self([0; 40]) + } +} +impl AsRef<[u8]> for OutputId { + fn as_ref(&self) -> &[u8] { + self.0.as_ref() + } +} +impl AsMut<[u8]> for OutputId { + fn as_mut(&mut self) -> &mut [u8] { + self.0.as_mut() + } +} + +#[derive(Clone, PartialEq, Eq, Debug)] +pub(crate) struct Output(pub(crate) EthereumInInstruction); +impl ReceivedOutput<::G, Address> for Output { + type Id = OutputId; + type TransactionId = [u8; 32]; + + // We only scan external outputs as we don't have branch/change/forwards + fn kind(&self) -> OutputType { + OutputType::External + } + + fn id(&self) -> Self::Id { + let mut id = [0; 40]; + id[.. 32].copy_from_slice(&self.0.id.0); + id[32 ..].copy_from_slice(&self.0.id.1.to_le_bytes()); + OutputId(id) + } + + fn transaction_id(&self) -> Self::TransactionId { + self.0.id.0 + } + + fn key(&self) -> ::G { + self.0.key_at_end_of_block + } + + fn presumed_origin(&self) -> Option
{ + Some(Address::from(self.0.from)) + } + + fn balance(&self) -> Balance { + let coin = coin_to_serai_coin(&self.0.coin).unwrap_or_else(|| { + panic!( + "mapping coin from an EthereumInInstruction with coin {}, which we don't handle.", + "this never should have been yielded" + ) + }); + Balance { coin, amount: amount_to_serai_amount(coin, self.0.amount) } + } + fn data(&self) -> &[u8] { + &self.0.data + } + + fn write(&self, writer: &mut W) -> io::Result<()> { + self.0.write(writer) + } + fn read(reader: &mut R) -> io::Result { + EthereumInInstruction::read(reader).map(Self) + } +} diff --git a/processor/ethereum/src/primitives/transaction.rs b/processor/ethereum/src/primitives/transaction.rs new file mode 100644 index 00000000..908358ec --- /dev/null +++ b/processor/ethereum/src/primitives/transaction.rs @@ -0,0 +1,117 @@ +use std::io; + +use rand_core::{RngCore, CryptoRng}; + +use ciphersuite::{group::GroupEncoding, Ciphersuite, Secp256k1}; +use frost::{dkg::ThresholdKeys, sign::PreprocessMachine}; + +use ethereum_serai::{crypto::PublicKey, machine::*}; + +use crate::output::OutputId; + +#[derive(Clone, Debug)] +pub(crate) struct Transaction(pub(crate) SignedRouterCommand); + +impl From for Transaction { + fn from(signed_router_command: SignedRouterCommand) -> Self { + Self(signed_router_command) + } +} + +impl scheduler::Transaction for Transaction { + fn read(reader: &mut impl io::Read) -> io::Result { + SignedRouterCommand::read(reader).map(Self) + } + fn write(&self, writer: &mut impl io::Write) -> io::Result<()> { + self.0.write(writer) + } +} + +#[derive(Clone, Debug)] +pub(crate) struct SignableTransaction(pub(crate) RouterCommand); + +#[derive(Clone)] +pub(crate) struct ClonableTransctionMachine(RouterCommand, ThresholdKeys); +impl PreprocessMachine for ClonableTransctionMachine { + type Preprocess = ::Preprocess; + type Signature = ::Signature; + type SignMachine = ::SignMachine; + + fn preprocess( + self, + rng: &mut R, + ) -> (Self::SignMachine, Self::Preprocess) { + // TODO: Use a proper error here, not an Option + RouterCommandMachine::new(self.1.clone(), self.0.clone()).unwrap().preprocess(rng) + } +} + +impl scheduler::SignableTransaction for SignableTransaction { + type Transaction = Transaction; + type Ciphersuite = Secp256k1; + type PreprocessMachine = ClonableTransctionMachine; + + fn read(reader: &mut impl io::Read) -> io::Result { + RouterCommand::read(reader).map(Self) + } + fn write(&self, writer: &mut impl io::Write) -> io::Result<()> { + self.0.write(writer) + } + + fn id(&self) -> [u8; 32] { + let mut res = [0; 32]; + // TODO: Add getter for the nonce + match self.0 { + RouterCommand::UpdateSeraiKey { nonce, .. } | RouterCommand::Execute { nonce, .. } => { + res[.. 8].copy_from_slice(&nonce.as_le_bytes()); + } + } + res + } + + fn sign(self, keys: ThresholdKeys) -> Self::PreprocessMachine { + ClonableTransctionMachine(self.0, keys) + } +} + +#[derive(Clone, PartialEq, Eq, Debug)] +pub(crate) struct Eventuality(pub(crate) PublicKey, pub(crate) RouterCommand); + +impl primitives::Eventuality for Eventuality { + type OutputId = OutputId; + + fn id(&self) -> [u8; 32] { + let mut res = [0; 32]; + match self.1 { + RouterCommand::UpdateSeraiKey { nonce, .. } | RouterCommand::Execute { nonce, .. } => { + res[.. 8].copy_from_slice(&nonce.as_le_bytes()); + } + } + res + } + + fn lookup(&self) -> Vec { + match self.1 { + RouterCommand::UpdateSeraiKey { nonce, .. } | RouterCommand::Execute { nonce, .. } => { + nonce.as_le_bytes().to_vec() + } + } + } + + fn singular_spent_output(&self) -> Option { + None + } + + fn read(reader: &mut impl io::Read) -> io::Result { + 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 write(&self, writer: &mut impl io::Write) -> io::Result<()> { + writer.write_all(self.0.point().to_bytes().as_slice())?; + self.1.write(writer) + } +} diff --git a/processor/ethereum/src/publisher.rs b/processor/ethereum/src/publisher.rs new file mode 100644 index 00000000..ad8bd09d --- /dev/null +++ b/processor/ethereum/src/publisher.rs @@ -0,0 +1,60 @@ +use core::future::Future; + +use crate::transaction::Transaction; + +#[derive(Clone)] +pub(crate) struct TransactionPublisher { + relayer_url: String, +} + +impl TransactionPublisher { + pub(crate) fn new(relayer_url: String) -> Self { + Self { relayer_url } + } +} + +impl signers::TransactionPublisher for TransactionPublisher { + type EphemeralError = (); + + fn publish( + &self, + tx: Transaction, + ) -> impl Send + Future> { + async move { + /* + use tokio::{ + io::{AsyncReadExt, AsyncWriteExt}, + net::TcpStream, + }; + + 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(()) + */ + todo!("TODO") + } + } +} diff --git a/processor/ethereum/src/rpc.rs b/processor/ethereum/src/rpc.rs new file mode 100644 index 00000000..58b3933e --- /dev/null +++ b/processor/ethereum/src/rpc.rs @@ -0,0 +1,135 @@ +use core::future::Future; +use std::sync::Arc; + +use ethereum_serai::{ + alloy::{ + rpc_types::{BlockTransactionsKind, BlockNumberOrTag}, + simple_request_transport::SimpleRequest, + provider::{Provider, RootProvider}, + }, +}; + +use serai_client::primitives::{NetworkId, Coin, Amount}; + +use scanner::ScannerFeed; + +use crate::block::{Epoch, FullEpoch}; + +#[derive(Clone)] +pub(crate) struct Rpc { + pub(crate) provider: Arc>, +} + +impl ScannerFeed for Rpc { + const NETWORK: NetworkId = NetworkId::Ethereum; + + // 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; + + type EphemeralError = String; + + fn latest_finalized_block_number( + &self, + ) -> impl Send + Future> { + async move { + let actual_number = self + .provider + .get_block(BlockNumberOrTag::Finalized.into(), BlockTransactionsKind::Hashes) + .await + .map_err(|e| format!("couldn't get the latest finalized block: {e:?}"))? + .ok_or_else(|| "there was no finalized block".to_string())? + .header + .number; + // Error if there hasn't been a full epoch yet + if actual_number < 32 { + Err("there has not been a completed epoch yet".to_string())? + } + // 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> { + async move { todo!("TODO") } + } + + fn unchecked_block_header_by_number( + &self, + number: u64, + ) -> impl Send + + Future::Header, Self::EphemeralError>> + { + async move { + let start = number * 32; + let prior_end_hash = if start == 0 { + [0; 32] + } else { + self + .provider + .get_block((start - 1).into(), BlockTransactionsKind::Hashes) + .await + .map_err(|e| format!("couldn't get block: {e:?}"))? + .ok_or_else(|| { + format!("ethereum node didn't have requested block: {number:?}. did we reorg?") + })? + .header + .hash + .into() + }; + + let end_header = self + .provider + .get_block((start + 31).into(), BlockTransactionsKind::Hashes) + .await + .map_err(|e| format!("couldn't get block: {e:?}"))? + .ok_or_else(|| { + format!("ethereum node didn't have requested block: {number:?}. did we reorg?") + })? + .header; + + let end_hash = end_header.hash.into(); + let time = end_header.timestamp; + + Ok(Epoch { prior_end_hash, start, end_hash, time }) + } + } + + #[rustfmt::skip] // It wants to improperly format the `async move` to a single line + fn unchecked_block_by_number( + &self, + number: u64, + ) -> impl Send + Future> { + async move { + todo!("TODO") + } + } + + fn dust(coin: Coin) -> Amount { + assert_eq!(coin.network(), NetworkId::Ethereum); + todo!("TODO") + } + + fn cost_to_aggregate( + &self, + coin: Coin, + _reference_block: &Self::Block, + ) -> impl Send + Future> { + async move { + assert_eq!(coin.network(), NetworkId::Ethereum); + // TODO + Ok(Amount(0)) + } + } +} diff --git a/processor/ethereum/src/scheduler.rs b/processor/ethereum/src/scheduler.rs new file mode 100644 index 00000000..6e17ef70 --- /dev/null +++ b/processor/ethereum/src/scheduler.rs @@ -0,0 +1,90 @@ +use serai_client::primitives::{NetworkId, Balance}; + +use ethereum_serai::{alloy::primitives::U256, router::PublicKey, machine::*}; + +use primitives::Payment; +use scanner::{KeyFor, AddressFor, EventualityFor}; + +use crate::{ + transaction::{SignableTransaction, Eventuality}, + rpc::Rpc, +}; + +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 + // TODO: Document the expectation all integrated coins have 18 decimals + let factor = U256::from(10_000_000_000u64); + U256::from(balance.amount.0) * factor +} + +#[derive(Clone)] +pub(crate) struct SmartContract { + pub(crate) chain_id: U256, +} +impl smart_contract_scheduler::SmartContract for SmartContract { + type SignableTransaction = SignableTransaction; + + fn rotate( + &self, + nonce: u64, + retiring_key: KeyFor, + new_key: KeyFor, + ) -> (Self::SignableTransaction, EventualityFor) { + let command = RouterCommand::UpdateSeraiKey { + chain_id: self.chain_id, + nonce: U256::try_from(nonce).unwrap(), + key: PublicKey::new(new_key).expect("rotating to an invald key"), + }; + ( + SignableTransaction(command.clone()), + Eventuality(PublicKey::new(retiring_key).expect("retiring an invalid key"), command), + ) + } + fn fulfill( + &self, + nonce: u64, + key: KeyFor, + payments: Vec>>, + ) -> Vec<(Self::SignableTransaction, EventualityFor)> { + let mut outs = Vec::with_capacity(payments.len()); + for payment in payments { + outs.push(OutInstruction { + target: if let Some(data) = payment.data() { + // 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 = [<[u8; 20]>::from(*payment.address()).as_slice(), data].concat(); + let mut reader = full_data.as_slice(); + + let mut calls = vec![]; + while !reader.is_empty() { + let Ok(call) = Call::read(&mut reader) else { break }; + calls.push(call); + } + // The above must have executed at least once since reader contains the address + assert_eq!(calls[0].to, <[u8; 20]>::from(*payment.address())); + + OutInstructionTarget::Calls(calls) + } else { + OutInstructionTarget::Direct((*payment.address()).into()) + }, + value: { balance_to_ethereum_amount(payment.balance()) }, + }); + } + + let command = RouterCommand::Execute { + chain_id: self.chain_id, + nonce: U256::try_from(nonce).unwrap(), + outs, + }; + + vec![( + SignableTransaction(command.clone()), + Eventuality(PublicKey::new(key).expect("fulfilling payments with an invalid key"), command), + )] + } +} + +pub(crate) type Scheduler = smart_contract_scheduler::Scheduler; diff --git a/processor/monero/Cargo.toml b/processor/monero/Cargo.toml index cc895eda..6ea49a0c 100644 --- a/processor/monero/Cargo.toml +++ b/processor/monero/Cargo.toml @@ -21,12 +21,9 @@ rand_core = { version = "0.6", default-features = false } rand_chacha = { version = "0.3", default-features = false, features = ["std"] } zeroize = { version = "1", default-features = false, features = ["std"] } -hex = { version = "0.4", default-features = false, features = ["std"] } scale = { package = "parity-scale-codec", version = "3", default-features = false, features = ["std"] } borsh = { version = "1", default-features = false, features = ["std", "derive", "de_strict_order"] } -transcript = { package = "flexible-transcript", path = "../../crypto/transcript", default-features = false, features = ["std", "recommended"] } -curve25519-dalek = { version = "4", default-features = false, features = ["alloc", "zeroize"] } dalek-ff-group = { path = "../../crypto/dalek-ff-group", default-features = false, features = ["std"] } ciphersuite = { path = "../../crypto/ciphersuite", default-features = false, features = ["std", "ed25519"] } dkg = { path = "../../crypto/dkg", default-features = false, features = ["std", "evrf-ed25519"] } @@ -41,8 +38,6 @@ zalloc = { path = "../../common/zalloc" } log = { version = "0.4", default-features = false, features = ["std"] } tokio = { version = "1", default-features = false, features = ["rt-multi-thread", "sync", "time", "macros"] } -serai-db = { path = "../../common/db" } - key-gen = { package = "serai-processor-key-gen", path = "../key-gen" } view-keys = { package = "serai-processor-view-keys", path = "../view-keys" } diff --git a/processor/scheduler/smart-contract/Cargo.toml b/processor/scheduler/smart-contract/Cargo.toml index 69ce9840..c43569fb 100644 --- a/processor/scheduler/smart-contract/Cargo.toml +++ b/processor/scheduler/smart-contract/Cargo.toml @@ -25,8 +25,6 @@ group = { version = "0.13", default-features = false } scale = { package = "parity-scale-codec", version = "3", default-features = false, features = ["std"] } borsh = { version = "1", default-features = false, features = ["std", "derive", "de_strict_order"] } -serai-primitives = { path = "../../../substrate/primitives", default-features = false, features = ["std"] } - serai-db = { path = "../../../common/db" } primitives = { package = "serai-processor-primitives", path = "../../primitives" } diff --git a/processor/scheduler/smart-contract/src/lib.rs b/processor/scheduler/smart-contract/src/lib.rs index 091ffe6a..7630a026 100644 --- a/processor/scheduler/smart-contract/src/lib.rs +++ b/processor/scheduler/smart-contract/src/lib.rs @@ -29,49 +29,61 @@ pub trait SmartContract: 'static + Send { /// Rotate from the retiring key to the new key. fn rotate( + &self, nonce: u64, retiring_key: KeyFor, new_key: KeyFor, ) -> (Self::SignableTransaction, EventualityFor); + /// Fulfill the set of payments, dropping any not worth handling. fn fulfill( + &self, starting_nonce: u64, + key: KeyFor, payments: Vec>>, ) -> Vec<(Self::SignableTransaction, EventualityFor)>; } /// A scheduler for a smart contract representing the Serai processor. #[allow(non_snake_case)] -#[derive(Clone, Default)] -pub struct Scheduler> { +#[derive(Clone)] +pub struct Scheduler> { + smart_contract: SC, _S: PhantomData, - _SC: PhantomData, } -fn fulfill_payments>( - txn: &mut impl DbTxn, - active_keys: &[(KeyFor, LifetimeStage)], - payments: Vec>>, -) -> KeyScopedEventualities { - let key = match active_keys[0].1 { - LifetimeStage::ActiveYetNotReporting | - LifetimeStage::Active | - LifetimeStage::UsingNewForChange => active_keys[0].0, - LifetimeStage::Forwarding | LifetimeStage::Finishing => active_keys[1].0, - }; - - let mut nonce = NextNonce::get(txn).unwrap_or(0); - let mut eventualities = Vec::with_capacity(1); - for (signable, eventuality) in SC::fulfill(nonce, payments) { - TransactionsToSign::::send(txn, &key, &signable); - nonce += 1; - eventualities.push(eventuality); +impl> Scheduler { + /// Create a new scheduler. + pub fn new(smart_contract: SC) -> Self { + Self { smart_contract, _S: PhantomData } + } + + fn fulfill_payments( + &self, + txn: &mut impl DbTxn, + active_keys: &[(KeyFor, LifetimeStage)], + payments: Vec>>, + ) -> KeyScopedEventualities { + let key = match active_keys[0].1 { + LifetimeStage::ActiveYetNotReporting | + LifetimeStage::Active | + LifetimeStage::UsingNewForChange => active_keys[0].0, + LifetimeStage::Forwarding | LifetimeStage::Finishing => active_keys[1].0, + }; + + let mut nonce = NextNonce::get(txn).unwrap_or(0); + let mut eventualities = Vec::with_capacity(1); + for (signable, eventuality) in self.smart_contract.fulfill(nonce, key, payments) { + TransactionsToSign::::send(txn, &key, &signable); + nonce += 1; + eventualities.push(eventuality); + } + NextNonce::set(txn, &nonce); + HashMap::from([(key.to_bytes().as_ref().to_vec(), eventualities)]) } - NextNonce::set(txn, &nonce); - HashMap::from([(key.to_bytes().as_ref().to_vec(), eventualities)]) } -impl> SchedulerTrait for Scheduler { +impl> SchedulerTrait for Scheduler { type EphemeralError = (); type SignableTransaction = SC::SignableTransaction; @@ -86,7 +98,7 @@ impl> SchedulerTrait for Scheduler impl Send + Future, Self::EphemeralError>> { async move { let nonce = NextNonce::get(txn).unwrap_or(0); - let (signable, eventuality) = SC::rotate(nonce, retiring_key, new_key); + let (signable, eventuality) = self.smart_contract.rotate(nonce, retiring_key, new_key); NextNonce::set(txn, &(nonce + 1)); TransactionsToSign::::send(txn, &retiring_key, &signable); Ok(HashMap::from([(retiring_key.to_bytes().as_ref().to_vec(), vec![eventuality])])) @@ -110,17 +122,19 @@ impl> SchedulerTrait for Scheduler( - txn, - active_keys, - update - .returns() - .iter() - .map(|to_return| { - Payment::new(to_return.address().clone(), to_return.output().balance(), None) - }) - .collect::>(), - )) + Ok( + self.fulfill_payments( + txn, + active_keys, + update + .returns() + .iter() + .map(|to_return| { + Payment::new(to_return.address().clone(), to_return.output().balance(), None) + }) + .collect::>(), + ), + ) } } @@ -131,6 +145,6 @@ impl> SchedulerTrait for Scheduler, LifetimeStage)], payments: Vec>>, ) -> impl Send + Future, Self::EphemeralError>> { - async move { Ok(fulfill_payments::(txn, active_keys, payments)) } + async move { Ok(self.fulfill_payments(txn, active_keys, payments)) } } } diff --git a/substrate/client/Cargo.toml b/substrate/client/Cargo.toml index 5f7a24d4..33bfabf9 100644 --- a/substrate/client/Cargo.toml +++ b/substrate/client/Cargo.toml @@ -65,6 +65,7 @@ borsh = ["serai-abi/borsh"] networks = [] bitcoin = ["networks", "dep:bitcoin"] +ethereum = ["networks"] monero = ["networks", "ciphersuite/ed25519", "monero-address"] # Assumes the default usage is to use Serai as a DEX, which doesn't actually diff --git a/substrate/client/src/networks/ethereum.rs b/substrate/client/src/networks/ethereum.rs new file mode 100644 index 00000000..09285169 --- /dev/null +++ b/substrate/client/src/networks/ethereum.rs @@ -0,0 +1,51 @@ +use core::{str::FromStr, fmt}; + +use borsh::{BorshSerialize, BorshDeserialize}; + +use crate::primitives::ExternalAddress; + +/// A representation of an Ethereum address. +#[derive(Clone, Copy, PartialEq, Eq, Debug, BorshSerialize, BorshDeserialize)] +pub struct Address([u8; 20]); + +impl From<[u8; 20]> for Address { + fn from(address: [u8; 20]) -> Self { + Self(address) + } +} + +impl From
for [u8; 20] { + fn from(address: Address) -> Self { + address.0 + } +} + +impl TryFrom for Address { + type Error = (); + fn try_from(data: ExternalAddress) -> Result { + Ok(Self(data.as_ref().try_into().map_err(|_| ())?)) + } +} +impl From
for ExternalAddress { + fn from(address: Address) -> ExternalAddress { + // This is 20 bytes which is less than MAX_ADDRESS_LEN + ExternalAddress::new(address.0.to_vec()).unwrap() + } +} + +impl FromStr for Address { + type Err = (); + fn from_str(str: &str) -> Result { + let Some(address) = str.strip_prefix("0x") else { Err(())? }; + if address.len() != 40 { + Err(())? + }; + Ok(Self(hex::decode(address.to_lowercase()).map_err(|_| ())?.try_into().unwrap())) + } +} + +impl fmt::Display for Address { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "0x{}", hex::encode(self.0)) + } +} diff --git a/substrate/client/src/networks/mod.rs b/substrate/client/src/networks/mod.rs index 63ebf481..7a99631a 100644 --- a/substrate/client/src/networks/mod.rs +++ b/substrate/client/src/networks/mod.rs @@ -1,5 +1,8 @@ #[cfg(feature = "bitcoin")] pub mod bitcoin; +#[cfg(feature = "ethereum")] +pub mod ethereum; + #[cfg(feature = "monero")] pub mod monero;