Skip to content

Commit

Permalink
chore(gear-programs): Add docs to historical-proxy (#267)
Browse files Browse the repository at this point in the history
  • Loading branch information
mertwole authored Dec 27, 2024
1 parent 7f556f2 commit 8b1018b
Show file tree
Hide file tree
Showing 7 changed files with 87 additions and 37 deletions.
31 changes: 22 additions & 9 deletions api/gear/historical_proxy.idl
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
/// Errors returned by the Historical Proxy service.
type ProxyError = enum {
/// Endpoint for requested slot not found.
NoEndpointForSlot: u64,
/// Failed to send message.
SendFailure: str,
ReplyTimeout: str,
/// Failed to receive reply.
ReplyFailure: str,
/// Failed to decode reply.
DecodeFailure: str,
NotAdmin,
/// `ethereum-event-client` returned error.
EthereumEventClient: Error,
};

Expand All @@ -29,28 +33,37 @@ constructor {
};

service HistoricalProxy {
AddEndpoint : (slot: u64, endpoint: actor_id) -> result (null, ProxyError);
EndpointFor : (slot: u64) -> result (actor_id, ProxyError);
/// Redirect message to ERC20 Relay service which is valid for `slot`.
/// If message is relayed successfully then reply from relay service is sent to
/// `client` address and proofs are returned.
/// Add new endpoint to the map. Endpoint will be effective for all the
/// requests with slots starting from `slot`.
///
/// This function can be called only by an admin.
AddEndpoint : (slot: u64, endpoint: actor_id) -> null;
/// Redirect message to `ethereum-event-client` program which is valid for `slot`.
/// If message is relayed successfully then reply is sent to `client` address
/// to `client_route` route.
///
/// # Parameters
///
/// - `slot`: slot for which message is relayed.
/// - `tx_index`: transaction index for message.
/// - `proofs`: SCALE encoded `EthToVaraEvent`.
/// - `client`: client address to send receipt to on success.
/// - `client_route`: route to send receipt to on success.
///
/// # Returns
///
/// - `(Vec<u8>, Vec<u8>)`: on success where first vector is receipt and second vector is reply from calling `client_route`.
/// - `ProxyError`: if redirect failed
///
Redirect : (slot: u64, proofs: vec u8, client: actor_id, client_route: vec u8) -> result (struct { vec u8, vec u8 }, ProxyError);
/// Get current service admin.
query Admin : () -> actor_id;
/// Get endpoint for the specified `slot`.
query EndpointFor : (slot: u64) -> result (actor_id, ProxyError);
/// Get endpoint map stored in this service.
query Endpoints : () -> vec struct { u64, actor_id };

events {
/// Tx receipt is checked to be valid and successfully sent to the
/// underlying program.
Relayed: struct { slot: u64, block_number: u64, transaction_index: u32 };
}
};
Expand Down
8 changes: 6 additions & 2 deletions gear-programs/historical-proxy/app/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,19 @@ use parity_scale_codec::{Decode, Encode};
use sails_rs::prelude::String;
use scale_info::TypeInfo;

/// Errors returned by the Historical Proxy service.
#[derive(Debug, Decode, Encode, TypeInfo)]
#[codec(crate = sails_rs::scale_codec)]
#[scale_info(crate = sails_rs::scale_info)]
pub enum ProxyError {
/// Endpoint for requested slot not found.
NoEndpointForSlot(u64),
/// Failed to send message.
SendFailure(String),
ReplyTimeout(String),
/// Failed to receive reply.
ReplyFailure(String),
/// Failed to decode reply.
DecodeFailure(String),
NotAdmin,
/// `ethereum-event-client` returned error.
EthereumEventClient(ethereum_event_client::Error),
}
2 changes: 0 additions & 2 deletions gear-programs/historical-proxy/app/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ pub struct HistoricalProxyProgram(RefCell<state::ProxyState>);

#[sails_rs::program]
impl HistoricalProxyProgram {
// Program's constructor
#[allow(clippy::new_without_default)]
pub fn new() -> Self {
let exec_context = GStdExecContext::new();
Expand All @@ -28,7 +27,6 @@ impl HistoricalProxyProgram {
}))
}

