Ethereum SignableTransaction, Eventuality

This commit is contained in:
Luke Parker 2024-09-18 00:54:20 -04:00
parent 8f2a9301cf
commit 433beac93a
11 changed files with 390 additions and 129 deletions

View file

@ -26,10 +26,18 @@ borsh = { version = "1", default-features = false, features = ["std", "derive",
ciphersuite = { path = "../../crypto/ciphersuite", default-features = false, features = ["std", "secp256k1"] } ciphersuite = { path = "../../crypto/ciphersuite", default-features = false, features = ["std", "secp256k1"] }
dkg = { path = "../../crypto/dkg", default-features = false, features = ["std", "evrf-secp256k1"] } dkg = { path = "../../crypto/dkg", default-features = false, features = ["std", "evrf-secp256k1"] }
frost = { package = "modular-frost", path = "../../crypto/frost", default-features = false } frost = { package = "modular-frost", path = "../../crypto/frost", default-features = false, features = ["secp256k1"] }
k256 = { version = "^0.13.1", default-features = false, features = ["std"] } k256 = { version = "^0.13.1", default-features = false, features = ["std"] }
ethereum-serai = { path = "./ethereum-serai", default-features = false, optional = true }
alloy-core = { version = "0.8", default-features = false }
alloy-rlp = { version = "0.3", default-features = false }
alloy-consensus = { version = "0.3", default-features = false }
alloy-rpc-types-eth = { version = "0.3", default-features = false }
alloy-simple-request-transport = { path = "../../networks/ethereum/alloy-simple-request-transport", default-features = false }
alloy-rpc-client = { version = "0.3", default-features = false }
alloy-provider = { version = "0.3", default-features = false }
serai-client = { path = "../../substrate/client", default-features = false, features = ["ethereum"] } serai-client = { path = "../../substrate/client", default-features = false, features = ["ethereum"] }
@ -48,6 +56,11 @@ scanner = { package = "serai-processor-scanner", path = "../scanner" }
smart-contract-scheduler = { package = "serai-processor-smart-contract-scheduler", path = "../scheduler/smart-contract" } smart-contract-scheduler = { package = "serai-processor-smart-contract-scheduler", path = "../scheduler/smart-contract" }
signers = { package = "serai-processor-signers", path = "../signers" } signers = { package = "serai-processor-signers", path = "../signers" }
ethereum-schnorr = { package = "ethereum-schnorr-contract", path = "../../networks/ethereum/schnorr" }
ethereum-primitives = { package = "serai-processor-ethereum-primitives", path = "./primitives" }
ethereum-router = { package = "serai-processor-ethereum-router", path = "./router" }
ethereum-erc20 = { package = "serai-processor-ethereum-erc20", path = "./erc20" }
bin = { package = "serai-processor-bin", path = "../bin" } bin = { package = "serai-processor-bin", path = "../bin" }
[features] [features]

View file

@ -24,7 +24,6 @@ alloy-core = { version = "0.8", default-features = false }
alloy-consensus = { version = "0.3", default-features = false } alloy-consensus = { version = "0.3", default-features = false }
alloy-sol-types = { version = "0.8", default-features = false } alloy-sol-types = { version = "0.8", default-features = false }
alloy-sol-macro = { version = "0.8", default-features = false }
alloy-rpc-types-eth = { version = "0.3", default-features = false } alloy-rpc-types-eth = { version = "0.3", default-features = false }
alloy-transport = { version = "0.3", default-features = false } alloy-transport = { version = "0.3", default-features = false }

View file

@ -27,7 +27,7 @@ use ethereum_schnorr::{PublicKey, Signature};
use ethereum_deployer::Deployer; use ethereum_deployer::Deployer;
use erc20::{Transfer, Erc20}; use erc20::{Transfer, Erc20};
use serai_client::{primitives::Amount, networks::ethereum::Address as SeraiAddress}; use serai_client::networks::ethereum::Address as SeraiAddress;
#[rustfmt::skip] #[rustfmt::skip]
#[expect(warnings)] #[expect(warnings)]
@ -159,8 +159,8 @@ impl InInstruction {
/// A list of `OutInstruction`s. /// A list of `OutInstruction`s.
#[derive(Clone)] #[derive(Clone)]
pub struct OutInstructions(Vec<abi::OutInstruction>); pub struct OutInstructions(Vec<abi::OutInstruction>);
impl From<&[(SeraiAddress, (Coin, Amount))]> for OutInstructions { impl From<&[(SeraiAddress, (Coin, U256))]> for OutInstructions {
fn from(outs: &[(SeraiAddress, (Coin, Amount))]) -> Self { fn from(outs: &[(SeraiAddress, (Coin, U256))]) -> Self {
Self( Self(
outs outs
.iter() .iter()
@ -184,7 +184,7 @@ impl From<&[(SeraiAddress, (Coin, Amount))]> for OutInstructions {
Coin::Ether => [0; 20].into(), Coin::Ether => [0; 20].into(),
Coin::Erc20(address) => address.into(), Coin::Erc20(address) => address.into(),
}, },
value: amount.0.try_into().expect("couldn't convert u64 to u256"), value: *amount,
} }
}) })
.collect(), .collect(),
@ -192,7 +192,7 @@ impl From<&[(SeraiAddress, (Coin, Amount))]> for OutInstructions {
} }
} }
/// Executed an command. /// An action which was executed by the Router.
#[derive(Clone, PartialEq, Eq, Debug)] #[derive(Clone, PartialEq, Eq, Debug)]
pub enum Executed { pub enum Executed {
/// Set a new key. /// Set a new key.
@ -218,6 +218,44 @@ impl Executed {
Executed::SetKey { nonce, .. } | Executed::Batch { nonce, .. } => *nonce, Executed::SetKey { nonce, .. } | Executed::Batch { nonce, .. } => *nonce,
} }
} }
/// Write the Executed.
pub fn write(&self, writer: &mut impl io::Write) -> io::Result<()> {
match self {
Self::SetKey { nonce, key } => {
writer.write_all(&[0])?;
writer.write_all(&nonce.to_le_bytes())?;
writer.write_all(key)
}
Self::Batch { nonce, message_hash } => {
writer.write_all(&[1])?;
writer.write_all(&nonce.to_le_bytes())?;
writer.write_all(message_hash)
}
}
}
/// Read an Executed.
pub fn read(reader: &mut impl io::Read) -> io::Result<Self> {
let mut kind = [0xff];
reader.read_exact(&mut kind)?;
if kind[0] >= 2 {
Err(io::Error::other("unrecognized type of Executed"))?;
}
let mut nonce = [0; 8];
reader.read_exact(&mut nonce)?;
let nonce = u64::from_le_bytes(nonce);
let mut payload = [0; 32];
reader.read_exact(&mut payload)?;
Ok(match kind[0] {
0 => Self::SetKey { nonce, key: payload },
1 => Self::Batch { nonce, message_hash: payload },
_ => unreachable!(),
})
}
} }
/// A view of the Router for Serai. /// A view of the Router for Serai.

