|
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 the change in voting power
|
|
must be strictly less than 1/3 per block. Otherwise it will be impossible for a
|
|
light client to prove the transition externally. See the `light client docs
|
|
<https://godoc.org/github.com/tendermint/tendermint/lite#hdr-How_We_Track_Validators>`__
|
|
for details on how it tracks validators. Tendermint core will fail with an
|
|
error if the change in voting power is more or equal than 1/3.
|
|
|
|
.. container:: toggle
|
|
|
|
.. container:: header
|
|
|
|
**Show/Hide Go Example**
|
|
|
|
.. code-block:: go
|
|
|
|
// Update the validator set
|
|
func (app *PersistentDummyApplication) EndBlock(req types.RequestEndBlock) types.ResponseEndBlock {
|
|
return types.ResponseEndBlock{ValidatorUpdates: app.ValUpdates}
|
|
}
|
|
|
|
.. 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();
|
|
}
|