* consolidate intro * start anatomy of sdk app * wokring * working * querier * working * workiiiing * finish * add dep and makefile * Apply suggestions from code review Co-Authored-By: Alessio Treglia <quadrispro@ubuntu.com> * typo * typo * Apply suggestions from code review Co-Authored-By: Alexander Bezobchuk <alexanderbez@users.noreply.github.com> Co-Authored-By: Federico Kunze <31522760+fedekunze@users.noreply.github.com> Co-Authored-By: Alessio Treglia <quadrispro@ubuntu.com> Co-Authored-By: frog power 4000 <rigel.rozanski@gmail.com> * refactor for new module interface * karoly review * Apply suggestions from code review Co-Authored-By: Karoly Albert Szabo <szabo.karoly.a@gmail.com> Co-Authored-By: Federico Kunze <31522760+fedekunze@users.noreply.github.com> * encoding * working on baseapp doc * baseapp work * reorg * almost there * finish first draft * remove old files * hans review' * jack review + clarification on ABCI methods
34 KiB
BaseApp
Pre-requisite Reading
Synopsis
This document describes baseapp, the abstraction that implements most of the common functionalities of an SDK application.
- Introduction
- Type Definition
- Constructor
- States
- Routing
- Main ABCI Messages
- RunTx(), AnteHandler and RunMsgs
- Other ABCI Message
Introduction
baseapp is a base class that implements the core of an SDK application, namely:
- The Application-Blockchain Interface, for the state-machine to communicate with the underlying consensus engine (e.g. Tendermint).
- A Router, to route messages and queries to the appropriate module.
- Different states, as the state-machine can have different parallel states updated based on the ABCI message received.
The goal of baseapp is to provide the fundamental layer of an SDK application that developers can easily extend to build their own custom application. Usually, developers will create a custom type for their application, like so:
type app struct {
*bam.BaseApp // reference to baseapp
cdc *codec.Codec
// list of application store keys
// list of application keepers
// module manager
}
Extending the application with baseapp gives the former access to all of baseapp's methods. This allows developers to compose their custom application with the modules they want, while not having to concern themselves with the hard work of implementing the ABCI, the routing and state management logic.
Type Definition
The baseapp type holds many important parameters for any Cosmos SDK based application. Let us go through the most important components.
Note: Not all parameters are described, only the most important ones. Refer to the type definition for the full list
First, the important parameters that are initialized during the initialization of the application:
CommitMultiStore: This is the main store of the application, which holds the canonical state that is committed at the end of each block. This store is not cached, meaning it is not used to update the application's intermediate (un-committed) states. TheCommitMultiStoreis a multi-store, meaning a store of stores. Each module of the application uses one or multipleKVStoresin the multi-store to persist their subset of the state.- Database: The
dbis used by theCommitMultiStoreto handle data storage. - Router: The
routerfacilitates the routing ofmessagesto the appropriate module for it to be processed. Heremessagerefers to the transaction components that need to be processed by the application in order to update the state, and not to ABCI messages which implement the interface between the application and the underlying consensus engine. - Query Router: The
query routerfacilitates the routing of queries to the appropriate module for it to be processed. Thesequeriesare not ABCI messages themselves, but they are relayed to the application from the underlying consensus engine via the ABCI messageQuery. TxDecoder: It is used to decode transaction[]byterelayed by the underlying Tendermint engine.- [
BaseKey]: This key is used to access the main store in theCommitMultiStore. The main store is used to persist data related to the core of the application, like consensus parameters. AnteHandler: This handler is used to handle signature verification and fee payment when a transaction is received.initChainer,beginBlockerandendBlocker: These are the functions executed when the application receives the [InitChain], [BeginBlock] and [EndBlock] ABCI messages from the underlying Tendermint engine.
Then, parameters used to define volatile states (i.e. cached states):
checkState: This state is updated duringCheckTx, and reset onCommit.deliverState: This state is updated duringDeliverTx, and reset onCommit.
Finally, a few more important parameters:
voteInfos: This parameter carries the list of validators whose precommit is missing, either because they did not vote or because the proposer did not include their vote. This information is carried by the context and can be used by the application for various things like punishing absent validators.minGasPrices: This parameter defines the minimum gas prices accepted by the node. This is a local parameter, meaning each full-node can set a differentminGasPrices. It is run by theanteHandlerduringCheckTx, mainly as a spam protection mechanism. The transaction enters the mempool only if the gas prices of the transaction is superior to one of the minimum gas price inminGasPrices(i.e. ifminGasPrices == 1uatom, 1upho, thegas-priceof the transaction must be superior to1uatomOR1upho).appVersion: Version of the application. It is set in the application's constructor function.
Constructor
NewBaseApp(name string, logger log.Logger, db dbm.DB, txDecoder sdk.TxDecoder, options ...func(*BaseApp),) is the constructor function for baseapp. It is called from the application's constructor function each time the full-node is started.
baseapp's constructor function is pretty straightforward. The only thing worth noting is the possibility to add additional options to baseapp by passing options functions to the constructor function, which will execute them in order. options are generally setter functions for important parameters, like SetPruning() to active pruning or SetMinGasPrices() to set the node's min-gas-prices.
A list of options examples can be found here. Naturally, developers can add additional options based on their application's needs.
States
baseapp handles various parallel states for different purposes. There is the main state, which is the canonical state of the application, and volatile states like checkState and deliverState, which are used to handle temporary states inbetween updates of the main state made during Commit.
Updated whenever an unconfirmed Updated whenever a transaction To serve user queries relayed
transaction is received from the is received from the underlying from the underlying consensus
underlying consensus engine via consensus engine (as part of a block) engine via the Query ABCI message
CheckTx proposal via DeliverTx
+----------------------+ +----------------------+ +----------------------+
| CheckState(t)(0) | | DeliverState(t)(0) | | QueryState(t) |
+----------------------+ | | | |
CheckTx(tx1) | | | | |
v | | | |
+----------------------+ | | | |
| CheckState(t)(1) | | | | |
+----------------------+ | | | |
CheckTx(tx2) | | | | |
v | | | |
+----------------------+ | | | |
| CheckState(t)(2) | | | | |
+----------------------+ | | | |
CheckTx(tx3) | | | | |
v | | | |
+----------------------+ | | | |
| CheckState(t)(3) | | | | |
+----------------------+ +----------------------+ | |
DeliverTx(tx1) | | | |
v v | |
+----------------------+ +----------------------+ | |
| | | DeliverState(t)(1) | | |
| | +----------------------+ | |
DeliverTx(tx2) | | | | |
| | v | |
| | +----------------------+ | |
| | | DeliverState(t)(2) | | |
| | +----------------------+ | |
DeliverTx(tx3) | | | | |
| | v | |
| | +----------------------+ | |
| | | DeliverState(t)(3) | | |
+----------------------+ +----------------------+ +----------------------+
Commit() | | |
v v v
+----------------------+ +----------------------+ +----------------------+
| CheckState(t+1)(0) | | DeliverState(t+1)(0) | | QueryState(t+1) |
+----------------------+ | | | |
. . .
. . .
. . .
Main State
The main state is the canonical state of the application. It is initialized on [InitChain](#initchain and updated on Commit at the end of each block.
+--------+ +--------+
| | | |
| S +----------------------------> | S' |
| | For each T in B: apply(T) | |
+--------+ +--------+
The main state is held by baseapp in a structure called the CommitMultiStore. This multi-store is used by developers to instantiate all the stores they need for each of their application's modules.
Volatile States
Volatile - or cached - states are used in between Commits to manage temporary states. They are reset to the latest version of the main state after it is committed. There are two main volatile states:
checkState: This cached state is initialized duringInitChain, updated duringCheckTxwhen an unconfirmed transaction is received, and reset to the main state onCommit.deliverState: This cached state is initialized duringBeginBlock, updated duringDeliverTxwhen a transaction included in a block is processed, and reset to the main state onCommit.
Both checkState and deliverState are of type state, which includes:
- A
CacheMultiStore, which is a cached version of the mainCommitMultiStore. A new version of this store is committed at the end of each successfulCheckTx()/DeliverTx()execution. - A
Context, which carries general information (like raw transaction size, block height, ...) that might be needed in order to process the transaction duringCheckTx()andDeliverTx(). Thecontextalso holds a cache-wrapped version of theCacheMultiStore, so that theCacheMultiStorecan maintain the correct version even if an internal step ofCheckTx()orDeliverTx()fails.
Routing
When messages and queries are received by the application, they must be routed to the appropriate module in order to be processed. Routing is done via baseapp, which holds a router for messages, and a query router for queries.
Message Routing
Messages need to be routed after they are extracted from transactions, which are sent from the underlying Tendermint engine via the CheckTx and DeliverTx ABCI messages. To do so, baseapp holds a router which maps paths (string) to the appropriate module handler. Usually, the path is the name of the module.
The application's router is initilalized with all the routes using the application's module manager, which itself is initialized with all the application's modules in the application's constructor.
Query Routing
Similar to messages, queries need to be routed to the appropriate module's querier. To do so, baseapp holds a query router, which maps paths (string) to the appropriate module querier. Usually, the path is the name of the module.
Just like the router, the query router is initilalized with all the query routes using the application's module manager, which itself is initialized with all the application's modules in the application's constructor.
Main ABCI Messages
The Application-Blockchain Interface (ABCI) is a generic interface that connects a state-machine with a consensus engine to form a functional full-node. It can be wrapped in any language, and needs to be implemented by each application-specific blockchain built on top of an ABCI-compatible consensus engine like Tendermint.
The consensus engine handles two main tasks:
- The networking logic, which mainly consists in gossiping block parts, transactions and consensus votes.
- The consensus logic, which results in the deterministic ordering of transactions in the form of blocks.
It is not the role of the consensus engine to define the state or the validity of transactions. Generally, transactions are handled by the consensus engine in the form of []bytes, and relayed to the application via the ABCI to be decoded and processed. At keys moments in the networking and consensus processes (e.g. beginning of a block, commit of a block, reception of an unconfirmed transaction, ...), the consensus engine emits ABCI messages for the state-machine to act on.
Developers building on top of the Cosmos SDK need not implement the ABCI themselves, as all the ABCI messages are implemented as a set of baseapp's methods in the Cosmos SDK. Let us go through the main ABCI messages that baseapp handles: CheckTx and DeliverTx. Note that these ABCI messages are different from the messages contained in transactions, the purpose of which is to trigger state-transitions.
CheckTx
The CheckTx ABCI message is sent by the underlying consensus engine when a new unconfirmed (i.e. not yet included in a valid block) transaction is received by a full-node, and it is handled by the CheckTx(req abci.RequestCheckTx) method of baseapp (abbreviated to CheckTx() thereafter). The role of CheckTx() is to guard the full-node's mempool (where unconfirmed transactions are stored until they are included in a block) from spam transactions. Unconfirmed transactions are relayed to peers only if they pass CheckTx().
CheckTx() can perform both stateful and stateless checks, but developers should strive to make them lightweight. In the Cosmos SDK, after decoding transactions, CheckTx() is implemented to do the following checks:
- Extract the
messages from the transaction. - Perform stateless checks by calling
ValidateBasic()on each of themessages. This is done first, as stateless checks are less computationally expensive than stateful checks. IfValidateBasic()fail,CheckTxreturns before running stateful checks, which saves resources. - Perform non-module related stateful checks on the account. This step is mainly about checking that the
messagesignatures are valid, that enough fees are provided and that the sending account has enough funds to pay for said fees. Note that no precisegascounting occurs here, asmessages are not processed. Usually, theanteHandlerwill check that thegasprovided with the transaction is superior to a minimum reference gas amount based on the raw transaction size, in order to avoid spam with transactions that provide 0 gas. - Ensure that a
Routeexists for eachmessage, but do not actually processmessages.Messages only need to be processed when the canonical state need to be updated, which happens duringDeliverTx.
Steps 2. and 3. are performed by the anteHandler in the RunTx() function, which CheckTx() calls with the runTxModeCheck mode. During each step of CheckTx(), a special volatile state called checkState is updated. This state is used to keep track of the temporary changes triggered by the CheckTx() calls of each transaction without modifying the main canonical state . For example, when a transaction goes through CheckTx(), the transaction's fees are deducted from the sender's account in checkState. If a second transaction is received from the same account before the first is processed, and the account has consumed all its funds in checkState during the first transaction, the second transaction will fail CheckTx() and be rejected. In any case, the sender's account will not actually pay the fees until the transaction is actually included in a block, because checkState never gets committed to the main state. checkState is reset to the latest state of the main state each time a blocks gets committed.
CheckTx() returns a response to the underlying consensus engine of type abci.ResponseCheckTx. The response contains:
Code (uint32): Response Code.0if successful.Data ([]byte): Result bytes, if any.Log (string):The output of the application's logger. May be non-deterministic.Info (string):Additional information. May be non-deterministic.GasWanted (int64): Amount of gas requested for transaction. It is provided by users when they generate the transaction.GasUsed (int64): Amount of gas consumed by transaction. DuringCheckTx, this value is computed by multiplying the standard cost of a transaction byte by the size of the raw transaction (click here for an example).Tags ([]cmn.KVPair): Key-Value tags for filtering and indexing transactions (eg. by account).Codespace (string): Namespace for the Code.
DeliverTx
When the underlying consensus engine receives a block proposal, each transaction in the block needs to be processed by the application. To that end, the underlying consensus engine sends a DeliverTx message to the application for each transaction in a sequential order. This ABCI message is handled by the DeliverTx() method of baseapp (abbreviated to DeliverTx() thereafter).
Before the first transaction of a given block is processed, a volatile state called deliverState is intialized during BeginBlock. This state is updated each time a transaction is processed via DeliverTx(), and committed to the main state when the block is committed, after what is is set to nil.
DeliverTx() performs the exact same steps as CheckTx(), with a little caveat at step 3 and the addition of a fifth step:
- The
anteHandlerdoes not check that the transaction'sgas-pricesis sufficient. That is because themin-gas-pricesvaluegas-pricesis checked against is local to the node, and therefore what is enough for one full-node might not be for another. This means that the proposer can potentially include transactions for free, although they are not incentivised to do so, as they earn a bonus on the total fee of the block they propose. - For each
messagein the transaction, route to the appropriate module'shandler. Additional stateful checks are performed, and the cache-wrapped multistore held indeliverState'scontextis updated by the module'skeeper. If thehandlerreturns successfully, the cache-wrapped multistore held incontextis written todeliverStateCacheMultiStore.
During step 5., each read/write to the store increases the value of GasConsumed. You can find the default cost of each operation here. At any point, if GasConsumed > GasWanted, the function returns with Code != 0 and DeliverTx() fails.
DeliverTx() returns a response to the underlying consensus engine of type abci.ResponseCheckTx. The response contains:
Code (uint32): Response Code.0if successful.Data ([]byte): Result bytes, if any.Log (string):The output of the application's logger. May be non-deterministic.Info (string):Additional information. May be non-deterministic.GasWanted (int64): Amount of gas requested for transaction. It is provided by users when they generate the transaction.GasUsed (int64): Amount of gas consumed by transaction. DuringDeliverTx, this value is computed by multiplying the standard cost of a transaction byte by the size of the raw transaction (click here for an example), and by adding gas each time a read/write to the store occurs.Tags ([]cmn.KVPair): Key-Value tags for filtering and indexing transactions (eg. by account).Codespace (string): Namespace for the Code.
RunTx(), AnteHandler and RunMsgs()
RunTx()
RunTx() is called from CheckTx()/DeliverTx() to handle the transaction, with runTxModeCheck or runTxModeDeliver as parameter to differentiate between the two modes of execution. Note that when RunTx() receives a transaction, it has already been decoded.
The first thing RunTx() does upon being called is to retrieve the context's CacheMultiStore by calling the getContextForTx() function with the appropriate mode (either runTxModeCheck or runTxModeDeliver). This CacheMultiStore is a cached version of the main store instantiated during BeginBlock for DeliverTx and during the Commit of the previous block for CheckTx. After that, two defer func() are called for gas management. They are executed when RunTx() returns and make sure gas is actually consumed, and will throw errors, if any.
After that, RunTx() calls ValidateBasic() on each messagein the Tx, which runs prelimary stateless validity checks. If any message fails to pass ValidateBasic(), RunTx() returns with an error.
Then, the anteHandler of the application is run (if it exists). In preparation of this step, both the checkState/deliverState's context and context's CacheMultiStore are cached-wrapped using the cacheTxContext() function. This allows RunTx() not to commit the changes made to the state during the execution of anteHandler if it ends up failing. It also prevents the module implementing the anteHandler from writing to state, which is an important part of the object-capabilities of the Cosmos SDK.
Finally, the RunMsgs() function is called to process the messagess in the Tx. In preparation of this step, just like with the anteHandler, both the checkState/deliverState's context and context's CacheMultiStore are cached-wrapped using the cacheTxContext() function.
AnteHandler
The AnteHandler is a special handler that implements the anteHandler interface and is used to authenticate the transaction before the transaction's internal messages are processed.
The AnteHandler is theoretically optional, but still a very important component of public blockchain networks. It serves 3 primary purposes:
- Be a primary line of defense against spam and second line of defense (the first one being the mempool) against transaction replay with fees deduction and
sequencechecking. - Perform preliminary stateful validity checks like ensuring signatures are valid or that the sender has enough funds to pay for fees.
- Play a role in the incentivisation of stakeholders via the collection of transaction fees.
baseapp holds an anteHandler as parameter, which is initialized in the application's constructor. The most widely used anteHandler today is that of the auth module.
RunMsgs()
RunMsgs() is called from RunTx() with runTxModeCheck as parameter to check the existence of a route for each message contained in the transaction, and with runTxModeDeliver to actually process the messages.
First, it retreives the message's route using the Msg.Route() method. Then, using the application's router and the route, it checks for the existence of a handler. At this point, if mode == runTxModeCheck, RunMsgs() returns. If instead mode == runTxModeDeliver, the handler function for the message is executed, before RunMsgs() returns.
Other ABCI Messages
InitChain
The InitChain ABCI message is sent from the underlying Tendermint engine when the chain is first started, and is handled by the InitChain(req abci.RequestInitChain) method of baseapp. It is mainly used to initialize parameters and state like:
- Consensus Parameters via
setConsensusParams. checkStateanddeliverStateviasetCheckStateandsetDeliverState.- The block gas meter, with infinite gas to process genesis transactions.
Finally, the InitChain(req abci.RequestInitChain) method of baseapp calls the initChainer() of the application in order to initialize the main state of the application from the genesis file and, if defined, call the InitGenesis function of each of the application's modules.
BeginBlock
The BeginBlock ABCI message is sent from the underlying Tendermint engine when a block proposal created by the correct proposer is received, before DeliverTx() is run for each transaction in the block. It allows developers to have logic be executed at the beginning of each block. In the Cosmos SDK, the BeginBlock(req abci.RequestBeginBlock) method does the following:
- Initialize
deliverStatewith the latest header using thereq abci.RequestBeginBlockpassed as parameter via thesetDeliverStatefunction. - Initialize the block gas meter with the
maxGaslimit. Thegasconsumed within the block cannot go abovemaxGas. This parameter is defined in the application's consensus parameters. - Run the application's
begingBlocker(), which mainly runs theBeginBlocker()method of each of the application's modules. - Set the
VoteInfosof the application, i.e. the list of validators whose precommit for the previous block was included by the proposer of the current block. This information is carried into theContextso that it can be used duringDeliverTxandEndBlock.
EndBlock
The EndBlock ABCI message is sent from the underlying Tendermint engine after DeliverTx as been run for each transactioni n the block. It allows developers to have logic be executed at the end of each block. In the Cosmos SDK, the bulk EndBlock(req abci.RequestEndBlock) method is to run the application's endBlocker(), which mainly runs the EndBlocker() method of each of the application's modules.
Commit
The Commit ABCI message is sent from the underlying Tendermint engine after the full-node has received precommits from 2/3+ of validators (weighted by voting power). On the baseapp end, the Commit(res abci.ResponseCommit) function is implemented to commit all the valid state transitions that occured during BeginBlock(), DeliverTx() and EndBlock() and to reset state for the next block.
To commit state-transitions, the Commit function calls the Write() function on deliverState.ms, where deliverState.ms is a cached multistore of the main store app.cms. Then, the Commit function sets checkState to the latest header (obtbained from deliverState.ctx.BlockHeader) and deliverState to nil.
Finally, Commit returns the hash of the commitment of app.cms back to the underlying consensus engine. This hash is used as a reference in the header of the next block.
Info
The Info ABCI message is a simple query from the underlying consensus engine, notably used to sync the latter with the application during a handshake that happens on startup. When called, the Info(res abci.ResponseInfo) function from baseapp will return the application's name, version and the hash of the last commit of app.cms.
Query
The Query ABCI message is used to serve queries received from the underlying consensus engine, including queries received via RPC like Tendermint RPC. It is the main entrypoint to build interfaces with the application. The application must respect a few rules when implementing the Query method, which are outlined here.
The baseapp implementation of the Query(req abci.RequestQuery) method is a simple dispatcher serving 4 main categories of queries:
- Application-related queries like querying the application's version, which are served via the
handleQueryAppmethod. - Direct queries to the multistore, which are served by the
handlerQueryStoremethod. These direct queryeis are different from custom queries which go throughapp.queryRouter, and are mainly used by third-party service provider like block explorers. - P2P queries, which are served via the
handleQueryP2Pmethod. These queries return eitherapp.addrPeerFilterorapp.ipPeerFilterthat contain the list of peers filtered by address or IP respectively. These lists are first initialized viaoptionsinbaseapp's constructor. - Custom queries, which encompass most queries, are served via the
handleQueryCustommethod. ThehandleQueryCustomcache-wraps the multistore before using thequeryRouteobtained fromapp.queryRouterto map the query to the appropriate module'squerier.
Next
Learn more about stores.