Cross-Chain Messages
On Linera, applications are meant to be multi-chain: They are instantiated on every chain where they are used. An application has the same application ID and bytecode everywhere, but a separate state on every chain. To coordinate, the instances can send cross-chain messages to each other. A message sent by an application is always handled by the same application on the target chain: The handling code is guaranteed to be the same as the sending code, but the state may be different.
For your application, you can specify any serializable type as the Message
type in your Contract
implementation. To send a message, use the
ContractRuntime
made available as an argument to the contract's [Contract::load
] constructor.
The runtime is usually stored inside the contract object, as we did when
writing the contract binary. We can then call
ContractRuntime::prepare_message
to start preparing a message, and then
send_to
to send it to a destination chain.
self.runtime
.prepare_message(message_contents)
.send_to(destination_chain_id);
It is also possible to send a message to a subscription channel, so that the
message is forwarded to the subscribers of that channel. All that has to be done
is specify a
ChannelName
as the destination parameter to send_to
.
After block execution in the sending chain, sent messages are placed in the
target chains' inboxes for processing. There is no guarantee that it will be
handled: For this to happen, an owner of the target chain needs to include it in
the incoming_messages
in one of their blocks. When that happens, the
contract's execute_message
method gets called on their chain.
While preparing the message to be sent, it is possible to enable authentication forwarding and/or tracking. Authentication forwarding means that the message is executed by the receiver with the same authenticated signer as the sender of the message, while tracking means that the message is sent back to the sender if the receiver rejects it. The example below enables both flags:
self.runtime
.prepare_message(message_contents)
.with_tracking()
.with_authentication()
.send_to(destination_chain_id);
Example: Fungible Token
In the fungible
example
application, such a message
can be the transfer of tokens from one chain to another. If the sender includes
a Transfer
operation on their chain, it decreases their account balance and
sends a Credit
message to the recipient's chain:
async fn execute_operation(&mut self, operation: Self::Operation) -> Self::Response {
match operation {
// ...
Operation::Transfer {
owner,
amount,
target_account,
} => {
self.check_account_authentication(owner)?;
self.state.debit(owner, amount).await?;
self.finish_transfer_to_account(amount, target_account, owner)
.await;
FungibleResponse::Ok
}
// ...
}
}
async fn finish_transfer_to_account(
&mut self,
amount: Amount,
target_account: Account,
source: AccountOwner,
) {
if target_account.chain_id == self.runtime.chain_id() {
self.state.credit(target_account.owner, amount).await;
} else {
let message = Message::Credit {
target: target_account.owner,
amount,
source,
};
self.runtime
.prepare_message(message)
.with_authentication()
.with_tracking()
.send_to(target_account.chain_id);
}
}
On the recipient's chain, execute_message
is called, which increases their
account balance.
async fn execute_message(&mut self, message: Message) {
match message {
Message::Credit {
amount,
target,
source,
} => {
// ...
self.state.credit(receiver, amount).await;
}
// ...
}
}