// Exposed service
pub fn historical_proxy(&self) -> service::HistoricalProxyService<GStdExecContext> {
service::HistoricalProxyService::new(&self.0, GStdExecContext::new())
}
Expand Down
49 changes: 39 additions & 10 deletions gear-programs/historical-proxy/app/src/service.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
// Incorporate code generated based on the IDL file

#[allow(dead_code)]
#[allow(clippy::module_inception)]
pub(crate) mod ethereum_event_client {
Expand All @@ -16,17 +15,41 @@ use crate::{
state::{ProxyState, Slot},
};

/// Events enmitted by the Historical Proxy service.
#[derive(Encode, TypeInfo)]
#[codec(crate = sails_rs::scale_codec)]
#[scale_info(crate = sails_rs::scale_info)]
enum Event {
/// Tx receipt is checked to be valid and successfully sent to the
/// underlying program.
Relayed {
/// Ethereum slot containing target transaction.
slot: u64,
/// Ethereum block number which contains target transaction.
block_number: u64,
/// Index of the target transaction in the `block_number`.
transaction_index: u32,
},
}

/// Historical Proxy service.
///
/// `etereum-event-client` programs can become outdated with Ethereum updates, so
/// every `ethereum-event-client` ever deployed is valid for some Ethereum slot interval.
///
/// When Ethereum updates in a way incompatible with `ethereum-event-cleint`(or if we need to
/// update `ethereum-event-client` for some other reason) we need to deploy a new version of
/// `ethereum-event-client` and still have access to the old one(in order to process
/// historical transactions).
///
/// This service provides such an access. For every `ethereum-event-client` ever deployed
/// it maps Ethereum slot from which this `ethereum-event-client` is valid from.
///
/// When user makes request to the Historical Proxy service he will specify Ethereum slot number
/// where the target transaction was sent. Historical Proxy will decide which `ethereum-event-client`
/// is responsible of processing transactions for this slot and will redirect user request to it.
/// If `ethereum-event-client` returned success its reply will be redirected to the program
/// that user have specified in his request. For more info see `redirect` implementation.
pub struct HistoricalProxyService<'a, ExecContext> {
state: &'a RefCell<ProxyState>,
exec_context: ExecContext,
Expand All @@ -44,45 +67,51 @@ where
}
}

/// Get current service admin.
pub fn admin(&self) -> ActorId {
self.state.borrow().admin
}

