Skip to content

Commit

Permalink
add support of calculating gas for reply
Browse files Browse the repository at this point in the history
  • Loading branch information
gshep committed Sep 16, 2024
1 parent d550ee1 commit 57bc017
Show file tree
Hide file tree
Showing 8 changed files with 299 additions and 53 deletions.
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions gear-programs/erc20-relay/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,4 @@ sails-idl-gen = { git = "https://github.com/gear-tech/sails.git", rev = "aab0873

[features]
wasm-binary = []
gas_calculation = ["erc20-relay-app/gas_calculation"]
8 changes: 8 additions & 0 deletions gear-programs/erc20-relay/app/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,11 @@ gstd.workspace = true
sails-rs = { git = "https://github.com/gear-tech/sails.git", rev = "aab08733ee9ccdb809dc9b29c57dd411b2b917b1", features = ["gclient"] }
tokio = { workspace = true, features = ["rt", "macros"] }
hex-literal.workspace = true
hex.workspace = true

[build-dependencies]
sails-client-gen = { git = "https://github.com/gear-tech/sails.git", rev = "aab08733ee9ccdb809dc9b29c57dd411b2b917b1" }

[features]
gas_calculation = []
mocks = []
16 changes: 16 additions & 0 deletions gear-programs/erc20-relay/app/build.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
use sails_client_gen::ClientGenerator;
use std::{env, path::PathBuf};

fn main() {
let mut path = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap());
path.pop();
path.pop();

let idl_file_path = path.join("vft-gateway/src/wasm/vft-gateway.idl");

// Generate client code from IDL file
ClientGenerator::from_idl_path(&idl_file_path)
.with_mocks("mocks")
.generate_to(PathBuf::from(env::var("OUT_DIR").unwrap()).join("vft-gateway.rs"))
.unwrap();
}
3 changes: 2 additions & 1 deletion gear-programs/erc20-relay/app/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,6 @@ pub enum Error {
InvalidBlockProof,
TrieDbFailure,
InvalidReceiptProof,
InvalidAmount,
ReplyTimeout,
ReplyHook,
}
62 changes: 51 additions & 11 deletions gear-programs/erc20-relay/app/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ use abi::ERC20_TREASURY;
use alloy_sol_types::SolEvent;
use cell::RefCell;
use checkpoint_light_client_io::{Handle, HandleResult};
use cmp::Ordering;
use collections::BTreeSet;
use error::Error;
use ethereum_common::{
beacon::{light::Block as LightBeaconBlock, BlockHeader as BeaconBlockHeader},
Expand All @@ -18,17 +18,30 @@ use ethereum_common::{
trie_db::{HashDB, Trie},
utils as eth_utils,
utils::ReceiptEnvelope,
H160, H256,
H160, H256, U256,
};
use sails_rs::{
gstd::{debug, msg, ExecContext, GStdExecContext},
gstd::{msg, ExecContext, GStdExecContext},
prelude::*,
};
use services::{Erc20Relay as Erc20RelayService, FTManage as FTManageService};

const CAPACITY: usize = 500_000;
const CAPACITY_MAP: usize = 100;

#[cfg(feature = "gas_calculation")]
const CAPACITY_STEP_SIZE: usize = 50_000;

static mut TRANSACTIONS: Option<BTreeSet<(u64, u64)>> = None;

fn transactions_mut() -> &'static mut BTreeSet<(u64, u64)> {
unsafe {
TRANSACTIONS
.as_mut()
.expect("Program should be constructed")
}
}

#[derive(Clone, Debug, Decode, TypeInfo)]
#[codec(crate = sails_rs::scale_codec)]
#[scale_info(crate = sails_rs::scale_info)]
Expand All @@ -51,28 +64,55 @@ pub struct State {
admin: ActorId,
map: Vec<(H160, ActorId)>,
checkpoints: ActorId,
// vft: ActorId,
// (slot, transaction_index)
transactions: Vec<(u64, u64)>,
vft: ActorId,
reply_timeout: u32,
reply_deposit: u64,
}

pub struct Erc20RelayProgram(RefCell<State>);

