Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ADR 32: Typed Events #7474

Merged
merged 21 commits into from
Oct 13, 2020
Merged
Changes from 17 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
319 changes: 319 additions & 0 deletions docs/architecture/adr-032-typed-events.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,319 @@
# ADR 032: Typed Events

## Changelog

- 28-Sept-2020: Initial Draft

## Authors

- Anil Kumar (@anilcse)
- Jack Zampolin (@jackzampolin)
- Adam Bozanich (@boz)

## Status

Proposed

jackzampolin marked this conversation as resolved.
Show resolved Hide resolved
## Abstract

Currently in the SDK, events are defined in the handlers for each message as well as `BeginBlock` and `EndBlock`. Each module doesn't have types defined for each event, they are implemented as `map[string]string`. Above all else this makes these events difficult to consume as it requires a great deal of raw string matching and parsing. This proposal focuses on updating the events to use **typed events** defined in each module such that emiting and subscribing to events will be much easier. This workflow comes from the experience of the Akash Network team.

## Context

Currently in the SDK, events are defined in the handlers for each message, meaning each module doesn't have a cannonical set of types for each event. Above all else this makes these events difficult to consume as it requires a great deal of raw string matching and parsing. This proposal focuses on updating the events to use **typed events** defined in each module such that emiting and subscribing to events will be much easier. This workflow comes from the experience of the Akash Network team.

