From d4059d64221793380ce766fc47fe82540d4f7941 Mon Sep 17 00:00:00 2001 From: Ethan Frey Date: Thu, 22 Apr 2021 22:22:11 +0200 Subject: [PATCH] Document submessage semantics --- SEMANTICS.md | 184 ++++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 154 insertions(+), 30 deletions(-) diff --git a/SEMANTICS.md b/SEMANTICS.md index cd8d39b073..3a343f22be 100644 --- a/SEMANTICS.md +++ b/SEMANTICS.md @@ -68,7 +68,6 @@ call. When we implement a contract, we provide the following entry point: - ```rust pub fn execute( deps: DepsMut, @@ -78,15 +77,19 @@ pub fn execute( ) -> Result { } ``` -With `DepsMut`, this can read and write to the backing `Storage`, as well as use the `Api` to validate addresses, -and `Query` the state of other contracts or native modules. Once it is done, it returns either `Ok(Response)` -or `Err(ContractError)`. Let's examine what happens next: +With `DepsMut`, this can read and write to the backing `Storage`, as well as use +the `Api` to validate addresses, and `Query` the state of other contracts or +native modules. Once it is done, it returns either `Ok(Response)` or +`Err(ContractError)`. Let's examine what happens next: -If it returns `Err`, this error is converted to a string representation (`err.to_string()`), and this is returned -to the SDK module. *All state changes are reverted* and `x/wasm` returns this error message, which will *generally* -(see submessage exception below) abort the transaction, and return this same error message to the external caller. +If it returns `Err`, this error is converted to a string representation +(`err.to_string()`), and this is returned to the SDK module. _All state changes +are reverted_ and `x/wasm` returns this error message, which will _generally_ +(see submessage exception below) abort the transaction, and return this same +error message to the external caller. -If it returns `Ok`, the `Response` object is parsed and processed. Let's look at the parts here: +If it returns `Ok`, the `Response` object is parsed and processed. Let's look at +the parts here: ```rust pub struct Response @@ -107,33 +110,37 @@ where } ``` -In the Cosmos SDK, a transaction returns a number of events to the user, along with an optional data "result". This -result is hashed into the next block hash to be provable and can return some essential state (although in general -client apps rely on Events more). This result is more commonly used to pass results between contracts or modules in -the sdk. +In the Cosmos SDK, a transaction returns a number of events to the user, along +with an optional data "result". This result is hashed into the next block hash +to be provable and can return some essential state (although in general client +apps rely on Events more). This result is more commonly used to pass results +between contracts or modules in the sdk. -If the contract sets `data`, this will be returned in the `result` field. `attributes` is a list of `{key, value}` -pairs which will be [appended to a default event](https://github.com/CosmWasm/wasmd/blob/master/x/wasm/types/types.go#L302-L321). +If the contract sets `data`, this will be returned in the `result` field. +`attributes` is a list of `{key, value}` pairs which will be +[appended to a default event](https://github.com/CosmWasm/wasmd/blob/master/x/wasm/types/types.go#L302-L321). The final result looks like this to the client: ```json { "type": "wasm", "attributes": [ - {"key": "contract_addr", "value": "cosmos1234567890qwerty"}, - {"key": "custom-key-1", "value": "custom-value-1"}, - {"key": "custom-key-2", "value": "custom-value-2"} + { "key": "contract_addr", "value": "cosmos1234567890qwerty" }, + { "key": "custom-key-1", "value": "custom-value-1" }, + { "key": "custom-key-2", "value": "custom-value-2" } ] } ``` ### Dispatching Messages -Now let's move onto the `messages` field. Some contracts are fine only talking with themselves, such as a cw20 -contract just adjusting it's balances on transfers. But many want to move tokens (native or cw20) or call into -other contracts for more complex actions. This is where messages come in. We return +Now let's move onto the `messages` field. Some contracts are fine only talking +with themselves, such as a cw20 contract just adjusting it's balances on +transfers. But many want to move tokens (native or cw20) or call into other +contracts for more complex actions. This is where messages come in. We return [`CosmosMsg`, which is a serializable representation](https://github.com/CosmWasm/cosmwasm/blob/v0.14.0-beta4/packages/std/src/results/cosmos_msg.rs#L18-L40) -of any external call the contract can make. It looks something like this (with `stargate` feature flag enabled): +of any external call the contract can make. It looks something like this (with +`stargate` feature flag enabled): ```rust pub enum CosmosMsg @@ -154,19 +161,136 @@ where } ``` -If a contract returns two messages - M1 and M2, these will both be parsed and executed in `x/wasm` -*with the permissions of the contract* (meaning `info.sender` will be the contract not the original caller). -If they return success, they will emit a new event with the custom attributes, the `data` field will be ignored, -and any messages they return will also be processed. If they return an error, the parent call will return an error, -thus rolling back state of the whole transaction. +If a contract returns two messages - M1 and M2, these will both be parsed and +executed in `x/wasm` _with the permissions of the contract_ (meaning +`info.sender` will be the contract not the original caller). If they return +success, they will emit a new event with the custom attributes, the `data` field +will be ignored, and any messages they return will also be processed. If they +return an error, the parent call will return an error, thus rolling back state +of the whole transaction. + +Note that the messages are executed _depth-first_. This means if contract A +returns M1 (`WasmMsg::Execute`) and M2 (`BankMsg::Send`), and contract B (from +the `WasmMsg::Execute`) returns N1 and N2 (eg. `StakingMsg` and +`DistributionMsg`), the order of execution would be **M1, N1, N2, M2**. -Note that the messages are executed *depth-first*. This means if contract A returns M1 (`WasmMsg::Execute`) and -M2 (`BankMsg::Send`), and contract B (from the `WasmMsg::Execute`) returns N1 and N2 (eg. `StakingMsg` and `DistributionMsg`), -the order of execution would be **M1, N1, N2, M2**. +FIXME: explain why we do this - "actor model" and "reentrancy" ### Submessages -Reply and reverting parts of code +As of CosmWasm 0.14 (April 2021), we have added yet one more way to dispatch +calls from the contract. A common request was the ability to get the result from +one of the messages you dispatched. For example, you want to create a new +contract with `WasmMsg::Instantiate`, but then you need to store the address of +the newly created contract in the caller. With `submessages`, this is now +possible. It also solves a similar use-case of capturing the error results, so +if you execute a message from eg. a cron contract, it can store the error +message and mark the message as run, rather than aborting the whole transaction. +It also allows for limiting the gas usage of the submessage (this is not +intended to be used for most cases, but is needed for eg. the cron job to +protect it from an infinite loop in the submessage burning all gas and aborting +the transaction). + +This makes use of `CosmosMsg` as above, but it wraps it inside a `SubMsg` +envelope: + +```rust +pub struct SubMsg +where + T: Clone + fmt::Debug + PartialEq + JsonSchema, +{ +pub id: u64, +pub msg: CosmosMsg, +pub gas_limit: Option, +pub reply_on: ReplyOn, +} + +pub enum ReplyOn { + /// Always perform a callback after SubMsg is processed + Always, + /// Only callback if SubMsg returned an error, no callback on success case + Error, + /// Only callback if SubMsg was successful, no callback on error case + Success, +} +``` + +What are the semantics of a submessage execution. First, we create a +sub-transaction context around the state, allowing it to read the latest state +written by the caller, but write to yet-another cache. If `gas_limit` is set, it +is sandboxed to how much gas it can use until it aborts with `OutOfGasError`. +This error is caught and returned to the caller like any other error returned +from contract execution (unless it burned the entire gas limit of the +transaction). What is more interesting is what happens on completion. + +If it return success, the temporary state is committed (into the caller's +cache), and the `Response` is processed as normal (an event is added to the +current EventManager, messages and submessages are executed). Once the +`Response` is fully processed, this may then be intercepted by the calling +contract (for `ReplyOn::Always` and `ReplyOn::Success`). On an error, the the +subcall will revert any partial state changes due to this message, but not +revert any state changes in the calling contract. The error may then be +intercepted by the calling contract (for `ReplyOn::Always` and +`ReplyOn::Error`). _In this case, the messages error doesn't abort the whole +transaction_ + +#### Handling the Reply + +In order to make use of `submessages`, the calling contract must have an extra +entry point: + +```rust +#[entry_point] +pub fn reply(deps: DepsMut, env: Env, msg: Reply) -> Result { } + +pub struct Reply { + pub id: u64, + /// ContractResult is just a nicely serializable version of `Result` + pub result: ContractResult, +} + +pub struct SubcallResponse { + pub events: Vec, + pub data: Option, +} +``` + +After the `submessage` is finished, the caller will get a chance to handle the +result. It will get the original `id` of the subcall so it can switch on how to +process this, and the `Result` of the execution, both success and error. Note +that it includes all events returned by the submessage, which applies to native +sdk modules as well (like Bank) as well as the data returned from below. This +and the original call id provide all context to continue processing it. If you +need more state, you must save some local context to the store (under the `id`) +before returning the `submessage` in the original `execute`, and load it in +`reply`. We explicitly prohibit passing information in contract memory, as that +is the key vector for reentrancy attacks, which are a large security surface +area in Ethereum. + +The `reply` call may return `Err` itself, in which case it is treated like the +caller errored, and aborting the transaction. (Unless, of course, the original +`execute` was itself called from a `submessage`, meaning just the state changes +since the call that returned `submessage` will be reverted, and this may be +handled at a higher level). However, on successful processing, `reply` may +return a normal `Response`, which will be processed as normal - events added to +the EventManager, and all `messages` and `submessages` dispatched as described +above. + +The one _critical difference_ with `reply`, is that we _do not drop data_. If +`reply` returns `data: Some(value)` in the `Response` object, we will overwrite +the `data` field returned by the caller. That is, if `execute` returns +`data: Some(b"first thought")` and the `reply` (with all the extra information +it is privvy to) returns `data: Some(b"better idea")`, then this will be +returned to the caller of `execute` (either the client or another transaction), +just as if the original `execute` and returned `data: Some(b"better idea")`. If +`reply` returns `data: None`, it will not modify any previously set data state. +If there are multiple submessages all setting this, only the last one is used +(they all overwrite any previous `data` value). + +`Submessages` (and their replies) are all executed before any `messages`. They +also follow the _depth first_ rules as with `messages`. Here is a simple +example. Contract A returns submessages S1 and S2, and message M1. submessage S1 +returns message N1. The order will be: **S1, N1, reply(S1), S2, reply(S2), M1** ## Query Semantics