#[sails_rs::program]
impl Erc20RelayProgram {
pub fn new(checkpoints: ActorId, _vft: ActorId) -> Self {
pub fn new(checkpoints: ActorId, vft: ActorId, reply_timeout: u32, reply_deposit: u64) -> Self {
unsafe {
TRANSACTIONS = Some(BTreeSet::new());
}

let exec_context = GStdExecContext::new();
Self(RefCell::new(State {
admin: exec_context.actor_id(),
map: Vec::with_capacity(CAPACITY_MAP),
checkpoints,
// vft,
transactions: Vec::with_capacity(CAPACITY),
vft,
reply_timeout,
reply_deposit,
}))
}

pub fn erc20_relay(&self) -> Erc20RelayService {
Erc20RelayService::new(&self.0)
pub fn gas_calculation(_reply_timeout: u32, _reply_deposit: u64) -> Self {
#[cfg(feature = "gas_calculation")]
{
let self_ = Self::new(
Default::default(),
Default::default(),
_reply_timeout,
_reply_deposit,
);

let transactions = transactions_mut();
for i in 0..CAPACITY_STEP_SIZE {
transactions.insert((0, i as u64));
}

self_
}

#[cfg(not(feature = "gas_calculation"))]
panic!("Please rebuild with enabled `gas_calculation` feature")
}

pub fn erc20_relay(&self) -> Erc20RelayService<GStdExecContext> {
Erc20RelayService::new(&self.0, GStdExecContext::new())
}

pub fn ft_manage(&self) -> FTManageService<GStdExecContext> {
Expand Down
174 changes: 135 additions & 39 deletions gear-programs/erc20-relay/app/src/services/erc20_relay.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
// Incorporate code generated based on the IDL file
#[allow(dead_code)]
mod vft {
include!(concat!(env!("OUT_DIR"), "/vft-gateway.rs"));
}

use super::*;
use ops::ControlFlow::*;
use sails_rs::{calls::ActionIo, gstd};
use vft::vft_gateway;

#[derive(Encode, TypeInfo)]
#[codec(crate = sails_rs::scale_codec)]
Expand All @@ -8,21 +16,29 @@ enum Event {
Relayed {
fungible_token: ActorId,
to: ActorId,
amount: u128,
amount: U256,
},
}

pub struct Erc20Relay<'a>(&'a RefCell<State>);
pub struct Erc20Relay<'a, ExecContext> {
state: &'a RefCell<State>,
exec_context: ExecContext,

Check failure on line 25 in gear-programs/erc20-relay/app/src/services/erc20_relay.rs

View workflow job for this annotation

GitHub Actions / lints

field `exec_context` is never read

Check failure on line 25 in gear-programs/erc20-relay/app/src/services/erc20_relay.rs

View workflow job for this annotation

GitHub Actions / build

field `exec_context` is never read
}

#[sails_rs::service(events = Event)]
impl<'a> Erc20Relay<'a> {
pub fn new(state: &'a RefCell<State>) -> Self {
Self(state)
impl<'a, T> Erc20Relay<'a, T>
where
T: ExecContext,
{
pub fn new(state: &'a RefCell<State>, exec_context: T) -> Self {
Self {
state,
exec_context,
}
}

pub async fn relay(&mut self, message: EthToVaraEvent) -> Result<(), Error> {
let (fungible_token, receipt, event) = self.prepare(&message)?;
let amount = u128::try_from(event.amount).map_err(|_| Error::InvalidAmount)?;

let EthToVaraEvent {
proof_block: BlockInclusionProof { block, mut headers },
Expand All @@ -32,7 +48,7 @@ impl<'a> Erc20Relay<'a> {
} = message;

// verify the proof of block inclusion
let checkpoints = self.0.borrow().checkpoints;
let checkpoints = self.state.borrow().checkpoints;
let slot = block.slot;
let checkpoint = Self::request_checkpoint(checkpoints, slot).await?;

Expand Down Expand Up @@ -69,23 +85,36 @@ impl<'a> Erc20Relay<'a> {
let (key_db, value_db) =
eth_utils::rlp_encode_index_and_receipt(&transaction_index, &receipt);
match trie.get(&key_db) {
Ok(Some(found_value)) if found_value == value_db => {
// TODO
debug!("Proofs are valid. Mint the tokens");
// TODO: save slot and index of the processed transaction

self.notify_on(Event::Relayed {
fungible_token,
to: ActorId::from(event.to.0),
amount,
})
.unwrap();
Ok(Some(found_value)) if found_value == value_db => (),
_ => return Err(Error::InvalidReceiptProof),
}

Ok(())
}
let amount = U256::from_little_endian(event.amount.as_le_slice());
let receiver = ActorId::from(event.to.0);
let call_payload =
vft_gateway::io::MintTokens::encode_call(fungible_token, receiver, amount);
let (vft, reply_timeout, reply_deposit) = {
let state = self.state.borrow();

_ => Err(Error::InvalidReceiptProof),
}
(state.vft, state.reply_timeout, state.reply_deposit)
};
gstd::msg::send_bytes_for_reply(vft, call_payload, 0, reply_deposit)
.map_err(|_| Error::SendFailure)?
.up_to(Some(reply_timeout))
.map_err(|_| Error::ReplyTimeout)?
.handle_reply(move || handle_reply(slot, transaction_index))
.map_err(|_| Error::ReplyHook)?
.await
.map_err(|_| Error::ReplyFailure)?;

self.notify_on(Event::Relayed {
fungible_token,
to: ActorId::from(event.to.0),
amount,
})
.expect("Unable to notify about relaying tokens");

Ok(())
}

fn prepare(
Expand All @@ -102,7 +131,7 @@ impl<'a> Erc20Relay<'a> {
}

let slot = message.proof_block.block.slot;
let mut state = self.0.borrow_mut();
let state = self.state.borrow_mut();
// decode log and pick the corresponding fungible token address if any
let (fungible_token, event) = receipt
.logs()
Expand All @@ -123,23 +152,19 @@ impl<'a> Erc20Relay<'a> {
.ok_or(Error::NotSupportedEvent)?;

// check for double spending
let index = state
.transactions
.binary_search_by(
|(slot_old, transaction_index_old)| match slot.cmp(slot_old) {
Ordering::Equal => message.transaction_index.cmp(transaction_index_old),
ordering => ordering,
},
)
.err()
.ok_or(Error::AlreadyProcessed)?;

if state.transactions.capacity() <= state.transactions.len() {
if index == state.transactions.len() - 1 {
return Err(Error::TooOldTransaction);
}
let transactions = transactions_mut();
let key = (slot, message.transaction_index);
if transactions.contains(&key) {
return Err(Error::AlreadyProcessed);
}

state.transactions.pop();
if CAPACITY <= transactions.len()
&& transactions
.first()
.map(|first| &key < first)
.unwrap_or(false)
{
return Err(Error::TooOldTransaction);
}

Ok((fungible_token, receipt, event))
Expand All @@ -160,4 +185,75 @@ impl<'a> Erc20Relay<'a> {
_ => panic!("Unexpected result to `GetCheckpointFor` request"),
}
}

pub fn fill_transactions(&mut self) -> bool {
#[cfg(feature = "gas_calculation")]
{
let transactions = transactions_mut();
if CAPACITY == transactions.len() {
return false;
}

let count = cmp::min(CAPACITY - transactions.len(), CAPACITY_STEP_SIZE);
let (last, _) = transactions.last().copied().unwrap();
for i in 0..count {
transactions.insert((last + 1, i as u64));
}

true
}

#[cfg(not(feature = "gas_calculation"))]
panic!("Please rebuild with enabled `gas_calculation` feature")
}

pub async fn calculate_gas_for_reply(
&mut self,
_slot: u64,
_transaction_index: u64,
) -> Result<(), Error> {
#[cfg(feature = "gas_calculation")]
{
let call_payload = vft_gateway::io::MintTokens::encode_call(
Default::default(),
Default::default(),
Default::default(),
);
let (reply_timeout, reply_deposit) = {
let state = self.state.borrow();

(state.reply_timeout, state.reply_deposit)
};
let source = self.exec_context.actor_id();
gstd::msg::send_bytes_for_reply(source, call_payload, 0, reply_deposit)
.map_err(|_| Error::SendFailure)?
.up_to(Some(reply_timeout))
.map_err(|_| Error::ReplyTimeout)?
.handle_reply(move || handle_reply(_slot, _transaction_index))
.map_err(|_| Error::ReplyHook)?
.await
.map_err(|_| Error::ReplyFailure)?;

Ok(())
}

#[cfg(not(feature = "gas_calculation"))]
panic!("Please rebuild with enabled `gas_calculation` feature")
}
}

fn handle_reply(slot: u64, transaction_index: u64) {
let reply_bytes = msg::load_bytes().expect("Unable to load bytes");
let reply = vft_gateway::io::MintTokens::decode_reply(&reply_bytes)
.expect("Unable to decode MintTokens reply");
if let Err(e) = reply {
panic!("Request to mint tokens failed: {e:?}");
}

let transactions = transactions_mut();
if CAPACITY <= transactions.len() {
transactions.pop_first();
}

transactions.insert((slot, transaction_index));
}
Loading

0 comments on commit 57bc017

Please sign in to comment.