|
|
- Application Development Guide
- =============================
-
- ABCI Design
- -----------
-
- The purpose of ABCI is to provide a clean interface between state
- transition machines on one computer and the mechanics of their
- replication across multiple computers. The former we call 'application
- logic' and the latter the 'consensus engine'. Application logic
- validates transactions and optionally executes transactions against some
- persistent state. A consensus engine ensures all transactions are
- replicated in the same order on every machine. We call each machine in a
- consensus engine a 'validator', and each validator runs the same
- transactions through the same application logic. In particular, we are
- interested in blockchain-style consensus engines, where transactions are
- committed in hash-linked blocks.
-
- The ABCI design has a few distinct components:
-
- - message protocol
-
- - pairs of request and response messages
- - consensus makes requests, application responds
- - defined using protobuf
-
- - server/client
-
- - consensus engine runs the client
- - application runs the server
- - two implementations:
-
- - async raw bytes
- - grpc
-
- - blockchain protocol
-
- - abci is connection oriented
- - Tendermint Core maintains three connections:
-
- - `mempool connection <#mempool-connection>`__: for checking if
- transactions should be relayed before they are committed; only
- uses ``CheckTx``
- - `consensus connection <#consensus-connection>`__: for executing
- transactions that have been committed. Message sequence is -
- for every block -
- ``BeginBlock, [DeliverTx, ...], EndBlock, Commit``
- - `query connection <#query-connection>`__: for querying the
- application state; only uses Query and Info
-
- The mempool and consensus logic act as clients, and each maintains an
- open ABCI connection with the application, which hosts an ABCI server.
- Shown are the request and response types sent on each connection.
-
- Message Protocol
- ----------------
-
- The message protocol consists of pairs of requests and responses. Some
- messages have no fields, while others may include byte-arrays, strings,
- or integers. See the ``message Request`` and ``message Response``
- definitions in `the protobuf definition
- file <https://github.com/tendermint/abci/blob/master/types/types.proto>`__,
- and the `protobuf
- documentation <https://developers.google.com/protocol-buffers/docs/overview>`__
- for more details.
-
- For each request, a server should respond with the corresponding
- response, where order of requests is preserved in the order of
- responses.
-
- Server
- ------
-
- To use ABCI in your programming language of choice, there must be a ABCI
- server in that language. Tendermint supports two kinds of implementation
- of the server:
-
- - Asynchronous, raw socket server (Tendermint Socket Protocol, also
- known as TSP or Teaspoon)
- - GRPC
-
- Both can be tested using the ``abci-cli`` by setting the ``--abci`` flag
- appropriately (ie. to ``socket`` or ``grpc``).
-
- See examples, in various stages of maintenance, in
- `Go <https://github.com/tendermint/abci/tree/master/server>`__,
- `JavaScript <https://github.com/tendermint/js-abci>`__,
- `Python <https://github.com/tendermint/abci/tree/master/example/python3/abci>`__,
- `C++ <https://github.com/mdyring/cpp-tmsp>`__, and
- `Java <https://github.com/jTendermint/jabci>`__.
-
- GRPC
- ~~~~
-
- If GRPC is available in your language, this is the easiest approach,
- though it will have significant performance overhead.
-
- To get started with GRPC, copy in the `protobuf
- file <https://github.com/tendermint/abci/blob/master/types/types.proto>`__
- and compile it using the GRPC plugin for your language. For instance,
- for golang, the command is
- ``protoc --go_out=plugins=grpc:. types.proto``. See the `grpc
- documentation for more details <http://www.grpc.io/docs/>`__. ``protoc``
- will autogenerate all the necessary code for ABCI client and server in
- your language, including whatever interface your application must
- satisfy to be used by the ABCI server for handling requests.
-
- TSP
- ~~~
-
- If GRPC is not available in your language, or you require higher
- performance, or otherwise enjoy programming, you may implement your own
- ABCI server using the Tendermint Socket Protocol, known affectionately
- as Teaspoon. The first step is still to auto-generate the relevant data
- types and codec in your language using ``protoc``. Messages coming over
- the socket are Protobuf3 encoded, but additionally length-prefixed to
- facilitate use as a streaming protocol. Protobuf3 doesn't have an
- official length-prefix standard, so we use our own. The first byte in
- the prefix represents the length of the Big Endian encoded length. The
- remaining bytes in the prefix are the Big Endian encoded length.
-
- For example, if the Protobuf3 encoded ABCI message is 0xDEADBEEF (4
- bytes), the length-prefixed message is 0x0104DEADBEEF. If the Protobuf3
- encoded ABCI message is 65535 bytes long, the length-prefixed message
- would be like 0x02FFFF....
-
- Note this prefixing does not apply for grpc.
-
- An ABCI server must also be able to support multiple connections, as
- Tendermint uses three connections.
-
- Client
- ------
-
- There are currently two use-cases for an ABCI client. One is a testing
- tool, as in the ``abci-cli``, which allows ABCI requests to be sent via
- command line. The other is a consensus engine, such as Tendermint Core,
- which makes requests to the application every time a new transaction is
- received or a block is committed.
-
- It is unlikely that you will need to implement a client. For details of
- our client, see
- `here <https://github.com/tendermint/abci/tree/master/client>`__.
-
- Most of the examples below are from `dummy application
- <https://github.com/tendermint/abci/blob/master/example/dummy/dummy.go>`__,
- which is a part of the abci repo. `persistent_dummy application
- <https://github.com/tendermint/abci/blob/master/example/dummy/persistent_dummy.go>`__
- is used to show ``BeginBlock``, ``EndBlock`` and ``InitChain``
- example implementations.
-
- Blockchain Protocol
- -------------------
-
- In ABCI, a transaction is simply an arbitrary length byte-array. It is
- the application's responsibility to define the transaction codec as they
- please, and to use it for both CheckTx and DeliverTx.
-
- Note that there are two distinct means for running transactions,
- corresponding to stages of 'awareness' of the transaction in the
- network. The first stage is when a transaction is received by a
- validator from a client into the so-called mempool or transaction pool -
- this is where we use CheckTx. The second is when the transaction is
- successfully committed on more than 2/3 of validators - where we use
- DeliverTx. In the former case, it may not be necessary to run all the
- state transitions associated with the transaction, as the transaction
- may not ultimately be committed until some much later time, when the
- result of its execution will be different. For instance, an Ethereum
- ABCI app would check signatures and amounts in CheckTx, but would not
- actually execute any contract code until the DeliverTx, so as to avoid
- executing state transitions that have not been finalized.
-
- To formalize the distinction further, two explicit ABCI connections are
- made between Tendermint Core and the application: the mempool connection
- and the consensus connection. We also make a third connection, the query
- connection, to query the local state of the app.
-
- Mempool Connection
- ~~~~~~~~~~~~~~~~~~
-
- The mempool connection is used *only* for CheckTx requests. Transactions
- are run using CheckTx in the same order they were received by the
- validator. If the CheckTx returns ``OK``, the transaction is kept in
- memory and relayed to other peers in the same order it was received.
- Otherwise, it is discarded.
-
- CheckTx requests run concurrently with block processing; so they should
- run against a copy of the main application state which is reset after
- every block. This copy is necessary to track transitions made by a
- sequence of CheckTx requests before they are included in a block. When a
- block is committed, the application must ensure to reset the mempool
- state to the latest committed state. Tendermint Core will then filter
- through all transactions in the mempool, removing any that were included
- in the block, and re-run the rest using CheckTx against the post-Commit
- mempool state.
-
- .. container:: toggle
-
- .. container:: header
-
- **Show/Hide Go Example**
-
- .. code-block:: go
-
- func (app *DummyApplication) CheckTx(tx []byte) types.Result {
- return types.OK
- }
-
- .. container:: toggle
-
- .. container:: header
-
- **Show/Hide Java Example**
-
- .. code-block:: java
-
- ResponseCheckTx requestCheckTx(RequestCheckTx req) {
- byte[] transaction = req.getTx().toByteArray();
-
- // validate transaction
-
- if (notValid) {
- return ResponseCheckTx.newBuilder().setCode(CodeType.BadNonce).setLog("invalid tx").build();
- } else {
- return ResponseCheckTx.newBuilder().setCode(CodeType.OK).build();
- }
- }
-
- Consensus Connection
- ~~~~~~~~~~~~~~~~~~~~
-
- The consensus connection is used only when a new block is committed, and
- communicates all information from the block in a series of requests:
- ``BeginBlock, [DeliverTx, ...], EndBlock, Commit``. That is, when a
- block is committed in the consensus, we send a list of DeliverTx
- requests (one for each transaction) sandwiched by BeginBlock and
- EndBlock requests, and followed by a Commit.
-
- DeliverTx
- ^^^^^^^^^
-
- DeliverTx is the workhorse of the blockchain. Tendermint sends the
- DeliverTx requests asynchronously but in order, and relies on the
- underlying socket protocol (ie. TCP) to ensure they are received by the
- app in order. They have already been ordered in the global consensus by
- the Tendermint protocol.
-
- DeliverTx returns a abci.Result, which includes a Code, Data, and Log.
- The code may be non-zero (non-OK), meaning the corresponding transaction
- should have been rejected by the mempool, but may have been included in
- a block by a Byzantine proposer.
-
- The block header will be updated (TODO) to include some commitment to
- the results of DeliverTx, be it a bitarray of non-OK transactions, or a
- merkle root of the data returned by the DeliverTx requests, or both.
-
- .. container:: toggle
-
- .. container:: header
-
- **Show/Hide Go Example**
-
- .. code-block:: go
-
- // tx is either "key=value" or just arbitrary bytes
- func (app *DummyApplication) DeliverTx(tx []byte) types.Result {
- parts := strings.Split(string(tx), "=")
- if len(parts) == 2 {
- app.state.Set([]byte(parts[0]), []byte(parts[1]))
- } else {
- app.state.Set(tx, tx)
- }
- return types.OK
- }
-
- .. container:: toggle
-
- .. container:: header
-
- **Show/Hide Java Example**
-
- .. code-block:: java
-
- /**
- * Using Protobuf types from the protoc compiler, we always start with a byte[]
- */
- ResponseDeliverTx deliverTx(RequestDeliverTx request) {
- byte[] transaction = request.getTx().toByteArray();
-
- // validate your transaction
-
- if (notValid) {
- return ResponseDeliverTx.newBuilder().setCode(CodeType.BadNonce).setLog("transaction was invalid").build();
- } else {
- ResponseDeliverTx.newBuilder().setCode(CodeType.OK).build();
- }
-
- }
-
- Commit
- ^^^^^^
-
- Once all processing of the block is complete, Tendermint sends the
- Commit request and blocks waiting for a response. While the mempool may
- run concurrently with block processing (the BeginBlock, DeliverTxs, and
- EndBlock), it is locked for the Commit request so that its state can be
- safely reset during Commit. This means the app *MUST NOT* do any
- blocking communication with the mempool (ie. broadcast\_tx) during
- Commit, or there will be deadlock. Note also that all remaining
- transactions in the mempool are replayed on the mempool connection
- (CheckTx) following a commit.
-
- The app should respond to the Commit request with a byte array, which is the deterministic
- state root of the application. It is included in the header of the next
- block. It can be used to provide easily verified Merkle-proofs of the
- state of the application.
-
- It is expected that the app will persist state to disk on Commit. The
- option to have all transactions replayed from some previous block is the
- job of the `Handshake <#handshake>`__.
-
- .. container:: toggle
-
- .. container:: header
-
- **Show/Hide Go Example**
-
- .. code-block:: go
-
- func (app *DummyApplication) Commit() types.Result {
- hash := app.state.Hash()
- return types.NewResultOK(hash, "")
- }
-
- .. container:: toggle
-
- .. container:: header
-
- **Show/Hide Java Example**
-
- .. code-block:: java
-
- ResponseCommit requestCommit(RequestCommit requestCommit) {
-
- // update the internal app-state
- byte[] newAppState = calculateAppState();
-
- // and return it to the node
- return ResponseCommit.newBuilder().setCode(CodeType.OK).setData(ByteString.copyFrom(newAppState)).build();
- }
-
- BeginBlock
- ^^^^^^^^^^
-
- The BeginBlock request can be used to run some code at the beginning of
- every block. It also allows Tendermint to send the current block hash
- and header to the application, before it sends any of the transactions.
-
- The app should remember the latest height and header (ie. from which it
- has run a successful Commit) so that it can tell Tendermint where to
- pick up from when it restarts. See information on the Handshake, below.
-
- .. container:: toggle
-
- .. container:: header
-
- **Show/Hide Go Example**
-
- .. code-block:: go
-
- // Track the block hash and header information
- func (app *PersistentDummyApplication) BeginBlock(params types.RequestBeginBlock) {
- // update latest block info
- app.blockHeader = params.Header
-
- // reset valset changes
- app.changes = make([]*types.Validator, 0)
- }
-
- .. container:: toggle
-
- .. container:: header
-
- **Show/Hide Java Example**
-
- .. code-block:: java
-
- /*
- * all types come from protobuf definition
- */
- ResponseBeginBlock requestBeginBlock(RequestBeginBlock req) {
-
- Header header = req.getHeader();
- byte[] prevAppHash = header.getAppHash().toByteArray();
- long prevHeight = header.getHeight();
- long numTxs = header.getNumTxs();
-
- // run your pre-block logic. Maybe prepare a state snapshot, message components, etc
-
- return ResponseBeginBlock.newBuilder().build();
- }
-
- EndBlock
- ^^^^^^^^
-
- The EndBlock request can be used to run some code at the end of every
- block. Additionally, the response may contain a list of validators,
- which can be used to update the validator set. To add a new validator or
- update an existing one, simply include them in the list returned in the
- EndBlock response. To remove one, include it in the list with a
- ``power`` equal to ``0``. Tendermint core will take care of updating the
- validator set. Note validator set changes are only available in v0.8.0
- and up.
-
- .. container:: toggle
-
- .. container:: header
-
- **Show/Hide Go Example**
-
- .. code-block:: go
-
- // Update the validator set
- func (app *PersistentDummyApplication) EndBlock(height uint64) (resEndBlock types.ResponseEndBlock) {
- return types.ResponseEndBlock{Diffs: app.changes}
- }
-
- .. container:: toggle
-
- .. container:: header
-
- **Show/Hide Java Example**
-
- .. code-block:: java
-
- /*
- * Assume that one validator changes. The new validator has a power of 10
- */
- ResponseEndBlock requestEndBlock(RequestEndBlock req) {
- final long currentHeight = req.getHeight();
- final byte[] validatorPubKey = getValPubKey();
-
- ResponseEndBlock.Builder builder = ResponseEndBlock.newBuilder();
- builder.addDiffs(1, Types.Validator.newBuilder().setPower(10L).setPubKey(ByteString.copyFrom(validatorPubKey)).build());
-
- return builder.build();
- }
-
- Query Connection
- ~~~~~~~~~~~~~~~~
-
- This connection is used to query the application without engaging
- consensus. It's exposed over the tendermint core rpc, so clients can
- query the app without exposing a server on the app itself, but they must
- serialize each query as a single byte array. Additionally, certain
- "standardized" queries may be used to inform local decisions, for
- instance about which peers to connect to.
-
- Tendermint Core currently uses the Query connection to filter peers upon
- connecting, according to IP address or public key. For instance,
- returning non-OK ABCI response to either of the following queries will
- cause Tendermint to not connect to the corresponding peer:
-
- - ``p2p/filter/addr/<addr>``, where ``<addr>`` is an IP address.
- - ``p2p/filter/pubkey/<pubkey>``, where ``<pubkey>`` is the hex-encoded
- ED25519 key of the node (not it's validator key)
-
- Note: these query formats are subject to change!
-
- .. container:: toggle
-
- .. container:: header
-
- **Show/Hide Go Example**
-
- .. code-block:: go
-
- func (app *DummyApplication) Query(reqQuery types.RequestQuery) (resQuery types.ResponseQuery) {
- if reqQuery.Prove {
- value, proof, exists := app.state.Proof(reqQuery.Data)
- resQuery.Index = -1 // TODO make Proof return index
- resQuery.Key = reqQuery.Data
- resQuery.Value = value
- resQuery.Proof = proof
- if exists {
- resQuery.Log = "exists"
- } else {
- resQuery.Log = "does not exist"
- }
- return
- } else {
- index, value, exists := app.state.Get(reqQuery.Data)
- resQuery.Index = int64(index)
- resQuery.Value = value
- if exists {
- resQuery.Log = "exists"
- } else {
- resQuery.Log = "does not exist"
- }
- return
- }
- }
-
- .. container:: toggle
-
- .. container:: header
-
- **Show/Hide Java Example**
-
- .. code-block:: java
-
- ResponseQuery requestQuery(RequestQuery req) {
- final boolean isProveQuery = req.getProve();
- final ResponseQuery.Builder responseBuilder = ResponseQuery.newBuilder();
-
- if (isProveQuery) {
- com.app.example.ProofResult proofResult = generateProof(req.getData().toByteArray());
- final byte[] proofAsByteArray = proofResult.getAsByteArray();
-
- responseBuilder.setProof(ByteString.copyFrom(proofAsByteArray));
- responseBuilder.setKey(req.getData());
- responseBuilder.setValue(ByteString.copyFrom(proofResult.getData()));
- responseBuilder.setLog(result.getLogValue());
- } else {
- byte[] queryData = req.getData().toByteArray();
-
- final com.app.example.QueryResult result = generateQueryResult(queryData);
-
- responseBuilder.setIndex(result.getIndex());
- responseBuilder.setValue(ByteString.copyFrom(result.getValue()));
- responseBuilder.setLog(result.getLogValue());
- }
-
- return responseBuilder.build();
- }
-
- Handshake
- ~~~~~~~~~
-
- When the app or tendermint restarts, they need to sync to a common
- height. When an ABCI connection is first established, Tendermint will
- call ``Info`` on the Query connection. The response should contain the
- LastBlockHeight and LastBlockAppHash - the former is the last block for
- which the app ran Commit successfully, the latter is the response
- from that Commit.
-
- Using this information, Tendermint will determine what needs to be
- replayed, if anything, against the app, to ensure both Tendermint and
- the app are synced to the latest block height.
-
- If the app returns a LastBlockHeight of 0, Tendermint will just replay
- all blocks.
-
- .. container:: toggle
-
- .. container:: header
-
- **Show/Hide Go Example**
-
- .. code-block:: go
-
- func (app *DummyApplication) Info(req types.RequestInfo) (resInfo types.ResponseInfo) {
- return types.ResponseInfo{Data: cmn.Fmt("{\"size\":%v}", app.state.Size())}
- }
-
- .. container:: toggle
-
- .. container:: header
-
- **Show/Hide Java Example**
-
- .. code-block:: java
-
- ResponseInfo requestInfo(RequestInfo req) {
- final byte[] lastAppHash = getLastAppHash();
- final long lastHeight = getLastHeight();
- return ResponseInfo.newBuilder().setLastBlockAppHash(ByteString.copyFrom(lastAppHash)).setLastBlockHeight(lastHeight).build();
- }
-
- Genesis
- ~~~~~~~
-
- ``InitChain`` will be called once upon the genesis. ``params`` includes the
- initial validator set. Later on, it may be extended to take parts of the
- consensus params.
-
- .. container:: toggle
-
- .. container:: header
-
- **Show/Hide Go Example**
-
- .. code-block:: go
-
- // Save the validators in the merkle tree
- func (app *PersistentDummyApplication) InitChain(params types.RequestInitChain) {
- for _, v := range params.Validators {
- r := app.updateValidator(v)
- if r.IsErr() {
- app.logger.Error("Error updating validators", "r", r)
- }
- }
- }
-
- .. container:: toggle
-
- .. container:: header
-
- **Show/Hide Java Example**
-
- .. code-block:: java
-
- /*
- * all types come from protobuf definition
- */
- ResponseInitChain requestInitChain(RequestInitChain req) {
- final int validatorsCount = req.getValidatorsCount();
- final List<Types.Validator> validatorsList = req.getValidatorsList();
-
- validatorsList.forEach((validator) -> {
- long power = validator.getPower();
- byte[] validatorPubKey = validator.getPubKey().toByteArray();
-
- // do somehing for validator setup in app
- });
-
- return ResponseInitChain.newBuilder().build();
- }
|