ISMP is a request response protocol bearing some similarities with HTTP, this means that a module makes a
request from a state machine to be delivered to another module on a counterparty state machine,
then the counterparty module creates a response to be forwarded back to the source after the request is processed.
An ISMP request can either be POST or GET similar to what is obtainable in HTTP.
pub struct Post {
/// The source state machine of this request.
pub source_chain: StateMachine,
/// The destination state machine of this request.
pub dest_chain: StateMachine,
/// The nonce of this request on the source chain
pub nonce: u64,
/// Module Id of the sending module
pub from: Vec<u8>,
/// Module ID of the receiving module
pub to: Vec<u8>,
/// Timestamp which this request expires in seconds.
pub timeout_timestamp: u64,
/// Encoded Request.
pub data: Vec<u8>,
}
/// The ISMP GET request.
pub struct Get {
/// The source state machine of this request.
pub source_chain: StateMachine,
/// The destination state machine of this request.
pub dest_chain: StateMachine,
/// The nonce of this request on the source chain
pub nonce: u64,
/// Module Id of the sending module
pub from: Vec<u8>,
/// Raw Storage keys that would be used to fetch the values from the counterparty
pub keys: Vec<Vec<u8>>,
/// Height at which to read the state machine.
pub height: u64,
/// Host timestamp at which this request expires in seconds
pub timeout_timestamp: u64,
}
pub enum Request {
/// A post request allows a module on a state machine to send arbitrary bytes to another module
/// living in another state machine.
Post(Post),
/// A get request allows a module on a state machine to read the storage of another module
/// living in another state machine.
Get(Get),
}
A post request is an intent to execute some instruction on the counterparty state machine, it carries a payload which is some opaque bytes to be delivered to the destination module on the counterparty state machine.
A GET request is an intent to fetch values for some keys in the database of the counterparty state machine.
While a POST request needs to be delivered to the counterparty for processing, a GET request is processed entirely offchain by any interested party.
A request is created and dispatched by the IsmpDispatch::dispatch_request
, a commitment of that
request which is a keccak256 hash of the request is inserted into the state trie and a Request
event is emitted, this
event informs any party that wishes to relay the requests to be aware of the existence of a new request.
A RequestMessage
that contains the request alongside a proof of membership is then submitted to the counterparty
chain, after verifying this proof, a receipt for the request is committed to storage, the destination
module or contract can create a response synchronously or asynchronously, whenever this response is available it would
be relayed back to the source chain.
A response to a post request is created and dispatched by the IsmpDispatch::dispatch_response
, a
commitment of that response which is a keccak256 hash of the response is inserted into the state trie, and a Response
event is emitted, this event informs any party that wishes to relay the response to be aware of its existence.
A ResponseMessage
that contains the response alongside a proof of membership is then submitted to the counterparty
chain, after verifying this proof, a receipt for the response is committed to storage, the receipt is used to prevent
processing duplicate responses.
Both Post and Get requests have a timeout, a Post request timeout is evaluated based on the timestamp of the destination chain, this means that post requests cannot time out if the destination chain does not progress, using the destination chain timestamp instead of the source chain’s timestamp prevents situations where a bad actor could submit a timeout for a request that was already successfully executed on the destination. Timeout messages are accompanied by a proof of non-membership.
Timeouts for Get requests are evaluated relative to the timestamp of the sending chain, the timestamp represents the time on the sending chain after which responses to a Get request will be rejected, no proofs are required.
There are two kind of responses in ISMP, the response to a Post request contains some opaque bytes that would be decoded by the destination module on the receiving chain.
The response to a Get request is a map of keys to values, the values for each key would be extracted from the state proof in the response message.
/// Response to a Post request
pub struct PostResponse {
/// The request that triggered this response.
pub post: Post,
/// The response message.
pub response: Vec<u8>,
}
/// The response to a Get request
pub struct GetResponse {
/// The Get request that triggered this response.
pub get: Get,
/// Values extracted from the state proof
pub values: BTreeMap<Vec<u8>, Option<Vec<u8>>>,
}
/// An ISMP Response
pub enum Response {
/// The response to a POST request
Post(PostResponse),
/// The response to a GET request
/// Note: This variant is use internally by the framework to dispatch get responses to the modules after state proofs are verified
/// it is never committed to storage.
Get(GetResponse),
}
The protocol requires that a commitment(a keccak256 hash) for outgoing requests and responses be committed into the
state trie by the IsmpDispatcher
.
For incoming requests, a receipt is stored for the request in storage, this helps prevent processing duplicate requests, it’s also necessary for generating non membership proofs for request timeouts.
Commitments to storage are the means through which state machine clients verify the state of a counterparty state
machine.
In ISMP it is required that a state machine commits a cryptographic hash of either a request or response to it's state
trie.
The process for hashing requests and responses is described in the functions below:
/// Return the keccak256 hash of a request
pub fn hash_request<H: IsmpHost>(req: &Request) -> H256 {
match req {
Request::Post(post) => {
let mut buf = Vec::new();
let source_chain = post.source_chain.to_string();
let dest_chain = post.dest_chain.to_string();
let nonce = post.nonce.to_be_bytes();
let timestamp = post.timeout_timestamp.to_be_bytes();
buf.extend_from_slice(source_chain.as_bytes());
buf.extend_from_slice(dest_chain.as_bytes());
buf.extend_from_slice(&nonce);
buf.extend_from_slice(×tamp);
buf.extend_from_slice(&post.from);
buf.extend_from_slice(&post.to);
buf.extend_from_slice(&post.data);
H::keccak256(&buf[..])
}
Request::Get(get) => {
let mut buf = Vec::new();
let source_chain = get.source_chain.to_string();
let dest_chain = get.dest_chain.to_string();
let nonce = get.nonce.to_be_bytes();
let height = get.height.to_be_bytes();
let timestamp = get.timeout_timestamp.to_be_bytes();
buf.extend_from_slice(source_chain.as_bytes());
buf.extend_from_slice(dest_chain.as_bytes());
buf.extend_from_slice(&nonce);
buf.extend_from_slice(&height);
buf.extend_from_slice(×tamp);
buf.extend_from_slice(&get.from);
buf.extend_from_slice(&get.keys.encode());
H::keccak256(&buf[..])
}
}
}
/// Return the keccak256 of a response
pub fn hash_response<H: IsmpHost>(res: &Response) -> H256 {
let (req, response) = match res {
Response::Post { ref post, response } => (post, response),
// Responses to get messages are never hashed
_ => return Default::default(),
};
let mut buf = Vec::new();
let source_chain = req.source_chain.to_string();
let dest_chain = req.dest_chain.to_string();
let nonce = req.nonce.to_be_bytes();
let timestamp = req.timeout_timestamp.to_be_bytes();
buf.extend_from_slice(source_chain.as_bytes());
buf.extend_from_slice(dest_chain.as_bytes());
buf.extend_from_slice(&nonce);
buf.extend_from_slice(×tamp);
buf.extend_from_slice(&req.data);
buf.extend_from_slice(&req.from);
buf.extend_from_slice(&req.to);
buf.extend_from_slice(response);
H::keccak256(&buf[..])
}