View file

@ -1,7 +1,7 @@
use ciphersuite::{Ciphersuite, Secp256k1}; use ciphersuite::{Ciphersuite, Secp256k1};
use dkg::ThresholdKeys; use dkg::ThresholdKeys;
use ethereum_serai::crypto::PublicKey; use ethereum_schnorr::PublicKey;
pub(crate) struct KeyGenParams; pub(crate) struct KeyGenParams;
impl key_gen::KeyGenParams for KeyGenParams { impl key_gen::KeyGenParams for KeyGenParams {

View file

@ -8,12 +8,10 @@ static ALLOCATOR: zalloc::ZeroizingAlloc<std::alloc::System> =
use std::sync::Arc; use std::sync::Arc;
use ethereum_serai::alloy::{ use alloy_core::primitives::U256;
primitives::U256, use alloy_simple_request_transport::SimpleRequest;
simple_request_transport::SimpleRequest, use alloy_rpc_client::ClientBuilder;
rpc_client::ClientBuilder, use alloy_provider::{Provider, RootProvider};
provider::{Provider, RootProvider},
};
use serai_env as env; use serai_env as env;

View file

@ -5,6 +5,9 @@ use ciphersuite::{Ciphersuite, Secp256k1};
use serai_client::networks::ethereum::Address; use serai_client::networks::ethereum::Address;
use primitives::{ReceivedOutput, EventualityTracker}; use primitives::{ReceivedOutput, EventualityTracker};
use ethereum_router::Executed;
use crate::{output::Output, transaction::Eventuality}; use crate::{output::Output, transaction::Eventuality};
// We interpret 32-block Epochs as singular blocks. // We interpret 32-block Epochs as singular blocks.
@ -37,9 +40,11 @@ impl primitives::BlockHeader for Epoch {
} }
} }
#[derive(Clone, Copy, PartialEq, Eq, Debug)] #[derive(Clone, PartialEq, Eq, Debug)]
pub(crate) struct FullEpoch { pub(crate) struct FullEpoch {
epoch: Epoch, epoch: Epoch,
outputs: Vec<Output>,
executed: Vec<Executed>,
} }
impl primitives::Block for FullEpoch { impl primitives::Block for FullEpoch {
@ -54,7 +59,8 @@ impl primitives::Block for FullEpoch {
self.epoch.end_hash self.epoch.end_hash
} }
fn scan_for_outputs_unordered(&self, key: Self::Key) -> Vec<Self::Output> { fn scan_for_outputs_unordered(&self, _key: Self::Key) -> Vec<Self::Output> {
// Only return these outputs for the latest key
todo!("TODO") todo!("TODO")
} }
@ -66,6 +72,33 @@ impl primitives::Block for FullEpoch {
<Self::Output as ReceivedOutput<Self::Key, Self::Address>>::TransactionId, <Self::Output as ReceivedOutput<Self::Key, Self::Address>>::TransactionId,
Self::Eventuality, Self::Eventuality,
> { > {
todo!("TODO") let mut res = HashMap::new();
for executed in &self.executed {
let Some(expected) =
eventualities.active_eventualities.remove(executed.nonce().to_le_bytes().as_slice())
else {
continue;
};
assert_eq!(
executed,
&expected.0,
"Router emitted distinct event for nonce {}",
executed.nonce()
);
/*
The transaction ID is used to determine how internal outputs from this transaction should
be handled (if they were actually internal or if they were just to an internal address).
The Ethereum integration doesn't have internal addresses, and this transaction wasn't made
by Serai. It was simply authorized by Serai yet may or may not be associated with other
actions we don't want to flag as our own.
Accordingly, we set the transaction ID to the nonce. This is unique barring someone finding
the preimage which hashes to this nonce, and won't cause any other data to be associated.
*/
let mut tx_id = [0; 32];
tx_id[.. 8].copy_from_slice(executed.nonce().to_le_bytes().as_slice());
res.insert(tx_id, expected);
}
res
} }
} }

