Abstract
Motivation
Prior Art
High Level Description
Detailed Description
Data extraction from Tendermint node
The Tendermint library is a base foundation for many networks (including the ones based on the cosmos-sdk
). One of its packages provides a basic internal indexing capability. The Indexer package gets the data from a synchronous event bus. This bus is also used in multiple components of a network node to communicate internally and trigger actions based on the node’s events. This package architecture will be leveraged to extract information from the process.
The modified version of a Tendermint package would be injected into the node itself, extending its indexing capabilities. The process will subscribe to the synchronous event bus, retrieve next events (like new block, new transaction, new evidence) and persist it into external storage (like filesystem). Operating on the event bus of the Tendermint library, allows to create an indexing process that is truly network agnostic.
To achieve that, the only change needed to be done to the network’s node is to extend its code replacing the original version of the library, with the augmented one. This will be done in go.mod
file using a simple replace on relative versions:
replace github.com/tendermint/tendermint => github.com/figment-networks/tendermint v0.34.9-augmented
This modification has to be done for all the crucial (and latest) versions of the node for every targeted network. To support all the type changes in the original network data structure throughout the history of the network, an external repository with protobuf types was created. This structure will always be consistent with the Graph Node code, regardless of any potential changes in the Tendermint library. The node will map its internal types onto respective external ones. The external types should match original Tendermint types as close as possible, to give the subgraph author a seamless experience.
Firehose data
Further in the process, the nodes output would be passed into dedicated Firehose stack instance to pass it into the graph node. Similarly to other Firehose integrations, a series of consecutive height-related rows is grouped into one structure, creating the “everything that happened at one height” protobuf structure called the EventList
:
message EventList {
EventBlock newblock = 1;
repeated EventTx transaction = 2;
EventValidatorSetUpdates validatorsetupdates = 3;
}
This structure itself is later wrapped in the bstream.Block
structure to be processed. And eventually passed into the Graph Node including the height and hash of the corresponding block.
Network data in Tendermint structures - cosmos-sdk example
Structures that are extracted from Tendermint are the core events of the Tendermint event bus (https://github.com/tendermint/tendermint/blob/v0.35.0/types/events.go).
In every Tendermint-based network event bus carries base structures like block or transaction. Most of the fields of that structures are strongly typed, however - not all. The network dependent data is passed as encoded (protobuf) byteslices.
The structure we’d be mostly focused on will be:
// Data contains the set of transactions included in the block
type Data struct {
Txs [][]byte `protobuf:"bytes,1,rep,name=txs,proto3" json:"txs,omitempty"`
}
The byteslices in that slice are protobuf encoded transactions supplied by higher libraries.
Using the example of the cosmos-sdk
(https://github.com/cosmos/cosmos-sdk/blob/master/proto/cosmos/tx/v1beta1/tx.proto#L14):
message Tx {
// body is the processable content of the transaction
TxBody body = 1;
// auth_info is the authorization related content of the transaction,
// specifically signers, signer modes and fee
AuthInfo auth_info = 2;
// signatures is a list of signatures that matches the length and order of
// AuthInfo's signer_infos to allow connecting signature meta information like
// public key and signing mode by position.
repeated bytes signatures = 3;
}
In particular, the cosmos-sdk
implemenation has additional transaction granularity, inside the TxBody
you will find:
message TxBody {
// messages is a list of messages to be executed. The required signers of
// those messages define the number and order of elements in AuthInfo's
// signer_infos and Tx's signatures. Each required signer address is added to
// the list only the first time it occurs.
// By convention, the first required signer (usually from the first message)
// is referred to as the primary signer and pays the fee for the whole
// transaction.
repeated google.protobuf.Any messages = 1;
// memo is any arbitrary note/comment to be added to the transaction.
// WARNING: in clients, any publicly exposed text should not be called memo,
// but should be called `note` instead (see https://github.com/cosmos/cosmos-sdk/issues/9122).
string memo = 2;
// timeout is the block height after which this transaction will not
// be processed by the chain
uint64 timeout_height = 3;
// extension_options are arbitrary options that can be added by chains
// when the default options are not sufficient. If any of these are present
// and can't be handled, the transaction will be rejected
repeated google.protobuf.Any extension_options = 1023;
// extension_options are arbitrary options that can be added by chains
// when the default options are not sufficient. If any of these are present
// and can't be handled, they will be ignored
repeated google.protobuf.Any non_critical_extension_options = 2047;
}
Using the example above, the fields: messages
, extension_options
and non_critical_extension_options
are lists of Google’s Any
type (https://github.com/protocolbuffers/protobuf/blob/master/src/google/protobuf/any.proto)
message Any {
string type_url = 1;
bytes value = 2;
}
This effectively means that inside, you should expect a pair of typename and bytearray.
Example #1:
Cosmos’ bank module (https://github.com/cosmos/cosmos-sdk/tree/master/x/bank) that is used to send transactions would have type_url
: cosmos.bank.v1beta1.MsgSend
and the value
of (https://github.com/cosmos/cosmos-`sdk/blob/master/proto/cosmos/bank/v1beta1/tx.proto#L21):
message MsgSend {
option (gogoproto.equal) = false;
option (gogoproto.goproto_getters) = false;
string from_address = 1 [(cosmos_proto.scalar) = "cosmos.AddressString"];
string to_address = 2 [(cosmos_proto.scalar) = "cosmos.AddressString"];
repeated cosmos.base.v1beta1.Coin amount = 3
[(gogoproto.nullable) = false, (gogoproto.castrepeated) = "github.com/cosmos/cosmos-sdk/types.Coins"];
}
Example #2:
Terra’s wasm module (https://github.com/terra-money/core/tree/main/x/wasm) that is used for wasm smart contract interaction, for the execution would have type_url
: terra.wasm.v1beta1.MsgExecuteContract
and the value
of (https://github.com/terra-money/core/blob/main/proto/terra/wasm/v1beta1/tx.proto#L95):
message MsgExecuteContract {
option (gogoproto.equal) = false;
option (gogoproto.goproto_getters) = false;
// Sender is the that actor that signed the messages
string sender = 1 [(gogoproto.moretags) = "yaml:\"sender\""];
// Contract is the address of the smart contract
string contract = 2 [(gogoproto.moretags) = "yaml:\"contract\""];
// ExecuteMsg json encoded message to be passed to the contract
bytes execute_msg = 3
[(gogoproto.moretags) = "yaml:\"execute_msg\"", (gogoproto.casttype) = "encoding/json.RawMessage"];
// Coins that are transferred to the contract on execution
repeated cosmos.base.v1beta1.Coin coins = 5 [
(gogoproto.moretags) = "yaml:\"coins\"",
(gogoproto.nullable) = false,
(gogoproto.castrepeated) = "github.com/cosmos/cosmos-sdk/types.Coins"
];
}
Example #3:
Osmosis’ gamm (Generalized Automated Market Maker ) module (https://github.com/osmosis-labs/osmosis/blob/main/x/gamm) message for joining the pool would have type_url
: osmosis.gamm.v1beta1.MsgJoinPool
and the value
of (https://github.com/osmosis-labs/osmosis/blob/main/proto/osmosis/gamm/v1beta1/tx.proto#L47):
message MsgJoinPool {
string sender = 1 [ (gogoproto.moretags) = "yaml:\"sender\"" ];
uint64 poolId = 2 [ (gogoproto.moretags) = "yaml:\"pool_id\"" ];
string shareOutAmount = 3 [
(gogoproto.customtype) = "github.com/cosmos/cosmos-sdk/types.Int",
(gogoproto.moretags) = "yaml:\"pool_amount_out\"",
(gogoproto.nullable) = false
];
repeated cosmos.base.v1beta1.Coin tokenInMaxs = 4 [
(gogoproto.moretags) = "yaml:\"token_in_max_amounts\"",
(gogoproto.nullable) = false
];
}
The relation between transaction types and data listed above (proto) shows great similarity to the relation between Ethereum logs and smart contract events (abi). This proves that this type of integration would give subgraph authors the most similar experience to the current Ethereum-stack development process. At the same time it would preserve the Tendermint types for people coming from Cosmos ecosystem.
Because all this data is heavily network based, it should be decoded inside the subgraph code using and subgraph author’s type definitions, and not before.
Graph Node Tendermint integration
Eventually the Tendermint Firehose gRPC endpoint will be consumed by Graph Node, which will implement a dedicated FirehoseBlockStream, Tendermint specific types, and filtering for triggers, ultimately reaching subgraph code.
Based on the previous section, to properly process network data subgraph runtime environment has to be extended with a way to decode protobuf bytes encoded information inside Tendermint payload. A similar solution already exists within the subgraph API for JSON data parsing (https://thegraph.com/docs/developer/assemblyscript-api#json-api).
Subgraph authors would need to add a set of protobuf files to their subgraph projects, based on their needs. Then, using the command line tool they should generate typescript bindings for the specific protobuf type. The generated code should include methods to properly decode structure.
my_type.fromBytes(data: Bytes): MyType
my_type.try_fromBytes(data: Bytes): Result<MyType, boolean>
my_type.fromString(data: String): MyType
my_type.try_fromString(data: String): Result<MyType, boolean>
Inside the runtime those functions should trigger rust host bindings and using prost
(or different version of the protobuf library) decode data into the desirable structures based on the protobuf definitions attached.
Optionally, the subgraph runtime environment may also be augmented with some form of declarative ‘pre-decoding’ to enable more granular data filtering. This has to be done by extending the Subgraph Manifest with the ability to tell (similarly to abi
) which field in structure is what type or in case of types.Any a set of types. This would allow subgraph authors to refer to that decoded field in filters, and subscribe in handlers for specific types of messages.
Operational requirements
In order to index Tendermint subgraphs, Indexers will therefore need to run the following:
- A Tendermint-based network node with augmented library
- Tendermint Firehose Component(s)
- A Graph Node with Firehose endpoint configured
GraphQL definition
No changes are anticipated to the schema.graphQL
Areas for further development:
- The initial Tendermint implementation targets base data, sent mostly in the block trigger and some event triggers from the
EventList
base structures (events). In further development this should be extended to include some more helpful triggers for subgraph authors.
Backwards Compatibility
Dependencies
Rationale and Alternatives
Copyright Waiver
Copyright and related rights waived via CC0.