use crate::{
consensus::{
ConsensusClient, ConsensusClientId, ConsensusStateId, StateCommitment, StateMachineHeight,
StateMachineId,
},
error::Error,
messaging::Keccak256,
prelude::Vec,
router::{GetResponse, IsmpRouter, Request},
};
use alloc::{
boxed::Box,
format,
string::{String, ToString},
};
use codec::{Decode, DecodeWithMemTracking, Encode};
use core::{fmt::Display, str::FromStr, time::Duration};
use primitive_types::H256;
pub trait IsmpHost: Keccak256 {
fn host_state_machine(&self) -> StateMachine;
fn latest_commitment_height(&self, id: StateMachineId) -> Result<u64, Error>;
fn state_machine_commitment(
&self,
height: StateMachineHeight,
) -> Result<StateCommitment, Error>;
fn consensus_update_time(
&self,
consensus_state_id: ConsensusStateId,
) -> Result<Duration, Error>;
fn state_machine_update_time(
&self,
state_machine_height: StateMachineHeight,
) -> Result<Duration, Error>;
fn consensus_client_id(
&self,
consensus_state_id: ConsensusStateId,
) -> Option<ConsensusClientId>;
fn consensus_state(&self, consensus_state_id: ConsensusStateId) -> Result<Vec<u8>, Error>;
fn timestamp(&self) -> Duration;
fn is_consensus_client_frozen(&self, consensus_state_id: ConsensusStateId)
-> Result<(), Error>;
fn request_commitment(&self, req: H256) -> Result<(), Error>;
fn next_nonce(&self) -> u64;
fn request_receipt(&self, req: &Request) -> Option<()>;
fn response_receipt(&self, res: &GetResponse) -> Option<()>;
fn store_consensus_state_id(
&self,
consensus_state_id: ConsensusStateId,
client_id: ConsensusClientId,
) -> Result<(), Error>;
fn store_consensus_state(
&self,
consensus_state_id: ConsensusStateId,
consensus_state: Vec<u8>,
) -> Result<(), Error>;
fn store_unbonding_period(
&self,
consensus_state_id: ConsensusStateId,
period: u64,
) -> Result<(), Error>;
fn store_consensus_update_time(
&self,
consensus_state_id: ConsensusStateId,
timestamp: Duration,
) -> Result<(), Error>;
fn store_state_machine_update_time(
&self,
state_machine_height: StateMachineHeight,
timestamp: Duration,
) -> Result<(), Error>;
fn store_state_machine_commitment(
&self,
height: StateMachineHeight,
state: StateCommitment,
) -> Result<(), Error>;
fn delete_state_commitment(&self, height: StateMachineHeight) -> Result<(), Error>;
fn freeze_consensus_client(&self, consensus_state_id: ConsensusStateId) -> Result<(), Error>;
fn store_latest_commitment_height(&self, height: StateMachineHeight) -> Result<(), Error>;
fn delete_request_commitment(&self, req: &Request) -> Result<Vec<u8>, Error>;
fn delete_request_receipt(&self, req: &Request) -> Result<Vec<u8>, Error>;
fn delete_response_receipt(&self, res: &GetResponse) -> Result<Vec<u8>, Error>;
fn store_request_receipt(&self, req: &Request, signer: &Vec<u8>) -> Result<Vec<u8>, Error>;
fn store_response_receipt(&self, req: &GetResponse, signer: &Vec<u8>)
-> Result<Vec<u8>, Error>;
fn store_request_commitment(&self, req: &Request, meta: Vec<u8>) -> Result<(), Error>;
fn on_request_timeout(&self, _req: &Request, _meta: Vec<u8>) -> Result<(), Error> {
Ok(())
}
fn consensus_client(&self, id: ConsensusClientId) -> Result<Box<dyn ConsensusClient>, Error> {
self.consensus_clients()
.into_iter()
.find(|client| client.consensus_client_id() == id)
.ok_or_else(|| Error::Custom(format!("Consensus client for id {id:?} not found")))
}
fn consensus_clients(&self) -> Vec<Box<dyn ConsensusClient>>;
fn challenge_period(&self, state_machine: StateMachineId) -> Option<Duration>;
fn store_challenge_period(
&self,
state_machine: StateMachineId,
period: u64,
) -> Result<(), Error>;
fn is_expired(&self, consensus_state_id: ConsensusStateId) -> Result<(), Error> {
let host_timestamp = self.timestamp();
let unbonding_period = self
.unbonding_period(consensus_state_id)
.ok_or(Error::UnnbondingPeriodNotConfigured { consensus_state_id })?;
let last_update = self.consensus_update_time(consensus_state_id)?;
if host_timestamp.saturating_sub(last_update) >= unbonding_period {
Err(Error::UnbondingPeriodElapsed { consensus_state_id })?
}
Ok(())
}
fn allowed_proxy(&self) -> Option<StateMachine>;
fn is_allowed_proxy(&self, source: &StateMachine) -> bool {
self.allowed_proxy().map(|proxy| proxy == *source).unwrap_or(false)
}
fn unbonding_period(&self, consensus_state_id: ConsensusStateId) -> Option<Duration>;
fn ismp_router(&self) -> Box<dyn IsmpRouter>;
fn is_router(&self) -> bool {
self.allowed_proxy()
.map(|proxy| proxy == self.host_state_machine())
.unwrap_or(false)
}
fn previous_commitment_height(&self, id: StateMachineId) -> Option<u64>;
}
#[derive(
Clone,
Debug,
Copy,
Encode,
Decode,
DecodeWithMemTracking,
PartialOrd,
Ord,
PartialEq,
Eq,
Hash,
scale_info::TypeInfo,
serde::Deserialize,
serde::Serialize,
)]
pub enum StateMachine {
#[codec(index = 0)]
Evm(u32),
#[codec(index = 1)]
Polkadot(u32),
#[codec(index = 2)]
Kusama(u32),
#[codec(index = 3)]
Substrate(ConsensusStateId),
#[codec(index = 4)]
Tendermint(ConsensusStateId),
#[codec(index = 5)]
Relay {
relay: ConsensusStateId,
para_id: u32,
},
}
impl StateMachine {
pub fn is_evm(&self) -> bool {
match self {
StateMachine::Evm(_) => true,
_ => false,
}
}
pub fn is_substrate(&self) -> bool {
match self {
StateMachine::Polkadot(_) |
StateMachine::Kusama(_) |
StateMachine::Substrate(_) |
StateMachine::Relay { .. } => true,
_ => false,
}
}
}
impl Display for StateMachine {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
let str = match self {
StateMachine::Evm(id) => {
format!("EVM-{id}")
},
StateMachine::Polkadot(id) => format!("POLKADOT-{id}"),
StateMachine::Kusama(id) => format!("KUSAMA-{id}"),
StateMachine::Substrate(id) => {
format!(
"SUBSTRATE-{}",
String::from_utf8(id.to_vec()).unwrap_or("XXXX".to_string())
)
},
StateMachine::Tendermint(id) =>
format!("TNDRMINT-{}", String::from_utf8(id.to_vec()).unwrap_or("XXXX".to_string())),
StateMachine::Relay { relay, para_id } => format!(
"RELAY-{}-{para_id}",
String::from_utf8(relay.to_vec()).unwrap_or("XXXX".to_string())
),
};
write!(f, "{}", str)
}
}
impl FromStr for StateMachine {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let s = match s {
name if name.starts_with("EVM-") => {
let id = name
.split('-')
.last()
.and_then(|id| u32::from_str(id).ok())
.ok_or_else(|| format!("invalid state machine: {name}"))?;
StateMachine::Evm(id)
},
name if name.starts_with("POLKADOT-") => {
let id = name
.split('-')
.last()
.and_then(|id| u32::from_str(id).ok())
.ok_or_else(|| format!("invalid state machine: {name}"))?;
StateMachine::Polkadot(id)
},
name if name.starts_with("RELAY-") => {
let values = name.split('-').collect::<Vec<_>>();
let id = values
.last()
.and_then(|id| u32::from_str(id).ok())
.ok_or_else(|| format!("invalid state machine: {name}"))?;
let relay = values
.get(1)
.and_then(|id| {
let bytes = id.as_bytes();
if bytes.len() == 4 {
let mut dest = [0u8; 4];
dest.copy_from_slice(bytes);
Some(dest)
} else {
None
}
})
.ok_or_else(|| format!("invalid state machine: {name}"))?;
StateMachine::Relay { relay, para_id: id }
},
name if name.starts_with("KUSAMA-") => {
let id = name
.split('-')
.last()
.and_then(|id| u32::from_str(id).ok())
.ok_or_else(|| format!("invalid state machine: {name}"))?;
StateMachine::Kusama(id)
},
name if name.starts_with("SUBSTRATE-") => {
let name = name
.split('-')
.last()
.ok_or_else(|| format!("invalid state machine: {name}"))?;
let mut id = [0u8; 4];
id.copy_from_slice(name.as_bytes());
StateMachine::Substrate(id)
},
name if name.starts_with("TNDRMINT-") => {
let name = name
.split('-')
.last()
.ok_or_else(|| format!("invalid state machine: {name}"))?;
let mut id = [0u8; 4];
id.copy_from_slice(name.as_bytes());
StateMachine::Tendermint(id)
},
name => Err(format!("Unknown state machine: {name}"))?,
};
Ok(s)
}
}
#[cfg(test)]
mod tests {
use crate::host::StateMachine;
use alloc::string::ToString;
use core::str::FromStr;
#[test]
fn state_machine_conversions() {
let grandpa = StateMachine::Substrate(*b"hybr");
let beefy = StateMachine::Tendermint(*b"hybr");
let solo_relay = StateMachine::Relay { relay: *b"CENJ", para_id: 1000 };
let grandpa_string = grandpa.to_string();
let beefy_string = beefy.to_string();
let solo_string = solo_relay.to_string();
dbg!(&grandpa_string);
dbg!(&beefy_string);
dbg!(&solo_string);
assert_eq!(grandpa, StateMachine::from_str(&grandpa_string).unwrap());
assert_eq!(beefy, StateMachine::from_str(&beefy_string).unwrap());
assert_eq!(solo_relay, StateMachine::from_str(&solo_string).unwrap());
}
#[test]
fn invalid_state_machine_conversions() {
let grandpa = StateMachine::Substrate(*b"\xf0\x28\x8c\xbc");
let beefy = StateMachine::Tendermint(*b"\xf0\x28\x8c\xbc");
let solo_relay = StateMachine::Relay { relay: *b"\xf0\x28\x8c\xbc", para_id: 1000 };
let grandpa_string = grandpa.to_string();
let beefy_string = beefy.to_string();
let solo_string = solo_relay.to_string();
dbg!(&grandpa_string);
dbg!(&beefy_string);
dbg!(&solo_string);
assert_eq!(grandpa_string, "SUBSTRATE-XXXX".to_string());
assert_eq!(beefy_string, "TNDRMINT-XXXX".to_string());
assert_eq!(solo_string, "RELAY-XXXX-1000".to_string());
}
}