View file

@ -1,3 +1,9 @@
pub(crate) mod output; pub(crate) mod output;
pub(crate) mod transaction; pub(crate) mod transaction;
pub(crate) mod block; pub(crate) mod block;
pub(crate) const DAI: [u8; 20] =
match const_hex::const_decode_to_array(b"0x6B175474E89094C44Da98b954EedeAC495271d0F") {
Ok(res) => res,
Err(_) => panic!("invalid non-test DAI hex address"),
};

View file

@ -2,10 +2,7 @@ use std::io;
use ciphersuite::{Ciphersuite, Secp256k1}; use ciphersuite::{Ciphersuite, Secp256k1};
use ethereum_serai::{ use alloy_core::primitives::U256;
alloy::primitives::U256,
router::{Coin as EthereumCoin, InInstruction as EthereumInInstruction},
};
use scale::{Encode, Decode}; use scale::{Encode, Decode};
use borsh::{BorshSerialize, BorshDeserialize}; use borsh::{BorshSerialize, BorshDeserialize};
@ -16,12 +13,9 @@ use serai_client::{
}; };
use primitives::{OutputType, ReceivedOutput}; use primitives::{OutputType, ReceivedOutput};
use ethereum_router::{Coin as EthereumCoin, InInstruction as EthereumInInstruction};
const DAI: [u8; 20] = use crate::DAI;
match const_hex::const_decode_to_array(b"0x6B175474E89094C44Da98b954EedeAC495271d0F") {
Ok(res) => res,
Err(_) => panic!("invalid non-test DAI hex address"),
};
fn coin_to_serai_coin(coin: &EthereumCoin) -> Option<Coin> { fn coin_to_serai_coin(coin: &EthereumCoin) -> Option<Coin> {
match coin { match coin {
@ -87,7 +81,7 @@ impl ReceivedOutput<<Secp256k1 as Ciphersuite>::G, Address> for Output {
} }
fn key(&self) -> <Secp256k1 as Ciphersuite>::G { fn key(&self) -> <Secp256k1 as Ciphersuite>::G {
self.0.key_at_end_of_block todo!("TODO")
} }
fn presumed_origin(&self) -> Option<Address> { fn presumed_origin(&self) -> Option<Address> {

View file

@ -1,101 +1,304 @@
use std::io; use std::{io, collections::HashMap};
use rand_core::{RngCore, CryptoRng}; use rand_core::{RngCore, CryptoRng};
use ciphersuite::{group::GroupEncoding, Ciphersuite, Secp256k1}; use ciphersuite::{Ciphersuite, Secp256k1};
use frost::{dkg::ThresholdKeys, sign::PreprocessMachine}; use frost::{
dkg::{Participant, ThresholdKeys},
FrostError,
algorithm::*,
sign::*,
};
use ethereum_serai::{crypto::PublicKey, machine::*}; use alloy_core::primitives::U256;
use serai_client::networks::ethereum::Address;
use scheduler::SignableTransaction;
use ethereum_primitives::keccak256;
use ethereum_schnorr::{PublicKey, Signature};
use ethereum_router::{Coin, OutInstructions, Executed, Router};
use crate::output::OutputId; use crate::output::OutputId;
#[derive(Clone, Debug)] #[derive(Clone, PartialEq, Debug)]
pub(crate) struct Transaction(pub(crate) SignedRouterCommand); pub(crate) enum Action {
SetKey { chain_id: U256, nonce: u64, key: PublicKey },
Batch { chain_id: U256, nonce: u64, outs: Vec<(Address, (Coin, U256))> },
}
impl From<SignedRouterCommand> for Transaction { #[derive(Clone, PartialEq, Eq, Debug)]
fn from(signed_router_command: SignedRouterCommand) -> Self { pub(crate) struct Eventuality(pub(crate) Executed);
Self(signed_router_command)
impl Action {
fn nonce(&self) -> u64 {
match self {
Action::SetKey { nonce, .. } | Action::Batch { nonce, .. } => *nonce,
}
}
fn message(&self) -> Vec<u8> {
match self {
Action::SetKey { chain_id, nonce, key } => Router::update_serai_key_message(*chain_id, *nonce, key),
Action::Batch { chain_id, nonce, outs } => Router::execute_message(*chain_id, *nonce, OutInstructions::from(outs.as_ref())),
}
}
pub(crate) fn eventuality(&self) -> Eventuality {
Eventuality(match self {
Self::SetKey { chain_id: _, nonce, key } => {
Executed::SetKey { nonce: *nonce, key: key.eth_repr() }
}
Self::Batch { chain_id, nonce, outs } => Executed::Batch {
nonce: *nonce,
message_hash: keccak256(Router::execute_message(
*chain_id,
*nonce,
OutInstructions::from(outs.as_ref()),
)),
},
})
} }
} }
#[derive(Clone, PartialEq, Debug)]
pub(crate) struct Transaction(Action, Signature);
impl scheduler::Transaction for Transaction { impl scheduler::Transaction for Transaction {
fn read(reader: &mut impl io::Read) -> io::Result<Self> { fn read(reader: &mut impl io::Read) -> io::Result<Self> {
SignedRouterCommand::read(reader).map(Self) /*
let buf: Vec<u8> = borsh::from_reader(reader)?;
// We can only read this from a &[u8], hence prior reading into a Vec<u8>
<TxLegacy as alloy_rlp::Decodable>::decode(&mut buf.as_slice())
.map(Self)
.map_err(io::Error::other)
*/
let action = Action::read(reader)?;
let signature = Signature::read(reader)?;
Ok(Transaction(action, signature))
} }
fn write(&self, writer: &mut impl io::Write) -> io::Result<()> { fn write(&self, writer: &mut impl io::Write) -> io::Result<()> {
self.0.write(writer) /*
let mut buf = Vec::with_capacity(256);
<TxLegacy as alloy_rlp::Encodable>::encode(&self.0, &mut buf);
borsh::BorshSerialize::serialize(&buf, writer)
*/
self.0.write(writer)?;
self.1.write(writer)?;
Ok(())
} }
} }
#[derive(Clone, Debug)] /// The HRAm to use for the Schnorr Solidity library.
pub(crate) struct SignableTransaction(pub(crate) RouterCommand); ///
/// This will panic if the public key being signed for is not representable within the Schnorr
/// Solidity library.
#[derive(Clone, Default, Debug)]
pub struct EthereumHram;
impl Hram<Secp256k1> for EthereumHram {
#[allow(non_snake_case)]
fn hram(
R: &<Secp256k1 as Ciphersuite>::G,
A: &<Secp256k1 as Ciphersuite>::G,
m: &[u8],
) -> <Secp256k1 as Ciphersuite>::F {
Signature::challenge(*R, &PublicKey::new(*A).unwrap(), m)
}
}
#[derive(Clone)] #[derive(Clone)]
pub(crate) struct ClonableTransctionMachine(RouterCommand, ThresholdKeys<Secp256k1>); pub(crate) struct ClonableTransctionMachine(ThresholdKeys<Secp256k1>, Action);
type LiteralAlgorithmMachine = AlgorithmMachine<Secp256k1, IetfSchnorr<Secp256k1, EthereumHram>>;
type LiteralAlgorithmSignMachine =
AlgorithmSignMachine<Secp256k1, IetfSchnorr<Secp256k1, EthereumHram>>;
pub(crate) struct ActionSignMachine(PublicKey, Action, LiteralAlgorithmSignMachine);
type LiteralAlgorithmSignatureMachine =
AlgorithmSignatureMachine<Secp256k1, IetfSchnorr<Secp256k1, EthereumHram>>;
pub(crate) struct ActionSignatureMachine(PublicKey, Action, LiteralAlgorithmSignatureMachine);
impl PreprocessMachine for ClonableTransctionMachine { impl PreprocessMachine for ClonableTransctionMachine {
type Preprocess = <RouterCommandMachine as PreprocessMachine>::Preprocess; type Preprocess = <LiteralAlgorithmMachine as PreprocessMachine>::Preprocess;
type Signature = <RouterCommandMachine as PreprocessMachine>::Signature; type Signature = Transaction;
type SignMachine = <RouterCommandMachine as PreprocessMachine>::SignMachine; type SignMachine = ActionSignMachine;
fn preprocess<R: RngCore + CryptoRng>( fn preprocess<R: RngCore + CryptoRng>(
self, self,
rng: &mut R, rng: &mut R,
) -> (Self::SignMachine, Self::Preprocess) { ) -> (Self::SignMachine, Self::Preprocess) {
// TODO: Use a proper error here, not an Option let (machine, preprocess) = AlgorithmMachine::new(IetfSchnorr::<Secp256k1, EthereumHram>::ietf(), self.0.clone())
RouterCommandMachine::new(self.1.clone(), self.0.clone()).unwrap().preprocess(rng) .preprocess(rng);
(ActionSignMachine(PublicKey::new(self.0.group_key()).expect("signing with non-representable key"), self.1, machine), preprocess)
} }
} }
impl scheduler::SignableTransaction for SignableTransaction { impl SignMachine<Transaction> for ActionSignMachine {
type Params = <LiteralAlgorithmSignMachine as SignMachine<
<LiteralAlgorithmMachine as PreprocessMachine>::Signature,
>>::Params;
type Keys = <LiteralAlgorithmSignMachine as SignMachine<
<LiteralAlgorithmMachine as PreprocessMachine>::Signature,
>>::Keys;
type Preprocess = <LiteralAlgorithmSignMachine as SignMachine<
<LiteralAlgorithmMachine as PreprocessMachine>::Signature,
>>::Preprocess;
type SignatureShare = <LiteralAlgorithmSignMachine as SignMachine<
<LiteralAlgorithmMachine as PreprocessMachine>::Signature,
>>::SignatureShare;
type SignatureMachine = ActionSignatureMachine;
fn cache(self) -> CachedPreprocess {
unimplemented!()
}
fn from_cache(
params: Self::Params,
keys: Self::Keys,
cache: CachedPreprocess,
) -> (Self, Self::Preprocess) {
unimplemented!()
}
fn read_preprocess<R: io::Read>(&self, reader: &mut R) -> io::Result<Self::Preprocess> {
self.2.read_preprocess(reader)
}
fn sign(
self,
commitments: HashMap<Participant, Self::Preprocess>,
msg: &[u8],
) -> Result<(Self::SignatureMachine, Self::SignatureShare), FrostError> {
assert!(msg.is_empty());
self
.2
.sign(commitments, &self.1.message())
.map(|(machine, shares)| (ActionSignatureMachine(self.0, self.1, machine), shares))
}
}
impl SignatureMachine<Transaction> for ActionSignatureMachine {
type SignatureShare = <LiteralAlgorithmSignatureMachine as SignatureMachine<
<LiteralAlgorithmMachine as PreprocessMachine>::Signature,
>>::SignatureShare;
fn read_share<R: io::Read>(&self, reader: &mut R) -> io::Result<Self::SignatureShare> {
self.2.read_share(reader)
}
fn complete(
self,
shares: HashMap<Participant, Self::SignatureShare>,
) -> Result<Transaction, FrostError> {
/*
match self.1 {
Action::SetKey { chain_id: _, nonce: _, key } => self.0.update_serai_key(key, signature),
Action::Batch { chain_id: _, nonce: _, outs } => self.0.execute(outs, signature),
}
*/
self.2.complete(shares).map(|signature| {
let s = signature.s;
let c = Signature::challenge(signature.R, &self.0, &self.1.message());
Transaction(self.1, Signature::new(c, s))
})
}
}
impl SignableTransaction for Action {
type Transaction = Transaction; type Transaction = Transaction;
type Ciphersuite = Secp256k1; type Ciphersuite = Secp256k1;
type PreprocessMachine = ClonableTransctionMachine; type PreprocessMachine = ClonableTransctionMachine;
fn read(reader: &mut impl io::Read) -> io::Result<Self> { fn read(reader: &mut impl io::Read) -> io::Result<Self> {
RouterCommand::read(reader).map(Self) let mut kind = [0xff];
reader.read_exact(&mut kind)?;
if kind[0] >= 2 {
Err(io::Error::other("unrecognized Action type"))?;
}
let mut chain_id = [0; 32];
reader.read_exact(&mut chain_id)?;
let chain_id = U256::from_le_bytes(chain_id);
let mut nonce = [0; 8];
reader.read_exact(&mut nonce)?;
let nonce = u64::from_le_bytes(nonce);
Ok(match kind[0] {
0 => {
let mut key = [0; 32];
reader.read_exact(&mut key)?;
let key =
PublicKey::from_eth_repr(key).ok_or_else(|| io::Error::other("invalid key in Action"))?;
Action::SetKey { chain_id, nonce, key }
}
1 => {
let mut outs_len = [0; 4];
reader.read_exact(&mut outs_len)?;
let outs_len = usize::try_from(u32::from_le_bytes(outs_len)).unwrap();
let mut outs = vec![];
for _ in 0 .. outs_len {
let address = borsh::from_reader(reader)?;
let coin = Coin::read(reader)?;
let mut amount = [0; 32];
reader.read_exact(&mut amount)?;
let amount = U256::from_le_bytes(amount);
outs.push((address, (coin, amount)));
}
Action::Batch { chain_id, nonce, outs }
}
_ => unreachable!(),
})
} }
fn write(&self, writer: &mut impl io::Write) -> io::Result<()> { fn write(&self, writer: &mut impl io::Write) -> io::Result<()> {
self.0.write(writer) match self {
Self::SetKey { chain_id, nonce, key } => {
writer.write_all(&[0])?;
writer.write_all(&chain_id.as_le_bytes())?;
writer.write_all(&nonce.to_le_bytes())?;
writer.write_all(&key.eth_repr())
}
Self::Batch { chain_id, nonce, outs } => {
writer.write_all(&[1])?;
writer.write_all(&chain_id.as_le_bytes())?;
writer.write_all(&nonce.to_le_bytes())?;
writer.write_all(&u32::try_from(outs.len()).unwrap().to_le_bytes())?;
for (address, (coin, amount)) in outs {
borsh::BorshSerialize::serialize(address, writer)?;
coin.write(writer)?;
writer.write_all(&amount.as_le_bytes())?;
}
Ok(())
}
}
} }
fn id(&self) -> [u8; 32] { fn id(&self) -> [u8; 32] {
let mut res = [0; 32]; let mut res = [0; 32];
// TODO: Add getter for the nonce res[.. 8].copy_from_slice(&self.nonce().to_le_bytes());
match self.0 {
RouterCommand::UpdateSeraiKey { nonce, .. } | RouterCommand::Execute { nonce, .. } => {
res[.. 8].copy_from_slice(&nonce.as_le_bytes());
}
}
res res
} }
fn sign(self, keys: ThresholdKeys<Self::Ciphersuite>) -> Self::PreprocessMachine { fn sign(self, keys: ThresholdKeys<Self::Ciphersuite>) -> Self::PreprocessMachine {
ClonableTransctionMachine(self.0, keys) ClonableTransctionMachine(keys, self)
} }
} }
#[derive(Clone, PartialEq, Eq, Debug)]
pub(crate) struct Eventuality(pub(crate) PublicKey, pub(crate) RouterCommand);
impl primitives::Eventuality for Eventuality { impl primitives::Eventuality for Eventuality {
type OutputId = OutputId; type OutputId = OutputId;
fn id(&self) -> [u8; 32] { fn id(&self) -> [u8; 32] {
let mut res = [0; 32]; let mut res = [0; 32];
match self.1 { res[.. 8].copy_from_slice(&self.0.nonce().to_le_bytes());
RouterCommand::UpdateSeraiKey { nonce, .. } | RouterCommand::Execute { nonce, .. } => {
res[.. 8].copy_from_slice(&nonce.as_le_bytes());
}
}
res res
} }
fn lookup(&self) -> Vec<u8> { fn lookup(&self) -> Vec<u8> {
match self.1 { self.0.nonce().to_le_bytes().to_vec()
RouterCommand::UpdateSeraiKey { nonce, .. } | RouterCommand::Execute { nonce, .. } => {
nonce.as_le_bytes().to_vec()
}
}
} }
fn singular_spent_output(&self) -> Option<Self::OutputId> { fn singular_spent_output(&self) -> Option<Self::OutputId> {
@ -103,15 +306,9 @@ impl primitives::Eventuality for Eventuality {
} }
fn read(reader: &mut impl io::Read) -> io::Result<Self> { fn read(reader: &mut impl io::Read) -> io::Result<Self> {
let point = Secp256k1::read_G(reader)?; Executed::read(reader).map(Self)
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<()> { fn write(&self, writer: &mut impl io::Write) -> io::Result<()> {
writer.write_all(self.0.point().to_bytes().as_slice())?; self.0.write(writer)
self.1.write(writer)
} }
} }

View file

@ -1,13 +1,9 @@
use core::future::Future; use core::future::Future;
use std::sync::Arc; use std::sync::Arc;
use ethereum_serai::{ use alloy_rpc_types_eth::{BlockTransactionsKind, BlockNumberOrTag};
alloy::{ use alloy_simple_request_transport::SimpleRequest;
rpc_types::{BlockTransactionsKind, BlockNumberOrTag}, use alloy_provider::{Provider, RootProvider};
simple_request_transport::SimpleRequest,
provider::{Provider, RootProvider},
},
};
use serai_client::primitives::{NetworkId, Coin, Amount}; use serai_client::primitives::{NetworkId, Coin, Amount};

View file

@ -1,14 +1,23 @@
use serai_client::primitives::{NetworkId, Balance}; use alloy_core::primitives::U256;
use ethereum_serai::{alloy::primitives::U256, router::PublicKey, machine::*}; use serai_client::primitives::{NetworkId, Coin, Balance};
use primitives::Payment; use primitives::Payment;
use scanner::{KeyFor, AddressFor, EventualityFor}; use scanner::{KeyFor, AddressFor, EventualityFor};
use crate::{ use ethereum_schnorr::PublicKey;
transaction::{SignableTransaction, Eventuality}, use ethereum_router::Coin as EthereumCoin;
rpc::Rpc,
}; use crate::{DAI, transaction::Action, rpc::Rpc};
fn coin_to_ethereum_coin(coin: Coin) -> EthereumCoin {
assert_eq!(coin.network(), NetworkId::Ethereum);
match coin {
Coin::Ether => EthereumCoin::Ether,
Coin::Dai => EthereumCoin::Erc20(DAI),
_ => unreachable!(),
}
}
fn balance_to_ethereum_amount(balance: Balance) -> U256 { fn balance_to_ethereum_amount(balance: Balance) -> U256 {
assert_eq!(balance.coin.network(), NetworkId::Ethereum); assert_eq!(balance.coin.network(), NetworkId::Ethereum);
@ -24,7 +33,7 @@ pub(crate) struct SmartContract {
pub(crate) chain_id: U256, pub(crate) chain_id: U256,
} }
impl smart_contract_scheduler::SmartContract<Rpc> for SmartContract { impl smart_contract_scheduler::SmartContract<Rpc> for SmartContract {
type SignableTransaction = SignableTransaction; type SignableTransaction = Action;
fn rotate( fn rotate(
&self, &self,
@ -32,16 +41,14 @@ impl smart_contract_scheduler::SmartContract<Rpc> for SmartContract {
retiring_key: KeyFor<Rpc>, retiring_key: KeyFor<Rpc>,
new_key: KeyFor<Rpc>, new_key: KeyFor<Rpc>,
) -> (Self::SignableTransaction, EventualityFor<Rpc>) { ) -> (Self::SignableTransaction, EventualityFor<Rpc>) {
let command = RouterCommand::UpdateSeraiKey { let action = Action::SetKey {
chain_id: self.chain_id, chain_id: self.chain_id,
nonce: U256::try_from(nonce).unwrap(), nonce,
key: PublicKey::new(new_key).expect("rotating to an invald key"), key: PublicKey::new(new_key).expect("rotating to an invald key"),
}; };
( (action.clone(), action.eventuality())
SignableTransaction(command.clone()),
Eventuality(PublicKey::new(retiring_key).expect("retiring an invalid key"), command),
)
} }
fn fulfill( fn fulfill(
&self, &self,
nonce: u64, nonce: u64,
@ -50,40 +57,20 @@ impl smart_contract_scheduler::SmartContract<Rpc> for SmartContract {
) -> Vec<(Self::SignableTransaction, EventualityFor<Rpc>)> { ) -> Vec<(Self::SignableTransaction, EventualityFor<Rpc>)> {
let mut outs = Vec::with_capacity(payments.len()); let mut outs = Vec::with_capacity(payments.len());
for payment in payments { for payment in payments {
outs.push(OutInstruction { outs.push((
target: if let Some(data) = payment.data() { payment.address().clone(),
// This introspects the Call serialization format, expecting the first 20 bytes to (
// be the address coin_to_ethereum_coin(payment.balance().coin),
// This avoids wasting the 20-bytes allocated within address balance_to_ethereum_amount(payment.balance()),
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 { // TODO: Per-batch gas limit
chain_id: self.chain_id, // TODO: Create several batches
nonce: U256::try_from(nonce).unwrap(), let action = Action::Batch { chain_id: self.chain_id, nonce, outs };
outs,
};
vec![( vec![(action.clone(), action.eventuality())]
SignableTransaction(command.clone()),
Eventuality(PublicKey::new(key).expect("fulfilling payments with an invalid key"), command),
)]
} }
} }