Open HCastano opened 2 years ago
hi guys 👋
how will XCM over SCs work in terms of:
cc @KiChjang
We add a new host function that allows sending XCM functions to a MultiLocation
. The contracts pallet decodes bytes and checks the XCM version and returns an error when the dest
is not able to receive the used version. Apart from that we do not care at all about the contents of bytes
.
ink_env::send_xcm(dest: MultiLocation, bytes: &[u8]) -> Result<(), ErrorCode>;
Whenever a contract is the target of an XCM message it gets called and the gas meter is set according to the BuyWeight
instruction of the received XCM message. We add another exported function xcm_received
that is called instead of call
in this case. seal_input
will return the XCM message and seal_caller
the MultiLocation
sender of the message.
XCMP's asynchronicity - How can SC logic "await"?
The send_xcm
function will be fire and forget. If a reply is expected this needs to be modeled in terms of code in the xcm_received
function. Specifying a callback at call side won't work because the XCM code is mostly opaque to pallet contracts for now.
I expect it to be done like this:
send_xcm
which finalizes the operation in storageXCMP irreversibility - How are cancelled SCs handled after assets have already been teleported away from the contract chain?
We can buffer all XCM messages and only emit them at the end if the contract call did not revert.
ink_env::send_xcm(location: MultiLocation, bytes: &[u8])
Example Usage in ink!:
// Contract developer is responsible for ensuring this
assert!(self.env().transferred_value >= execution_to_buy);
// It's the responsibility of the contract developer to ensure that the right amount of Weight is paid for.
// We may also need to assume that the `MultiAsset` being sent is initially just the Native asset of the chain
let xcm_msg: XcmMessage = {
…,
QueryId,
WithdrawAsset(MultiAsset),
BuyExecution(execution_to_buy),
RefundSurplus(…)
};
// TODO: XCM currently doesn't support a `SmartContract` `MultiLocation` destination, we'll need to add support for that
let location = MultiLocation(…);
ink_env::send_xcm(location, xcm_msg.encode()) -> Result<(), XcmSendError>;
enum XcmSendError {
XcmVersionNotSupported,
UnknownLocation,
}
pallet-contracts
seal_send_xcm(location: MultiLocation, msg: &[u8]) ➜ XcmSendError
MultiLocation
to contracts.BuyExecution
has to be reflected to contracts.
transferred_value
is sufficient, otherwise they sabotage their own contract.BuyExecution(value, …)
for the callback into their contract.Currently, pallet-contracts
enforces the free balance of a contract's account to be larger than the existential deposit. This is so no dust related accidents can happen. With these changes we can no longer enforce this invariant because we can't control the contents of the XCM (it could send away its own balance). I suggest:
pallet-contracts
makes sure that the initial deposit that is subtracted from the caller in order to pay for the contract data structure is at least the existential deposit. It cannot be moved away by XCM and it can only be removed when the contract terminates.
This makes sure that a contract's acount always exist on-chain and no dust related accidents can happen.
extern "C" {
/// Called by pallet_contracts when a xcm message is addressed
/// to a contract.
pub fn xcm_receive() {
xcm_receive(seal_caller(), seal_input());
}
}
// In `ink!`:
#[ink(xcm_receive)]
pub fn ink_xcm_receive(src: MultiLocation, msg: &[u8]) {
// Handle stuff
// Called by `xcm_receive`
}
Two problems here:
We figured that maybe if a receive failed we could emit an event to notify external actors. However, a problem arises when we think about who actually ends up paying for the event. We aren't able to allocate any weight in WithdrawAsset
or BuyExecution
since that weight could be spent before we are able to emit an event.
// Event emitted by `pallet-contracts`
struct XcmReceiveFailure {
err: DispatchError,
location: MultiLocation,
}
We figured that we should provide xcm_receive
anyway, even without a foolproof solution to reversion and error reporting. Sending out an event on receive failure might also be too complicated.
We basically provide this as a "no guards included" bare functionality and monitor what people build with it. This will give us more insight in howto provide more safe abstractions.
Persisting some of our discussions:
#[ink(xcm_receive, max_gas_consumed = 50_000)]
pub fn ink_xcm_receive(src: MultiLocation, msg: &[u8]) {
if msg[0] == 1 {
// expensive computation
} else {
// cheap computation
}
}
The max_gas_consumed
value is put into the metadata, so that the calling parachain has information about which value they should put for BuyExecution
: it would be weight_limit: Some(max_gas_consumed)
.
Note: Gas is a synonym for Weight.
How a contract would be called:
XCM {
BuyExecution(…),
Transact(…),
}
Necessary follow-up issues:
pallet-contracts
for dry_run_xcm_receive
.cargo-contract
to execute dry_run_xcm_receive
.Test use-cases for our MVP:
Persisting this here: It's still unclear how the UI would display our MVP in a meaningful way.
We will do the MVP as a chain extension in a separate repository to get around the fact that we can't depend on polkadot from substrate. This will be useful for that: https://github.com/paritytech/substrate/issues/11751
Delayed to next month because it needs to be merged into polkadot
and that takes more work than just an MVP: Docs, Tests, Review cycle.
@athei
Can you elaborate on why the callback design was discarded in favor of polling?
pallet-xcm
's OnResponse already provides the dispatch callbacks for queries (notify).
Because it is unclear who pays for for the execution when there is no extrinsic attached to the execution. Even without the problem of fees we have the problem that the callback will be called from on_initialize
which we want to avoid. Additionally it introduces difficult error states: What if the max_weight
supplied to the call back is too low. You have no way of recovering. The contract state will be in limbo waiting for the the reply. Attaching this to an extrinsic will allow for re-try with higher weight.
It seems to me as a much simpler design.
The Vision
Contracts should be able to execute and send arbitrary XCM messages. This will allow contracts to participate in the wider ecosystem. A common use case would be to make use of assets on statemint.
The Plan
A while ago I created a chain extension to support executing, sending and receiving XCM messages from a contract: https://github.com/paritytech/pallet-contracts-xcm
This is a prototype but already outlines all the features we need:
ink_xcm_receive
. This solves the weight problems around receiving replies.It doesn't do this in a secure way, though. It is a prototype. For example, it doesn't charge storage deposits for the reply id and reply buffer.
It is a chain extension rather than a core API solely because of technical reasons: It depends on the XCM crates which are part of the
polkadot
repo. However, pallet-contracts is in thesubstrate
repository on whichpolkadot
depends. Thereforepallet-contracts
can't depend on XCM as this would be a circle. As a workaround we placed the code into another repository. The only way to do that is a chain extension. The real implementation should be bundled withpallet-contracts
once we have the monorepo. It still needs to be optional as some chains might decide not to implement it. Hence it might make sense to keep it a chain extension rather than a core API.Open Questions
If you want to help us out and contribute to this issue, in this section you can find open questions and tasks where we would appreciate any input.