wasmd/EVENTS.md

19 KiB

Event System

Usage in the SDK

Events are an essential part of the Cosmos SDK. They are similar to "logs" in Ethereum and allow a blockchain app to attach key-value pairs to a transaction that can later be used to search for it or extract some information in human readable form. Events are not written to the application state, nor do they form part of the AppHash, but mainly intended for client use (and become an essential API for any reactive app or app that searches for txs).

In contrast, transactions also have a binary "data" field that is part of the AppHash (provable with light client proofs, part of consensus). This data is not searchable, but given a tx hash, you can be guaranteed what the data returned is. This is often empty, but sometimes custom protobuf formats to return essential information from an execution.

Every message in the SDK may add events to the EventManager and these are then added to the final ABCI result that is returned to Tendermint. Events are exposed in 3 different ways over the Tendermint API (which is the only way a client can query). First of all is the events field on the transaction result (when you query a transaction by hash, you can see all event emitted by it). Secondly is the log field on the same transaction result. And third is the query interface to search or subscribe for transactions.

The log field actually has the best data. It contains an array of array of events. The first array is one entry per incoming message. Transactions in the Cosmos SDK may consist of multiple messages that are executed atomically. Maybe we send tokens, then issue a swap on a DEX. Each action would return it's own list of Events and in the logs, these are separated. For each message, it maintains a list of Events, exactly in the order returned by the application. This is JSON encoded and can be parsed by a client. In fact this is how CosmJS gets the events it shows to the client.

In Tendermint 0.35, the events field will be one flattened list of events over all messages. Just as if we concatenated all the per-message arrays contained in the log field. This fix was made as part of an event system refactoring. This refactoring is also giving us pluggable event indexing engines, so we can use eg. PostgreSQL to store and query the events with more powerful indexes.

However, currently (until Tendermint 0.34 used in Cosmos SDK 0.40-0.43), all events of one transaction are "flat-mapped" on type. Meaning all events with type wasm get merged into one. This makes the API not very useful to understanding more complex events currently. There are also a number of limitations of the power of queries in the search interface.

Given the state of affairs, and given that we seek to provide a stable API for contracts looking into the future, we consider the log output and the Tendermint 0.35 event handling to be the standard that clients should adhere to. And we will expose a similar API to the smart contracts internally (all events from the message appended, unmerged).

Data Format

The event has a string type, and a list of attributes. Each of them being a key value pair. All of these maintain a consistent order (and avoid dictionaries/hashes). Here is a simple Event in JSON:

{ 
    "type": "wasm", 
    "attributes": [
        {"key": "_contract_address", "value": "cosmos1pkptre7fdkl6gfrzlesjjvhxhlc3r4gmmk8rs6"}, 
        {"key": "transfered", "value": "777000"}
    ]
}

And here is a sample log output for a transaction with one message, which emitted 2 events:

[
    [
        { 
            "type": "message", 
            "attributes": [
                {"key": "module", "value": "bank"}, 
                {"key": "action", "value": "send"}
            ]
        },
        { 
            "type": "transfer", 
            "attributes": [
                {"key": "recipient", "value": "cosmos1pkptre7fdkl6gfrzlesjjvhxhlc3r4gmmk8rs6"}, 
                {"key": "amount", "value": "777000uatom"}
            ]
        }
    ]
]

Default Events in the SDK

There are two places events that are emitted in every transaction regardless of the module which is executed. The first is {"type": "message"} defining an action attribute. This is emitted for each top-level (user-signed) message, but the action names have changed between 0.42 and 0.43.

The other place is in the signature verification AnteHandler, where it emits information on the account sequences and signatures on the transaction.

These are all handled in BaseApp and the middleware before any module is called and thus not exposed to CosmWasm contracts at all.

Standard Events in the SDK

The events that will actually make it to the contracts are the events that are emitted by the other modules / keepers. Let's look at some good examples of what they look like:

The most basic one is bank, which emits two events on every send, a custom "transfer" event as well as "sender" information under the standard "message" type. Replacing variables with string literals, they look like this:

sdk.NewEvent(
    "transfer"
    sdk.NewAttribute("recipient", toAddr.String()),
    sdk.NewAttribute("sender", fromAddr.String()),
    sdk.NewAttribute("amount", amt.String()),  // eg 12456uatom
),
sdk.NewEvent(
    "message",
    sdk.NewAttribute("sender", fromAddr.String()),
),

The delegation module seems a bit more refined, emitting a generic "message" type event in msg_server.go including the module name, before emitting some custom event types closer to the actual code logic in keeper.go.

This looks something like:

sdk.NewEvent(
    "message",
    sdk.NewAttribute("module", "distribution"),
    sdk.NewAttribute("sender", msg.DelegatorAddress),
),
sdk.NewEvent(
    "set_withdraw_address",
    sdk.NewAttribute("withdraw_address", withdrawAddr.String()),
),

Usage in wasmd

In x/wasm we also use Events system. On one hand, the Go implementation of x/wasm emits standard events for each message it processes, using the distribution module as an example. Furthermore, it allows contracts to emit custom events based on their execution state, so they can for example say "dex swap, BTC-ATOM, in 0.23, out 512" which require internal knowledge of the contract and is very useful for custom dApp UIs.

x/wasm is also a consumer of events, since when a smart contract executes a SubMsg and processes the reply, it receives not only the data response from the message execution, but also the list of events. This makes it even more important for us to document a standard event processing format.

Standard Events in x/wasm

Following the model of distribution, we will split the emitted events into two parts. All calls to the message server, will receive the following event:

sdk.NewEvent(
    "message",
    sdk.NewAttribute("module", "wasm"),
    // Note: this was "signer" before 0.18
    sdk.NewAttribute("sender", msg.Sender),  
),

No further information will be added to the generic "message" type, but rather be contained in a more context-specific event type. Here are some examples:

// Store Code
sdk.NewEvent(
    "store_code",
    sdk.NewAttribute("code_id", fmt.Sprintf("%d", codeID)),
    // features required by the contract (new in 0.18)
    // see https://github.com/CosmWasm/wasmd/issues/574
    sdk.NewAttribute("feature", "stargate"),
    sdk.NewAttribute("feature", "staking"),
)

// Instantiate Contract
sdk.NewEvent(
    "instantiate",
    sdk.NewAttribute("code_id", fmt.Sprintf("%d", msg.CodeID)),
    sdk.NewAttribute("_contract_addr", contractAddr.String()),
)

// Execute Contract
sdk.NewEvent(
    "execute",
    sdk.NewAttribute("_contract_addr", contractAddr.String()),
)

// Migrate Contract
sdk.NewEvent(
    "migrate",
    // Note: this is the new code id that is being migrated to
    sdk.NewAttribute("code_id", fmt.Sprintf("%d", msg.CodeID)),
    sdk.NewAttribute("_contract_addr", contractAddr.String()),
)

// Set new admin
sdk.NewEvent(
    "update_admin",
    sdk.NewAttribute("_contract_addr", contractAddr.String()),
    sdk.NewAttribute("admin", msg.NewAdmin),
)

// Clear admin
sdk.NewEvent(
    "clear_admin",
    sdk.NewAttribute("_contract_addr", contractAddr.String()),
)

// Pin Code
sdk.NewEvent(
    "pin_code",
    sdk.NewAttribute("code_id", strconv.FormatUint(msg.CodeID, 10)),
)

// Unpin Code
sdk.NewEvent(
    "unpin_code",
    sdk.NewAttribute("code_id", strconv.FormatUint(msg.CodeID, 10)),
)

// Emitted when processing a submessage reply
sdk.NewEvent(
    "reply",
    sdk.NewAttribute("_contract_addr", contractAddr.String()),
    // If the submessage was successful, and reply is processing the success case
    sdk.NewAttribute("mode", "handle_success"),
    // If the submessage returned an error that was "caught" by the reply block
    sdk.NewAttribute("mode", "handle_failure"),
)

// Emitted when handling sudo
sdk.NewEvent(
    "sudo",
    sdk.NewAttribute("_contract_addr", contractAddr.String()),
)