[Our platform](http://github.com/ovrclk/akash) requires a number of programatic on chain interactions both on the provider (datacenter - to bid on new orders and listen for leases created) and user (application developer - to send the app manifest to the provider) side. In addition the Akash team is now maintaining the IBC [`relayer`](https://github.com/ovrclk/relayer), another very event driven process. In working on these core pieces of infrastructure, and integrating lessons learned from Kubernetes developement, our team has developed a standard method for defining and consuming typed events in SDK modules. We have found that it is extremely useful in building this type of event driven application.

As the SDK gets used more extensively for apps like `peggy`, other peg zones, IBC, DeFi, etc... there will be an exploding demand for event driven applications to support new features desired by users. We propose upstreaming our findings into the SDK to enable all SDK applications to quickly and easily build event driven apps to aid their core application. Wallets, exchanges, explorers, and defi protocols all stand to benefit from this work.

If this proposal is accepted, users will be able to build event driven SDK apps in go by just writing `EventHandler`s for their specific event types and passing them to `EventEmitters` that are defined in the SDK.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree, event driven API is very useful for app developers.
That being said, I'm wondering if designing a subscription mechanism for messages (emitted when a Msg lands into the blockchain) would solve it in a better way? Msgs already have a well defined structure and an "url" (type).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There does seem to be a lot of redundancy I agree, but events are already used pretty widely in the ecosystem and sometimes communicate things not covered in Msgs. For instance a cosmwasm contract could have the side effect of creating a gov proposal, even if the Msg submitted to the blockchain was just to call a contract.

Copy link
Contributor

@amaury1093 amaury1093 Oct 9, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For instance a cosmwasm contract could have the side effect of creating a gov proposal, even if the Msg submitted to the blockchain was just to call a contract.

If we go with Msg-based OCAPS (i.e. assuming both ADR031 and ADR033 get merged), this contract would need to send a MsgSubmitProposal, which in turn will emit the corresponding Msg Response.

I'd like to see a use-case in this ADR where a message subscription mechanism is not enough, as I feel ADR031 and ADR033 can get to the same goal in a more elegant way. We could of course still emit events, because of inertia in the ecosystem, but they will be Msg Responses and not typed events.

Copy link
Member

@aaronc aaronc Oct 9, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd like to see a use-case in this ADR where a message subscription mechanism is not enough, as I feel ADR031 and ADR033 can get to the same goal in a more elegant way. We could of course still emit events, because of inertia in the ecosystem, but they will be Msg Responses and not typed events.

I don't think it's just inertia. Does the cosmwasm use case I mentioned above not seem like something unaddressed by Msg responses?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Possibly with ADR 033 though, we can capture all of the nested Msgs that get sent between different modules and maybe it would have the same result...

Copy link
Member Author

@jackzampolin jackzampolin Oct 9, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can someone link to ADR 33? I don't see the open PR...

Also this is a basic improvement to the current event system that is really low impact. It can be additive with other changes, but will make using the tendermint websocket easier for developers. I would also personally like this change for the relayer and the IBC code specifically.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One thing that isn't covered by Msg subscription is the emission of events in EndBlock. See the band protocol example: https://blog.cosmos.network/guide-to-building-defi-using-band-protocol-oracle-and-cosmos-ibc-fa5348832f84

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If this proposal is accepted, users will be able to build event driven SDK apps

To clarify, these typed events are designed for external/client-side applications? I think it is very good to provide the best event support possible for them... working on consuming this since early 2018 and it has been rough getting much working out of it... almost never well tested or structured. I am very happy for such improvements.

If you are saying, as I believe some are interpreting, of using Events to communicate between modules in the SDK, then I have some hesitation. Not that it is bad, but adds a lot of potentials that need to be considered.

Anyway the use of "SDK apps" can be interpretted two ways. I would prefer "SDK modules" or "SDK clients" (and we have strong support for such clients in JS/TS... not just Go.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jackzampolin , could you add a note that subscription to Msg doesn't solve all use cases?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ethanfrey this work would also allow for better TS/JS libs around events as well.


The end of this proposal contains a detailed example of how to consume events after this refactor.

This proposal is specifically about how to consume these events as a client of the blockchain, not for intermodule communication.

## Decision

__Step-1__: Implement additional functionality in the `types` package: `EmitTypedEvent` and `ParseTypedEvent` functions

```go
// types/events.go

// EmitTypedEvent takes typed event and emits converting it into sdk.Event
func (em *EventManager) EmitTypedEvent(event proto.Message) error {
evtType := proto.MessageName(event)
evtJSON, err := codec.ProtoMarshalJSON(event)
if err != nil {
return err
}

var attrMap map[string]json.RawMessage
err = json.Unmarshal(evtJSON, &attrMap)
if err != nil {
return err
}

var attrs []abci.EventAttribute
for k, v := range attrMap {
attrs = append(attrs, abci.EventAttribute{
Key: []byte(k),
Value: v,
jackzampolin marked this conversation as resolved.
Show resolved Hide resolved
})
}
jackzampolin marked this conversation as resolved.
Show resolved Hide resolved

em.EmitEvent(Event{
Type: evtType,
Attributes: attrs,
})

return nil
}

// ParseTypedEvent converts abci.Event back to typed event
func ParseTypedEvent(event abci.Event) (proto.Message, error) {
concreteGoType := proto.MessageType(event.Type)
if concreteGoType == nil {
return nil, fmt.Errorf("failed to retrieve the message of type %q", event.Type)
}

var value reflect.Value
if concreteGoType.Kind() == reflect.Ptr {
value = reflect.New(concreteGoType.Elem())
} else {
value = reflect.Zero(concreteGoType)
}

protoMsg, ok := value.Interface().(proto.Message)
if !ok {
return nil, fmt.Errorf("%q does not implement proto.Message", event.Type)
}

attrMap := make(map[string]json.RawMessage)
for _, attr := range event.Attributes {
attrMap[string(attr.Key)] = attr.Value
}

attrBytes, err := json.Marshal(attrMap)
if err != nil {
return nil, err
}

err = jsonpb.Unmarshal(strings.NewReader(string(attrBytes)), protoMsg)
if err != nil {
return nil, err
}

return protoMsg, nil
}
```

Here, the `EmitTypedEvent` is a method on `EventManager` which takes typed event as input and apply json serialization on it. Then it maps the JSON key/value pairs to `event.Attributes` and emits it in form of `sdk.Event`. `Event.Type` will be the type URL of the proto message.
jackzampolin marked this conversation as resolved.
Show resolved Hide resolved

When we subscribe to emitted events on the tendermint websocket, they are emitted in the form of an `abci.Event`. `ParseTypedEvent` parses the event back to it's original proto message.

__Step-2__: Add proto definitions for typed events for msgs in each module:

For example, let's take `MsgSubmitProposal` of `gov` module and implement this event's type.

```protobuf
// proto/cosmos/gov/v1beta1/gov.proto
// Add typed event definition

package cosmos.gov.v1beta1;

message EventSubmitProposal {
string from_address = 1;
uint64 proposal_id = 2;
TextProposal proposal = 3;
}
Comment on lines +124 to +128
Copy link
Contributor

@amaury1093 amaury1093 Oct 12, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

EventSubmitProposal is typically a use-case where I believe a Msg response makes more sense. With ADR031+ADR033, I would call this struct MsgSubmitProposalResponse.

Could we maybe put an example that's not directly a response from a Msg?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think MsgSubmitProposalResponse would only include proposal_id in ADR 031. There's overlap for sure. Combining MsgSubmitProposal + MsgSubmitProposalResponse would give the same info but is maybe more cumbersome.

```

__Step-3__: Refactor event emission to use the typed event created and emit using `sdk.EmitTypedEvent`:

```go
// x/gov/handler.go
func handleMsgSubmitProposal(ctx sdk.Context, keeper keeper.Keeper, msg types.MsgSubmitProposalI) (*sdk.Result, error) {
...
types.Context.EventManager().EmitTypedEvent(
&EventSubmitProposal{
FromAddress: fromAddress,
ProposalId: id,
Proposal: proposal,
},
)
...
}
```

#### How to subscribe to these typed events in `Client`

> NOTE: Full code example below

Users will be able to subscribe using `client.Context.Client.Subscribe` and consume events which are emitted using `EventHandler`s.
jackzampolin marked this conversation as resolved.
Show resolved Hide resolved

Akash Network has built a simple [`pubsub`](https://github.com/ovrclk/akash/blob/90d258caeb933b611d575355b8df281208a214f8/pubsub/bus.go#L20). This can be used to subscribe to `abci.Events` and [publish](https://github.com/ovrclk/akash/blob/90d258caeb933b611d575355b8df281208a214f8/events/publish.go#L21) them as typed events.

Please see the below code sample for more detail on this flow looks for clients.

## Consequences

### Positive

* Improves consistency of implementation for the events currently in the sdk
* Provides a much more ergonomic way to handle events and facilitates writing event driven applications
* This implementation will support a middleware ecosystem of `EventHandler`s

### Negative


## Detailed code example of publishing events

This ADR also proposes adding affordances to emit and consume these events. This way developers will only need to write
`EventHandler`s which define the actions they desire to take.

```go
// EventEmitter is a type that describes event emitter functions
// This should be defined in `types/events.go`
type EventEmitter func(context.Context, client.Context, ...EventHandler) error

// EventHandler is a type of function that handles events coming out of the event bus
// This should be defined in `types/events.go`
type EventHandler func(proto.Message) error

// Sample use of the functions below
func main() {
ctx, cancel := context.WithCancel(context.Background())

if err := TxEmitter(ctx, client.Context{}.WithNodeURI("tcp://localhost:26657"), SubmitProposalEventHandler); err != nil {
cancel()
panic(err)
}

return
}

// SubmitProposalEventHandler is an example of an event handler that prints proposal details
// when any EventSubmitProposal is emitted.
func SubmitProposalEventHandler(ev proto.Message) (err error) {
switch event := ev.(type) {
// Handle governance proposal events creation events
case govtypes.EventSubmitProposal:
// Users define business logic here e.g.
fmt.Println(ev.FromAddress, ev.ProposalId, ev.Proposal)
return nil
default:
return nil
}
}

// TxEmitter is an example of an event emitter that emits just transaction events. This can and
jackzampolin marked this conversation as resolved.
Show resolved Hide resolved
// should be implemented somewhere in the SDK. The SDK can include an EventEmitters for tm.event='Tx'
// and/or tm.event='NewBlock' (the new block events may contain typed events)
func TxEmitter(ctx context.Context, cliCtx client.Context, ehs ...EventHandler) (err error) {
// Instantiate and start tendermint RPC client
client, err := cliCtx.GetNode()
if err != nil {
return err
}

if err = client.Start(); err != nil {
return err
}

// Start the pubsub bus
bus := pubsub.NewBus()
defer bus.Close()

// Initialize a new error group
eg, ctx := errgroup.WithContext(ctx)

// Publish chain events to the pubsub bus
eg.Go(func() error {
return PublishChainTxEvents(ctx, client, bus, simapp.ModuleBasics)
})

// Subscribe to the bus events
subscriber, err := bus.Subscribe()
if err != nil {
return err
}

// Handle all the events coming out of the bus
eg.Go(func() error {
var err error
for {
select {
case <-ctx.Done():
return nil
case <-subscriber.Done():
return nil
case ev := <-subscriber.Events():
for _, eh := range ehs {
if err = eh(ev); err != nil {
break
}
}
}
}
return nil
})

return group.Wait()
}

// PublishChainTxEvents events using tmclient. Waits on context shutdown signals to exit.
func PublishChainTxEvents(ctx context.Context, client tmclient.EventsClient, bus pubsub.Bus, mb module.BasicManager) (err error) {
jackzampolin marked this conversation as resolved.
Show resolved Hide resolved
// Subscribe to transaction events
txch, err := client.Subscribe(ctx, "txevents", "tm.event='Tx'", 100)
if err != nil {
return err
}

// Unsubscribe from transaction events on function exit
defer func() {
err = client.UnsubscribeAll(ctx, "txevents")
}()

// Use errgroup to manage concurrency
g, ctx := errgroup.WithContext(ctx)

// Publish transaction events in a goroutine
g.Go(func() error {
var err error
for {
select {
case <-ctx.Done():
break
case ed := <-ch:
switch evt := ed.Data.(type) {
case tmtypes.EventDataTx:
if !evt.Result.IsOK() {
continue
}
// range over events, parse them using the basic manager and
// send them to the pubsub bus
for _, abciEv := range events {
typedEvent, err := sdk.ParseTypedEvent(abciEv)
if err != nil {
return er
}
if err := bus.Publish(typedEvent); err != nil {
bus.Close()
return
}
continue
}
}
}
}
return err
})

// Exit on error or context cancelation
return g.Wait()
}
```

## References
- [Publish Custom Events via a bus](https://github.com/ovrclk/akash/blob/90d258caeb933b611d575355b8df281208a214f8/events/publish.go#L19-L58)
- [Consuming the events in `Client`](https://github.com/ovrclk/deploy/blob/bf6c633ab6c68f3026df59efd9982d6ca1bf0561/cmd/event-handlers.go#L57)