pub fn endpoint_for(&mut self, slot: Slot) -> Result<ActorId, ProxyError> {
/// Get endpoint for the specified `slot`.
pub fn endpoint_for(&self, slot: Slot) -> Result<ActorId, ProxyError> {
self.state.borrow().endpoints.endpoint_for(slot)
}

pub fn add_endpoint(&mut self, slot: Slot, endpoint: ActorId) -> Result<(), ProxyError> {
/// Add new endpoint to the map. Endpoint will be effective for all the
/// requests with slots starting from `slot`.
///
/// This function can be called only by an admin.
pub fn add_endpoint(&mut self, slot: Slot, endpoint: ActorId) {
let source = self.exec_context.actor_id();

let mut state = self.state.borrow_mut();
if source != state.admin {
return Err(ProxyError::NotAdmin);
panic!("Not an admin");
}

state.endpoints.push(slot, endpoint);
Ok(())
}

/// Get endpoint map stored in this service.
pub fn endpoints(&self) -> Vec<(Slot, ActorId)> {
self.state.borrow().endpoints.endpoints()
}

/// Redirect message to ERC20 Relay service which is valid for `slot`.
/// If message is relayed successfully then reply from relay service is sent to
/// `client` address and proofs are returned.
/// Redirect message to `ethereum-event-client` program which is valid for `slot`.
/// If message is relayed successfully then reply is sent to `client` address
/// to `client_route` route.
///
/// # Parameters
///
/// - `slot`: slot for which message is relayed.
/// - `tx_index`: transaction index for message.
/// - `proofs`: SCALE encoded `EthToVaraEvent`.
/// - `client`: client address to send receipt to on success.
/// - `client_route`: route to send receipt to on success.
///
/// # Returns
///
/// - `(Vec<u8>, Vec<u8>)`: on success where first vector is receipt and second vector is reply from calling `client_route`.
/// - `ProxyError`: if redirect failed
///
#[allow(clippy::await_holding_refcell_ref)]
pub async fn redirect(
&mut self,
Expand Down
21 changes: 15 additions & 6 deletions gear-programs/historical-proxy/app/src/state.rs
Original file line number Diff line number Diff line change
@@ -1,25 +1,30 @@
use super::error::ProxyError;
use super::{ActorId, Vec};

pub type Slot = u64;

/// State of the Historical Proxy service.
pub struct ProxyState {
pub admin: ActorId,
pub endpoints: EndpointList,
}

/// Mapping between endpoints and Ethereum slots they're active from.
///
/// ### Invariant
///
/// Endpoints are stored in ascending order, sorted by slot number.
#[derive(Default)]
pub struct EndpointList(Vec<(Slot, ActorId)>);

impl Default for EndpointList {
fn default() -> Self {
Self::new()
}
}

impl EndpointList {
pub fn new() -> Self {
Self(Vec::with_capacity(2))
}

/// Add new endpoint that will be active starting from `slot`(inclusive).
///
/// Panics if provided `slot` <= greatest already existing slot.
pub fn push(&mut self, slot: Slot, actor_id: ActorId) {
assert!(
self.0.is_empty() || self.0[self.0.len() - 1].0 < slot,
Expand All @@ -28,10 +33,14 @@ impl EndpointList {
self.0.push((slot, actor_id));
}

/// Get list of currently active endpoints. Returns `Vec<(Slot, ActorId)>`
/// where `ActorId` means endpoint address and `Slot` means Ethereum slot
/// this endpoint is active from(inclusive).
pub fn endpoints(&self) -> Vec<(Slot, ActorId)> {
self.0.clone()
}

/// Get endpoint for the specified slot. Will return error if endpoint is not found.
pub fn endpoint_for(&self, slot: Slot) -> Result<ActorId, ProxyError> {
match self.0.binary_search_by(|(s, _)| s.cmp(&slot)) {
Ok(i) => Ok(self.0[i].1),
Expand Down
10 changes: 4 additions & 6 deletions gear-programs/historical-proxy/app/src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,20 +59,19 @@ async fn test_utility_functions() {
.add_endpoint(42, ActorId::from(0x42))
.send_recv(proxy_program_id)
.await
.unwrap()
.unwrap();

let recv_endpoint = HistoricalProxyC::new(remoting.clone())
.endpoint_for(43)
.send_recv(proxy_program_id)
.recv(proxy_program_id)
.await
.unwrap();

assert_eq!(recv_endpoint, Ok(endpoint1.1));

let recv_endpoint = HistoricalProxyC::new(remoting.clone())
.endpoint_for(41)
.send_recv(proxy_program_id)
.recv(proxy_program_id)
.await
.unwrap();

Expand All @@ -93,19 +92,18 @@ async fn test_utility_functions() {
.add_endpoint(84, ActorId::from(0x800))
.send_recv(proxy_program_id)
.await
.unwrap()
.unwrap();

let endpoint_for_slot_0 = HistoricalProxyC::new(remoting.clone())
.endpoint_for(43)
.send_recv(proxy_program_id)
.recv(proxy_program_id)
.await
.unwrap();
assert_eq!(endpoint_for_slot_0, Ok(ActorId::from(0x42)));

let endpoint_for_slot_1 = HistoricalProxyC::new(remoting.clone())
.endpoint_for(85)
.send_recv(proxy_program_id)
.recv(proxy_program_id)
.await
.unwrap();

Expand Down
3 changes: 1 addition & 2 deletions gear-programs/historical-proxy/app/tests/gclient.rs
Original file line number Diff line number Diff line change
Expand Up @@ -106,12 +106,11 @@ async fn proxy() {
)
.send_recv(proxy_program_id)
.await
.unwrap()
.unwrap();

let endpoint = proxy_client
.endpoint_for(message.proof_block.block.slot)
.send_recv(proxy_program_id)
.recv(proxy_program_id)
.await
.unwrap()
.unwrap();
Expand Down

0 comments on commit 8b1018b

Please sign in to comment.