This project is to demonstrate the use of pallet-ibc.
The runtime in this project is mainly composed of two special pallets.
One is pallet-ibc and the other is a template pallet that is defined in the pallets/template
directory.
pallet-ibc provides IBC support and the template pallet contains the demo logic of the chain.
And here is a glue package that allows an RPC client based on substrate-subxt to interact with the demo chain through RPC.
The repository also includes implementation of relayer process defined in ICS 018 and a cli tool to make the cross-chain work.
Follow these steps to prepare a local Substrate development environment 🛠️
Setup instructions can be found at the Substrate Developer Hub.
Once the development environment is set up, build the node template. This command will build the Wasm and native code:
git clone https://github.com/cdot-network/ibc-demo.git
cd ibc-demo
git submodule update --init
cargo build --release
Start demo chains and send packet via IBC protocol:
Open a terminal and run the following command to start a test chain called appia.
./target/release/node-template --base-path /tmp/chain-appia --dev
Open another terminal and start the flaminia test chain.
./target/release/node-template --base-path /tmp/chain-flaminia --dev --port 20333 --ws-port 8844
Create a client of flaminia chain on appia chain, and then create a client of appia chain on flaminia.
./target/release/cli appia-client-id create-client flaminia-client-id
./target/release/cli flaminia-client-id create-client appia-client-id
Bind ports for two chains.
./target/release/cli appia bind-port bank
./target/release/cli flaminia bind-port bank
# ./target/release/cli appia release-port bank // don't
Open a new terminal and run relayer.
export RUST_LOG=relayer=info
./target/release/relayer -c relayer/config.toml
Use the cli tool to initiate a connection. The 2 client IDs below "53a9...fa10 779c...8e03" are from "relayer/config.toml"
./target/release/cli appia conn-open-init 53a954d6a7b1c595e025226e5f2a1782fdea30cd8b0d207ed4cdb040af3bfa10 779ca65108d1d515c3e4bc2e9f6d2f90e27b33b147864d1cd422d9f92ce08e03
When the log shows that the connection status of both chains is open, initiate a channel handshake. The connection ID below "d93f...bd11" is from the stdout of the command above
./target/release/cli appia chan-open-init d93fc49e1b2087234a1e2fc204b500da5d16874e631e761bdab932b37907bd11 bank bank
When the log shows that the channel status of both chains is open, you can send cross-chain messages as the following command. The 2 channel IDs below "00e2...86ac a161...601e" are from stdout of the command above, "01020304" is the data to send by channel, in Hex format.
./target/release/cli appia send-packet 1 1000 bank 00e2e14470ed9a017f586dfe6b76bb0871a8c91c3151778de110db3dfcc286ac bank a1611bcd0ba368e921b1bd3eb4aa66534429b14837725e8cef28182c25db601e 01020304
After some blocks, you can see that the flamenia log shows that the packet has been received.
- In cli, substrate-subxt invokes the pallet's callable functions by the macro
substrate_subxt_proc_macro::Call
. Please refer to document substrate_subxt_proc_macro::Call for details.
USAGE:
cli <CHAIN> create-client <chain-name>
After the command is triggered, the following functions are executed in sequence.
// https://github.com/cdot-network/ibc-demo/tree/master/cli/src/main.rs
async fn create_client(
...
) -> Result<(), Box<dyn Error>> {
...
}
// https://github.com/cdot-network/ibc-demo/tree/master/pallets/template/src/lib.rs
pub fn test_create_client(
...
) -> dispatch::DispatchResult {
...
}
// https://github.com/cdot-network/ibc-demo/tree/master/pallets/ibc/src/lib.rs
pub fn create_client(
...
) -> dispatch::DispatchResult {
...
}
USAGE:
cli <CHAIN> bind-port <identifier>
After the command is triggered, the following functions are executed in sequence.
// https://github.com/cdot-network/ibc-demo/tree/master/cli/src/main.rs
async fn bind_port(addr: &str, identifier: Vec<u8>) -> Result<(), Box<dyn Error>> {
...
}
// https://github.com/cdot-network/ibc-demo/tree/master/pallets/template/src/lib.rs
pub fn test_bind_port(origin, identifier: Vec<u8>) -> dispatch::DispatchResult {
...
}
// https://github.com/cdot-network/ibc-demo/tree/master/pallets/ibc/src/lib.rs
pub fn bind_port(identifier: Vec<u8>, module_index: u8) -> dispatch::DispatchResult {
...
}
USAGE:
relayer --config <FILE>
After the command is triggered, the following function keep scanning the 2 chains(A & B) for the jobs:
- Block header synchronization
- Connection handshakes
- Channel handshakes
- Packet flow
async fn relay(
...
) -> Result<(), Box<dyn Error>> {
...
// Block header synchronization
// For the 2 parties on inter-blockchain communication, if one chain(counterparty_client) doesn't have latest block header of the other chain(client).
if counterparty_client_state.latest_height < block_number {
...
}
...
// Scanning the 2 chains' connection state
// If it detects any party of the 2 can move connection opening handshakes state forward, it sends a request to the corresponding party.
for connection in client_state.connections.iter() {
...
// If current handshake state is (A,B)->(INIT, none), it sends a request to chain B, which converts the handshake state to (INIT, TRYOPEN)
if connection_end.state == ConnectionState::Init
&& remote_connection_end.state == ConnectionState::None {
...
}
// If current handshake state is (A,B)->(INIT, TRYOPEN), it sends a request to chain B, which converts the handshake state to (OPEN, TRYOPEN)
else if connection_end.state == ConnectionState::TryOpen
&& remote_connection_end.state == ConnectionState::Init
{
...
}
// If current handshake state is (A,B)->(OPEN, TRYOPEN), it sends a request to chain B, which converts the handshake state to (OPEN, OPEN)
else if connection_end.state == ConnectionState::Open
&& remote_connection_end.state == ConnectionState::TryOpen {
...
}
}
...
// Scanning the 2 chains' channel state
// If it detects any party of the 2 can move channel opening handshakes state forward, it sends a request to the corresponding party.
for channel in client_state.channels.iter() {
...
// If current handshake state is (A,B)->(INIT, none), it sends a request to chain B, which converts the handshake state to (INIT, TRYOPEN)
if channel_end.state == ChannelState::Init && remote_channel_end.state == ChannelState::None {
...
}
// If current handshake state is (A,B)->(INIT, TRYOPEN), it sends a request to chain B, which converts the handshake state to (OPEN, TRYOPEN)
else if channel_end.state == ChannelState::TryOpen
&& remote_channel_end.state == ChannelState::Init
{
...
}
// If current handshake state is (A,B)->(OPEN, TRYOPEN), it sends a request to chain B, which converts the handshake state to (OPEN, OPEN)
else if channel_end.state == ChannelState::Open
&& remote_channel_end.state == ChannelState::TryOpen {
...
}
}
// Scanning the 2 events in packet flow: RawEvent::SendPacket, RawEvent::RecvPacket
// If it detects either event from one party(chain A), it sends corresponding request to the other party(chain B) to move the packet flow forward
for event in events.into_iter() {
match event.event {
node_runtime::Event::pallet_ibc(pallet_ibc::RawEvent::SendPacket( // Detects event RawEvent::SendPacket from one party(chain A), who initializes sending packet
...
) {
...
let datagram = Datagram::PacketRecv {
...
};
tx.send(datagram).unwrap(); // Sends the packet to the other party(chain B)
}
node_runtime::Event::pallet_ibc(pallet_ibc::RawEvent::RecvPacket( // Detects event RawEvent::RecvPacket, an acknowledgement, from the other party(chain B), who acknowledges after receiving the packet
...
)) => {
...
let datagram = Datagram::PacketAcknowledgement {
...
}
};
tx.send(datagram).unwrap(); // Sends the acknowledgement to chain A
}
...
}
}
USAGE:
cli <CHAIN> conn-open-init <client-identifier> <counterparty-client-identifier>
After the command is triggered, the following functions are executed in sequence.
// https://github.com/cdot-network/ibc-demo/tree/master/cli/src/main.rs
async fn conn_open_init(
...
) -> Result<(), Box<dyn Error>> {
...
}
// https://github.com/cdot-network/ibc-demo/tree/master/pallets/template/src/lib.rs
pub fn test_conn_open_init(
...
) -> dispatch::DispatchResult {
...
}
// https://github.com/cdot-network/ibc-demo/tree/master/pallets/ibc/src/lib.rs
pub fn conn_open_init(
...
) -> dispatch::DispatchResult {
...
}
USAGE:
cli <CHAIN> chan-open-init [FLAGS] <connection-identifier> <port-identifier> <counterparty-port-identifier>
After the command is triggered, the following functions are executed in sequence.
// https://github.com/cdot-network/ibc-demo/tree/master/cli/src/main.rs
async fn chan_open_init(
...
) -> Result<(), Box<dyn Error>> {
...
}
// https://github.com/cdot-network/ibc-demo/tree/master/pallets/template/src/lib.rs
pub fn test_chan_open_init(
...
) -> dispatch::DispatchResult {
...
}
// https://github.com/cdot-network/ibc-demo/tree/master/pallets/ibc/src/lib.rs
pub fn chan_open_init(
...
) -> dispatch::DispatchResult {
...
}
USAGE:
cli <CHAIN> send-packet <sequence> <timeout-height> <source-port> <source-channel> <dest-port> <dest-channel> <data>
After the command is triggered, the following functions are executed in sequence.
// https://github.com/cdot-network/ibc-demo/tree/master/cli/src/main.rs
async fn send_packet(
...
) -> Result<(), Box<dyn Error>> {
...
}
// https://github.com/cdot-network/ibc-demo/tree/master/pallets/template/src/lib.rs
pub fn test_send_packet(
...
) -> dispatch::DispatchResult {
...
}
// https://github.com/cdot-network/ibc-demo/tree/master/pallets/ibc/src/lib.rs
pub fn send_packet(
...
) -> dispatch::DispatchResult {
...
}