Message lane integration documentation (#736)

* how-to-integrate-message-lane-module

* added README stub for bridge-runtime-common

* added README stub for pallet-bridge-call-dispatch

* bridge-runtime-common documentation

* call dispatch module documentation

* some fixes

* more fixes

* more fixes

* more fixes

* more fixes for runtime-common/README.md

* more fixes in call-dispatch/README.md

* more fixes in call-dispatch/README.md

* more fixes in call-dispatch/README.md

* more fixes in message-lane/README.md

* more fixes in message-lane/README.md

* Wrap most text at 100 characters

* Clean up some of the formatting

* Fix broken link

* Stop running CI for README changes

* Don't run any CI steps on documentation changes

Co-authored-by: Hernando Castano <castano.ha@gmail.com>
This commit is contained in:
Svyatoslav Nikolsky
2021-02-24 09:31:28 +03:00
committed by Bastian Köcher
parent 89b0f7beda
commit 4a1a990fa6
3 changed files with 527 additions and 24 deletions
+183
View File
@@ -0,0 +1,183 @@
# Helpers for Message Lane Module Integration
The [`messages`](./src/messages.rs) module of this crate contains a bunch of helpers for integrating
message lane module into your runtime. Basic prerequisites of these helpers are:
- we're going to bridge Substrate-based chain with another Substrate-based chain;
- both chains have [message lane module](../../modules/message-lane/README.md), Substrate bridge
module and the [call dispatch module](../../modules/call-dispatch/README.md);
- all message lanes are identical and may be used to transfer the same messages;
- the messages sent over the bridge are dispatched using
[call dispatch module](../../modules/call-dispatch/README.md);
- the messages are `pallet_bridge_call_dispatch::MessagePayload` structures, where `call` field is
encoded `Call` of the target chain. This means that the `Call` is opaque to the
[message lane module](../../modules/message-lane/README.md) instance at the source chain.
It is pre-encoded by the message submitter;
- all proofs in the [message lane module](../../modules/message-lane/README.md) transactions are
based on the storage proofs from the bridged chain: storage proof of the outbound message (value
from the `pallet_message_lane::Store::MessagePayload` map), storage proof of the outbound lane
state (value from the `pallet_message_lane::Store::OutboundLanes` map) and storage proof of the
inbound lane state (value from the `pallet_message_lane::Store::InboundLanes` map);
- storage proofs are built at the finalized headers of the corresponding chain. So all message lane
transactions with proofs are verifying storage proofs against finalized chain headers from
Substrate bridge module.
**IMPORTANT NOTE**: after reading this document, you may refer to our test runtimes
([rialto_messages.rs](../millau/runtime/src/rialto_messages.rs) and/or
[millau_messages.rs](../rialto/runtime/src/millau_messages.rs)) to see how to use these helpers.
## Contents
- [`MessageBridge` Trait](#messagebridge-trait)
- [`ChainWithMessageLanes` Trait ](#chainwithmessagelanes-trait)
- [Helpers for the Source Chain](#helpers-for-the-source-chain)
- [Helpers for the Target Chain](#helpers-for-the-target-chain)
## `MessageBridge` Trait
The essence of your integration will be a struct that implements a `MessageBridge` trait. Let's
review every method and give some implementation hints here:
- `MessageBridge::maximal_extrinsic_size_on_target_chain`: you will need to return the maximal
extrinsic size of the target chain from this function. This may be the constant that is updated
when your runtime is upgraded, or you may use the
[message lane parameters functionality](../../modules/message-lane/README.md#Non-Essential-Functionality)
to allow the pallet owner to update this value more frequently (you may also want to use this
functionality for all constants that are used in other methods described below).
- `MessageBridge::weight_limits_of_message_on_bridged_chain`: you'll need to return a range of
dispatch weights that the outbound message may take at the target chain. Please keep in mind that
our helpers assume that the message is an encoded call of the target chain. But we never decode
this call at the source chain. So you can't simply get dispatch weight from pre-dispatch
information. Instead there are two options to prepare this range: if you know which calls are to
be sent over your bridge, then you may just return weight ranges for these particular calls.
Otherwise, if you're going to accept all kinds of calls, you may just return range `[0; maximal
incoming message dispatch weight]`. If you choose the latter, then you shall remember that the
delivery transaction itself has some weight, so you can't accept messages with weight equal to
maximal weight of extrinsic at the target chain. In our test chains, we reject all messages that
have declared dispatch weight larger than 50% of the maximal bridged extrinsic weight.
- `MessageBridge::weight_of_delivery_transaction`: you will need to return the maximal weight of the
delivery transaction that delivers a given message to the target chain. There are three main
things to notice:
1. weight, returned from this function is then used to compute the fee that the
message sender needs to pay for the delivery transaction. So it shall not be a simple dispatch
weight of delivery call - it should be the "weight" of the transaction itself, including per-byte
"weight", "weight" of signed extras and etc.
1. the delivery transaction brings storage proof of
the message, not the message itself. So your transaction will include extra bytes. We suggest
computing the size of single empty value storage proof at the source chain, increase this value a
bit and hardcode it in the source chain runtime code. This size then must be added to the size of
payload and included in the weight computation;
1. before implementing this function, please take
a look at the
[weight formula of delivery transaction](../../modules/message-lane/README.md#Weight-of-receive_messages_proof-call).
It adds some extra weight for every additional byte of the proof (everything above
`pallet_message_lane::EXPECTED_DEFAULT_MESSAGE_LENGTH`), so it's not trivial. Even better, please
refer to [our implementation](../millau/runtime/src/rialto_messages.rs) for test chains for
details.
- `MessageBridge::weight_of_delivery_confirmation_transaction_on_this_chain`: you'll need to return
the maximal weight of a single message delivery confirmation transaction on this chain. All points
from the previous paragraph are also relevant here.
- `MessageBridge::this_weight_to_this_balance`: this function needs to convert weight units into fee
units on this chain. Most probably this can be done by calling
`pallet_transaction_payment::Config::WeightToFee::calc()` for passed weight.
- `MessageBridge::bridged_weight_to_bridged_balance`: this function needs to convert weight units
into fee units on the target chain. The best case is when you have the same conversion formula on
both chains - then you may just call the same formula from the previous paragraph. Otherwise,
you'll need to hardcode this formula into your runtime.
- `MessageBridge::bridged_balance_to_this_balance`: this may be the easiest method to implement and
the hardest to maintain at the same time. If you don't have any automatic methods to determine
conversion rate, then you'll probably need to maintain it by yourself (by updating conversion
rate, stored in runtime storage). This means that if you're too late with an update, then you risk
to accept messages with lower-than-expected fee. So it may be wise to have some reserve in this
conversion rate, even if that means larger delivery and dispatch fees.
## `ChainWithMessageLanes` Trait
Apart from its methods, `MessageBridge` also has two associated types that are implementing the
`ChainWithMessageLanes` trait. One is for this chain and the other is for the bridged chain. The
trait is quite simple and can easily be implemented - you just need to specify types used at the
corresponding chain. There are two exceptions, though. Both may be changed in the future. Here they
are:
- `ChainWithMessageLanes::Call`: it isn't a good idea to reference bridged chain runtime from your
runtime (cyclic references + maintaining on upgrades). So you can't know the type of bridged chain
call in your runtime. This type isn't actually used at this chain, so you may use `()` instead.
- `ChainWithMessageLanes::MessageLaneInstance`: this is used to compute runtime storage keys. There
may be several instances of message lane pallet, included in the Runtime. Every instance stores
messages and these messages stored under different keys. When we are verifying storage proofs from
the bridged chain, we should know which instance we're talking to. This is fine, but there's
significant inconvenience with that - this chain runtime must have the same message lane pallet
instance. This does not necessarily mean that we should use the same instance on both chains -
this instance may be used to bridge with another chain/instance, or may not be used at all.
## Helpers for the Source Chain
The helpers for the Source Chain reside in the `source` submodule of the
[`messages`](./src/messages.rs) module. The structs are: `FromThisChainMessagePayload`,
`FromBridgedChainMessagesDeliveryProof`, `FromThisChainMessageVerifier`. And the helper functions
are: `maximal_message_size`, `verify_chain_message`, `verify_messages_delivery_proof` and
`estimate_message_dispatch_and_delivery_fee`.
`FromThisChainMessagePayload` is a message that the sender sends through our bridge. It is the
`pallet_bridge_call_dispatch::MessagePayload`, where `call` field is encoded target chain call. So
at this chain we don't see internals of this call - we just know its size.
`FromThisChainMessageVerifier` is an implementation of `bp_message_lane::LaneMessageVerifier`. It
has following checks in its `verify_message` method:
1. it'll verify that the used outbound lane is enabled in our runtime;
1. it'll reject messages if there are too many undelivered outbound messages at this lane. The
sender need to wait while relayers will do their work before sending the message again;
1. it'll reject a message if it has the wrong dispatch origin declared. Like if the submitter is not
the root of this chain, but it tries to dispatch the message at the target chain using
`pallet_bridge_call_dispatch::CallOrigin::SourceRoot` origin. Or he has provided wrong signature
in the `pallet_bridge_call_dispatch::CallOrigin::TargetAccount` origin;
1. it'll reject a message if the delivery and dispatch fee that the submitter wants to pay is lesser
than the fee that is computed using the `estimate_message_dispatch_and_delivery_fee` function.
`estimate_message_dispatch_and_delivery_fee` returns a minimal fee that the submitter needs to pay
for sending a given message. The fee includes: payment for the delivery transaction at the target
chain, payment for delivery confirmation transaction on this chain, payment for `Call` dispatch at
the target chain and relayer interest.
`FromBridgedChainMessagesDeliveryProof` holds the lane identifier and the storage proof of this
inbound lane state at the bridged chain. This also holds the hash of the target chain header, that
was used to generate this storage proof. The proof is verified by the
`verify_messages_delivery_proof`, which simply checks that the target chain header is finalized
(using Substrate bridge module) and then reads the inbound lane state from the proof.
`verify_chain_message` function checks that the message may be delivered to the bridged chain. There
are two main checks:
1. that the message size is less than or equal to the `2/3` of maximal extrinsic size at the target
chain. We leave `1/3` for signed extras and for the storage proof overhead;
1. that the message dispatch weight is less than or equal to the `1/2` of maximal normal extrinsic
weight at the target chain. We leave `1/2` for the delivery transaction overhead.
## Helpers for the Target Chain
The helpers for the target chain reside in the `target` submodule of the
[`messages`](./src/messages.rs) module. The structs are: `FromBridgedChainMessagePayload`,
`FromBridgedChainMessagesProof`, `FromBridgedChainMessagesProof`. And the helper functions are:
`maximal_incoming_message_dispatch_weight`, `maximal_incoming_message_size` and
`verify_messages_proof`.
`FromBridgedChainMessagePayload` corresponds to the `FromThisChainMessagePayload` at the bridged
chain. We expect that messages with this payload are stored in the `OutboundMessages` storage map of
the [message lane module](../../modules/message-lane/README.md). This map is used to build
`FromBridgedChainMessagesProof`. The proof holds the lane id, range of message nonces included in
the proof, storage proof of `OutboundMessages` entries and the hash of bridged chain header that has
been used to build the proof. Additionally, there's storage proof may contain the proof of outbound
lane state. It may be required to prune `relayers` entries at this chain (see
[message lane module documentation](../../modules/message-lane/README.md#What-about-other-Constants-in-the-Message-Lane-Module-Configuration-Trait)
for details). This proof is verified by the `verify_messages_proof` function.
+61
View File
@@ -0,0 +1,61 @@
# Call Dispatch Module
The call dispatch module has a single internal (only callable by other runtime modules) entry point
for dispatching encoded calls (`pallet_bridge_call_dispatch::Module::dispatch`). Every dispatch
(successful or not) emits a corresponding module event. The module doesn't have any call-related
requirements - they may come from the bridged chain over some message lane, or they may be crafted
locally. But in this document we'll mostly talk about this module in the context of bridges.
Every message that is being dispatched has three main characteristics:
- `bridge` is the 4-bytes identifier of the bridge where this message comes from. This may be the
identifier of the bridged chain (like `b"rlto"` for messages coming from `Rialto`), or the
identifier of the bridge itself (`b"rimi"` for `Rialto` <-> `Millau` bridge);
- `id` is the unique id of the message within the given bridge. For messages coming from the
[message lane module](../message-lane/README.md), it may worth to use a tuple
`(LaneId, MessageNonce)` to identify a message;
- `message` is the `pallet_bridge_call_dispatch::MessagePayload` structure. The `call` field is set
to the (potentially) encoded `Call` of this chain.
The easiest way to understand what is happening when a `Call` is being dispatched, is to look at the
module events set:
- `MessageRejected` event is emitted if a message has been rejected even before it has reached the
module. Dispatch then is called just to reflect the fact that message has been received, but we
have failed to pre-process it (e.g. because we have failed to decode `MessagePayload` structure
from the proof);
- `MessageVersionSpecMismatch` event is emitted if current runtime specification version differs
from the version that has been used to encode the `Call`. The message payload has the
`spec_version`, that is filled by the message submitter. If this value differs from the current
runtime version, dispatch mechanism rejects to dispatch the message. Without this check, we may
decode the wrong `Call` for example if method arguments were changed;
- `MessageCallDecodeFailed` event is emitted if we have failed to decode `Call` from the payload.
This may happen if the submitter has provided incorrect value in the `call` field, or if source
chain storage has been corrupted. The `Call` is decoded after `spec_version` check, so we'll never
try to decode `Call` from other runtime version;
- `MessageSignatureMismatch` event is emitted if submitter has chose to dispatch message using
specified this chain account (`pallet_bridge_call_dispatch::CallOrigin::TargetAccount` origin),
but he has failed to prove that he owns the private key for this account;
- `MessageCallRejected` event is emitted if the module has been deployed with some call filter and
this filter has rejected the `Call`. In your bridge you may choose to reject all messages except
e.g. balance transfer calls;
- `MessageWeightMismatch` event is emitted if the message submitter has specified invalid `Call`
dispatch weight in the `weight` field of the message payload. The value of this field is compared
to the pre-dispatch weight of the decoded `Call`. If it is less than the actual pre-dispatch
weight, the dispatch is rejected. Keep in mind, that even if post-dispatch weight will be less
than specified, the submitter still have to declare (and pay for) the maximal possible weight
(that is the pre-dispatch weight);
- `MessageDispatched` event is emitted if the message has passed all checks and we have actually
dispatched it. The dispatch may still fail, though - that's why we are including the dispatch
result in the event payload.
When we talk about module in context of bridges, these events are helping in following cases:
1. when the message submitter has access to the state of both chains and wants to monitor what has
happened with his message. Then he could use the message id (that he gets from the
[message lane module events](../message-lane/README.md#General-Information)) to filter events of
call dispatch module at the target chain and actually see what has happened with his message;
1. when the message submitter only has access to the source chain state (for example, when sender is
the runtime module at the source chain). In this case, your bridge may have additional mechanism
to deliver dispatch proofs (which are storage proof of module events) back to the source chain,
thus allowing the submitter to see what has happened with his messages.
+282 -23
View File
@@ -1,28 +1,262 @@
# Message Lane Module
The Message Lane Module is used to deliver messages from source to target chain. Message is (almost) opaque to the module and the final goal is to hand message to the message dispatch mechanism.
The message lane module is used to deliver messages from source chain to target chain. Message is
(almost) opaque to the module and the final goal is to hand message to the message dispatch
mechanism.
## Contents
- [Overview](#overview)
- [Message Workflow](#message-workflow)
- [Integrating Message Lane Module into Runtime](#integrating-message-lane-module-into-runtime)
- [Non-Essential Functionality](#non-essential-functionality)
- [Weights of Module Extrinsics](#weights-of-module-extrinsics)
## Overview
*In progress*
Message lane is an unidirectional channel, where messages are sent from source chain to the target
chain. At the same time, a single instance of message lane module supports both outbound lanes and
inbound lanes. So the chain where the module is deployed (this chain), may act as a source chain for
outbound messages (heading to a bridged chain) and as a target chain for inbound messages (coming
from a bridged chain).
## Weights of module extrinsics
Message lane module supports multiple message lanes. Every message lane is identified with a 4-byte
identifier. Messages sent through the lane are assigned unique (for this lane) increasing integer
value that is known as nonce ("number that can only be used once"). Messages that are sent over the
same lane are guaranteed to be delivered to the target chain in the same order they're sent from
the source chain. In other words, message with nonce `N` will be delivered right before delivering a
message with nonce `N+1`.
Single message lane may be seen as a transport channel for single application (onchain, offchain or
mixed). At the same time the module itself never dictates any lane or message rules. In the end, it
is the runtime developer who defines what message lane and message mean for this runtime.
## Message Workflow
The message "appears" when its submitter calls the `send_message()` function of the module. The
submitter specifies the lane that he's willing to use, the message itself and the fee that he's
willing to pay for the message delivery and dispatch. If a message passes all checks, the nonce is
assigned and the message is stored in the module storage. The message is in an "undelivered" state
now.
We assume that there are external, offchain actors, called relayers, that are submitting module
related transactions to both target and source chains. The pallet itself has no assumptions about
relayers incentivization scheme, but it has some callbacks for paying rewards. See
[Integrating Message Lane Module into runtime](#Integrating-Message-Lane-Module-into-runtime)
for details.
Eventually, some relayer would notice this message in the "undelivered" state and it would decide to
deliver this message. Relayer then crafts `receive_messages_proof()` transaction (aka delivery
transaction) for the message lane module instance, deployed at the target chain. Relayer provides
his account id at the source chain, the proof of message (or several messages), the number of
messages in the transaction and their cumulative dispatch weight. Once a transaction is mined, the
message is considered "delivered".
Once a message is delivered, the relayer may want to confirm delivery back to the source chain.
There are two reasons why he would want to do that. The first is that we intentionally limit number
of "delivered", but not yet "confirmed" messages at inbound lanes
(see [What about other Constants in the Message Lane Module Configuration Trait](#What-about-other-Constants-in-the-Message-Lane-Module-Configuration-Trait) for explanation).
So at some point, the target chain may stop accepting new messages until relayers confirm some of
these. The second is that if the relayer wants to be rewarded for delivery, he must prove the fact
that he has actually delivered the message. And this proof may only be generated after the delivery
transaction is mined. So relayer crafts the `receive_messages_delivery_proof()` transaction (aka
confirmation transaction) for the message lane module instance, deployed at the source chain. Once
this transaction is mined, the message is considered "confirmed".
The "confirmed" state is the final state of the message. But there's one last thing related to the
message - the fact that it is now "confirmed" and reward has been paid to the relayer (or at least
callback for this has been called), must be confirmed to the target chain. Otherwise, we may reach
the limit of "unconfirmed" messages at the target chain and it will stop accepting new messages. So
relayer sometimes includes a nonce of the latest "confirmed" message in the next
`receive_messages_proof()` transaction, proving that some messages have been confirmed.
## Integrating Message Lane Module into Runtime
As it has been said above, the message lane module supports both outbound and inbound message lanes.
So if we will integrate a module in some runtime, it may act as the source chain runtime for
outbound messages and as the target chain runtime for inbound messages. In this section, we'll
sometimes refer to the chain we're currently integrating with, as this chain and the other chain as
bridged chain.
Message lane module doesn't simply accept transactions that are claiming that the bridged chain has
some updated data for us. Instead of this, the module assumes that the bridged chain is able to
prove that updated data in some way. The proof is abstracted from the module and may be of any kind.
In our Substrate-to-Substrate bridge we're using runtime storage proofs. Other bridges may use
transaction proofs, Substrate header digests or anything else that may be proved.
**IMPORTANT NOTE**: everything below in this chapter describes details of the message lane module
configuration. But if you interested in well-probed and relatively easy integration of two
Substrate-based chains, you may want to look at the
[bridge-runtime-common](../../bin/runtime-common/README.md) crate. This crate is providing a lot of
helpers for integration, which may be directly used from within your runtime. Then if you'll decide
to change something in this scheme, get back here for detailed information.
### General Information
The message lane module supports instances. Every module instance is supposed to bridge this chain
and some bridged chain. To bridge with another chain, using another instance is suggested (this
isn't forced anywhere in the code, though).
Message submitters may track message progress by inspecting module events. When Message is accepted,
the `MessageAccepted` event is emitted in the `send_message()` transaction. The event contains both
message lane identifier and nonce that has been assigned to the message. When a message is delivered
to the target chain, the `MessagesDelivered` event is emitted from the
`receive_messages_delivery_proof()` transaction. The `MessagesDelivered` contains the message lane
identifier and inclusive range of delivered message nonces.
### How to plug-in Message Lane Module to Send Messages to the Bridged Chain?
The `pallet_message_lane::Config` trait has 3 main associated types that are used to work with
outbound messages. The `pallet_message_lane::Config::TargetHeaderChain` defines how we see the
bridged chain as the target for our outbound messages. It must be able to check that the bridged
chain may accept our message - like that the message has size below maximal possible transaction
size of the chain and so on. And when the relayer sends us a confirmation transaction, this
implementation must be able to parse and verify the proof of messages delivery. Normally, you would
reuse the same (configurable) type on all chains that are sending messages to the same bridged
chain.
The `pallet_message_lane::Config::LaneMessageVerifier` defines a single callback to verify outbound
messages. The simplest callback may just accept all messages. But in this case you'll need to answer
many questions first. Who will pay for the delivery and confirmation transaction? Are we sure that
someone will ever deliver this message to the bridged chain? Are we sure that we don't bloat our
runtime storage by accepting this message? What if the message is improperly encoded or has some
fields set to invalid values? Answering all those (and similar) questions would lead to correct
implementation.
There's another thing to consider when implementing type for use in
`pallet_message_lane::Config::LaneMessageVerifier`. It is whether we treat all message lanes
identically, or they'll have different sets of verification rules? For example, you may reserve
lane#1 for messages coming from some 'wrapped-token' pallet - then you may verify in your
implementation that the origin is associated with this pallet. Lane#2 may be reserved for 'system'
messages and you may charge zero fee for such messages. You may have some rate limiting for messages
sent over the lane#3. Or you may just verify the same rules set for all outbound messages - it is
all up to the `pallet_message_lane::Config::LaneMessageVerifier` implementation.
The last type is the `pallet_message_lane::Config::MessageDeliveryAndDispatchPayment`. When all
checks are made and we have decided to accept the message, we're calling the
`pay_delivery_and_dispatch_fee()` callback, passing the corresponding argument of the `send_message`
function. Later, when message delivery is confirmed, we're calling `pay_relayers_rewards()`
callback, passing accounts of relayers and messages that they have delivered. The simplest
implementation of this trait is in the [`instant_payments.rs`](./src/instant_payments.rs) module and
simply calls `Currency::transfer()` when those callbacks are called. So `Currency` units are
transferred between submitter, 'relayers fund' and relayers accounts. Other implementations may use
more or less sophisticated techniques - the whole relayers incentivization scheme is not a part of
the message lane module.
### I have a Message Lane Module in my Runtime, but I Want to Reject all Outbound Messages. What shall I do?
You should be looking at the `bp_message_lane::source_chain::ForbidOutboundMessages` structure
[`bp_message_lane::source_chain`](../../primitives/message-lane/src/source_chain.rs). It implements
all required traits and will simply reject all transactions, related to outbound messages.
### How to plug-in Message Lane Module to Receive Messages from the Bridged Chain?
The `pallet_message_lane::Config` trait has 2 main associated types that are used to work with
inbound messages. The `pallet_message_lane::Config::SourceHeaderChain` defines how we see the
bridged chain as the source or our inbound messages. When relayer sends us a delivery transaction,
this implementation must be able to parse and verify the proof of messages wrapped in this
transaction. Normally, you would reuse the same (configurable) type on all chains that are sending
messages to the same bridged chain.
The `pallet_message_lane::Config::MessageDispatch` defines a way on how to dispatch delivered
messages. Apart from actually dispatching the message, the implementation must return the correct
dispatch weight of the message before dispatch is called.
### I have a Message Lane Module in my Runtime, but I Want to Reject all Inbound Messages. What
shall I do?
You should be looking at the `bp_message_lane::target_chain::ForbidInboundMessages` structure from
the [`bp_message_lane::target_chain`](../../primitives/message-lane/src/target_chain.rs) module. It
implements all required traits and will simply reject all transactions, related to inbound messages.
### What about other Constants in the Message Lane Module Configuration Trait?
Message is being stored in the source chain storage until its delivery will be confirmed. After
that, we may safely remove the message from the storage. Lane messages are removed (pruned) when
someone sends a new message using the same lane. So the message submitter pays for that pruning. To
avoid pruning too many messages in a single transaction, there's
`pallet_message_lane::Config::MaxMessagesToPruneAtOnce` configuration parameter. We will never prune
more than this number of messages in the single transaction. That said, the value should not be too
big to avoid waste of resources when there are no messages to prune.
To be able to reward the relayer for delivering messages, we store a map of message nonces range =>
identifier of the relayer that has delivered this range at the target chain runtime storage. If a
relayer delivers multiple consequent ranges, they're merged into single entry. So there may be more
than one entry for the same relayer. Eventually, this whole map must be delivered back to the source
chain to confirm delivery and pay rewards. So to make sure we are able to craft this confirmation
transaction, we need to: (1) keep the size of this map below a certain limit and (2) make sure that
the weight of processing this map is below a certain limit. Both size and processing weight mostly
depend on the number of entries. The number of entries is limited with the
`pallet_message_lane::ConfigMaxUnrewardedRelayerEntriesAtInboundLane` parameter. Processing weight
also depends on the total number of messages that are being confirmed, because every confirmed
message needs to be read. So there's another
`pallet_message_lane::Config::MaxUnconfirmedMessagesAtInboundLane` parameter for that.
When choosing values for these parameters, you must also keep in mind that if proof in your scheme
is based on finality of headers (and it is the most obvious option for Substrate-based chains with
finality notion), then choosing too small values for these parameters may cause significant delays
in message delivery. That's because there too many actors involved in this scheme: 1) authorities
that are finalizing headers of the target chain need to finalize header with non-empty map; 2) the
headers relayer then needs to submit this header and its finality proof to the source chain; 3) the
messages relayer must then send confirmation transaction (storage proof of this map) to the source
chain; 4) when the confirmation transaction will be mined at some header, source chain authorities
must finalize this header; 5) the headers relay then needs to submit this header and its finality
proof to the target chain; 6) only now the messages relayer may submit new messages from the source
to target chain and prune the entry from the map.
Delivery transaction requires the relayer to provide both number of entries and total number of
messages in the map. This means that the module never charges an extra cost for delivering a map -
the relayer would need to pay exactly for the number of entries+messages it has delivered. So the
best guess for values of these parameters would be the pair that would occupy `N` percent of the
maximal transaction size and weight of the source chain. The `N` should be large enough to process
large maps, at the same time keeping reserve for future source chain upgrades.
## Non-Essential Functionality
Apart from the message related calls, the module exposes a set of auxiliary calls. They fall in two
groups, described in the next two paragraphs.
There may be a special account in every runtime where the message lane module is deployed. This
account, named 'module owner', is like a module-level sudo account - he's able to halt all and
result all module operations without requiring runtime upgrade. The module may have no message
owner, but we suggest to use it at least for initial deployment. To calls that are related to this
account are:
- `fn set_owner()`: current module owner may call it to transfer "ownership" to another account;
- `fn halt_operations()`: the module owner (or sudo account) may call this function to stop all
module operations. After this call, all message-related transactions will be rejected until
further `resume_operations` call'. This call may be used when something extraordinary happens with
the bridge;
- `fn resume_operations()`: module owner may call this function to resume bridge operations. The
module will resume its regular operations after this call.
Apart from halting and resuming the bridge, the module owner may also tune module configuration
parameters without runtime upgrades. The set of parameters needs to be designed in advance, though.
The module configuration trait has associated `Parameter` type, which may be e.g. enum and represent
a set of parameters that may be updated by the module owner. For example, if your bridge needs to
convert sums between different tokens, you may define a 'conversion rate' parameter and let the
module owner update this parameter when there are significant changes in the rate. The corresponding
module call is `fn update_pallet_parameter()`.
## Weights of Module Extrinsics
The main assumptions behind weight formulas is:
- all possible costs are paid in advance by the message submitter;
- whenever possible, relayer tries to minimize cost of its transactions. So e.g. even though sender always pays for delivering outbound lane state proof, relayer may not include it in the delivery transaction (unless message lane module on target chain requires that);
- weight formula should incentivize relayer to not to submit any redundand data in the extrinsics arguments;
- the extrinsic shall never be executing slower (i.e. has larger actual weight) than defined by the formula.
- whenever possible, relayer tries to minimize cost of its transactions. So e.g. even though sender
always pays for delivering outbound lane state proof, relayer may not include it in the delivery
transaction (unless message lane module on target chain requires that);
- weight formula should incentivize relayer to not to submit any redundant data in the extrinsics
arguments;
- the extrinsic shall never be executing slower (i.e. has larger actual weight) than defined by the
formula.
### Weight of `send_message` call
#### Related benchmarks
| Benchmark | Description |
|-----------------------------------|--------------------------------------------------------|
| `send_minimal_message_worst_case` | Sends 0-size message with worst possible conditions |
| `send_1_kb_message_worst_case` | Sends 1KB-size message with worst possible conditions |
| `send_16_kb_message_worst_case` | Sends 16KB-size message with worst possible conditions |
|-----------------------------------|-----------------------------------------------------|
`send_minimal_message_worst_case` | Sends 0-size message with worst possible conditions |
`send_1_kb_message_worst_case` | Sends 1KB-size message with worst possible conditions |
`send_16_kb_message_worst_case` | Sends 16KB-size message with worst possible conditions |
#### Weight formula
@@ -52,13 +286,19 @@ Where:
*\* - In all benchmarks all received messages are dispatched and their dispatch cost is near to zero*
*\*\* - Trie leafs are assumed to have minimal values. The proof is derived from the minimal proof by including more trie nodes. That's because according to `receive_message_proofs_with_large_leaf` and `receive_message_proofs_with_extra_nodes` benchmarks, increasing proof by including more nodes has slightly larger impact on performance than increasing values stored in leafs*.
*\*\* - Trie leafs are assumed to have minimal values. The proof is derived from the minimal proof
by including more trie nodes. That's because according to `receive_message_proofs_with_large_leaf`
and `receive_message_proofs_with_extra_nodes` benchmarks, increasing proof by including more nodes
has slightly larger impact on performance than increasing values stored in leafs*.
#### Weight formula
The weight formula is:
```
Weight = BaseWeight + OutboundStateDeliveryWeight + MessagesCount * MessageDeliveryWeight + MessagesDispatchWeight + Max(0, ActualProofSize - ExpectedProofSize) * ProofByteDeliveryWeight
Weight = BaseWeight + OutboundStateDeliveryWeight
+ MessagesCount * MessageDeliveryWeight
+ MessagesDispatchWeight
+ Max(0, ActualProofSize - ExpectedProofSize) * ProofByteDeliveryWeight
```
Where:
@@ -78,22 +318,34 @@ Where:
We have following checks in `send_message` transaction on the source chain:
- message size should be less than or equal to `2/3` of maximal extrinsic size on the target chain;
- message dispatch weight should be less than or equal to the `1/2` of maximal extrinsic dispatch weight on the target chain.
- message dispatch weight should be less than or equal to the `1/2` of maximal extrinsic dispatch
weight on the target chain.
Delivery transaction is an encoded delivery call and signed extensions. So we have `1/3` of maximal extrinsic size reserved for:
- storage proof, excluding the message itself. Currently, on our test chains, the overhead is always within `EXTRA_STORAGE_PROOF_SIZE` limits (1024 bytes);
- signed extras and other call arguments (`relayer_id: SourceChain::AccountId`, `messages_count: u32`, `dispatch_weight: u64`).
Delivery transaction is an encoded delivery call and signed extensions. So we have `1/3` of maximal
extrinsic size reserved for:
- storage proof, excluding the message itself. Currently, on our test chains, the overhead is always
within `EXTRA_STORAGE_PROOF_SIZE` limits (1024 bytes);
- signed extras and other call arguments (`relayer_id: SourceChain::AccountId`, `messages_count:
u32`, `dispatch_weight: u64`).
On Millau chain, maximal extrinsic size is `0.75 * 2MB`, so `1/3` is `512KB` (`524_288` bytes). This should be enough to cover these extra arguments and signed extensions.
On Millau chain, maximal extrinsic size is `0.75 * 2MB`, so `1/3` is `512KB` (`524_288` bytes). This
should be enough to cover these extra arguments and signed extensions.
Let's exclude message dispatch cost from single message delivery transaction weight formula:
```
Weight = BaseWeight + OutboundStateDeliveryWeight + MessageDeliveryWeight + Max(0, ActualProofSize - ExpectedProofSize) * ProofByteDeliveryWeight
Weight = BaseWeight + OutboundStateDeliveryWeight + MessageDeliveryWeight
+ Max(0, ActualProofSize - ExpectedProofSize) * ProofByteDeliveryWeight
```
So we have `1/2` of maximal extrinsic weight to cover these components. `BaseWeight`, `OutboundStateDeliveryWeight` and `MessageDeliveryWeight` are determined using benchmarks and are hardcoded into runtime. Adequate relayer would only include required trie nodes into the proof. So if message size would be maximal (`2/3` of `MaximalExtrinsicSize`), then the extra proof size would be `MaximalExtrinsicSize / 3 * 2 - EXPECTED_DEFAULT_MESSAGE_LENGTH`.
So we have `1/2` of maximal extrinsic weight to cover these components. `BaseWeight`,
`OutboundStateDeliveryWeight` and `MessageDeliveryWeight` are determined using benchmarks and are
hardcoded into runtime. Adequate relayer would only include required trie nodes into the proof. So
if message size would be maximal (`2/3` of `MaximalExtrinsicSize`), then the extra proof size would
be `MaximalExtrinsicSize / 3 * 2 - EXPECTED_DEFAULT_MESSAGE_LENGTH`.
Both conditions are verified by `pallet_message_lane::ensure_weights_are_correct` and `pallet_message_lane::ensure_able_to_receive_messages` functions, which must be called from every runtime' tests.
Both conditions are verified by `pallet_message_lane::ensure_weights_are_correct` and
`pallet_message_lane::ensure_able_to_receive_messages` functions, which must be called from every
runtime's tests.
### Weight of `receive_messages_delivery_proof` call
@@ -109,7 +361,9 @@ Both conditions are verified by `pallet_message_lane::ensure_weights_are_correct
The weight formula is:
```
Weight = BaseWeight + MessagesCount * MessageConfirmationWeight + RelayersCount * RelayerRewardWeight + Max(0, ActualProofSize - ExpectedProofSize) * ProofByteDeliveryWeight
Weight = BaseWeight + MessagesCount * MessageConfirmationWeight
+ RelayersCount * RelayerRewardWeight
+ Max(0, ActualProofSize - ExpectedProofSize) * ProofByteDeliveryWeight
```
Where:
@@ -127,6 +381,11 @@ Where:
#### Why we're always able to craft `receive_messages_delivery_proof` transaction?
There can be at most `<PeerRuntime as pallet_message_lane::Config>::MaxUnconfirmedMessagesAtInboundLane` messages and at most `<PeerRuntime as pallet_message_lane::Config>::MaxUnrewardedRelayerEntriesAtInboundLane` unrewarded relayers in the single delivery confirmation transaction.
There can be at most `<PeerRuntime as pallet_message_lane::Config>::MaxUnconfirmedMessagesAtInboundLane`
messages and at most
`<PeerRuntime as pallet_message_lane::Config>::MaxUnrewardedRelayerEntriesAtInboundLane` unrewarded
relayers in the single delivery confirmation transaction.
We're checking that this transaction may be crafted in the `pallet_message_lane::ensure_able_to_receive_confirmation` function, which must be called from every runtime' tests.
We're checking that this transaction may be crafted in the
`pallet_message_lane::ensure_able_to_receive_confirmation` function, which must be called from every
runtime' tests.