diff --git a/relayer/Cargo.toml b/relayer/Cargo.toml deleted file mode 100644 index 109f47ce..00000000 --- a/relayer/Cargo.toml +++ /dev/null @@ -1,39 +0,0 @@ -[package] -name = "axelar-cgp-sui" -version = "0.0.1" -license = "Apache-2.0" -publish = false -edition = "2021" - -[dependencies] -anyhow = "1.0.75" -async-trait = "0.1.74" -tokio = "1.33.0" -serde = "1.0.189" -axum = "0.6.20" -thiserror = "1.0.49" -strum = "0.25.0" -strum_macros = "0.25.3" -tracing = "0.1.39" -serde_json = "1.0.107" - -clap = { version = "4.4.6", features = ["env"] } - -rxrust = "1.0.0-beta.4" - -futures = "0.3.28" -bcs = "0.1.6" - -sui-sdk = { git = "https://github.com/MystenLabs/sui.git", package = "sui-sdk", branch = "testnet" } -sui-keys = { git = "https://github.com/MystenLabs/sui.git", package = "sui-keys", branch = "testnet" } -shared-crypto = { git = "https://github.com/MystenLabs/sui.git", package = "shared-crypto", branch = "testnet" } -telemetry-subscribers = { git = "https://github.com/MystenLabs/sui.git", package = "telemetry-subscribers", branch = "testnet" } - -[dev-dependencies] -hex = "0.4.3" -test-cluster = { git = "https://github.com/MystenLabs/sui.git", package = "test-cluster", branch = "framework/testnet" } -sui-move-build = { git = "https://github.com/MystenLabs/sui.git", package = "sui-move-build", branch = "framework/testnet" } - -[[bin]] -name = "sui-axelar-relayer" -path = "src/relayer.rs" diff --git a/relayer/src/handlers.rs b/relayer/src/handlers.rs deleted file mode 100644 index 3d2faf3e..00000000 --- a/relayer/src/handlers.rs +++ /dev/null @@ -1,101 +0,0 @@ -// Copyright (c) Mysten Labs, Inc. -// SPDX-License-Identifier: Apache-2.0 - -use std::str::FromStr; - -use anyhow::anyhow; -use axum::extract::State; -use axum::Json; -use shared_crypto::intent::Intent; -use sui_keys::keystore::AccountKeystore; -use sui_sdk::rpc_types::SuiTransactionBlockEffectsAPI; -use sui_sdk::rpc_types::{SuiExecutionStatus, SuiTransactionBlockResponseOptions}; -use sui_sdk::types::programmable_transaction_builder::ProgrammableTransactionBuilder; -use sui_sdk::types::quorum_driver_types::ExecuteTransactionRequestType; -use sui_sdk::types::transaction::{ - CallArg, ObjectArg, Transaction, TransactionData, TransactionKind, -}; -use sui_sdk::types::Identifier; - -use crate::types::{Error, Input, ProcessCommandsResponse}; -use crate::RelayerState; - -// handler for `/process_commands` endpoint -pub async fn process_commands( - State(state): State, - Json(input): Json, -) -> Result { - let payload = bcs::to_bytes(&input)?; - - let mut ptb = ProgrammableTransactionBuilder::default(); - let validator = ObjectArg::SharedObject { - id: state.validators, - initial_shared_version: state.validators_shared_version.into(), - mutable: true, - }; - ptb.move_call( - state.gateway_package_id, - Identifier::from_str("gateway")?, - Identifier::from_str("process_commands")?, - vec![], - vec![ - CallArg::Object(validator), - CallArg::Pure(bcs::to_bytes(&payload)?), - ], - )?; - let pt = ptb.finish(); - - // using read write lock to ensure same coins are not used in multiple tx simultaneously. - // todo: this could become performance bottleneck, use coin management to increase throughput if needed. - let sui_client = state.sui_client.write().await; - - let dry_run = sui_client - .read_api() - .dev_inspect_transaction_block( - state.signer_address, - TransactionKind::ProgrammableTransaction(pt.clone()), - None, - None, - ) - .await?; - - if let SuiExecutionStatus::Failure { error } = dry_run.effects.status() { - return Err(Error::SuiTransactionExecutionFailure(error.clone())); - } - - let coins = sui_client - .coin_read_api() - .get_coins(state.signer_address, None, None, None) - .await? - .data; - - let coins = coins.into_iter().map(|c| c.object_ref()).collect(); - let gas_price = sui_client - .governance_api() - .get_reference_gas_price() - .await?; - - let data = - TransactionData::new_programmable(state.signer_address, coins, pt, 10000000, gas_price); - - let signature = state - .keystore - .sign_secure(&state.signer_address, &data, Intent::sui_transaction()) - .map_err(|e| anyhow!(e))?; - - let resp = sui_client - .quorum_driver_api() - .execute_transaction_block( - Transaction::from_data(data, Intent::sui_transaction(), vec![signature]), - SuiTransactionBlockResponseOptions::default(), - Some(ExecuteTransactionRequestType::WaitForLocalExecution), - ) - .await?; - - // todo: invoke subsequence contract calls? - - // todo: deal with errors - Ok(ProcessCommandsResponse { - tx_hash: resp.digest, - }) -} diff --git a/relayer/src/listener.rs b/relayer/src/listener.rs deleted file mode 100644 index 84a60dd0..00000000 --- a/relayer/src/listener.rs +++ /dev/null @@ -1,96 +0,0 @@ -// Copyright (c) Mysten Labs, Inc. -// SPDX-License-Identifier: Apache-2.0 - -use std::convert::Infallible; -use std::fmt::Debug; - -use futures::stream::StreamExt; -use rxrust::observer::Observer; -use rxrust::subject::SubjectThreads; -use serde::{Deserialize, Serialize}; -use tracing::{error, info}; - -use sui_sdk::rpc_types::{EventFilter, SuiEvent}; -use sui_sdk::types::base_types::ObjectID; -use sui_sdk::types::parse_sui_struct_tag; -use sui_sdk::SuiClient; - -pub type Subject = SubjectThreads; -pub struct SuiListener { - client: SuiClient, - gateway: ObjectID, -} - -impl SuiListener { - pub fn new(sui_client: SuiClient, gateway: ObjectID) -> Self { - SuiListener { - client: sui_client, - gateway, - } - } - pub async fn listen(self, mut subject: Subject) { - // todo: use event query api instead of ws subscription for replay support. - let event_type = format!("{}::{}::{}", self.gateway, T::EVENT_MODULE, T::EVENT_TYPE); - info!("{event_type}"); - let mut events = self - .client - .event_api() - .subscribe_event(EventFilter::All(vec![EventFilter::MoveEventType( - parse_sui_struct_tag(&event_type).unwrap(), - )])) - .await - .expect("Cannot subscribe to Sui events."); - - info!("Start listening to Sui events: {event_type}"); - - while let Some(ev) = events.next().await { - match T::parse_event(ev.expect("Subscription erred.")) { - Ok(ev) => subject.next(ev), - Err(e) => error!("Error: {e}"), - } - } - - // todo: reconnect - panic!("Subscription to event '{event_type}' stopped.") - } -} - -pub trait SuiAxelarEvent { - const EVENT_MODULE: &'static str; - const EVENT_TYPE: &'static str; - fn parse_event(event: SuiEvent) -> Result - where - Self: Sized; -} - -#[derive(Deserialize, Serialize, Debug, Clone)] -pub struct ContractCall { - source: Vec, - destination: Vec, - destination_address: Vec, - payload: Vec, -} - -impl SuiAxelarEvent for ContractCall { - const EVENT_MODULE: &'static str = "gateway"; - const EVENT_TYPE: &'static str = "ContractCall"; - fn parse_event(event: SuiEvent) -> Result { - // TODO: extra check for event type - Ok(bcs::from_bytes(&event.bcs)?) - } -} - -#[derive(Deserialize, Serialize, Debug, Clone)] -pub struct OperatorshipTransferred { - epoch: u64, - new_operators_hash: Vec, -} - -impl SuiAxelarEvent for OperatorshipTransferred { - const EVENT_MODULE: &'static str = "validators"; - const EVENT_TYPE: &'static str = "OperatorshipTransferred"; - fn parse_event(event: SuiEvent) -> Result { - // TODO: extra check for event type - Ok(bcs::from_bytes(&event.bcs)?) - } -} diff --git a/relayer/src/relayer.rs b/relayer/src/relayer.rs deleted file mode 100644 index 2e217e81..00000000 --- a/relayer/src/relayer.rs +++ /dev/null @@ -1,160 +0,0 @@ -// Copyright (c) Mysten Labs, Inc. -// SPDX-License-Identifier: Apache-2.0 - -use std::net::SocketAddr; -use std::sync::Arc; -use std::time::Duration; - -use axum::routing::post; -use axum::Router; -use clap::Parser; -use futures::future::try_join_all; -use rxrust::observable::ObservableItem; -use sui_keys::keystore::{AccountKeystore, InMemKeystore, Keystore}; -use sui_sdk::types::base_types::{ObjectID, SuiAddress}; -use sui_sdk::types::crypto::SignatureScheme; -use sui_sdk::{SuiClient, SuiClientBuilder}; -use telemetry_subscribers::TelemetryConfig; -use tokio::sync::RwLock; -use tokio::task::JoinHandle; -use tracing::info; - -use crate::handlers::process_commands; -use crate::listener::{ - ContractCall, OperatorshipTransferred, Subject, SuiAxelarEvent, SuiListener, -}; -use crate::types::Error; - -mod handlers; -mod listener; -mod types; - -#[derive(Parser)] -#[clap( - name = "sui-axelar-relayer", - rename_all = "kebab-case", - author, - version -)] -pub struct SuiAxelarRelayer { - #[clap( - long, - env, - default_value = "you parade planet become era edit fuel birth arrow cry grunt snow" - )] - signer_mnemonic: String, - #[clap(long, env, default_value = "http://127.0.0.1:9000")] - sui_fn_url: String, - #[clap(long, env, default_value = "ws://127.0.0.1:9000")] - sui_ws_url: String, - #[clap(long, env, default_value = "127.0.0.1:10000")] - listen_address: SocketAddr, - #[clap( - long, - env, - default_value = "0x8adcad97bc1e3a03ee414a2539e41cc9b312459b092f8e96707f823e0e04e628" - )] - gateway_package_id: ObjectID, - #[clap( - long, - env, - default_value = "0x85f390983494351c94f83d43d5e178dfbb963a8e31a6194fede3c7f9ffbb5143" - )] - validators: ObjectID, - #[clap(long, env, default_value = "7")] - validators_shared_version: u64, -} - -#[derive(Clone)] -pub struct RelayerState { - signer_address: SuiAddress, - keystore: Arc, - sui_client: Arc>, - gateway_package_id: ObjectID, - validators: ObjectID, - validators_shared_version: u64, -} - -impl SuiAxelarRelayer { - pub async fn start(self) -> Result<(), Error> { - info!("Starting Sui Axelar relayer"); - - info!("Sui Fullnode: {}", self.sui_fn_url); - let sui_client = SuiClientBuilder::default() - .ws_ping_interval(Duration::from_secs(20)) - .ws_url(&self.sui_ws_url) - .build(&self.sui_fn_url) - .await?; - - let mut keystore = Keystore::InMem(InMemKeystore::default()); - let signer_address = - keystore.import_from_mnemonic(&self.signer_mnemonic, SignatureScheme::ED25519, None)?; - - info!("Relayer signer Sui address: {signer_address}"); - - let state = RelayerState { - signer_address, - keystore: Arc::new(keystore), - sui_client: Arc::new(RwLock::new(sui_client.clone())), - gateway_package_id: self.gateway_package_id, - validators: self.validators, - validators_shared_version: self.validators_shared_version, - }; - - let api = self.start_api_service(state).await; - - let (contract_call_handle, contract_call) = self - .start_event_listener::(sui_client.clone(), self.gateway_package_id) - .await; - - contract_call.subscribe(|call| { - // todo: pass to axelar - println!("{call:?}") - }); - - let (operatorship_transferred_handle, operatorship_transferred) = self - .start_event_listener::(sui_client, self.gateway_package_id) - .await; - - operatorship_transferred.subscribe(|call| { - // todo: pass to axelar - println!("{call:?}") - }); - - try_join_all(vec![ - api, - contract_call_handle, - operatorship_transferred_handle, - ]) - .await?; - Ok(()) - } - - async fn start_event_listener( - &self, - client: SuiClient, - gateway_package_id: ObjectID, - ) -> (JoinHandle<()>, Subject) { - let sui_listener = SuiListener::new(client, gateway_package_id); - let event = Subject::::default(); - (tokio::spawn(sui_listener.listen(event.clone())), event) - } - - async fn start_api_service(&self, state: RelayerState) -> JoinHandle<()> { - let app = Router::new() - .route("/process_commands", post(process_commands)) - .with_state(state); - let server = axum::Server::bind(&self.listen_address).serve(app.into_make_service()); - let addr = server.local_addr(); - let handle = tokio::spawn(async move { server.await.unwrap() }); - - info!("Sui Axelar relayer listening on {addr}"); - handle - } -} - -#[tokio::main] -pub async fn main() -> Result<(), Error> { - let (_guard, _) = TelemetryConfig::new().with_env().init(); - SuiAxelarRelayer::parse().start().await -} diff --git a/relayer/src/types.rs b/relayer/src/types.rs deleted file mode 100644 index b4f987c9..00000000 --- a/relayer/src/types.rs +++ /dev/null @@ -1,200 +0,0 @@ -// Copyright (c) Mysten Labs, Inc. -// SPDX-License-Identifier: Apache-2.0 - -use std::str::FromStr; - -use axum::http::StatusCode; -use axum::response::{IntoResponse, Response}; -use axum::Json; -use serde::de::Error as DeError; -use serde::ser::Error as SerdeError; -use serde::{Deserialize, Deserializer, Serialize, Serializer}; -use serde_json::json; -use strum_macros::{Display, EnumDiscriminants, EnumIter, EnumProperty}; -use sui_sdk::types::base_types::SuiAddress; -use sui_sdk::types::digests::TransactionDigest; -use thiserror::Error; -use tokio::task::JoinError; - -#[derive(Debug, Error, EnumDiscriminants, EnumProperty)] -#[strum_discriminants( - name(ErrorType), - derive(Display, EnumIter), - strum(serialize_all = "kebab-case") -)] -#[allow(clippy::enum_variant_names)] -pub enum Error { - #[error(transparent)] - UncategorizedError(#[from] anyhow::Error), - - #[error(transparent)] - SuiError(#[from] sui_sdk::error::Error), - - #[error(transparent)] - TokioJoinError(#[from] JoinError), - - #[error(transparent)] - BCSError(#[from] bcs::Error), - - #[error("Transaction execution failure: {0}")] - SuiTransactionExecutionFailure(String), -} - -impl Serialize for Error { - fn serialize(&self, serializer: S) -> Result - where - S: Serializer, - { - let error = json!(self); - error.serialize(serializer) - } -} - -impl IntoResponse for Error { - fn into_response(self) -> Response { - (StatusCode::INTERNAL_SERVER_ERROR, self.to_string()).into_response() - } -} - -#[derive(Serialize, Deserialize, Debug)] -pub struct ProcessCommandsResponse { - pub tx_hash: TransactionDigest, -} - -impl IntoResponse for ProcessCommandsResponse { - fn into_response(self) -> Response { - Json(self).into_response() - } -} - -#[derive(Serialize, Deserialize, Debug)] -#[serde(rename_all = "camelCase")] -pub struct AxelarMessage { - chain_id: u64, - command_ids: Vec, - commands: Vec, - #[serde(serialize_with = "to_bcs_bytes_vec")] - params: Vec, -} - -fn to_bcs_bytes_vec(params: &[AxelarParameter], serializer: S) -> Result -where - S: Serializer, -{ - let params = params - .iter() - .map(bcs::to_bytes) - .collect::, _>>() - .map_err(S::Error::custom)?; - params.serialize(serializer) -} - -fn to_bcs_bytes(data: &T, serializer: S) -> Result -where - S: Serializer, - T: Serialize, -{ - bcs::to_bytes(data) - .map_err(S::Error::custom)? - .serialize(serializer) -} - -#[derive(Serialize, Deserialize, Debug)] -#[serde(untagged, rename_all = "camelCase")] -pub enum AxelarParameter { - GenericMessage { - source_chain: String, - source_address: String, - target_id: SuiAddress, - payload_hash: Vec, - }, - TransferOperatorshipMessage { - operators: Vec>, - #[serde(deserialize_with = "string_to_u128_vec")] - weights: Vec, - #[serde(deserialize_with = "string_to_u128")] - threshold: u128, - }, -} - -// this is needed to workaround serde's untagged enum deserialization -// https://github.com/serde-rs/serde/issues/1682 -fn string_to_u128_vec<'de, D>(deserializer: D) -> Result, D::Error> -where - D: Deserializer<'de>, -{ - let input = Vec::::deserialize(deserializer)?; - input - .iter() - .map(|s| u128::from_str(s)) - .collect::, _>>() - .map_err(D::Error::custom) -} - -fn string_to_u128<'de, D>(deserializer: D) -> Result -where - D: Deserializer<'de>, -{ - let input = String::deserialize(deserializer)?; - u128::from_str(&input).map_err(D::Error::custom) -} - -#[derive(Serialize, Deserialize, Debug)] -pub struct Proof { - operators: Vec>, - #[serde(deserialize_with = "string_to_u128_vec")] - weights: Vec, - #[serde(deserialize_with = "string_to_u128")] - threshold: u128, - signatures: Vec>, -} - -#[derive(Serialize, Deserialize, Debug)] -pub struct Input { - #[serde(serialize_with = "to_bcs_bytes")] - pub message: AxelarMessage, - #[serde(serialize_with = "to_bcs_bytes")] - pub proof: Proof, -} - -#[cfg(test)] -mod test { - use std::str::FromStr; - - use sui_sdk::types::base_types::SuiAddress; - - use crate::types::AxelarParameter::TransferOperatorshipMessage; - use crate::types::{AxelarMessage, Input, Proof}; - - #[test] - fn test_message_to_bcs() { - // From preset/index.js - let input = hex::decode("8501010000000000000001000000000000000000000000000000000000000000000000000000000000000101147472616e736665724f70657261746f727368697001440121037286a4f1177bea06c8e15cf6ec3df0b7747a01ac2329ca2999dfd74eff59902801c80000000000000000000000000000001400000000000000000000000000000087010121037286a4f1177bea06c8e15cf6ec3df0b7747a01ac2329ca2999dfd74eff59902801640000000000000000000000000000000a000000000000000000000000000000014198b04944e2009969c93226ec6c97a7b9cc655b4ac52f7eeefd6cf107981c063a56a419cb149ea8a9cd49e8c745c655c5ccc242d35a9bebe7cebf6751121092a301").unwrap(); - let operator = - hex::decode("037286a4f1177bea06c8e15cf6ec3df0b7747a01ac2329ca2999dfd74eff599028") - .unwrap(); - - let message = AxelarMessage { - chain_id: 1, - command_ids: vec![SuiAddress::from_str( - "0x0000000000000000000000000000000000000000000000000000000000000001", - ) - .unwrap()], - commands: vec!["transferOperatorship".into()], - params: vec![TransferOperatorshipMessage { - operators: vec![operator.to_vec()], - weights: vec![200], - threshold: 20, - }], - }; - let proof = Proof { - operators: vec![operator.to_vec()], - weights: vec![100], - threshold: 10, - signatures: vec![hex::decode("98b04944e2009969c93226ec6c97a7b9cc655b4ac52f7eeefd6cf107981c063a56a419cb149ea8a9cd49e8c745c655c5ccc242d35a9bebe7cebf6751121092a301").unwrap()], - }; - let payload = bcs::to_bytes(&Input { message, proof }).unwrap(); - - assert_eq!(input, payload) - } -}