Note that every event that affects a contract (not store code, pin or unpin) will return the contract_addr as _contract_addr. The events that are related to a particular wasm code (store code, instantiate, pin, unpin, and migrate) will emit that as code_id. All attributes prefixed with _ are reserved and may not be emitted by a smart contract, so we use the underscore prefix consistently with attributes that may be injected into custom events.

Emitted Custom Events from a Contract

When a CosmWasm contract returns a Response from one of the calls, it may return a list of attributes as well as a list of events (in addition to data and a list of messages to dispatch). These are then processed in x/wasm to create events that are emitted to the blockchain.

If the response contains a non-empty list of attributes, x/wasm will emit a wasm type event. This event will always be tagged with _contract_address by the Go module, so this is trust-worthy. The contract itself cannot overwrite this field. Beyond this, the attributes returned by the contract, these are appended to the same event.

A contact may also return custom events. These are multiple events, each with their own type as well as attributes. When they are received, x/wasm prepends wasm- to the event type returned by the contact to avoid them trying to fake an eg. transfer event from the bank module. The output here may look like:

sdk.NewEvent(
    "wasm-promote"
    sdk.NewAttribute("_contract_addr", contractAddr.String()),
    sdk.NewAttribute("batch_id", "6"),
    sdk.NewAttribute("address", "cosmos1234567"),
    sdk.NewAttribute("address", "cosmos1765432"),
),
sdk.NewEvent(
    "wasm-promote"
    sdk.NewAttribute("_contract_addr", contractAddr.String()),
    sdk.NewAttribute("batch_id", "7"),
    sdk.NewAttribute("address", "cosmos19875632"),
)

Note that these custom events also have the _contract_address attribute automatically injected for easier attribution in the clients. The multiple event API was designed to allow the contract to make logical groupings that are persisted in the event system, more than flattening them all into one event like:

sdk.NewEvent(
    "wasm"
    sdk.NewAttribute("_contract_addr", contractAddr.String()),
    sdk.NewAttribute("action", "promote"),
    sdk.NewAttribute("batch_id", "6"),
    sdk.NewAttribute("address", "cosmos1234567"),
    sdk.NewAttribute("address", "cosmos1765432"),
    sdk.NewAttribute("batch_id", "7"),
    sdk.NewAttribute("address", "cosmos19875632"),
)

If the Response contains neither event nor attributes, not wasm* events will be emitted, just the standard message type as well as the action-dependent event (like execute or migrate). This is a significant change from pre-0.18 versions where one could count on the wasm event to always be emitted. Now it is recommended to search for execute._contract_address="foo" to find all transactions related to the contract.

Validation Rules

While the wasm and wasm-* namespacing does sandbox the smart contract events and limits malicious activity they could undertake, we also perform a number of further validation checks on the contracts:

  • No attribute key may start with _. This is currently used for _contract_address and is reserved for a namespace for injecting more trusted attributes from the x/wasm module as opposed to the contract itself
  • Event types are trimmed of whitespace, and must have at least two characters prior to prepending wasm-. If the contract returns " hello\n", the event type will look like wasm-hello. If it emits " a ", this will be rejected with an error (aborting execution!)
  • Attribute keys and values (both in attributes and under events) are trimmed of leading/trailing whitespace. If they are empty after trimming, they are rejected as above (aborting the execution). Otherwise, they are passed verbatim.

Event Details for wasmd

Beyond the basic Event system and emitted events, we must handle more advanced cases in x/wasm and thus add some more logic to the event processing. Remember that CosmWasm contracts dispatch other messages themselves, so far from the flattened event structure, or even a list of list (separated by message index in the tx), we actually have a tree of messages, each with their own events. And we must flatten that in a meaningful way to feed it into the event system.

Furthermore, with the sub-message reply handlers, we end up with eg. "Contract A execute", "Contract B execute", "Contract A reply". If we return all events by all of these, we may end up with many repeated event types and a confusing results. However, we may use the standard "message" events to separate the sub-messages as it marks where the next one starts. With careful analysis of the "sender" field on these "message" markers, we may be able to reconstruct much of the tree execution path. We should ensure all this information is exposed in the most consistent way possible.

Combining Events from Sub-Messages

