From 330e0ce3c1e6000aa6c39cbda4eea84153978842 Mon Sep 17 00:00:00 2001 From: Jack Zampolin Date: Mon, 28 Sep 2020 18:13:16 -0600 Subject: [PATCH 01/11] Typed events ADR --- docs/architecture/adr-031-typed-events.md | 423 ++++++++++++++++++++++ 1 file changed, 423 insertions(+) create mode 100644 docs/architecture/adr-031-typed-events.md diff --git a/docs/architecture/adr-031-typed-events.md b/docs/architecture/adr-031-typed-events.md new file mode 100644 index 000000000000..c36d135083c0 --- /dev/null +++ b/docs/architecture/adr-031-typed-events.md @@ -0,0 +1,423 @@ +# ADR 030: Typed Events + +## Changelog + +- 28-Sept-2020: Initial Draft + +## Authors + +- Anil Kumar (@anilcse) +- Jack Zampolin (@jackzampolin) +- Adam Bozanich (@boz) + +## Status + +Proposed + +## Context + +Currently in the SDK, events are not properly organized (event data is defined in the handlers for each message) and are prone to inconsistencies. This makes customizing events difficult. In addition, they are difficult to consume. This proposal focuses on updating the events to use **typed events** in each module such that emiting and subscribing to events will be much more egrnomic. This will make use of the SDK to build event driven processes much easier and enable rapid development of things like relayers and automated transaction bots for SDK based chains. These types of bots enable will enable easy building of new features at exchanges, wallets, explorers, and defi protocols. IBC especially will benefit from this proposal. + +The end of this proposal contains a detailed example of how to consume events after this refactor. + +## Decision + +__Step-1__: Declare event types for `sdk.Msg`s a module implements using the typed event interface: `sdk.ModuleEvent`. We first need to define this interface and supporting types. + +```go +// types/events.go + +// ModuleEvent is the interface that all message events will implement +type ModuleEvent interface { + Context() BaseModuleEvent + ABCIEvent() Event +} + +// BaseModuleEvent contains information about the Module and Action of an event +type BaseModuleEvent struct { + Module string + Action string +} +``` + +The `BaseModuleEvent` struct will be used for basic context about the event. It aids in routing events to their +`Module` and `Action` specific event parsers. + +__Step 2__: Implement additional functionality in the `types` package: utility functions and a parser to route the event to its proper module. + +When we subscribe to emitted events on the tendermint websocket, they are emitted in the form of an `abci.Event`. The parser will process this event using `sdk.NewSDKEvent(abci.Event)` to enable passing of the processed event to the proper module. + +```go +// types/events.go + +// SDKEvent contains the string representation of the event and the module information +type SDKEvent struct { + Sev StringEvent + Base BaseModuleEvent +} + +// NewSDKEvent parses abci.Event into an sdk.SDKEvent +func NewSDKEvent(bev abci.Event) (ev SDKEvent, err error) { + sev := StringifyEvent(bev) + module, err := GetEventString(sev.Attributes, AttributeKeyModule) + if err != nil { + return + } + action, err := GetEventString(sev.Attributes, AttributeKeyAction) + if err != nil { + return + } + return SDKEvent{sev, BaseModuleEvent{module, action}}, nil +} + +// GetEventString take sdk attributes, key and returns value for that key. +func GetEventString(attrs []Attribute, key string) (string, error) { + for _, attr := range attrs { + if attr.Key == key { + return attr.Value, nil + } + } + return "", fmt.Errorf("not found") +} + +// GetEventUint64 take sdk attributes, key and returns uint64 value. +// Returns error incase of failure. +func GetEventUint64(attrs []Attribute, key string) (uint64, error) { + sval, err := GetEventString(attrs, key) + if err != nil { + return 0, err + } + return strconv.ParseUint(sval, 10, 64) +} + +// Other type functions for use in the individual module parsers +// e.g. func GetEventFloat64(attrs []Attribute, key) (float64, error) {} +``` + +__Step-3__: Add `AppModuleBasic.ParseEvent` and define `app.BasicManager.ParseEvent`: + +A `ParseEvent` function will need to be added to the `sdk.AppModuleBasic` interface. + +```go +type AppModuleBasic interface { + ... + ParseEvent(ev sdk.SDKEvent) (sdk.ModuleEvent, error) + ... +} + +// ParseEvent takes an sdk.SDKEvent and returns the module specific sdk.ModuleEvent +func (bm BasicManager) ParseEvent(ev sdk.SDKEvent) (sdk.ModuleEvent, error) { + for m, b := range bm { + if m == ev.Base.Module { + return b.ParseEvent(cdc) + } + } + return nil, fmt.Errorf("failed to parse event") +} +``` + +__Step-4__: Define typed events for msgs in `x//types/events.go`: + +For example, let's take `MsgSubmitProposal` of `gov` module and implement this event's type. + +```go +// x/gov/types/events.go +func NewEventSubmitProposal(from sdk.Address, id govtypes.ProposalID, proposal govtypes.TextProposal) EventSubmitProposal { + return EventSubmitProposal{ + ID: id, + FromAddress: from, + Proposal: proposal, + } +} + +type EventSubmitProposal struct { + FromAddress AccAddress + ID ProposalID + Proposal types.TextProposal +} + +func (ev EventSubmitProposal) Context() sdk.BaseModuleEvent { + return BaseModuleEvent{ + Module: "gov", + Action: "submit_proposal", + } +} + +func (ev EventSubmitProposal) ABCIEvent() sdk.Event { + return types.NewEvent("cosmos-sdk-events", + sdk.NewAttribute(sdk.AttributeKeyModule, ev.Context().Module), + sdk.NewAttribute(sdk.AttributeKeyAction, ev.Context().Action), + sdk.NewAttribute("from", ev.FromAddress.String()), + sdk.NewAttribute("title", ev.Proposal.Title.String()), + sdk.NewAttribute("description", ev.Proposal.Description.String()), + ) +} +``` + +__Step-5__: Define `ParseEvent` for each module in their respective `x//module.go`: + +```go +// x/gov/module.go + +// ParseEvent turns an sdk.SDKEvent into the gov specific event type and error if any occurred +func (AppModuleBasic) ParseEvent(ev sdk.SDKEvent) (sdk.ModuleEvent, error) { + if ev.Sev.Type != sdk.EventTypeMessage { + return nil, fmt.Errorf("unknown message type") + } + + if ev.Base.Module != ModuleName { + return nil, fmt.Errorf("wrong module: %s not %s", ev.Base.Module, ModuleName) + } + + switch ev.Base.Action { + case "submit_proposal": + addr, err := sdk.GetEventString(ev.Sev.Attributes, "from") + if err != nil { + return nil, err + } + + proposalId, err := sdk.GetEventUint64(ev.Sev.Attributes, "proposal_id") + if err != nil { + return nil, err + } + + proposal, err := parseProposalFromEvent(ev.Sev.Attributes, "id") + if err != nil { + return nil, err + } + + from, err := sdk.AccAddressFromBech32(addr) + if err != nil { + return nil, err + } + + return NewEventSubmitProposal(from, proposalId, proposal), nil + case "proposal_deposit": + // TODO: Implement + case "submit_proposal": + // TODO: Implement + case "proposal_deposit": + // TODO: Implement + case "proposal_vote": + // TODO: Implement + case "inactive_proposal": + // TODO: Implement + case "active_proposal": + // TODO: Implement + default: + return nil, fmt.Errorf("unsupported event type for gov") + } +} + +// parseProposalFromEvent returns the TextProposal from []sdk.Attributes +func parseProposalFromEvent(attrs []sdk.Attribute) ([]byte, error) { + description, err := sdk.GetEventString(attrs, "description") + if err != nil { + return govtypes.TextProposal{}, err + } + + title, err := sdk.GetEventString(attrs, "title") + if err != nil { + return govtypes.TextProposal{}, err + } + + return govtypes.TextProposal{ + Title: title, + Description: description, + }, nil +} +``` + +__Step-6__: Refactor event emission to use the types created: + +Emiting events is similar to the current method: + +```go +// x/gov/handler.go +func handleMsgSubmitProposal(ctx sdk.Context, keeper keeper.Keeper, msg types.MsgSubmitProposalI) (*sdk.Result, error) { + ... + types.Context.EventManager().EmitEvent( + NewEventSubmitProposal(fromAddress, id, proposal).ABCIEvent(), + ) + ... +} +``` + +#### 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. + +Akash Network has built a simple [`pubsub`](https://github.com/ovrclk/akash/blob/master/pubsub/bus.go). This can be used to subscribe to `abci.Events` and [publish](https://github.com/ovrclk/akash/blob/master/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 + +* Requires a substantial amount of additional code in each module. For new developers and chains, this can be +partially mitigaed by code generation in [`starport`](https://github.com/tendermint/starport). + +## 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(sdk.ModuleEvent) 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 sdk.ModuleEvent) (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.ID, ev.Proposal) + return nil + default: + return nil + } +} + +// TxEmitter is an example of an event emitter that emits just transaction events. This can and +// 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 { + 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 { + return err + } + } + } + } + }) + + 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) { + // 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 { + sdkEv, err := sdk.NewSDKEvent(abciEv) + if err != nil { + return err + } + moduleEvent, err := mb.ParseEvent(abciEv) + if err != nil { + return er + } + if err := bus.Publish(moduleEvent); err != nil { + bus.Close() + return + } + continue + } + } + } + } + return err + }) + + // Exit on error or context cancelation + return g.Wait() +} +``` + +## References +- [Event types for a module](https://github.com/ovrclk/akash/blob/master/x/deployment/types/event.go#L24) +- [Emit Events](https://github.com/ovrclk/akash/blob/master/x/deployment/keeper/keeper.go#L129) +- [Publish Custom Events via a bus](https://github.com/ovrclk/akash/blob/master/events/publish.go#L19-L58) +- [Consuming the events in `Client`](https://github.com/jackzampolin/deploy/blob/master/cmd/event-handlers.go#L57) From 85b582ff49892b42d8016f1c5810ab413ae01be5 Mon Sep 17 00:00:00 2001 From: Jack Zampolin Date: Mon, 28 Sep 2020 18:28:57 -0600 Subject: [PATCH 02/11] Update spacing in code examples --- docs/architecture/adr-031-typed-events.md | 142 +++++++++++----------- 1 file changed, 70 insertions(+), 72 deletions(-) diff --git a/docs/architecture/adr-031-typed-events.md b/docs/architecture/adr-031-typed-events.md index c36d135083c0..4523a3732812 100644 --- a/docs/architecture/adr-031-typed-events.md +++ b/docs/architecture/adr-031-typed-events.md @@ -30,7 +30,7 @@ __Step-1__: Declare event types for `sdk.Msg`s a module implements using the typ // ModuleEvent is the interface that all message events will implement type ModuleEvent interface { Context() BaseModuleEvent - ABCIEvent() Event + ABCIEvent() Event } // BaseModuleEvent contains information about the Module and Action of an event @@ -67,27 +67,27 @@ func NewSDKEvent(bev abci.Event) (ev SDKEvent, err error) { if err != nil { return } - return SDKEvent{sev, BaseModuleEvent{module, action}}, nil + return SDKEvent{sev, BaseModuleEvent{module, action}}, nil } // GetEventString take sdk attributes, key and returns value for that key. func GetEventString(attrs []Attribute, key string) (string, error) { - for _, attr := range attrs { - if attr.Key == key { - return attr.Value, nil - } - } - return "", fmt.Errorf("not found") + for _, attr := range attrs { + if attr.Key == key { + return attr.Value, nil + } + } + return "", fmt.Errorf("not found") } // GetEventUint64 take sdk attributes, key and returns uint64 value. // Returns error incase of failure. func GetEventUint64(attrs []Attribute, key string) (uint64, error) { - sval, err := GetEventString(attrs, key) - if err != nil { - return 0, err - } - return strconv.ParseUint(sval, 10, 64) + sval, err := GetEventString(attrs, key) + if err != nil { + return 0, err + } + return strconv.ParseUint(sval, 10, 64) } // Other type functions for use in the individual module parsers @@ -107,7 +107,7 @@ type AppModuleBasic interface { // ParseEvent takes an sdk.SDKEvent and returns the module specific sdk.ModuleEvent func (bm BasicManager) ParseEvent(ev sdk.SDKEvent) (sdk.ModuleEvent, error) { - for m, b := range bm { + for m, b := range bm { if m == ev.Base.Module { return b.ParseEvent(cdc) } @@ -123,11 +123,11 @@ For example, let's take `MsgSubmitProposal` of `gov` module and implement this e ```go // x/gov/types/events.go func NewEventSubmitProposal(from sdk.Address, id govtypes.ProposalID, proposal govtypes.TextProposal) EventSubmitProposal { - return EventSubmitProposal{ - ID: id, - FromAddress: from, - Proposal: proposal, - } + return EventSubmitProposal{ + ID: id, + FromAddress: from, + Proposal: proposal, + } } type EventSubmitProposal struct { @@ -144,13 +144,13 @@ func (ev EventSubmitProposal) Context() sdk.BaseModuleEvent { } func (ev EventSubmitProposal) ABCIEvent() sdk.Event { - return types.NewEvent("cosmos-sdk-events", - sdk.NewAttribute(sdk.AttributeKeyModule, ev.Context().Module), - sdk.NewAttribute(sdk.AttributeKeyAction, ev.Context().Action), - sdk.NewAttribute("from", ev.FromAddress.String()), - sdk.NewAttribute("title", ev.Proposal.Title.String()), - sdk.NewAttribute("description", ev.Proposal.Description.String()), - ) + return types.NewEvent("cosmos-sdk-events", + sdk.NewAttribute(sdk.AttributeKeyModule, ev.Context().Module), + sdk.NewAttribute(sdk.AttributeKeyAction, ev.Context().Action), + sdk.NewAttribute("from", ev.FromAddress.String()), + sdk.NewAttribute("title", ev.Proposal.Title.String()), + sdk.NewAttribute("description", ev.Proposal.Description.String()), + ) } ``` @@ -164,7 +164,7 @@ func (AppModuleBasic) ParseEvent(ev sdk.SDKEvent) (sdk.ModuleEvent, error) { if ev.Sev.Type != sdk.EventTypeMessage { return nil, fmt.Errorf("unknown message type") } - + if ev.Base.Module != ModuleName { return nil, fmt.Errorf("wrong module: %s not %s", ev.Base.Module, ModuleName) } @@ -175,22 +175,18 @@ func (AppModuleBasic) ParseEvent(ev sdk.SDKEvent) (sdk.ModuleEvent, error) { if err != nil { return nil, err } - proposalId, err := sdk.GetEventUint64(ev.Sev.Attributes, "proposal_id") if err != nil { return nil, err } - proposal, err := parseProposalFromEvent(ev.Sev.Attributes, "id") if err != nil { return nil, err } - from, err := sdk.AccAddressFromBech32(addr) if err != nil { return nil, err } - return NewEventSubmitProposal(from, proposalId, proposal), nil case "proposal_deposit": // TODO: Implement @@ -316,44 +312,46 @@ func TxEmitter(ctx context.Context, cliCtx client.Context, ehs ...EventHandler) return err } - if err = client.Start(); err != nil { - return err - } + if err = client.Start(); err != nil { + return err + } - // Start the pubsub bus - bus := pubsub.NewBus() - defer bus.Close() + // Start the pubsub bus + bus := pubsub.NewBus() + defer bus.Close() - // Initialize a new error group - eg, ctx := errgroup.WithContext(ctx) + // 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) - }) + // 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 - } + // 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 { - 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 { - return err - } - } - } - } + 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() @@ -362,21 +360,21 @@ func TxEmitter(ctx context.Context, cliCtx client.Context, ehs ...EventHandler) // 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) { // Subscribe to transaction events - txch, err := client.Subscribe(ctx, "txevents", "tm.event='Tx'", 100) - if err != nil { - return err + 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") - }() + 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 { + g.Go(func() error { var err error for { select { @@ -412,7 +410,7 @@ func PublishChainTxEvents(ctx context.Context, client tmclient.EventsClient, bus }) // Exit on error or context cancelation - return g.Wait() + return g.Wait() } ``` From 9adaf97f8c43ae02d2dd991b4a882e5451453b62 Mon Sep 17 00:00:00 2001 From: Jack Zampolin Date: Tue, 29 Sep 2020 12:24:40 -0600 Subject: [PATCH 03/11] Update context --- docs/architecture/adr-031-typed-events.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/docs/architecture/adr-031-typed-events.md b/docs/architecture/adr-031-typed-events.md index 4523a3732812..da749c5ae68f 100644 --- a/docs/architecture/adr-031-typed-events.md +++ b/docs/architecture/adr-031-typed-events.md @@ -16,7 +16,13 @@ Proposed ## Context -Currently in the SDK, events are not properly organized (event data is defined in the handlers for each message) and are prone to inconsistencies. This makes customizing events difficult. In addition, they are difficult to consume. This proposal focuses on updating the events to use **typed events** in each module such that emiting and subscribing to events will be much more egrnomic. This will make use of the SDK to build event driven processes much easier and enable rapid development of things like relayers and automated transaction bots for SDK based chains. These types of bots enable will enable easy building of new features at exchanges, wallets, explorers, and defi protocols. IBC especially will benefit from this proposal. +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. The end of this proposal contains a detailed example of how to consume events after this refactor. From 2fc1cf9b673b6399e898358bc76d3a78efcaa0b1 Mon Sep 17 00:00:00 2001 From: akhilkumarpilli Date: Tue, 6 Oct 2020 16:53:02 +0530 Subject: [PATCH 04/11] Update Events ADR --- docs/architecture/adr-031-typed-events.md | 276 +++++++--------------- 1 file changed, 79 insertions(+), 197 deletions(-) diff --git a/docs/architecture/adr-031-typed-events.md b/docs/architecture/adr-031-typed-events.md index da749c5ae68f..f8af585c8940 100644 --- a/docs/architecture/adr-031-typed-events.md +++ b/docs/architecture/adr-031-typed-events.md @@ -28,218 +28,106 @@ The end of this proposal contains a detailed example of how to consume events af ## Decision -__Step-1__: Declare event types for `sdk.Msg`s a module implements using the typed event interface: `sdk.ModuleEvent`. We first need to define this interface and supporting types. +__Step-1__: Implement additional functionality in the `types` package: `EmitTypedEvent` and `ParseTypedEvent` functions ```go // types/events.go -// ModuleEvent is the interface that all message events will implement -type ModuleEvent interface { - Context() BaseModuleEvent - ABCIEvent() Event -} - -// BaseModuleEvent contains information about the Module and Action of an event -type BaseModuleEvent struct { - Module string - Action string -} -``` - -The `BaseModuleEvent` struct will be used for basic context about the event. It aids in routing events to their -`Module` and `Action` specific event parsers. - -__Step 2__: Implement additional functionality in the `types` package: utility functions and a parser to route the event to its proper module. - -When we subscribe to emitted events on the tendermint websocket, they are emitted in the form of an `abci.Event`. The parser will process this event using `sdk.NewSDKEvent(abci.Event)` to enable passing of the processed event to the proper module. - -```go -// types/events.go - -// SDKEvent contains the string representation of the event and the module information -type SDKEvent struct { - Sev StringEvent - Base BaseModuleEvent -} - -// NewSDKEvent parses abci.Event into an sdk.SDKEvent -func NewSDKEvent(bev abci.Event) (ev SDKEvent, err error) { - sev := StringifyEvent(bev) - module, err := GetEventString(sev.Attributes, AttributeKeyModule) - if err != nil { - return - } - action, err := GetEventString(sev.Attributes, AttributeKeyAction) - if err != nil { - return - } - return SDKEvent{sev, BaseModuleEvent{module, action}}, nil -} +// 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, + }) + } + + em.EmitEvent(Event{ + Type: evtType, + Attributes: attrs, + }) -// GetEventString take sdk attributes, key and returns value for that key. -func GetEventString(attrs []Attribute, key string) (string, error) { - for _, attr := range attrs { - if attr.Key == key { - return attr.Value, nil - } - } - return "", fmt.Errorf("not found") + return nil } -// GetEventUint64 take sdk attributes, key and returns uint64 value. -// Returns error incase of failure. -func GetEventUint64(attrs []Attribute, key string) (uint64, error) { - sval, err := GetEventString(attrs, key) - if err != nil { - return 0, err - } - return strconv.ParseUint(sval, 10, 64) +// 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) + } + + value := reflect.New(concreteGoType).Elem() + 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 } - -// Other type functions for use in the individual module parsers -// e.g. func GetEventFloat64(attrs []Attribute, key) (float64, error) {} ``` -__Step-3__: Add `AppModuleBasic.ParseEvent` and define `app.BasicManager.ParseEvent`: +Here, `EmitTypedEvent` is the method of `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`. In this method, `Event.Type` will be type URL of the proto message taken. -A `ParseEvent` function will need to be added to the `sdk.AppModuleBasic` interface. - -```go -type AppModuleBasic interface { - ... - ParseEvent(ev sdk.SDKEvent) (sdk.ModuleEvent, error) - ... -} - -// ParseEvent takes an sdk.SDKEvent and returns the module specific sdk.ModuleEvent -func (bm BasicManager) ParseEvent(ev sdk.SDKEvent) (sdk.ModuleEvent, error) { - for m, b := range bm { - if m == ev.Base.Module { - return b.ParseEvent(cdc) - } - } - return nil, fmt.Errorf("failed to parse event") -} -``` +Next, `ParseTypedEvent` is the method which takes which `abci.Event` and converts back it into `proto.Message` i.e., back to typed event. When we subscribe to emitted events on the tendermint websocket, they are emitted in the form of an `abci.Event`. So this method will take those emitted event and parse it into typed event. -__Step-4__: Define typed events for msgs in `x//types/events.go`: +__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. -```go -// x/gov/types/events.go -func NewEventSubmitProposal(from sdk.Address, id govtypes.ProposalID, proposal govtypes.TextProposal) EventSubmitProposal { - return EventSubmitProposal{ - ID: id, - FromAddress: from, - Proposal: proposal, - } -} - -type EventSubmitProposal struct { - FromAddress AccAddress - ID ProposalID - Proposal types.TextProposal -} - -func (ev EventSubmitProposal) Context() sdk.BaseModuleEvent { - return BaseModuleEvent{ - Module: "gov", - Action: "submit_proposal", - } -} - -func (ev EventSubmitProposal) ABCIEvent() sdk.Event { - return types.NewEvent("cosmos-sdk-events", - sdk.NewAttribute(sdk.AttributeKeyModule, ev.Context().Module), - sdk.NewAttribute(sdk.AttributeKeyAction, ev.Context().Action), - sdk.NewAttribute("from", ev.FromAddress.String()), - sdk.NewAttribute("title", ev.Proposal.Title.String()), - sdk.NewAttribute("description", ev.Proposal.Description.String()), - ) -} -``` - -__Step-5__: Define `ParseEvent` for each module in their respective `x//module.go`: - -```go -// x/gov/module.go - -// ParseEvent turns an sdk.SDKEvent into the gov specific event type and error if any occurred -func (AppModuleBasic) ParseEvent(ev sdk.SDKEvent) (sdk.ModuleEvent, error) { - if ev.Sev.Type != sdk.EventTypeMessage { - return nil, fmt.Errorf("unknown message type") - } +```protobuf +// proto/cosmos/gov/v1beta1/gov.proto +// Add typed event definition - if ev.Base.Module != ModuleName { - return nil, fmt.Errorf("wrong module: %s not %s", ev.Base.Module, ModuleName) - } - - switch ev.Base.Action { - case "submit_proposal": - addr, err := sdk.GetEventString(ev.Sev.Attributes, "from") - if err != nil { - return nil, err - } - proposalId, err := sdk.GetEventUint64(ev.Sev.Attributes, "proposal_id") - if err != nil { - return nil, err - } - proposal, err := parseProposalFromEvent(ev.Sev.Attributes, "id") - if err != nil { - return nil, err - } - from, err := sdk.AccAddressFromBech32(addr) - if err != nil { - return nil, err - } - return NewEventSubmitProposal(from, proposalId, proposal), nil - case "proposal_deposit": - // TODO: Implement - case "submit_proposal": - // TODO: Implement - case "proposal_deposit": - // TODO: Implement - case "proposal_vote": - // TODO: Implement - case "inactive_proposal": - // TODO: Implement - case "active_proposal": - // TODO: Implement - default: - return nil, fmt.Errorf("unsupported event type for gov") - } -} +package cosmos.gov.v1beta1; -// parseProposalFromEvent returns the TextProposal from []sdk.Attributes -func parseProposalFromEvent(attrs []sdk.Attribute) ([]byte, error) { - description, err := sdk.GetEventString(attrs, "description") - if err != nil { - return govtypes.TextProposal{}, err - } - - title, err := sdk.GetEventString(attrs, "title") - if err != nil { - return govtypes.TextProposal{}, err - } - - return govtypes.TextProposal{ - Title: title, - Description: description, - }, nil +message EventSubmitProposal { + string from_address = 1; + uint64 proposal_id = 2 [(gogoproto.enumvalue_customname) = "ID"]; + TextProposal proposal = 3; } ``` -__Step-6__: Refactor event emission to use the types created: - -Emiting events is similar to the current method: +__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().EmitEvent( - NewEventSubmitProposal(fromAddress, id, proposal).ABCIEvent(), + types.Context.EventManager().EmitTypedEvent( + &EventSubmitProposal{ + FromAddress: fromAddress, + ID: id, + Proposal: proposal, + }, ) ... } @@ -280,7 +168,7 @@ 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(sdk.ModuleEvent) error +type EventHandler func(proto.Message) error // Sample use of the functions below func main() { @@ -296,7 +184,7 @@ func main() { // SubmitProposalEventHandler is an example of an event handler that prints proposal details // when any EventSubmitProposal is emitted. -func SubmitProposalEventHandler(ev sdk.ModuleEvent) (err error) { +func SubmitProposalEventHandler(ev proto.Message) (err error) { switch event := ev.(type) { // Handle governance proposal events creation events case govtypes.EventSubmitProposal: @@ -395,15 +283,11 @@ func PublishChainTxEvents(ctx context.Context, client tmclient.EventsClient, bus // range over events, parse them using the basic manager and // send them to the pubsub bus for _, abciEv := range events { - sdkEv, err := sdk.NewSDKEvent(abciEv) - if err != nil { - return err - } - moduleEvent, err := mb.ParseEvent(abciEv) + typedEvent, err := sdk.ParseTypedEvent(abciEv) if err != nil { return er } - if err := bus.Publish(moduleEvent); err != nil { + if err := bus.Publish(typedEvent); err != nil { bus.Close() return } @@ -421,7 +305,5 @@ func PublishChainTxEvents(ctx context.Context, client tmclient.EventsClient, bus ``` ## References -- [Event types for a module](https://github.com/ovrclk/akash/blob/master/x/deployment/types/event.go#L24) -- [Emit Events](https://github.com/ovrclk/akash/blob/master/x/deployment/keeper/keeper.go#L129) - [Publish Custom Events via a bus](https://github.com/ovrclk/akash/blob/master/events/publish.go#L19-L58) - [Consuming the events in `Client`](https://github.com/jackzampolin/deploy/blob/master/cmd/event-handlers.go#L57) From f63304627e688ea84a503342887efdac618d0bea Mon Sep 17 00:00:00 2001 From: akhilkumarpilli Date: Tue, 6 Oct 2020 16:53:44 +0530 Subject: [PATCH 05/11] Update ADR number --- docs/architecture/adr-031-typed-events.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/architecture/adr-031-typed-events.md b/docs/architecture/adr-031-typed-events.md index f8af585c8940..b46f421327b9 100644 --- a/docs/architecture/adr-031-typed-events.md +++ b/docs/architecture/adr-031-typed-events.md @@ -1,4 +1,4 @@ -# ADR 030: Typed Events +# ADR 031: Typed Events ## Changelog From 027995eae70a5baebbc00025e5de21fd5e58555b Mon Sep 17 00:00:00 2001 From: akhilkumarpilli Date: Tue, 6 Oct 2020 17:13:22 +0530 Subject: [PATCH 06/11] Rename and update ADR --- ...r-031-typed-events.md => adr-032-typed-events.md} | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) rename docs/architecture/{adr-031-typed-events.md => adr-032-typed-events.md} (92%) diff --git a/docs/architecture/adr-031-typed-events.md b/docs/architecture/adr-032-typed-events.md similarity index 92% rename from docs/architecture/adr-031-typed-events.md rename to docs/architecture/adr-032-typed-events.md index b46f421327b9..5fe6b874a491 100644 --- a/docs/architecture/adr-031-typed-events.md +++ b/docs/architecture/adr-032-typed-events.md @@ -1,4 +1,4 @@ -# ADR 031: Typed Events +# ADR 032: Typed Events ## Changelog @@ -95,9 +95,9 @@ func ParseTypedEvent(event abci.Event) (proto.Message, error) { } ``` -Here, `EmitTypedEvent` is the method of `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`. In this method, `Event.Type` will be type URL of the proto message taken. +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. -Next, `ParseTypedEvent` is the method which takes which `abci.Event` and converts back it into `proto.Message` i.e., back to typed event. When we subscribe to emitted events on the tendermint websocket, they are emitted in the form of an `abci.Event`. So this method will take those emitted event and parse it into typed event. +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: @@ -111,7 +111,7 @@ package cosmos.gov.v1beta1; message EventSubmitProposal { string from_address = 1; - uint64 proposal_id = 2 [(gogoproto.enumvalue_customname) = "ID"]; + uint64 proposal_id = 2; TextProposal proposal = 3; } ``` @@ -125,7 +125,7 @@ func handleMsgSubmitProposal(ctx sdk.Context, keeper keeper.Keeper, msg types.Ms types.Context.EventManager().EmitTypedEvent( &EventSubmitProposal{ FromAddress: fromAddress, - ID: id, + ProposalId: id, Proposal: proposal, }, ) @@ -189,7 +189,7 @@ func SubmitProposalEventHandler(ev proto.Message) (err error) { // Handle governance proposal events creation events case govtypes.EventSubmitProposal: // Users define business logic here e.g. - fmt.Println(ev.FromAddress, ev.ID, ev.Proposal) + fmt.Println(ev.FromAddress, ev.ProposalId, ev.Proposal) return nil default: return nil From 8b95d42a407f1bdb4e4804b71a4def801b87a42a Mon Sep 17 00:00:00 2001 From: akhilkumarpilli Date: Wed, 7 Oct 2020 17:30:12 +0530 Subject: [PATCH 07/11] Modify parseTypedEvent code in ADR --- docs/architecture/adr-032-typed-events.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/docs/architecture/adr-032-typed-events.md b/docs/architecture/adr-032-typed-events.md index 5fe6b874a491..6d71cbdc4ca5 100644 --- a/docs/architecture/adr-032-typed-events.md +++ b/docs/architecture/adr-032-typed-events.md @@ -70,7 +70,13 @@ func ParseTypedEvent(event abci.Event) (proto.Message, error) { return nil, fmt.Errorf("failed to retrieve the message of type %q", event.Type) } - value := reflect.New(concreteGoType).Elem() + 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) From 3bf2ab8dda83a08f7e2f362fe6a4dff28cc96f07 Mon Sep 17 00:00:00 2001 From: Jack Zampolin Date: Wed, 7 Oct 2020 12:37:55 -0600 Subject: [PATCH 08/11] Update docs/architecture/adr-032-typed-events.md Co-authored-by: Aaron Craelius --- docs/architecture/adr-032-typed-events.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/docs/architecture/adr-032-typed-events.md b/docs/architecture/adr-032-typed-events.md index 6d71cbdc4ca5..7adcde32832e 100644 --- a/docs/architecture/adr-032-typed-events.md +++ b/docs/architecture/adr-032-typed-events.md @@ -159,8 +159,6 @@ Please see the below code sample for more detail on this flow looks for clients. ### Negative -* Requires a substantial amount of additional code in each module. For new developers and chains, this can be -partially mitigaed by code generation in [`starport`](https://github.com/tendermint/starport). ## Detailed code example of publishing events From 850439895215543f599321550130234fd8ff7508 Mon Sep 17 00:00:00 2001 From: Jack Zampolin Date: Fri, 9 Oct 2020 13:39:55 -0600 Subject: [PATCH 09/11] Address PR Comments --- docs/architecture/adr-032-typed-events.md | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/docs/architecture/adr-032-typed-events.md b/docs/architecture/adr-032-typed-events.md index 7adcde32832e..4dc5c7462778 100644 --- a/docs/architecture/adr-032-typed-events.md +++ b/docs/architecture/adr-032-typed-events.md @@ -14,6 +14,10 @@ Proposed +## Abstract + +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. + ## 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. @@ -145,7 +149,7 @@ func handleMsgSubmitProposal(ctx sdk.Context, keeper keeper.Keeper, msg types.Ms Users will be able to subscribe using `client.Context.Client.Subscribe` and consume events which are emitted using `EventHandler`s. -Akash Network has built a simple [`pubsub`](https://github.com/ovrclk/akash/blob/master/pubsub/bus.go). This can be used to subscribe to `abci.Events` and [publish](https://github.com/ovrclk/akash/blob/master/events/publish.go#L21) them as typed events. +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. @@ -309,5 +313,5 @@ func PublishChainTxEvents(ctx context.Context, client tmclient.EventsClient, bus ``` ## References -- [Publish Custom Events via a bus](https://github.com/ovrclk/akash/blob/master/events/publish.go#L19-L58) -- [Consuming the events in `Client`](https://github.com/jackzampolin/deploy/blob/master/cmd/event-handlers.go#L57) +- [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) From 64b5ee968f70190f58bcbaf0c88bcbd42bc9ed44 Mon Sep 17 00:00:00 2001 From: Jack Zampolin Date: Sat, 10 Oct 2020 09:10:00 -0600 Subject: [PATCH 10/11] Address PR comments --- docs/architecture/adr-032-typed-events.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/architecture/adr-032-typed-events.md b/docs/architecture/adr-032-typed-events.md index 4dc5c7462778..33758900d47a 100644 --- a/docs/architecture/adr-032-typed-events.md +++ b/docs/architecture/adr-032-typed-events.md @@ -16,7 +16,7 @@ Proposed ## Abstract -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. +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 @@ -30,6 +30,8 @@ If this proposal is accepted, users will be able to build event driven SDK apps 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 From b3325ff0e8ead91763ed34e0e0ef75ab5d01b4b9 Mon Sep 17 00:00:00 2001 From: Jack Zampolin Date: Tue, 13 Oct 2020 09:16:42 -0600 Subject: [PATCH 11/11] Add link to adr in readme --- docs/architecture/README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/architecture/README.md b/docs/architecture/README.md index a2bce8f771a0..9a401c17e347 100644 --- a/docs/architecture/README.md +++ b/docs/architecture/README.md @@ -56,3 +56,5 @@ Please add a entry below in your Pull Request for an ADR. - [ADR 027: Deterministic Protobuf Serialization](./adr-027-deterministic-protobuf-serialization.md) - [ADR 029: Fee Grant Module](./adr-029-fee-grant-module.md) - [ADR 031: Protobuf Msg Services](./adr-031-msg-service.md) +- [ADR 032: Typed Events](./adr-031-typed-events.md) +