30 KiB
order | synopsis |
---|---|
1 | This document describes `BaseApp`, the abstraction that implements the core functionalities of an SDK application. |
Baseapp
Pre-requisite Readings {hide}
- Anatomy of an SDK application {prereq}
- Lifecycle of an SDK transaction {prereq}
Introduction
BaseApp
is a base type 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 volatile 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 {
// reference to a BaseApp
*bam.BaseApp
// 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.
+++ 7d7821b9af/baseapp/baseapp.go (L54-L108)
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 bootstrapping 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 volatile (un-committed) states. TheCommitMultiStore
is a multi-store, meaning a store of stores. Each module of the application uses one or multipleKVStores
in the multi-store to persist their subset of the state.- Database: The
db
is used by theCommitMultiStore
to handle data persistence. - Router: The
router
facilitates the routing ofmessages
to the appropriate module for it to be processed. Here amessage
refers 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 router
facilitates the routing of queries to the appropriate module for it to be processed. Thesequeries
are 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 raw transaction bytes relayed 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, fee payment, and other pre-message execution checks when a transaction is received. It's executed duringCheckTx/RecheckTx
andDeliverTx
.InitChainer
,BeginBlocker
andEndBlocker
: These are the functions executed when the application receives theInitChain
,BeginBlock
andEndBlock
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 set tonil
onCommit
and gets re-initialized on BeginBlock.
Finally, a few more important parameterd:
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 used in theAnteHandler
duringCheckTx
, mainly as a spam protection mechanism. The transaction enters the mempool only if the gas prices of the transaction are greater than one of the minimum gas price inminGasPrices
(e.g. ifminGasPrices == 1uatom,1photon
, thegas-price
of the transaction must be greater than1uatom
OR1photon
).appVersion
: Version of the application. It is set in the application's constructor function.
Constructor
func NewBaseApp(
name string, logger log.Logger, db dbm.DB, txDecoder sdk.TxDecoder, options ...func(*BaseApp),
) *BaseApp {
// ...
}
The BaseApp
constructor function is pretty straightforward. The only thing worth noting is the
possibility to provide additional options
to the BaseApp
, which will execute them in order. The options
are generally setter
functions
for important parameters, like SetPruning()
to set pruning options or SetMinGasPrices()
to set
the node's min-gas-prices
.
Naturally, developers can add additional options
based on their application's needs.
State Updates
The BaseApp
maintains two primary volatile states and a root or main state. The main state
is the canonical state of the application and the volatile states, checkState
and deliverState
,
are used to handle state transitions in-between the main state made during Commit
.
Internally, there is only a single CommitMultiStore
which we refer to as the main or root state.
From this root state, we derive two volatile state through a mechanism called cache-wrapping. The
types can be illustrated as follows:
InitChain State Updates
During InitChain
, the two volatile states, checkState
and deliverState
are set by cache-wrapping
the root CommitMultiStore
. Any subsequent reads and writes happen on cached versions of the CommitMultiStore
.
CheckTx State Updates
During CheckTx
, the checkState
, which is based off of the last committed state from the root
store, is used for any reads and writes. Here we only execute the AnteHandler
and verify a router
exists for every message in the transaction. Note, when we execute the AnteHandler
, we cache-wrap
the already cache-wrapped checkState
. This has the side effect that if the AnteHandler
fails,
the state transitions won't be reflected in the checkState
-- i.e. checkState
is only updated on
success.
BeginBlock State Updates
During BeginBlock
, the deliverState
is set for use in subsequent DeliverTx
ABCI messages. The
deliverState
is based off of the last committed state from the root store and is cache-wrapped.
Note, the deliverState
is set to nil
on Commit
.
DeliverTx State Updates
The state flow for DeliverTx
is nearly identical to CheckTx
except state transitions occur on
the deliverState
and messages in a transaction are executed. Similarly to CheckTx
, state transitions
occur on a doubly cache-wrapped state -- deliverState
. Successful message execution results in
writes being committed to deliverState
. Note, if message execution fails, state transitions from
the AnteHandler are persisted.
Commit State Updates
During Commit
all the state transitions that occurred in the deliverState
are finally written to
the root CommitMultiStore
which in turn is committed to disk and results in a new application
root hash. These state transitions are now considered final. Finally, the checkState
is set to the
newly committed state and deliverState
is set to nil
to be reset on BeginBlock
.
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
Message
s 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.
+++ https://github.com/cosmos/cosmos-sdk/blob/master/baseapp/router.go
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 message
s, queries
need to be routed to the appropriate module's querier. To do so, baseapp
holds a query router
, which maps module names to module querier
s. The queryRouter
is called during the initial stages of query
processing, which is done via the Query
ABCI message.
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 baseapp
comes with a built-in implementation of the interface. Let us go through the main ABCI messages that baseapp
implements: CheckTx
and DeliverTx
CheckTx
CheckTx
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. 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
message
s 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,CheckTx
returns before running stateful checks, which saves resources. - Perform non-module related stateful checks on the account. This step is mainly about checking
that the
message
signatures are valid, that enough fees are provided and that the sending account has enough funds to pay for said fees. Note that no precisegas
counting occurs here, asmessage
s are not processed. Usually, theAnteHandler
will check that thegas
provided 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
Route
exists for eachmessage
, but do not actually processmessage
s.Message
s 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. The
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.0
if 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. Next is an example: +++7d7821b9af/x/auth/ante/basic.go (L104)
Events ([]cmn.KVPair)
: Key-Value tags for filtering and indexing transactions (eg. by account). Seeevent
s for more.Codespace (string)
: Namespace for the Code.
RecheckTx
After Commit
, CheckTx
is run again on all transactions that remain in the node's local mempool
after filtering those included in the block. To prevent the mempool from rechecking all transactions
every time a block is committed, the configuration option mempool.recheck=false
can be set. As of
Tendermint v0.32.1, an additional Type
parameter is made available to the CheckTx
function that
indicates whether an incoming transaction is new (CheckTxType_New
), or a recheck (CheckTxType_Recheck
).
This allows certain checks like signature verification can be skipped during CheckTxType_Recheck
.
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.
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
AnteHandler
does not check that the transaction'sgas-prices
is sufficient. That is because themin-gas-prices
valuegas-prices
is 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
message
in the transaction, route to the appropriate module'shandler
. Additional stateful checks are performed, and the cache-wrapped multistore held indeliverState
'scontext
is updated by the module'skeeper
. If thehandler
returns successfully, the cache-wrapped multistore held incontext
is written todeliverState
CacheMultiStore
.
During step 5., each read/write to the store increases the value of GasConsumed
. You can find the default cost of each operation:
+++ 7d7821b9af/store/types/gas.go (L142-L150)
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.ResponseDeliverTx
. The response contains:
Code (uint32)
: Response Code.0
if 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, and by adding gas each time a read/write to the store occurs.Events ([]cmn.KVPair)
: Key-Value tags for filtering and indexing transactions (eg. by account). Seeevent
s for more.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 message
in the Tx
, which runs preliminary 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.
+++ 7d7821b9af/baseapp/baseapp.go (L587)
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 messages
s 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.
+++ 7d7821b9af/types/handler.go (L8)
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
sequence
checking. - 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 paraemter, which is initialized in the application's constructor. The most widely used anteHandler
today is that of the auth
module.
Click here for more on the anteHandler
.
RunMsgs
RunMsgs
is called from RunTx
with runTxModeCheck
as parameter to check the existence of a route for each message the transaction, and with runTxModeDeliver
to actually process the message
s.
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. It is mainly used to initialize parameters and state like:
- Consensus Parameters via
setConsensusParams
. checkState
anddeliverState
viasetCheckState
andsetDeliverState
.- 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
deliverState
with the latest header using thereq abci.RequestBeginBlock
passed as parameter via thesetDeliverState
function. +++7d7821b9af/baseapp/baseapp.go (L387-L397)
This function also resets the main gas meter. - Initialize the block gas meter with the
maxGas
limit. Thegas
consumed within the block cannot go abovemaxGas
. This parameter is defined in the application's consensus parameters. - Run the application's
beginBlocker()
, which mainly runs theBeginBlocker()
method of each of the application's modules. - Set the
VoteInfos
of 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 theContext
so that it can be used duringDeliverTx
andEndBlock
.
EndBlock
The EndBlock
ABCI message is sent from the underlying Tendermint engine after DeliverTx
as been run for each transaction in 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.
Each query
comes with a path
, which contains multiple string
s. By convention, the first element of the path
(path[0]
) contains the category of query
(app
, p2p
, store
or custom
). The baseapp
implementation of the Query(req abci.RequestQuery)
method is a simple dispatcher serving these 4 main categories of queries:
- Application-related queries like querying the application's version, which are served via the
handleQueryApp
method. - Direct queries to the multistore, which are served by the
handlerQueryStore
method. 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
handleQueryP2P
method. These queries return eitherapp.addrPeerFilter
orapp.ipPeerFilter
that contain the list of peers filtered by address or IP respectively. These lists are first initialized viaoptions
inbaseapp
's constructor. - Custom queries, which encompass most queries, are served via the
handleQueryCustom
method. ThehandleQueryCustom
cache-wraps the multistore before using thequeryRoute
obtained fromapp.queryRouter
to map the query to the appropriate module'squerier
.
Next {hide}
Learn more about transactions {hide}