#[cfg(feature = "std")]
use crate::TssSignature;
use crate::{NetworkId, TssPublicKey};
#[cfg(feature = "std")]
use anyhow::{Context, Result};
use scale_codec::{Decode, DecodeWithMemTracking, Encode};
use scale_info::{prelude::vec::Vec, TypeInfo};
use serde::{Deserialize, Serialize};
use sha3::{Digest, Keccak256};
#[cfg(feature = "std")]
use std::ops::Range;
#[cfg(feature = "std")]
use std::sync::Arc;
pub type Address32 = [u8; 32];
pub type MessageId = [u8; 32];
pub type Hash = [u8; 32];
pub type BatchId = u64;
const GMP_VERSION: &str = "Analog GMP v3";
pub trait FixedSizeEncodable {
fn left_pad_32(&self) -> [u8; 32];
}
macro_rules! impl_fixed_size_encodable {
($($n:expr),*) => {
$(
impl FixedSizeEncodable for [u8; $n] {
fn left_pad_32(&self) -> [u8; 32] {
let mut out = [0u8; 32];
out[32-$n..].copy_from_slice(self);
out
}
}
)*
}
}
impl_fixed_size_encodable!(
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25,
26, 27, 28, 29, 30, 31, 32
);
#[derive(Debug, Clone, Decode, DecodeWithMemTracking, Encode, TypeInfo, PartialEq)]
pub struct GmpParams {
pub network: NetworkId,
pub gateway: Address32,
}
impl GmpParams {
pub fn new(network: NetworkId, gateway: Address32) -> Self {
Self { network, gateway }
}
pub fn hash(&self, payload: &[u8]) -> Vec<u8> {
let mut data: Vec<u8> = Vec::new();
data.extend_from_slice(GMP_VERSION.as_bytes());
data.extend_from_slice(&self.network.to_be_bytes());
data.extend_from_slice(&self.gateway);
data.extend_from_slice(payload);
data
}
}
#[cfg_attr(feature = "std", derive(Serialize, Deserialize,))]
#[cfg_attr(not(feature = "std"), derive(Debug,))]
#[derive(
Clone, Default, Decode, DecodeWithMemTracking, Encode, TypeInfo, Eq, PartialEq, Ord, PartialOrd,
)]
pub struct GmpMessage {
pub src_network: NetworkId,
pub dest_network: NetworkId,
pub src: Address32,
pub dest: Address32,
pub nonce: u64,
pub gas_limit: u64,
pub bytes: Vec<u8>,
}
impl GmpMessage {
const HEADER_LEN: usize = 224;
pub fn encoded_len(&self) -> usize {
Self::HEADER_LEN + self.bytes.len()
}
fn encode_header(&self) -> [u8; Self::HEADER_LEN] {
let mut hdr = [0u8; Self::HEADER_LEN];
hdr[32..64].copy_from_slice(&self.src.left_pad_32());
hdr[64..96].copy_from_slice(&self.src_network.to_be_bytes().left_pad_32());
hdr[96..128].copy_from_slice(&self.dest.left_pad_32());
hdr[128..160].copy_from_slice(&self.dest_network.to_be_bytes().left_pad_32());
hdr[160..192].copy_from_slice(&self.gas_limit.to_be_bytes().left_pad_32());
hdr[192..Self::HEADER_LEN].copy_from_slice(&self.nonce.to_be_bytes().left_pad_32());
hdr
}
pub fn message_id(&self) -> MessageId {
let header = self.encode_header();
Keccak256::digest(header).into()
}
}
#[cfg(feature = "std")]
impl std::fmt::Display for GmpMessage {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
f.write_str(&hex::encode(self.message_id()))
}
}
#[cfg(feature = "std")]
impl std::fmt::Debug for GmpMessage {
fn fmt(&self, fmt: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
fmt.debug_struct("GmpMessage")
.field("_id", &format_args!("{}", &hex::encode(self.message_id())))
.field("src_network", &self.src_network)
.field("dest_network", &self.dest_network)
.field("src", &format_args!("{}", &hex::encode(self.src)))
.field("dest", &format_args!("{}", &hex::encode(self.dest)))
.field("noce", &self.nonce)
.field("gas_limit", &self.gas_limit)
.field("bytes", &format_args!("{}", &hex::encode(&self.bytes)))
.finish()
}
}
#[derive(
Debug,
Default,
Clone,
Copy,
Decode,
DecodeWithMemTracking,
Encode,
TypeInfo,
PartialEq,
Eq,
Serialize,
Deserialize,
)]
pub struct BatchGasParams {
pub batch_gas_limit: u64,
pub batch_exec_gas: u64,
pub reg_op_exec_gas: u64,
pub unreg_op_exec_gas: u64,
pub msg_op_exec_gas: u64,
pub msg_byte_gas: u64,
}
#[cfg_attr(feature = "std", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, Decode, DecodeWithMemTracking, Encode, TypeInfo, PartialEq)]
pub enum GatewayOp {
SendMessage(GmpMessage),
RegisterShard(
#[cfg_attr(feature = "std", serde(with = "crate::shard::serde_tss_public_key"))]
TssPublicKey,
u16,
),
UnregisterShard(
#[cfg_attr(feature = "std", serde(with = "crate::shard::serde_tss_public_key"))]
TssPublicKey,
u16,
),
}
impl GatewayOp {
fn code(&self) -> u8 {
match self {
GatewayOp::SendMessage(_) => 1,
GatewayOp::RegisterShard(_, _) => 2,
GatewayOp::UnregisterShard(_, _) => 3,
}
}
fn hash(&self) -> [u8; 32] {
let mut bytes = [0; 96];
match self {
Self::SendMessage(msg) => {
let data = Keccak256::digest(&msg.bytes);
bytes[..32].copy_from_slice(&msg.message_id());
bytes[32..64].copy_from_slice(&data);
return Keccak256::digest(&bytes[..64]).into();
},
Self::RegisterShard(pubkey, sessions) => {
bytes[31..64].copy_from_slice(pubkey);
bytes[64..96].copy_from_slice(&sessions.to_be_bytes().left_pad_32());
},
Self::UnregisterShard(pubkey, sessions) => {
bytes[31..64].copy_from_slice(pubkey);
bytes[64..96].copy_from_slice(&sessions.to_be_bytes().left_pad_32());
},
}
Keccak256::digest(bytes).into()
}
pub fn gas(&self, params: &BatchGasParams) -> u64 {
match self {
Self::SendMessage(msg) => {
params.msg_op_exec_gas
+ msg.bytes.len() as u64 * params.msg_byte_gas
+ msg.gas_limit
},
Self::RegisterShard(_, _) => params.reg_op_exec_gas,
Self::UnregisterShard(_, _) => params.unreg_op_exec_gas,
}
}
}
#[cfg(feature = "std")]
impl std::fmt::Display for GatewayOp {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
Self::SendMessage(msg) => {
writeln!(f, "send_message {}", hex::encode(msg.message_id()))
},
Self::RegisterShard(key, sessions) => {
writeln!(f, "register_shard {} {}", hex::encode(key), sessions)
},
Self::UnregisterShard(key, sessions) => {
writeln!(f, "unregister_shard {} {}", hex::encode(key), sessions)
},
}
}
}
#[cfg_attr(feature = "std", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, Decode, DecodeWithMemTracking, Encode, TypeInfo, PartialEq)]
pub struct GatewayMessage {
pub ops: Vec<GatewayOp>,
}
impl GatewayMessage {
pub fn new(ops: Vec<GatewayOp>) -> Self {
Self { ops }
}
pub fn hash(&self, batch_id: BatchId) -> [u8; 32] {
let mut ops_hash = [0; 32];
for op in &self.ops {
let mut ops_hasher = Keccak256::new();
ops_hasher.update(ops_hash);
let mut op_code = [0; 32];
op_code[31] = op.code();
ops_hasher.update(op_code);
let op_hash = op.hash();
ops_hasher.update(op_hash);
ops_hash = ops_hasher.finalize().into();
}
let mut buf = [0; 96];
buf[..32].copy_from_slice(&[0u8; 32]);
buf[32..64].copy_from_slice(&batch_id.to_be_bytes().left_pad_32());
buf[64..].copy_from_slice(&ops_hash);
Keccak256::digest(buf).into()
}
pub fn gas(&self, params: &BatchGasParams) -> u64 {
self.ops.iter().fold(0u64, |acc, op| acc.saturating_add(op.gas(params)))
}
}
pub struct BatchBuilder {
params: BatchGasParams,
gas: u64,
ops: Vec<GatewayOp>,
}
impl BatchBuilder {
pub fn new(params: BatchGasParams) -> Self {
Self {
gas: params.batch_exec_gas,
params,
ops: Default::default(),
}
}
pub fn take_batch(&mut self) -> Option<GatewayMessage> {
if self.ops.is_empty() {
return None;
}
self.gas = 0;
let ops = core::mem::take(&mut self.ops);
Some(GatewayMessage::new(ops))
}
pub fn push(&mut self, op: GatewayOp) -> Option<GatewayMessage> {
let gas = op.gas(&self.params);
let batch =
if self.gas + gas > self.params.batch_gas_limit { self.take_batch() } else { None };
self.ops.push(op);
batch
}
}
#[cfg_attr(feature = "std", derive(Serialize, Deserialize))]
#[derive(
Debug, Clone, Decode, DecodeWithMemTracking, Encode, TypeInfo, Eq, PartialEq, Ord, PartialOrd,
)]
pub enum GmpEvent {
ShardRegistered(
#[cfg_attr(feature = "std", serde(with = "crate::shard::serde_tss_public_key"))]
TssPublicKey,
),
ShardUnregistered(
#[cfg_attr(feature = "std", serde(with = "crate::shard::serde_tss_public_key"))]
TssPublicKey,
),
MessageReceived(GmpMessage),
MessageExecuted(MessageId),
BatchExecuted {
batch_id: BatchId,
tx_hash: Option<Hash>,
},
}
#[cfg(feature = "std")]
impl std::fmt::Display for GmpEvent {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
Self::ShardRegistered(key) => {
writeln!(f, "shard_registered {}", hex::encode(key))
},
Self::ShardUnregistered(key) => {
writeln!(f, "shard_unregistered {}", hex::encode(key))
},
Self::MessageReceived(msg) => {
writeln!(f, "message_received {}", hex::encode(msg.message_id()))
},
Self::MessageExecuted(msg) => {
writeln!(f, "message_executed {}", hex::encode(msg))
},
Self::BatchExecuted { batch_id, tx_hash } => {
let tx_hash = tx_hash.as_ref().map_or("None".to_string(), hex::encode);
writeln!(f, "batch_executed {batch_id} with tx_hash {tx_hash}")
},
}
}
}
#[cfg(feature = "std")]
#[derive(Debug, Clone, PartialEq, PartialOrd, Serialize, Deserialize)]
pub struct Route {
pub network_id: NetworkId,
pub gateway: Address32,
pub max_gas_limit: u64,
pub msg_gas: u64,
pub msg_byte_gas: u64,
pub gas_price: f64,
pub msg_fee: u64,
}
#[cfg(feature = "std")]
#[async_trait::async_trait]
pub trait IConnect: Send + Sync + 'static {
fn chain(&self) -> &dyn IChain;
async fn connect(&self, url: String) -> Result<Arc<dyn IConnector>>;
async fn connect_admin(&self, url: String) -> Result<Arc<dyn IConnectorAdmin>>;
}
#[cfg(feature = "std")]
pub trait IChain: Send + Sync + 'static {
fn network_id(&self) -> NetworkId;
fn address(&self) -> Address32;
fn format_address(&self, address: Address32) -> String;
fn parse_address(&self, address: &str) -> Result<Address32>;
}
#[cfg(feature = "std")]
#[async_trait::async_trait]
pub trait IConnector: Send + Sync + 'static {
fn chain(&self) -> &dyn IChain;
async fn finalized_block(&self) -> Result<u64>;
async fn read_events(&self, gateway: Address32, blocks: Range<u64>) -> Result<Vec<GmpEvent>>;
async fn submit_commands(
&self,
gateway: Address32,
batch: BatchId,
msg: GatewayMessage,
gas_price: u128,
signer: TssPublicKey,
sig: TssSignature,
) -> Result<(), String>;
async fn gas_price(&self) -> Result<u128>;
}
#[cfg(feature = "std")]
#[async_trait::async_trait]
pub trait IConnectorAdmin: IConnector {
async fn faucet(&self, balance: u128) -> Result<()>;
async fn transfer(&self, address: Address32, amount: u128) -> Result<()>;
async fn balance(&self, address: Address32) -> Result<u128>;
async fn deploy_gateway(&self, proxy: &[u8], gateway: &[u8]) -> Result<(Address32, u64)>;
async fn redeploy_gateway(&self, proxy: Address32, gateway: &[u8]) -> Result<()>;
async fn contract_bytecode_matches(&self, address: Address32, bytecode: &[u8]) -> Result<bool>;
async fn implementation(&self, proxy: Address32) -> Result<Address32>;
async fn admin(&self, gateway: Address32) -> Result<Address32>;
async fn set_admin(&self, gateway: Address32, admin: Address32) -> Result<()>;
async fn shards(&self, gateway: Address32) -> Result<Vec<TssPublicKey>>;
async fn set_shards(
&self,
gateway: Address32,
register: &[(TssPublicKey, u16)],
revoke: &[(TssPublicKey, u16)],
) -> Result<()>;
async fn routes(&self, gateway: Address32) -> Result<Vec<Route>>;
async fn set_route(&self, gateway: Address32, route: Route) -> Result<()>;
async fn set_prices(&self, gateway: Address32, prices: &[f64]) -> Result<()>;
async fn deploy_tester(&self, gateway: Address32, tester: &[u8]) -> Result<(Address32, u64)>;
async fn estimate_message_gas_limit(
&self,
contract: Address32,
src_network: NetworkId,
src: Address32,
payload: Vec<u8>,
) -> Result<u64>;
async fn estimate_message_cost(
&self,
gateway: Address32,
dest_network: NetworkId,
msg_size: u16,
gas_limit: u64,
) -> Result<u128>;
#[allow(clippy::too_many_arguments)]
async fn send_messages(
&self,
src: Address32,
dest_network: NetworkId,
dest: Address32,
gas_limit: u64,
msg_cost: u128,
payload: Vec<u8>,
amplification: u16,
) -> Result<Vec<MessageId>>;
async fn recv_messages(
&self,
contract: Address32,
blocks: Range<u64>,
) -> Result<Vec<GmpMessage>>;
async fn block_gas_limit(&self) -> Result<u64>;
async fn withdraw_funds(
&self,
gateway: Address32,
amount: u128,
address: Address32,
) -> Result<()>;
async fn debug_transaction(&self, tx: Hash) -> Result<String>;
}
#[cfg(feature = "std")]
pub struct AdminConnector<T>(T);
#[cfg(feature = "std")]
impl<T: IConnector> AdminConnector<T> {
pub fn new(connector: T) -> Self {
Self(connector)
}
fn context(&self, method: &str) -> String {
format!("{}: {method} failed", self.0.chain().network_id())
}
}
#[cfg(feature = "std")]
#[async_trait::async_trait]
impl<T: IConnector> IConnector for AdminConnector<T> {
fn chain(&self) -> &dyn IChain {
self.0.chain()
}
async fn finalized_block(&self) -> Result<u64> {
self.0.finalized_block().await.with_context(|| self.context("finalized_block"))
}
async fn read_events(&self, gateway: Address32, blocks: Range<u64>) -> Result<Vec<GmpEvent>> {
self.0
.read_events(gateway, blocks)
.await
.with_context(|| self.context("read_events"))
}
async fn submit_commands(
&self,
gateway: Address32,
batch: BatchId,
msg: GatewayMessage,
gas_price: u128,
signer: TssPublicKey,
sig: TssSignature,
) -> Result<(), String> {
self.0.submit_commands(gateway, batch, msg, gas_price, signer, sig).await
}
async fn gas_price(&self) -> Result<u128> {
self.0.gas_price().await.with_context(|| self.context("gas_price"))
}
}
#[cfg(feature = "std")]
#[async_trait::async_trait]
impl<T: IConnectorAdmin> IConnectorAdmin for AdminConnector<T> {
async fn faucet(&self, balance: u128) -> Result<()> {
self.0.faucet(balance).await.with_context(|| self.context("faucet"))
}
async fn transfer(&self, address: Address32, amount: u128) -> Result<()> {
self.0.transfer(address, amount).await.with_context(|| self.context("transfer"))
}
async fn balance(&self, address: Address32) -> Result<u128> {
self.0.balance(address).await.with_context(|| self.context("balance"))
}
async fn deploy_gateway(&self, proxy: &[u8], gateway: &[u8]) -> Result<(Address32, u64)> {
self.0
.deploy_gateway(proxy, gateway)
.await
.with_context(|| self.context("deploy_gateway"))
}
async fn redeploy_gateway(&self, proxy: Address32, gateway: &[u8]) -> Result<()> {
self.0
.redeploy_gateway(proxy, gateway)
.await
.with_context(|| self.context("redeploy_gateway"))
}
async fn contract_bytecode_matches(&self, address: Address32, bytecode: &[u8]) -> Result<bool> {
self.0
.contract_bytecode_matches(address, bytecode)
.await
.with_context(|| self.context("contract_bytecode_matches"))
}
async fn implementation(&self, proxy: Address32) -> Result<Address32> {
self.0
.implementation(proxy)
.await
.with_context(|| self.context("implementation"))
}
async fn admin(&self, gateway: Address32) -> Result<Address32> {
self.0.admin(gateway).await.with_context(|| self.context("admin"))
}
async fn set_admin(&self, gateway: Address32, admin: Address32) -> Result<()> {
self.0
.set_admin(gateway, admin)
.await
.with_context(|| self.context("set_admin"))
}
async fn shards(&self, gateway: Address32) -> Result<Vec<TssPublicKey>> {
self.0.shards(gateway).await.with_context(|| self.context("shards"))
}
async fn set_shards(
&self,
gateway: Address32,
register: &[(TssPublicKey, u16)],
revoke: &[(TssPublicKey, u16)],
) -> Result<()> {
self.0
.set_shards(gateway, register, revoke)
.await
.with_context(|| self.context("set_shards"))
}
async fn routes(&self, gateway: Address32) -> Result<Vec<Route>> {
self.0.routes(gateway).await.with_context(|| self.context("routes"))
}
async fn set_route(&self, gateway: Address32, route: Route) -> Result<()> {
self.0
.set_route(gateway, route)
.await
.with_context(|| self.context("set_route"))
}
async fn set_prices(&self, gateway: Address32, prices: &[f64]) -> Result<()> {
self.0
.set_prices(gateway, prices)
.await
.with_context(|| self.context("set_prices"))
}
async fn deploy_tester(&self, gateway: Address32, tester: &[u8]) -> Result<(Address32, u64)> {
self.0
.deploy_tester(gateway, tester)
.await
.with_context(|| self.context("deploy_tester"))
}
async fn estimate_message_gas_limit(
&self,
contract: Address32,
src_network: NetworkId,
src: Address32,
payload: Vec<u8>,
) -> Result<u64> {
self.0
.estimate_message_gas_limit(contract, src_network, src, payload)
.await
.with_context(|| self.context("estimate_message_gas_limit"))
}
async fn estimate_message_cost(
&self,
gateway: Address32,
dest_network: NetworkId,
msg_size: u16,
gas_limit: u64,
) -> Result<u128> {
self.0
.estimate_message_cost(gateway, dest_network, msg_size, gas_limit)
.await
.with_context(|| self.context("estimate_message_cost"))
}
async fn send_messages(
&self,
src: Address32,
dest_network: NetworkId,
dest: Address32,
gas_limit: u64,
msg_cost: u128,
payload: Vec<u8>,
amplification: u16,
) -> Result<Vec<MessageId>> {
self.0
.send_messages(src, dest_network, dest, gas_limit, msg_cost, payload, amplification)
.await
.with_context(|| self.context("send_messages"))
}
async fn recv_messages(
&self,
contract: Address32,
blocks: Range<u64>,
) -> Result<Vec<GmpMessage>> {
self.0
.recv_messages(contract, blocks)
.await
.with_context(|| self.context("recv_messages"))
}
async fn block_gas_limit(&self) -> Result<u64> {
self.0.block_gas_limit().await.with_context(|| self.context("block_gas_limit"))
}
async fn withdraw_funds(
&self,
gateway: Address32,
amount: u128,
address: Address32,
) -> Result<()> {
self.0
.withdraw_funds(gateway, amount, address)
.await
.with_context(|| self.context("withdraw_funds"))
}
async fn debug_transaction(&self, tx: Hash) -> Result<String> {
self.0
.debug_transaction(tx)
.await
.with_context(|| self.context("debug_transaction"))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn boxed() {
std::collections::HashMap::<NetworkId, Box<dyn IConnectorAdmin>>::default();
}
}