github.com/cosmos/cosmos-sdk@v0.50.10/docs/architecture/adr-038-state-listening.md (about)

     1  # ADR 038: KVStore state listening
     2  
     3  ## Changelog
     4  
     5  * 11/23/2020: Initial draft
     6  * 10/06/2022: Introduce plugin system based on hashicorp/go-plugin
     7  * 10/14/2022:
     8      * Add `ListenCommit`, flatten the state writes in a block to a single batch.
     9      * Remove listeners from cache stores, should only listen to `rootmulti.Store`.
    10      * Remove `HaltAppOnDeliveryError()`, the errors are propagated by default, the implementations should return nil if don't want to propogate errors.
    11  * 26/05/2023: Update with ABCI 2.0
    12  
    13  ## Status
    14  
    15  Proposed
    16  
    17  ## Abstract
    18  
    19  This ADR defines a set of changes to enable listening to state changes of individual KVStores and exposing these data to consumers.
    20  
    21  ## Context
    22  
    23  Currently, KVStore data can be remotely accessed through [Queries](https://github.com/cosmos/cosmos-sdk/blob/master/docs/building-modules/messages-and-queries.md#queries)
    24  which proceed either through Tendermint and the ABCI, or through the gRPC server.
    25  In addition to these request/response queries, it would be beneficial to have a means of listening to state changes as they occur in real time.
    26  
    27  ## Decision
    28  
    29  We will modify the `CommitMultiStore` interface and its concrete (`rootmulti`) implementations and introduce a new `listenkv.Store` to allow listening to state changes in underlying KVStores. We don't need to listen to cache stores, because we can't be sure that the writes will be committed eventually, and the writes are duplicated in `rootmulti.Store` eventually, so we should only listen to `rootmulti.Store`.
    30  We will introduce a plugin system for configuring and running streaming services that write these state changes and their surrounding ABCI message context to different destinations.
    31  
    32  ### Listening
    33  
    34  In a new file, `store/types/listening.go`, we will create a `MemoryListener` struct for streaming out protobuf encoded KV pairs state changes from a KVStore.
    35  The `MemoryListener` will be used internally by the concrete `rootmulti` implementation to collect state changes from KVStores.
    36  
    37  ```go
    38  // MemoryListener listens to the state writes and accumulate the records in memory.
    39  type MemoryListener struct {
    40  	stateCache []StoreKVPair
    41  }
    42  
    43  // NewMemoryListener creates a listener that accumulate the state writes in memory.
    44  func NewMemoryListener() *MemoryListener {
    45  	return &MemoryListener{}
    46  }
    47  
    48  // OnWrite writes state change events to the internal cache
    49  func (fl *MemoryListener) OnWrite(storeKey StoreKey, key []byte, value []byte, delete bool) {
    50  	fl.stateCache = append(fl.stateCache, StoreKVPair{
    51  		StoreKey: storeKey.Name(),
    52  		Delete:   delete,
    53  		Key:      key,
    54  		Value:    value,
    55  	})
    56  }
    57  
    58  // PopStateCache returns the current state caches and set to nil
    59  func (fl *MemoryListener) PopStateCache() []StoreKVPair {
    60  	res := fl.stateCache
    61  	fl.stateCache = nil
    62  	return res
    63  }
    64  ```
    65  
    66  We will also define a protobuf type for the KV pairs. In addition to the key and value fields this message
    67  will include the StoreKey for the originating KVStore so that we can collect information from separate KVStores and determine the source of each KV pair.
    68  
    69  ```protobuf
    70  message StoreKVPair {
    71    optional string store_key = 1; // the store key for the KVStore this pair originates from
    72    required bool set = 2; // true indicates a set operation, false indicates a delete operation
    73    required bytes key = 3;
    74    required bytes value = 4;
    75  }
    76  ```
    77  
    78  ### ListenKVStore
    79  
    80  We will create a new `Store` type `listenkv.Store` that the `rootmulti` store will use to wrap a `KVStore` to enable state listening.
    81  We will configure the `Store` with a `MemoryListener` which will collect state changes for output to specific destinations.
    82  
    83  ```go
    84  // Store implements the KVStore interface with listening enabled.
    85  // Operations are traced on each core KVStore call and written to any of the
    86  // underlying listeners with the proper key and operation permissions
    87  type Store struct {
    88      parent    types.KVStore
    89      listener  *types.MemoryListener
    90      parentStoreKey types.StoreKey
    91  }
    92  
    93  // NewStore returns a reference to a new traceKVStore given a parent
    94  // KVStore implementation and a buffered writer.
    95  func NewStore(parent types.KVStore, psk types.StoreKey, listener *types.MemoryListener) *Store {
    96      return &Store{parent: parent, listener: listener, parentStoreKey: psk}
    97  }
    98  
    99  // Set implements the KVStore interface. It traces a write operation and
   100  // delegates the Set call to the parent KVStore.
   101  func (s *Store) Set(key []byte, value []byte) {
   102      types.AssertValidKey(key)
   103      s.parent.Set(key, value)
   104      s.listener.OnWrite(s.parentStoreKey, key, value, false)
   105  }
   106  
   107  // Delete implements the KVStore interface. It traces a write operation and
   108  // delegates the Delete call to the parent KVStore.
   109  func (s *Store) Delete(key []byte) {
   110      s.parent.Delete(key)
   111      s.listener.OnWrite(s.parentStoreKey, key, nil, true)
   112  }
   113  ```
   114  
   115  ### MultiStore interface updates
   116  
   117  We will update the `CommitMultiStore` interface to allow us to wrap a `Memorylistener` to a specific `KVStore`.
   118  Note that the `MemoryListener` will be attached internally by the concrete `rootmulti` implementation.
   119  
   120  ```go
   121  type CommitMultiStore interface {
   122      ...
   123  
   124      // AddListeners adds a listener for the KVStore belonging to the provided StoreKey
   125      AddListeners(keys []StoreKey)
   126  
   127      // PopStateCache returns the accumulated state change messages from MemoryListener
   128      PopStateCache() []StoreKVPair
   129  }
   130  ```
   131  
   132  
   133  ### MultiStore implementation updates
   134  
   135  We will adjust the `rootmulti` `GetKVStore` method to wrap the returned `KVStore` with a `listenkv.Store` if listening is turned on for that `Store`.
   136  
   137  ```go
   138  func (rs *Store) GetKVStore(key types.StoreKey) types.KVStore {
   139      store := rs.stores[key].(types.KVStore)
   140  
   141      if rs.TracingEnabled() {
   142          store = tracekv.NewStore(store, rs.traceWriter, rs.traceContext)
   143      }
   144      if rs.ListeningEnabled(key) {
   145          store = listenkv.NewStore(store, key, rs.listeners[key])
   146      }
   147  
   148      return store
   149  }
   150  ```
   151  
   152  We will implement `AddListeners` to manage KVStore listeners internally and implement `PopStateCache`
   153  for a means of retrieving the current state.
   154  
   155  ```go
   156  // AddListeners adds state change listener for a specific KVStore
   157  func (rs *Store) AddListeners(keys []types.StoreKey) {
   158  	listener := types.NewMemoryListener()
   159  	for i := range keys {
   160  		rs.listeners[keys[i]] = listener
   161  	}
   162  }
   163  ```
   164  
   165  ```go
   166  func (rs *Store) PopStateCache() []types.StoreKVPair {
   167  	var cache []types.StoreKVPair
   168  	for _, ls := range rs.listeners {
   169  		cache = append(cache, ls.PopStateCache()...)
   170  	}
   171  	sort.SliceStable(cache, func(i, j int) bool {
   172  		return cache[i].StoreKey < cache[j].StoreKey
   173  	})
   174  	return cache
   175  }
   176  ```
   177  
   178  We will also adjust the `rootmulti` `CacheMultiStore` and `CacheMultiStoreWithVersion` methods to enable listening in
   179  the cache layer.
   180  
   181  ```go
   182  func (rs *Store) CacheMultiStore() types.CacheMultiStore {
   183      stores := make(map[types.StoreKey]types.CacheWrapper)
   184      for k, v := range rs.stores {
   185          store := v.(types.KVStore)
   186          // Wire the listenkv.Store to allow listeners to observe the writes from the cache store,
   187          // set same listeners on cache store will observe duplicated writes.
   188          if rs.ListeningEnabled(k) {
   189              store = listenkv.NewStore(store, k, rs.listeners[k])
   190          }
   191          stores[k] = store
   192      }
   193      return cachemulti.NewStore(rs.db, stores, rs.keysByName, rs.traceWriter, rs.getTracingContext())
   194  }
   195  ```
   196  
   197  ```go
   198  func (rs *Store) CacheMultiStoreWithVersion(version int64) (types.CacheMultiStore, error) {
   199   // ...
   200  
   201          // Wire the listenkv.Store to allow listeners to observe the writes from the cache store,
   202          // set same listeners on cache store will observe duplicated writes.
   203          if rs.ListeningEnabled(key) {
   204              cacheStore = listenkv.NewStore(cacheStore, key, rs.listeners[key])
   205          }
   206  
   207          cachedStores[key] = cacheStore
   208      }
   209  
   210      return cachemulti.NewStore(rs.db, cachedStores, rs.keysByName, rs.traceWriter, rs.getTracingContext()), nil
   211  }
   212  ```
   213  
   214  ### Exposing the data
   215  
   216  #### Streaming Service
   217  
   218  We will introduce a new `ABCIListener` interface that plugs into the BaseApp and relays ABCI requests and responses
   219  so that the service can group the state changes with the ABCI requests.
   220  
   221  ```go
   222  // baseapp/streaming.go
   223  
   224  // ABCIListener is the interface that we're exposing as a streaming service.
   225  type ABCIListener interface {
   226  	// ListenFinalizeBlock updates the streaming service with the latest FinalizeBlock messages
   227  	ListenFinalizeBlock(ctx context.Context, req abci.RequestFinalizeBlock, res abci.ResponseFinalizeBlock) error
   228  	// ListenCommit updates the steaming service with the latest Commit messages and state changes
   229  	ListenCommit(ctx context.Context, res abci.ResponseCommit, changeSet []*StoreKVPair) error
   230  }
   231  ```
   232  
   233  #### BaseApp Registration
   234  
   235  We will add a new method to the `BaseApp` to enable the registration of `StreamingService`s:
   236  
   237   ```go
   238   // SetStreamingService is used to set a streaming service into the BaseApp hooks and load the listeners into the multistore
   239  func (app *BaseApp) SetStreamingService(s ABCIListener) {
   240      // register the StreamingService within the BaseApp
   241      // BaseApp will pass BeginBlock, DeliverTx, and EndBlock requests and responses to the streaming services to update their ABCI context
   242      app.abciListeners = append(app.abciListeners, s)
   243  }
   244  ```
   245  
   246  We will add two new fields to the `BaseApp` struct:
   247  
   248  ```go
   249  type BaseApp struct {
   250  
   251      ...
   252  
   253      // abciListenersAsync for determining if abciListeners will run asynchronously.
   254      // When abciListenersAsync=false and stopNodeOnABCIListenerErr=false listeners will run synchronized but will not stop the node.
   255      // When abciListenersAsync=true stopNodeOnABCIListenerErr will be ignored.
   256      abciListenersAsync bool
   257  
   258      // stopNodeOnABCIListenerErr halts the node when ABCI streaming service listening results in an error.
   259      // stopNodeOnABCIListenerErr=true must be paired with abciListenersAsync=false.
   260      stopNodeOnABCIListenerErr bool
   261  }
   262  ```
   263  
   264  #### ABCI Event Hooks
   265  
   266  We will modify the `FinalizeBlock` and `Commit` methods to pass ABCI requests and responses
   267  to any streaming service hooks registered with the `BaseApp`.
   268  
   269  ```go
   270  func (app *BaseApp) FinalizeBlock(req abci.RequestFinalizeBlock) abci.ResponseFinalizeBlock {
   271  
   272      var abciRes abci.ResponseFinalizeBlock
   273      defer func() {
   274          // call the streaming service hook with the FinalizeBlock messages
   275          for _, abciListener := range app.abciListeners {
   276              ctx := app.finalizeState.ctx
   277              blockHeight := ctx.BlockHeight()
   278              if app.abciListenersAsync {
   279                  go func(req abci.RequestFinalizeBlock, res abci.ResponseFinalizeBlock) {
   280                      if err := app.abciListener.FinalizeBlock(blockHeight, req, res); err != nil {
   281                          app.logger.Error("FinalizeBlock listening hook failed", "height", blockHeight, "err", err)
   282                      }
   283                  }(req, abciRes)
   284              } else {
   285                  if err := app.abciListener.ListenFinalizeBlock(blockHeight, req, res); err != nil {
   286                      app.logger.Error("FinalizeBlock listening hook failed", "height", blockHeight, "err", err)
   287                      if app.stopNodeOnABCIListenerErr {
   288                          os.Exit(1)
   289                      }
   290                  }
   291              }
   292          }
   293      }()
   294  
   295      ...
   296  
   297      return abciRes
   298  }
   299  ```
   300  
   301  ```go
   302  func (app *BaseApp) Commit() abci.ResponseCommit {
   303  
   304      ...
   305  
   306      res := abci.ResponseCommit{
   307          Data:         commitID.Hash,
   308          RetainHeight: retainHeight,
   309      }
   310  
   311      // call the streaming service hook with the Commit messages
   312      for _, abciListener := range app.abciListeners {
   313          ctx := app.deliverState.ctx
   314          blockHeight := ctx.BlockHeight()
   315          changeSet := app.cms.PopStateCache()
   316          if app.abciListenersAsync {
   317              go func(res abci.ResponseCommit, changeSet []store.StoreKVPair) {
   318                  if err := app.abciListener.ListenCommit(ctx, res, changeSet); err != nil {
   319                      app.logger.Error("ListenCommit listening hook failed", "height", blockHeight, "err", err)
   320                  }
   321              }(res, changeSet)
   322          } else {
   323              if err := app.abciListener.ListenCommit(ctx, res, changeSet); err != nil {
   324                  app.logger.Error("ListenCommit listening hook failed", "height", blockHeight, "err", err)
   325                  if app.stopNodeOnABCIListenerErr {
   326                      os.Exit(1)
   327                  }
   328              }
   329          }
   330      }
   331  
   332      ...
   333  
   334      return res
   335  }
   336  ```
   337  
   338  #### Go Plugin System
   339  
   340  We propose a plugin architecture to load and run `Streaming` plugins and other types of implementations. We will introduce a plugin
   341  system over gRPC that is used to load and run Cosmos-SDK plugins. The plugin system uses [hashicorp/go-plugin](https://github.com/hashicorp/go-plugin).
   342  Each plugin must have a struct that implements the `plugin.Plugin` interface and an `Impl` interface for processing messages over gRPC.
   343  Each plugin must also have a message protocol defined for the gRPC service:
   344  
   345  ```go
   346  // streaming/plugins/abci/{plugin_version}/interface.go
   347  
   348  // Handshake is a common handshake that is shared by streaming and host.
   349  // This prevents users from executing bad plugins or executing a plugin
   350  // directory. It is a UX feature, not a security feature.
   351  var Handshake = plugin.HandshakeConfig{
   352      ProtocolVersion:  1,
   353      MagicCookieKey:   "ABCI_LISTENER_PLUGIN",
   354      MagicCookieValue: "ef78114d-7bdf-411c-868f-347c99a78345",
   355  }
   356  
   357  // ListenerPlugin is the base struc for all kinds of go-plugin implementations
   358  // It will be included in interfaces of different Plugins
   359  type ABCIListenerPlugin struct {
   360      // GRPCPlugin must still implement the Plugin interface
   361      plugin.Plugin
   362      // Concrete implementation, written in Go. This is only used for plugins
   363      // that are written in Go.
   364      Impl baseapp.ABCIListener
   365  }
   366  
   367  func (p *ListenerGRPCPlugin) GRPCServer(_ *plugin.GRPCBroker, s *grpc.Server) error {
   368      RegisterABCIListenerServiceServer(s, &GRPCServer{Impl: p.Impl})
   369      return nil
   370  }
   371  
   372  func (p *ListenerGRPCPlugin) GRPCClient(
   373      _ context.Context,
   374      _ *plugin.GRPCBroker,
   375      c *grpc.ClientConn,
   376  ) (interface{}, error) {
   377      return &GRPCClient{client: NewABCIListenerServiceClient(c)}, nil
   378  }
   379  ```
   380  
   381  The `plugin.Plugin` interface has two methods `Client` and `Server`. For our GRPC service these are `GRPCClient` and `GRPCServer`
   382  The `Impl` field holds the concrete implementation of our `baseapp.ABCIListener` interface written in Go.
   383  Note: this is only used for plugin implementations written in Go.
   384  
   385  The advantage of having such a plugin system is that within each plugin authors can define the message protocol in a way that fits their use case.
   386  For example, when state change listening is desired, the `ABCIListener` message protocol can be defined as below (*for illustrative purposes only*).
   387  When state change listening is not desired than `ListenCommit` can be omitted from the protocol.
   388  
   389  ```protobuf
   390  syntax = "proto3";
   391  
   392  ...
   393  
   394  message Empty {}
   395  
   396  message ListenFinalizeBlockRequest {
   397    RequestFinalizeBlock  req = 1;
   398    ResponseFinalizeBlock res = 2;
   399  }
   400  message ListenCommitRequest {
   401    int64                block_height = 1;
   402    ResponseCommit       res          = 2;
   403    repeated StoreKVPair changeSet    = 3;
   404  }
   405  
   406  // plugin that listens to state changes
   407  service ABCIListenerService {
   408    rpc ListenFinalizeBlock(ListenFinalizeBlockRequest) returns (Empty);
   409    rpc ListenCommit(ListenCommitRequest) returns (Empty);
   410  }
   411  ```
   412  
   413  ```protobuf
   414  ...
   415  // plugin that doesn't listen to state changes
   416  service ABCIListenerService {
   417    rpc ListenFinalizeBlock(ListenFinalizeBlockRequest) returns (Empty);
   418    rpc ListenCommit(ListenCommitRequest) returns (Empty);
   419  }
   420  ```
   421  
   422  Implementing the service above:
   423  
   424  ```go
   425  // streaming/plugins/abci/{plugin_version}/grpc.go
   426  
   427  var (
   428      _ baseapp.ABCIListener = (*GRPCClient)(nil)
   429  )
   430  
   431  // GRPCClient is an implementation of the ABCIListener and ABCIListenerPlugin interfaces that talks over RPC.
   432  type GRPCClient struct {
   433      client ABCIListenerServiceClient
   434  }
   435  
   436  func (m *GRPCClient) ListenFinalizeBlock(goCtx context.Context, req abci.RequestFinalizeBlock, res abci.ResponseFinalizeBlock) error {
   437      ctx := sdk.UnwrapSDKContext(goCtx)
   438      _, err := m.client.ListenDeliverTx(ctx, &ListenDeliverTxRequest{BlockHeight: ctx.BlockHeight(), Req: req, Res: res})
   439      return err
   440  }
   441  
   442  func (m *GRPCClient) ListenCommit(goCtx context.Context, res abci.ResponseCommit, changeSet []store.StoreKVPair) error {
   443      ctx := sdk.UnwrapSDKContext(goCtx)
   444      _, err := m.client.ListenCommit(ctx, &ListenCommitRequest{BlockHeight: ctx.BlockHeight(), Res: res, ChangeSet: changeSet})
   445      return err
   446  }
   447  
   448  // GRPCServer is the gRPC server that GRPCClient talks to.
   449  type GRPCServer struct {
   450      // This is the real implementation
   451      Impl baseapp.ABCIListener
   452  }
   453  
   454  func (m *GRPCServer) ListenFinalizeBlock(ctx context.Context, req *ListenFinalizeBlockRequest) (*Empty, error) {
   455      return &Empty{}, m.Impl.ListenFinalizeBlock(ctx, req.Req, req.Res)
   456  }
   457  
   458  func (m *GRPCServer) ListenCommit(ctx context.Context, req *ListenCommitRequest) (*Empty, error) {
   459      return &Empty{}, m.Impl.ListenCommit(ctx, req.Res, req.ChangeSet)
   460  }
   461  
   462  ```
   463  
   464  And the pre-compiled Go plugin `Impl`(*this is only used for plugins that are written in Go*):
   465  
   466  ```go
   467  // streaming/plugins/abci/{plugin_version}/impl/plugin.go
   468  
   469  // Plugins are pre-compiled and loaded by the plugin system
   470  
   471  // ABCIListener is the implementation of the baseapp.ABCIListener interface
   472  type ABCIListener struct{}
   473  
   474  func (m *ABCIListenerPlugin) ListenFinalizeBlock(ctx context.Context, req abci.RequestFinalizeBlock, res abci.ResponseFinalizeBlock) error {
   475      // send data to external system
   476  }
   477  
   478  func (m *ABCIListenerPlugin) ListenCommit(ctx context.Context, res abci.ResponseCommit, changeSet []store.StoreKVPair) error {
   479      // send data to external system
   480  }
   481  
   482  func main() {
   483      plugin.Serve(&plugin.ServeConfig{
   484          HandshakeConfig: grpc_abci_v1.Handshake,
   485          Plugins: map[string]plugin.Plugin{
   486             "grpc_plugin_v1": &grpc_abci_v1.ABCIListenerGRPCPlugin{Impl: &ABCIListenerPlugin{}},
   487          },
   488  
   489          // A non-nil value here enables gRPC serving for this streaming...
   490          GRPCServer: plugin.DefaultGRPCServer,
   491      })
   492  }
   493  ```
   494  
   495  We will introduce a plugin loading system that will return `(interface{}, error)`.
   496  This provides the advantage of using versioned plugins where the plugin interface and gRPC protocol change over time.
   497  In addition, it allows for building independent plugin that can expose different parts of the system over gRPC.
   498  
   499  ```go
   500  func NewStreamingPlugin(name string, logLevel string) (interface{}, error) {
   501      logger := hclog.New(&hclog.LoggerOptions{
   502         Output: hclog.DefaultOutput,
   503         Level:  toHclogLevel(logLevel),
   504         Name:   fmt.Sprintf("plugin.%s", name),
   505      })
   506  
   507      // We're a host. Start by launching the streaming process.
   508      env := os.Getenv(GetPluginEnvKey(name))
   509      client := plugin.NewClient(&plugin.ClientConfig{
   510         HandshakeConfig: HandshakeMap[name],
   511         Plugins:         PluginMap,
   512         Cmd:             exec.Command("sh", "-c", env),
   513         Logger:          logger,
   514         AllowedProtocols: []plugin.Protocol{
   515             plugin.ProtocolNetRPC, plugin.ProtocolGRPC},
   516      })
   517  
   518      // Connect via RPC
   519      rpcClient, err := client.Client()
   520      if err != nil {
   521         return nil, err
   522      }
   523  
   524      // Request streaming plugin
   525      return rpcClient.Dispense(name)
   526  }
   527  
   528  ```
   529  
   530  We propose a `RegisterStreamingPlugin` function for the App to register `NewStreamingPlugin`s with the App's BaseApp.
   531  Streaming plugins can be of `Any` type; therefore, the function takes in an interface vs a concrete type.
   532  For example, we could have plugins of `ABCIListener`, `WasmListener` or `IBCListener`. Note that `RegisterStreamingPluing` function
   533  is helper function and not a requirement. Plugin registration can easily be moved from the App to the BaseApp directly.
   534  
   535  ```go
   536  // baseapp/streaming.go
   537  
   538  // RegisterStreamingPlugin registers streaming plugins with the App.
   539  // This method returns an error if a plugin is not supported.
   540  func RegisterStreamingPlugin(
   541      bApp *BaseApp,
   542      appOpts servertypes.AppOptions,
   543      keys map[string]*types.KVStoreKey,
   544      streamingPlugin interface{},
   545  ) error {
   546      switch t := streamingPlugin.(type) {
   547      case ABCIListener:
   548          registerABCIListenerPlugin(bApp, appOpts, keys, t)
   549      default:
   550          return fmt.Errorf("unexpected plugin type %T", t)
   551      }
   552      return nil
   553  }
   554  ```
   555  
   556  ```go
   557  func registerABCIListenerPlugin(
   558      bApp *BaseApp,
   559      appOpts servertypes.AppOptions,
   560      keys map[string]*store.KVStoreKey,
   561      abciListener ABCIListener,
   562  ) {
   563      asyncKey := fmt.Sprintf("%s.%s.%s", StreamingTomlKey, StreamingABCITomlKey, StreamingABCIAsync)
   564      async := cast.ToBool(appOpts.Get(asyncKey))
   565      stopNodeOnErrKey := fmt.Sprintf("%s.%s.%s", StreamingTomlKey, StreamingABCITomlKey, StreamingABCIStopNodeOnErrTomlKey)
   566      stopNodeOnErr := cast.ToBool(appOpts.Get(stopNodeOnErrKey))
   567      keysKey := fmt.Sprintf("%s.%s.%s", StreamingTomlKey, StreamingABCITomlKey, StreamingABCIKeysTomlKey)
   568      exposeKeysStr := cast.ToStringSlice(appOpts.Get(keysKey))
   569      exposedKeys := exposeStoreKeysSorted(exposeKeysStr, keys)
   570      bApp.cms.AddListeners(exposedKeys)
   571      app.SetStreamingManager(
   572  		storetypes.StreamingManager{
   573  			ABCIListeners: []storetypes.ABCIListener{abciListener},
   574  			StopNodeOnErr: stopNodeOnErr,
   575  		},
   576  	)
   577  }
   578  ```
   579  
   580  ```go
   581  func exposeAll(list []string) bool {
   582      for _, ele := range list {
   583          if ele == "*" {
   584              return true
   585          }
   586      }
   587      return false
   588  }
   589  
   590  func exposeStoreKeys(keysStr []string, keys map[string]*types.KVStoreKey) []types.StoreKey {
   591      var exposeStoreKeys []types.StoreKey
   592      if exposeAll(keysStr) {
   593          exposeStoreKeys = make([]types.StoreKey, 0, len(keys))
   594          for _, storeKey := range keys {
   595              exposeStoreKeys = append(exposeStoreKeys, storeKey)
   596          }
   597      } else {
   598          exposeStoreKeys = make([]types.StoreKey, 0, len(keysStr))
   599          for _, keyStr := range keysStr {
   600              if storeKey, ok := keys[keyStr]; ok {
   601                  exposeStoreKeys = append(exposeStoreKeys, storeKey)
   602              }
   603          }
   604      }
   605      // sort storeKeys for deterministic output
   606      sort.SliceStable(exposeStoreKeys, func(i, j int) bool {
   607          return exposeStoreKeys[i].Name() < exposeStoreKeys[j].Name()
   608      })
   609  
   610      return exposeStoreKeys
   611  }
   612  ```
   613  
   614  The `NewStreamingPlugin` and `RegisterStreamingPlugin` functions are used to register a plugin with the App's BaseApp.
   615  
   616  e.g. in `NewSimApp`:
   617  
   618  ```go
   619  func NewSimApp(
   620      logger log.Logger,
   621      db dbm.DB,
   622      traceStore io.Writer,
   623      loadLatest bool,
   624      appOpts servertypes.AppOptions,
   625      baseAppOptions ...func(*baseapp.BaseApp),
   626  ) *SimApp {
   627  
   628      ...
   629  
   630      keys := sdk.NewKVStoreKeys(
   631         authtypes.StoreKey, banktypes.StoreKey, stakingtypes.StoreKey,
   632         minttypes.StoreKey, distrtypes.StoreKey, slashingtypes.StoreKey,
   633         govtypes.StoreKey, paramstypes.StoreKey, ibchost.StoreKey, upgradetypes.StoreKey,
   634         evidencetypes.StoreKey, ibctransfertypes.StoreKey, capabilitytypes.StoreKey,
   635      )
   636  
   637      ...
   638  
   639      // register streaming services
   640      streamingCfg := cast.ToStringMap(appOpts.Get(baseapp.StreamingTomlKey))
   641      for service := range streamingCfg {
   642          pluginKey := fmt.Sprintf("%s.%s.%s", baseapp.StreamingTomlKey, service, baseapp.StreamingPluginTomlKey)
   643          pluginName := strings.TrimSpace(cast.ToString(appOpts.Get(pluginKey)))
   644          if len(pluginName) > 0 {
   645              logLevel := cast.ToString(appOpts.Get(flags.FlagLogLevel))
   646              plugin, err := streaming.NewStreamingPlugin(pluginName, logLevel)
   647              if err != nil {
   648                  tmos.Exit(err.Error())
   649              }
   650              if err := baseapp.RegisterStreamingPlugin(bApp, appOpts, keys, plugin); err != nil {
   651                  tmos.Exit(err.Error())
   652              }
   653          }
   654      }
   655  
   656      return app
   657  ```
   658  
   659  #### Configuration
   660  
   661  The plugin system will be configured within an App's TOML configuration files.
   662  
   663  ```toml
   664  # gRPC streaming
   665  [streaming]
   666  
   667  # ABCI streaming service
   668  [streaming.abci]
   669  
   670  # The plugin version to use for ABCI listening
   671  plugin = "abci_v1"
   672  
   673  # List of kv store keys to listen to for state changes.
   674  # Set to ["*"] to expose all keys.
   675  keys = ["*"]
   676  
   677  # Enable abciListeners to run asynchronously.
   678  # When abciListenersAsync=false and stopNodeOnABCIListenerErr=false listeners will run synchronized but will not stop the node.
   679  # When abciListenersAsync=true stopNodeOnABCIListenerErr will be ignored.
   680  async = false
   681  
   682  # Whether to stop the node on message deliver error.
   683  stop-node-on-err = true
   684  ```
   685  
   686  There will be four parameters for configuring `ABCIListener` plugin: `streaming.abci.plugin`, `streaming.abci.keys`, `streaming.abci.async` and `streaming.abci.stop-node-on-err`.
   687  `streaming.abci.plugin` is the name of the plugin we want to use for streaming, `streaming.abci.keys` is a set of store keys for stores it listens to,
   688  `streaming.abci.async` is bool enabling asynchronous listening and `streaming.abci.stop-node-on-err` is a bool that stops the node when true and when operating
   689  on synchronized mode `streaming.abci.async=false`. Note that `streaming.abci.stop-node-on-err=true` will be ignored if `streaming.abci.async=true`.
   690  
   691  The configuration above support additional streaming plugins by adding the plugin to the `[streaming]` configuration section
   692  and registering the plugin with `RegisterStreamingPlugin` helper function.
   693  
   694  Note the that each plugin must include `streaming.{service}.plugin` property as it is a requirement for doing the lookup and registration of the plugin
   695  with the App. All other properties are unique to the individual services.
   696  
   697  #### Encoding and decoding streams
   698  
   699  ADR-038 introduces the interfaces and types for streaming state changes out from KVStores, associating this
   700  data with their related ABCI requests and responses, and registering a service for consuming this data and streaming it to some destination in a final format.
   701  Instead of prescribing a final data format in this ADR, it is left to a specific plugin implementation to define and document this format.
   702  We take this approach because flexibility in the final format is necessary to support a wide range of streaming service plugins. For example,
   703  the data format for a streaming service that writes the data out to a set of files will differ from the data format that is written to a Kafka topic.
   704  
   705  ## Consequences
   706  
   707  These changes will provide a means of subscribing to KVStore state changes in real time.
   708  
   709  ### Backwards Compatibility
   710  
   711  * This ADR changes the `CommitMultiStore` interface, implementations supporting the previous version of this interface will not support the new one
   712  
   713  ### Positive
   714  
   715  * Ability to listen to KVStore state changes in real time and expose these events to external consumers
   716  
   717  ### Negative
   718  
   719  * Changes `CommitMultiStore` interface and its implementations
   720  
   721  ### Neutral
   722  
   723  * Introduces additional- but optional- complexity to configuring and running a cosmos application
   724  * If an application developer opts to use these features to expose data, they need to be aware of the ramifications/risks of that data exposure as it pertains to the specifics of their application