-
Notifications
You must be signed in to change notification settings - Fork 38
feat: add experimental XCM CE with companion pallet #152
base: polkadot-v0.9.39
Are you sure you want to change the base?
Conversation
2708b052 wip: move error to CE from pallet 8441e0f3 wip: add event for callback failure 841a6ae6 wip: remove `ink_env` dep from primitives 24b61a18 wip: change xcm ce extension id to 04 f2a20149 wip: minor refactoring and add to shibuya 4ee1d597 wip: lot of refactoring for ce types eb8af147 wip: rename types to primitives, add new query disptach 8ce0b068 wip: complete CE + basic tests 20612bd5 wip: add pallet+CE
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When you need a review for a large commit, you should prepare better.
The first thing someone sees is the PR summary which is in this case 1 sentence.
That's fine if you're adding a few changes and it's obvious from the code.
For your case, with 1200 LoC, it's not enough.
Please expand it and give a summary of what you're doing, the structure, assumptions/simplifications, etc. The less effort other person has to spend on understanding
what you're trying to say, the more you'll get from the interaction.
A direct follow-up would be the rustdoc
of the lib.rs files which is non-existant, essentially.
Personally, when I open a new piece of code (e.g. a pallet), I will first check the rustdoc at the top. 🤷♂️
My recommendation is to polish the PR summary, docs and code (maybe cleanup some unnecessary TODOs?).
For the functionality wise, it's an interesting solution!
Clearly significant amount of effort was put into this, well done! 💪
Maybe I missed this but it's unclear to me:
- who pays for the execution of receiving the query response? (not the smart contract call, just the query processing)
- rent fee isn't handled
- query Ids cannot grow indefinitely (I know you're aware of this)
Such questions/explanations should be in PR summary, IMO.
/// Register a new query | ||
/// THIS IS ONLY FOR WEIGHTS BENCHMARKING, since we cannot benchmark non-dispatch |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You can benchmark anything actually, just call that function in the benchmark macro. These calls don't have to be extrinsic calls
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I didn't know that awesome.
I'll try benchmarking the non dispatch new_query
and remove this afterwards
pub struct Pallet<T>(_); | ||
|
||
#[pallet::config] | ||
pub trait Config: frame_system::Config + pallet_xcm::Config + pallet_contracts::Config { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That is tight coupling with pallet_contracts
. Fine for PoC, but for production, it's better to handle such requirements via associated types.
fn prepare_execute<E: Ext<T = T>>( | ||
&mut self, | ||
env: Environment<E, InitState>, | ||
) -> DispatchResult<RetVal> { | ||
let mut env = env.buf_in_buf_out(); | ||
// input parsing | ||
let len = env.in_len(); | ||
let input: VersionedXcm<RuntimeCallOf<T>> = env.read_as_unbounded(len)?; | ||
|
||
let mut xcm = unwrap!(input.try_into(), BadVersion); | ||
// calculate the weight | ||
let weight = unwrap!(T::Weigher::weight(&mut xcm), CannotWeigh); | ||
|
||
// save the prepared xcm | ||
self.prepared_execute = Some(PreparedExecution { xcm, weight }); | ||
// write the output to buffer | ||
weight.using_encoded(|w| env.write(w, true, None))?; | ||
|
||
Ok(RetVal::Converging(XcmCeError::Success.into())) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What's the weight associated with this?
Can it be spammed until resources are exhausted?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nothing as of now, weights are overall a TODO still.
Best choice would be moving most of this logic to pallet and benchmark that for weights.
/// Register the new query | ||
/// TODO: figure out weights |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Weight of registration, weight of execution (and payment for it), rent fee (and reimbursment?).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Only weight of registration and rent fee (still a TODO).
Execution weight will be paid while sending the XCM calculated via PriceForSiblingDelivery
in cumulus_pallet_xcmp_queue
config which right now is set to ()
.
This is because the weight of callback/notify is set inside the query instruction itself and query method does not take xcm as input (chicken and egg problem), for example in the below instruction.
ReportTransactStatus(QueryResponseInfo {
destination: (Parent, Parachain(1)).into(),
query_id,
max_weight: Weight::from_parts(100_000_000_000_000, 1024 * 1024 * 1024)), -----> this will be copied to `QueryResponse` that will be sent to querier chain
})
For the above query instruction below query response would be sent and max_weight
is copied to it. The OnResponse
handler will ensure that notify dispatch's weight is less than max_weight
before calling it.
QueryResponse {
query_id,
response,
max_weight: Weight::from_parts(100_000_000_000_000, 1024 * 1024 * 1024)),
querier: Some(Here),
},
EDIT: or if I remember correctly, pallet_xcm::send
dispatch also takes care of XCM weight into its weight? In that case WeightInfoBounds
should take care of max_weight
from every query instruction
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok, I'm not sure that PriceForSiblingDelivery
is supposed to take care of that.
In the context we're talking about, it's charging the processing of query response (even before it's polled from the smart contract).
I still don't think that current pallet-xcm
system will be sufficient to handle this scenario properly 🙂 .
BUT - I'd have to spend some time on investigating this to really be sure and make concrete suggestions.
impl<T: Config, W: CEWeightInfo> XCMExtension<T, W> { | ||
/// Returns the weight for given XCM and saves it (in CE, per-call scratch buffer) for | ||
/// execution | ||
fn prepare_execute<E: Ext<T = T>>( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can you explain why we need prepare_execute
and why it can not be done as a first step when invoking execute
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No particular reason, It can be done in one step.
I started this by updating the original XCM CE PoC (by Alex) for XCMv3 and it had this structure so I just kept it.
Also it is kind of useful for smart contract developers to get the execution weight before actually executing it.
Pull Request Summary
Add the experimental XCM CE along with companion pallet.
Callback Design
The callback design make use of
pallet_xcm
'sOnResponse
handler which has capability to notify a dispatch on a XCM response (if notify query is registered).For us that dispatch is companion pallet's
on_callback_received
which will route the xcm response (Response
enum) back to contract via abare_call
(if wasm contract)Let's understand the control flow,
Contract A
will call the XCM CE (XCMExtension
) to register a new query of typeQueryType::WasmContractCallback { .. }
.new_query
pallet_xcm
'snew_notify_query
and sets the notify’s dispatch to its dispatchableon_callback_recieved()
and saves theQueryType
for the registeredquery_id
and return thequery_id
.Contract A
send the XCM with help of CE/Precompilepallet_xcm
’sOnResponse
pallet_xcm
’sOnResponse
calls theon_callback_recieved()
on_callback_recieved()
on receiving response calls theCallbackHandler
CallbackHandler
will then route the response based on theQueryType
, for example to a Wasm contract’s methodOpen Questions
1. Who pays for the execution of callback?
In current design, when registering a query callback weight is not taken into account. This is because the weight of callback/notify is set inside the query instruction itself and query method does not take xcm as input (chicken and egg problem), for example in the below instruction.
For the above query instruction below query response would be sent and
max_weight
is copied to it. TheOnResponse
handler will ensure that notify dispatch's weight is less thanmax_weight
before calling it.Therefore, the weight for callback and for processing the callback should be charged when when sending the XCM.
This can be achieved in two manners,
WeightInfoBounds
: where all query instructions'sQueryResponseInfo::max_weight
is taken into account. Since weight ofpallet_xcm::send()
dispatch also includes the XCM weight.Kusama uses WeightInfoBounds but does not take
max_weight
into account, it would be similar but also takemax_weight
into account.PriceForSiblingDelivery
- When calculating the XCM sending fee, this is ideal place for it actually. The XCM sending fee should take into account themax_weight
from every query instruction. Right now,PriceForSiblingDelivery
in xcm router is set to()
.3. What happens when callback failed?
If in any situation callback fails due to any reason (gas limit, wasm trapped, etc) that would quite difficult for contract to manage. It's still not fatal because contract can implement it's own query timeout based on block number and consider the query expired in such case. Having said that, it would be a very good idea to save the response for contract to manually poll later as a fallback in case of callback error.
Obviously rent fee + weight associated with it should be charged during query registration itself which will make registering query a bit costlier.
Still a TODO
4. How is rent fee handled for registering query and rent refund?
Not handled right now, still a TODO
5. Query Ids cannot grow indefinitely
We are using
pallet_xcm
's query mechanism for this registering/managing queries which does not recycle query id, that means query id is incremental, thus limited. On top of that,pallet_xcm
usesu64
for query ids, which although has very large max value but still can be a issue in future (well if that comes, we'll probably need a storage migration to change it tou128
).TODO
Check list