Pallet ISMP
This is the implementation of ISMP for substrate chains. It is the foundational component that allows communication over ISMP. It correctly composes the various ISMP components in the runtime.
Runtime Integration
Including pallet-ismp
in a substrate runtime requires implementing the pallet config.
parameter_types! {
// For example, the hyperbridge parachain on Polkadot
pub const Coprocessor: Option<StateMachine> = Some(StateMachine::Polkadot(3367));
// The host state machine of this pallet, your state machine id goes here
pub const HostStateMachine: StateMachine = StateMachine::Polkadot(1000); // polkadot
// pub const HostStateMachine: StateMachine = StateMachine::Kusama(1000); // kusama
// pub const HostStateMachine: StateMachine = StateMachine::Substrate(*b"MYID"); // solochain
}
impl pallet_ismp::Config for Runtime {
// Configure the runtime event
type RuntimeEvent = RuntimeEvent;
// Permissioned origin who can create or update consensus clients
type AdminOrigin = EnsureRoot<AccountId>;
// The state machine identifier for this state machine
type HostStateMachine = HostStateMachine;
// The pallet_timestamp pallet
type TimestampProvider = Timestamp;
// The currency implementation that is offered to relayers
// this could also be `frame_support::traits::tokens::fungible::ItemOf`
type Currency = Balances;
// The balance type for the currency implementation
type Balance = Balance;
// Router implementation for routing requests/responses to their respective modules
type Router = Router;
// Optional coprocessor for incoming requests/responses
type Coprocessor = Coprocessor;
// Supported consensus clients
type ConsensusClients = (
// as an example, the parachain consensus client
ismp_parachain::ParachainConsensusClient<Runtime, IsmpParachain>,
);
// Offchain database implementation. Outgoing requests and responses are
// inserted in this database, while their commitments are stored onchain.
type OffchainDB = TransparentOffchainDB;
// Weight provider for local modules
type WeightProvider = ();
}
construct_runtime! {
// ...
Ismp: pallet_ismp
}
Config
Let's go through some of the ISMP specific components of the configuration.
-
HostStateMachine
: This is the state machine identifier for your chain, it will be used as the source value for all requests that are dispatched from this chain For parachains, this should be your parachain id e.gStateMachine::Polkadot(1000)
. -
Coprocessor
: ISMP is built around the idea of a coprocessor that aggregates consensus and state proofs from multiple state machines into a more succinct proof that is cheaply verifiable. This component defines the state machine identifier of the supported coprocessor, Hyperbridge is a coprocessor for ISMP. -
ConsensusClients
: This is a tuple of types that implement theConsensusClient
interface, it defines all the consensus algorithms supported by this deployment of the protocol. -
OffchainDB
: This implementation provides the interface for persisting requests and responses to the offchain db. Only commitments of requests and responses are stored onchain -
Router
: The router is a type that provides anIsmpModule
implementation for a module id.
Router
The IsmpRouter
is a module which produces an IsmpModule
implementation for a given module identifier.
#[derive(Default)]
struct Router;
impl IsmpRouter for Router {
fn module_for_id(&self, id: Vec<u8>) -> Result<Box<dyn IsmpModule>, Error> {
let module = match id.as_slice() {
YOUR_MODULE_ID => Box::new(YourModule::default()),
// ... other modules
_ => Err(Error::ModuleNotFound(id))?
};
Ok(module)
}
}
/// Some custom module capable of processing some incoming/request or response.
/// This could also be a pallet itself.
#[derive(Default)]
struct YourModule;
impl IsmpModule for YourModule {
/// Called by the ISMP hanlder, to notify module of a new POST request
/// the module may choose to respond immediately, or in a later block
fn on_accept(&self, request: Post) -> Result<(), Error> {
// do something useful with the request
Ok(())
}
/// Called by the ISMP hanlder, to notify module of a response to a previously
/// sent out request
fn on_response(&self, response: Response) -> Result<(), Error> {
// do something useful with the response
Ok(())
}
/// Called by the ISMP hanlder, to notify module of requests that were previously
/// sent but have now timed-out
fn on_timeout(&self, request: Timeout) -> Result<(), Error> {
// revert any state changes that were made prior to dispatching the request
Ok(())
}
}
WeightProvider
This type allows providing the static benchmarks for all ismp modules, it should identify modules by their id and return the weights for each IsmpModule
callback
struct YourModuleBenchmarks;
impl pallet_ismp::weights::IsmpModuleWeight for YourModuleBenchmarks {
/// Should return the weight used in processing this request
fn on_accept(&self, request: &Post) -> Weight {
todo!("Return benchmark weight")
}
/// Should return the weight used in processing this timeout
fn on_timeout(&self, request: &Timeout) -> Weight {
todo!("Return benchmark weight")
}
/// Should return the weight used in processing this response
fn on_response(&self, response: &Response) -> Weight {
todo!("Return benchmark weight")
}
}
struct ModuleWeightProvider
impl pallet_ismp::WeightProvider for ModuleWeightProvider {
fn module_callback(dest_module: ModuleId) -> Option<Box<dyn IsmpModuleWeight>> {
match dest_module.to_bytes().as_slice() {
YOUR_MODULE_ID => {
Some(Box::new(YourModuleBenchmarks::default()))
}
// ... other modules
_ => None
}
}
}
Interfaces
The pallet_ismp::Pallet<T>
implements the neccessary interfaces for the ISMP framework. These are:
IsmpHost
: Pallet ISMP implementsIsmpHost
interface providing all the storage and cryptographic requirements for the ISMP handlers. Modules that need to interact with the low-level ISMP framework can use this interface to access the necessary storage items they wish to read.
IsmpDispatcher
: It implementsIsmpDispatcher
allowing it to dispatch requests and responses. This is the low-level ISMP framework dispatcher. It can be used to dispatch requests that are not addressed to Hyperbridge and perhaps meant for other state machines. Dispatching requests to be Hyperbridge should be done throught thepallet-hyperbridge
module. Which also implements theIsmpDispatcher
interface but collects the necessary fees.
Calls
-
create_consensus_client
This is a priviledged call used to initialize the consensus state of a consensus client. Consensus clients must to be initialized with a trusted state, so this call must only be called by a trusted party.
-
update_consensus_state
This is a priviledged call used to update the unbonding period or challenge_period for a consensus client. It must only be called by a trusted parties to prevent consensus exploits.
-
handle_unsigned
Execute the provided batch of ISMP messages, this will short-circuit and revert if any of the provided messages are invalid. This is an unsigned extrinsic that permits anyone
execute ISMP messages for free, provided they have valid proofs and the messages havenot been previously processed.
The dispatch origin for this call must be an unsigned one.
Emits different message events based on the Message received if successful.
Only available when the pallet is built with the unsigned
feature flag.
-
handle
Execute the provided batch of ISMP messages. This call will short-circuit and revert if any of the provided messages are invalid.
The dispatch origin for this call must be a signed one.
Emits different message events based on the Message received if successful.
Only available when the unsigned
feature flag is disabled.
-
fund_message
During periods of high transaction fees on the destination chain, you can increase the relayer fee for in-flight requests and responses to incentivize their delivery. Simply call this function with the request/response commitment and the desired fee increase amount. Should not be called on a message that has been completed (delivered or timed-out) as those funds will be lost forever.
Transaction fees
Pallet ISMP offers a two different approaches to transaction fees.
Unsigned
This essentially means all cross-chain messages received are executed for free as unsigned transactions. The upside to this is that it cannot be exploited as a spam vector, since the transaction pool will check if the submitted extrinsics are valid before they are included in the pool. This validity check ensures that the transaction can be successfully executed and contains valid proofs. Malformed messages or those with invalid proofs are filtered out by the transaction pool validation logic preventing unnecessary processing and potential network congestion.
Signed
In this method, relayers and users will need to pay the native token for executing cross-chain messages. This is likely more preferrable but requires that the token be widely available.
Miscellaneous
Offchain Indexing
The pallet-ismp
only stores "commitments" (hashes) of requests onchain for storage proofs, while the full requests are stored offchain and using the offchain indexing api. It would be prudent to enable offchain indexing by default in the node, so all nodes on the network store all requests offchain. You can do this in your run
function in command.rs
. Here's an example
/// Parse command line arguments into service configuration.
pub fn run() -> Result<()> {
let mut cli = Cli::from_args();
// all full nodes should store request/responses, otherwise they'd basically be useless without
// it.
cli.run.base.offchain_worker_params.indexing_enabled = true;
// .. other stuff
}
Signed Extensions
The teseract messaging relayer expects the following signed extensions to be present in the runtime in the same order listed below
/// The SignedExtension to the basic transaction logic.
pub type SignedExtra = (
frame_system::CheckNonZeroSender<Runtime>,
frame_system::CheckSpecVersion<Runtime>,
frame_system::CheckTxVersion<Runtime>,
frame_system::CheckGenesis<Runtime>,
frame_system::CheckEra<Runtime>,
frame_system::CheckNonce<Runtime>,
frame_system::CheckWeight<Runtime>,
pallet_transaction_payment::ChargeTransactionPayment<Runtime>,
frame_metadata_hash_extension::CheckMetadataHash<Runtime>,
);