Each time a contract is executed, it not only returns the message event from its call, the execute event for the contact and the wasm event with any custom fields from the contract itself. It will also return the same set of information for all messages that it returned, which were later dispatched. The event system was really designed for one main action emitting events, so we define a structure to flatten this event tree:

  • We only emit one event of type message. This is the top-level call, just like the standard Go modules. For all dispatched submessages, we filter out this event type.
  • All events are returned in execution order as defined by CosmWasm docs
  • x/wasm keeper emits a custom event for each call to a contract entry point. Not just execute, instantiate, and migrate, but also reply, sudo and all ibc entry points.
  • This means all wasm* events are preceeded by the cosmwasm entry point that returned them.

To make this more clear, I will provide an example of executing a contract, which returns two messages, one to instantiate a new contract and the other to set the withdrawl address, while also using ReplyOnSuccess for the instantiation (to get the address). It will emit a series of events that looks something like this:

/// original execution (top-level message is the only one that gets the message tag)
sdk.NewEvent(
    "message",
    sdk.NewAttribute("module", "wasm"),
    sdk.NewAttribute("sender", msg.Sender),  
),

// top-level exection call
sdk.NewEvent(
    "execute",
    sdk.NewAttribute("_contract_addr", contractAddr.String()),
),
sdk.NewEvent(
    "wasm",
    sdk.NewAttribute("_contract_addr", contractAddr.String()),
    sdk.NewAttribute("custom", "from contract"),
),

// instantiating contract (first dipatched message)
sdk.NewEvent(
    "instantiate",
    sdk.NewAttribute("code_id", fmt.Sprintf("%d", msg.CodeID)),
    sdk.NewAttribute("_contract_addr", newContract.String()),
)
// didn't emit any attributes, but one event
sdk.NewEvent(
    "wasm-custom",
    sdk.NewAttribute("_contract_addr", newContract.String()),
    sdk.NewAttribute("foobar", "baz"),
),

// handling the reply (this doesn't emit a message event as it never goes through the message server)
sdk.NewEvent(
    "reply",
    sdk.NewAttribute("_contract_addr", contractAddr.String()),
    sdk.NewAttribute("mode", "handle_success"),
),
sdk.NewEvent(
    "wasm",
    sdk.NewAttribute("_contract_addr", contractAddr.String()),
    sdk.NewAttribute("custom", "from contract"),
),

// calling the distribution module (second dispatched message)
sdk.NewEvent(
    "set_withdraw_address",
    sdk.NewAttribute("withdraw_address", withdrawAddr.String()),
),

Exposing Events to Reply

When the reply clause in a contract is called, it will receive the data returned from the message it applies to, as well as all events from that message. In the above case, when the reply function was called on contractAddr in response to initializing a contact, it would get the binary-encoded initData in the data field, and the following in the events field:

sdk.NewEvent(
    "instantiate",
    sdk.NewAttribute("code_id", fmt.Sprintf("%d", msg.CodeID)),
    sdk.NewAttribute("_contract_addr", newContract.String()),
)
sdk.NewEvent(
    "wasm-custom",
    sdk.NewAttribute("_contract_addr", newContract.String()),
    sdk.NewAttribute("foobar", "baz"),
),

If the original contract execution example above was actually the result of a message returned by an eg. factory contract, and it registered a ReplyOn clause, the reply function on that contract would receive the entire 11 events in the example above, and would need to use the message markers to locate the segment of interest.

Governance Events

The governance process is handled by the cosmos-sdk gov module. We do not emit any events of type "message" anymore in v0.18+. Context-specific events are emitted as described above. Execution and Migration return some contract result though that are emitted as:

sdk.NewEvent(
    "gov_contract_result",
    sdk.NewAttribute("result", hex.EncodeToString(data)),
)

IBC Events

All IBC entry points are only called by external accounts and not from contracts. They need to contain proofs of state of other blockchains and cannot be called by other contracts on the same chain. Therefore, the event emitted are not essential for cross-contract calls, and x/wasm does not emit custom events for these actions.

There are well-defined events emitted by the IBC base layer and are required for the relayer functionality. If you wish to subscribe to these, you can find them defined in the ibc-go codebase.