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"] }
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"] }
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"] }
@ -48,6 +56,11 @@ scanner = { package = "serai-processor-scanner", path = "../scanner" }
smart-contract-scheduler = { package = "serai-processor-smart-contract-scheduler", path = "../scheduler/smart-contract" }
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" }
[features]

View file

@ -24,7 +24,6 @@ alloy-core = { version = "0.8", default-features = false }
alloy-consensus = { version = "0.3", 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-transport = { version = "0.3", default-features = false }

View file

@ -27,7 +27,7 @@ use ethereum_schnorr::{PublicKey, Signature};
use ethereum_deployer::Deployer;
use erc20::{Transfer, Erc20};
use serai_client::{primitives::Amount, networks::ethereum::Address as SeraiAddress};
use serai_client::networks::ethereum::Address as SeraiAddress;
#[rustfmt::skip]
#[expect(warnings)]
@ -159,8 +159,8 @@ impl InInstruction {
/// A list of `OutInstruction`s.
#[derive(Clone)]
pub struct OutInstructions(Vec<abi::OutInstruction>);
impl From<&[(SeraiAddress, (Coin, Amount))]> for OutInstructions {
fn from(outs: &[(SeraiAddress, (Coin, Amount))]) -> Self {
impl From<&[(SeraiAddress, (Coin, U256))]> for OutInstructions {
fn from(outs: &[(SeraiAddress, (Coin, U256))]) -> Self {
Self(
outs
.iter()
@ -184,7 +184,7 @@ impl From<&[(SeraiAddress, (Coin, Amount))]> for OutInstructions {
Coin::Ether => [0; 20].into(),
Coin::Erc20(address) => address.into(),
},
value: amount.0.try_into().expect("couldn't convert u64 to u256"),
value: *amount,
}
})
.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)]
pub enum Executed {
/// Set a new key.
@ -218,6 +218,44 @@ impl Executed {
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.

View file

@ -1,7 +1,7 @@
use ciphersuite::{Ciphersuite, Secp256k1};
use dkg::ThresholdKeys;
use ethereum_serai::crypto::PublicKey;
use ethereum_schnorr::PublicKey;
pub(crate) struct 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 ethereum_serai::alloy::{
primitives::U256,
simple_request_transport::SimpleRequest,
rpc_client::ClientBuilder,
provider::{Provider, RootProvider},
};
use alloy_core::primitives::U256;
use alloy_simple_request_transport::SimpleRequest;
use alloy_rpc_client::ClientBuilder;
use alloy_provider::{Provider, RootProvider};
use serai_env as env;

View file

@ -5,6 +5,9 @@ use ciphersuite::{Ciphersuite, Secp256k1};
use serai_client::networks::ethereum::Address;
use primitives::{ReceivedOutput, EventualityTracker};
use ethereum_router::Executed;
use crate::{output::Output, transaction::Eventuality};
// 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 {
epoch: Epoch,
outputs: Vec<Output>,
executed: Vec<Executed>,
}
impl primitives::Block for FullEpoch {
@ -54,7 +59,8 @@ impl primitives::Block for FullEpoch {
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")
}
@ -66,6 +72,33 @@ impl primitives::Block for FullEpoch {
<Self::Output as ReceivedOutput<Self::Key, Self::Address>>::TransactionId,
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 transaction;
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 ethereum_serai::{
alloy::primitives::U256,
router::{Coin as EthereumCoin, InInstruction as EthereumInInstruction},
};
use alloy_core::primitives::U256;
use scale::{Encode, Decode};
use borsh::{BorshSerialize, BorshDeserialize};
@ -16,12 +13,9 @@ use serai_client::{
};
use primitives::{OutputType, ReceivedOutput};
use ethereum_router::{Coin as EthereumCoin, InInstruction as EthereumInInstruction};
const DAI: [u8; 20] =
match const_hex::const_decode_to_array(b"0x6B175474E89094C44Da98b954EedeAC495271d0F") {
Ok(res) => res,
Err(_) => panic!("invalid non-test DAI hex address"),
};
use crate::DAI;
fn coin_to_serai_coin(coin: &EthereumCoin) -> Option<Coin> {
match coin {
@ -87,7 +81,7 @@ impl ReceivedOutput<<Secp256k1 as Ciphersuite>::G, Address> for Output {
}
fn key(&self) -> <Secp256k1 as Ciphersuite>::G {
self.0.key_at_end_of_block
todo!("TODO")
}
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 ciphersuite::{group::GroupEncoding, Ciphersuite, Secp256k1};
use frost::{dkg::ThresholdKeys, sign::PreprocessMachine};
use ciphersuite::{Ciphersuite, Secp256k1};
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;
#[derive(Clone, Debug)]
pub(crate) struct Transaction(pub(crate) SignedRouterCommand);
#[derive(Clone, PartialEq, Debug)]
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 {
fn from(signed_router_command: SignedRouterCommand) -> Self {
Self(signed_router_command)
#[derive(Clone, PartialEq, Eq, Debug)]
pub(crate) struct Eventuality(pub(crate) Executed);
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 {
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<()> {
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)]
pub(crate) struct SignableTransaction(pub(crate) RouterCommand);
/// The HRAm to use for the Schnorr Solidity library.
///
/// 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)]
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 {
type Preprocess = <RouterCommandMachine as PreprocessMachine>::Preprocess;
type Signature = <RouterCommandMachine as PreprocessMachine>::Signature;
type SignMachine = <RouterCommandMachine as PreprocessMachine>::SignMachine;
type Preprocess = <LiteralAlgorithmMachine as PreprocessMachine>::Preprocess;
type Signature = Transaction;
type SignMachine = ActionSignMachine;
fn preprocess<R: RngCore + CryptoRng>(
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)
let (machine, preprocess) = AlgorithmMachine::new(IetfSchnorr::<Secp256k1, EthereumHram>::ietf(), self.0.clone())
.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 Ciphersuite = Secp256k1;
type PreprocessMachine = ClonableTransctionMachine;
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<()> {
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] {
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[.. 8].copy_from_slice(&self.nonce().to_le_bytes());
res
}
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 {
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[.. 8].copy_from_slice(&self.0.nonce().to_le_bytes());
res
}
fn lookup(&self) -> Vec<u8> {
match self.1 {
RouterCommand::UpdateSeraiKey { nonce, .. } | RouterCommand::Execute { nonce, .. } => {
nonce.as_le_bytes().to_vec()
}
}
self.0.nonce().to_le_bytes().to_vec()
}
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> {
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,
))
Executed::read(reader).map(Self)
}
fn write(&self, writer: &mut impl io::Write) -> io::Result<()> {
writer.write_all(self.0.point().to_bytes().as_slice())?;
self.1.write(writer)
self.0.write(writer)
}
}

View file

@ -1,13 +1,9 @@
use core::future::Future;
use std::sync::Arc;
use ethereum_serai::{
alloy::{
rpc_types::{BlockTransactionsKind, BlockNumberOrTag},
simple_request_transport::SimpleRequest,
provider::{Provider, RootProvider},
},
};
use alloy_rpc_types_eth::{BlockTransactionsKind, BlockNumberOrTag};
use alloy_simple_request_transport::SimpleRequest;
use alloy_provider::{Provider, RootProvider};
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 scanner::{KeyFor, AddressFor, EventualityFor};
use crate::{
transaction::{SignableTransaction, Eventuality},
rpc::Rpc,
};
use ethereum_schnorr::PublicKey;
use ethereum_router::Coin as EthereumCoin;
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 {
assert_eq!(balance.coin.network(), NetworkId::Ethereum);
@ -24,7 +33,7 @@ pub(crate) struct SmartContract {
pub(crate) chain_id: U256,
}
impl smart_contract_scheduler::SmartContract<Rpc> for SmartContract {
type SignableTransaction = SignableTransaction;
type SignableTransaction = Action;
fn rotate(
&self,
@ -32,16 +41,14 @@ impl smart_contract_scheduler::SmartContract<Rpc> for SmartContract {
retiring_key: KeyFor<Rpc>,
new_key: KeyFor<Rpc>,
) -> (Self::SignableTransaction, EventualityFor<Rpc>) {
let command = RouterCommand::UpdateSeraiKey {
let action = Action::SetKey {
chain_id: self.chain_id,
nonce: U256::try_from(nonce).unwrap(),
nonce,
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),
)
(action.clone(), action.eventuality())
}
fn fulfill(
&self,
nonce: u64,
@ -50,40 +57,20 @@ impl smart_contract_scheduler::SmartContract<Rpc> for SmartContract {
) -> Vec<(Self::SignableTransaction, EventualityFor<Rpc>)> {
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()) },
});
outs.push((
payment.address().clone(),
(
coin_to_ethereum_coin(payment.balance().coin),
balance_to_ethereum_amount(payment.balance()),
),
));
}
let command = RouterCommand::Execute {
chain_id: self.chain_id,
nonce: U256::try_from(nonce).unwrap(),
outs,
};
// TODO: Per-batch gas limit
// TODO: Create several batches
let action = Action::Batch { chain_id: self.chain_id, nonce, outs };
vec![(
SignableTransaction(command.clone()),
Eventuality(PublicKey::new(key).expect("fulfilling payments with an invalid key"), command),
)]
vec![(action.clone(), action.eventuality())]
}
}