|
@ -0,0 +1,630 @@ |
|
|
|
|
|
# 1 Guide Assumptions |
|
|
|
|
|
|
|
|
|
|
|
This guide is designed for beginners who want to get started with a Tendermint |
|
|
|
|
|
Core application from scratch. It does not assume that you have any prior |
|
|
|
|
|
experience with Tendermint Core. |
|
|
|
|
|
|
|
|
|
|
|
Tendermint Core is Byzantine Fault Tolerant (BFT) middleware that takes a state |
|
|
|
|
|
transition machine - written in any programming language - and securely |
|
|
|
|
|
replicates it on many machines. |
|
|
|
|
|
|
|
|
|
|
|
Although Tendermint Core is written in the Golang programming language, prior |
|
|
|
|
|
knowledge of it is not required for this guide. You can learn it as we go due |
|
|
|
|
|
to it's simplicity. However, you may want to go through [Learn X in Y minutes |
|
|
|
|
|
Where X=Go](https://learnxinyminutes.com/docs/go/) first to familiarize |
|
|
|
|
|
yourself with the syntax. |
|
|
|
|
|
|
|
|
|
|
|
By following along with this guide, you'll create a Tendermint Core project |
|
|
|
|
|
called kvstore, a (very) simple distributed BFT key-value store. |
|
|
|
|
|
|
|
|
|
|
|
# 1 Creating a built-in application in Go |
|
|
|
|
|
|
|
|
|
|
|
Running your application inside the same process as Tendermint Core will give |
|
|
|
|
|
you the best possible performance. |
|
|
|
|
|
|
|
|
|
|
|
For other languages, your application have to communicate with Tendermint Core |
|
|
|
|
|
through a TCP, Unix domain socket or gRPC. |
|
|
|
|
|
|
|
|
|
|
|
## 1.1 Installing Go |
|
|
|
|
|
|
|
|
|
|
|
Please refer to [the official guide for installing |
|
|
|
|
|
Go](https://golang.org/doc/install). |
|
|
|
|
|
|
|
|
|
|
|
Verify that you have the latest version of Go installed: |
|
|
|
|
|
|
|
|
|
|
|
```sh |
|
|
|
|
|
$ go version |
|
|
|
|
|
go version go1.12.7 darwin/amd64 |
|
|
|
|
|
``` |
|
|
|
|
|
|
|
|
|
|
|
Make sure you have `$GOPATH` environment variable set: |
|
|
|
|
|
|
|
|
|
|
|
```sh |
|
|
|
|
|
$ echo $GOPATH |
|
|
|
|
|
/Users/melekes/go |
|
|
|
|
|
``` |
|
|
|
|
|
|
|
|
|
|
|
## 1.2 Creating a new Go project |
|
|
|
|
|
|
|
|
|
|
|
We'll start by creating a new Go project. |
|
|
|
|
|
|
|
|
|
|
|
```sh |
|
|
|
|
|
$ mkdir -p $GOPATH/src/github.com/me/kvstore |
|
|
|
|
|
$ cd $GOPATH/src/github.com/me/kvstore |
|
|
|
|
|
``` |
|
|
|
|
|
|
|
|
|
|
|
Inside the example directory create a `main.go` file with the following content: |
|
|
|
|
|
|
|
|
|
|
|
```go |
|
|
|
|
|
package main |
|
|
|
|
|
|
|
|
|
|
|
import ( |
|
|
|
|
|
"fmt" |
|
|
|
|
|
) |
|
|
|
|
|
|
|
|
|
|
|
func main() { |
|
|
|
|
|
fmt.Println("Hello, Tendermint Core") |
|
|
|
|
|
} |
|
|
|
|
|
``` |
|
|
|
|
|
|
|
|
|
|
|
When run, this should print "Hello, Tendermint Core" to the standard output. |
|
|
|
|
|
|
|
|
|
|
|
```sh |
|
|
|
|
|
$ go run main.go |
|
|
|
|
|
Hello, Tendermint Core |
|
|
|
|
|
``` |
|
|
|
|
|
|
|
|
|
|
|
## 1.3 Writing a Tendermint Core application |
|
|
|
|
|
|
|
|
|
|
|
Tendermint Core communicates with the application through the Application |
|
|
|
|
|
BlockChain Interface (ABCI). All message types are defined in the [protobuf |
|
|
|
|
|
file](https://github.com/tendermint/tendermint/blob/develop/abci/types/types.proto). |
|
|
|
|
|
This allows Tendermint Core to run applications written in any programming |
|
|
|
|
|
language. |
|
|
|
|
|
|
|
|
|
|
|
Create a file called `app.go` with the following content: |
|
|
|
|
|
|
|
|
|
|
|
```go |
|
|
|
|
|
package main |
|
|
|
|
|
|
|
|
|
|
|
import ( |
|
|
|
|
|
abcitypes "github.com/tendermint/tendermint/abci/types" |
|
|
|
|
|
) |
|
|
|
|
|
|
|
|
|
|
|
type KVStoreApplication struct {} |
|
|
|
|
|
|
|
|
|
|
|
var _ abcitypes.Application = (*KVStoreApplication)(nil) |
|
|
|
|
|
|
|
|
|
|
|
func NewKVStoreApplication() *KVStoreApplication { |
|
|
|
|
|
return &KVStoreApplication{} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
func (KVStoreApplication) Info(req abcitypes.RequestInfo) abcitypes.ResponseInfo { |
|
|
|
|
|
return abcitypes.ResponseInfo{} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
func (KVStoreApplication) SetOption(req abcitypes.RequestSetOption) abcitypes.ResponseSetOption { |
|
|
|
|
|
return abcitypes.ResponseSetOption{} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
func (KVStoreApplication) DeliverTx(req abcitypes.RequestDeliverTx) abcitypes.ResponseDeliverTx { |
|
|
|
|
|
return abcitypes.ResponseDeliverTx{Code: 0} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
func (KVStoreApplication) CheckTx(req abcitypes.RequestCheckTx) abcitypes.ResponseCheckTx { |
|
|
|
|
|
return abcitypes.ResponseCheckTx{Code: 0} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
func (KVStoreApplication) Commit() abcitypes.ResponseCommit { |
|
|
|
|
|
return abcitypes.ResponseCommit{} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
func (KVStoreApplication) Query(req abcitypes.RequestQuery) abcitypes.ResponseQuery { |
|
|
|
|
|
return abcitypes.ResponseQuery{Code: 0} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
func (KVStoreApplication) InitChain(req abcitypes.RequestInitChain) abcitypes.ResponseInitChain { |
|
|
|
|
|
return abcitypes.ResponseInitChain{} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
func (KVStoreApplication) BeginBlock(req abcitypes.RequestBeginBlock) abcitypes.ResponseBeginBlock { |
|
|
|
|
|
return abcitypes.ResponseBeginBlock{} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
func (KVStoreApplication) EndBlock(req abcitypes.RequestEndBlock) abcitypes.ResponseEndBlock { |
|
|
|
|
|
return abcitypes.ResponseEndBlock{} |
|
|
|
|
|
} |
|
|
|
|
|
``` |
|
|
|
|
|
|
|
|
|
|
|
Now I will go through each method explaining when it's called and adding |
|
|
|
|
|
required business logic. |
|
|
|
|
|
|
|
|
|
|
|
### 1.3.1 CheckTx |
|
|
|
|
|
|
|
|
|
|
|
When a new transaction is added to the Tendermint Core, it will ask the |
|
|
|
|
|
application to check it (validate the format, signatures, etc.). |
|
|
|
|
|
|
|
|
|
|
|
```go |
|
|
|
|
|
func (app *KVStoreApplication) isValid(tx []byte) (code uint32) { |
|
|
|
|
|
// check format |
|
|
|
|
|
parts := bytes.Split(tx, []byte("=")) |
|
|
|
|
|
if len(parts) != 2 { |
|
|
|
|
|
return 1 |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
key, value := parts[0], parts[1] |
|
|
|
|
|
|
|
|
|
|
|
// check if the same key=value already exists |
|
|
|
|
|
err := app.db.View(func(txn *badger.Txn) error { |
|
|
|
|
|
item, err := txn.Get(key) |
|
|
|
|
|
if err != nil && err != badger.ErrKeyNotFound { |
|
|
|
|
|
return err |
|
|
|
|
|
} |
|
|
|
|
|
if err == nil { |
|
|
|
|
|
return item.Value(func(val []byte) error { |
|
|
|
|
|
if bytes.Equal(val, value) { |
|
|
|
|
|
code = 2 |
|
|
|
|
|
} |
|
|
|
|
|
return nil |
|
|
|
|
|
}) |
|
|
|
|
|
} |
|
|
|
|
|
return nil |
|
|
|
|
|
}) |
|
|
|
|
|
if err != nil { |
|
|
|
|
|
panic(err) |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
return code |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
func (app *KVStoreApplication) CheckTx(req abcitypes.RequestCheckTx) abcitypes.ResponseCheckTx { |
|
|
|
|
|
code := app.isValid(req.Tx) |
|
|
|
|
|
return abcitypes.ResponseCheckTx{Code: code, GasWanted: 1} |
|
|
|
|
|
} |
|
|
|
|
|
``` |
|
|
|
|
|
|
|
|
|
|
|
Don't worry if this does not compile yet. |
|
|
|
|
|
|
|
|
|
|
|
If the transaction does not have a form of `{bytes}={bytes}`, we return `1` |
|
|
|
|
|
code. When the same key=value already exist (same key and value), we return `2` |
|
|
|
|
|
code. For others, we return a zero code indicating that they are valid. |
|
|
|
|
|
|
|
|
|
|
|
Note that anything with non-zero code will be considered invalid (`-1`, `100`, |
|
|
|
|
|
etc.) by Tendermint Core. |
|
|
|
|
|
|
|
|
|
|
|
Valid transactions will eventually be committed given they are not too big and |
|
|
|
|
|
have enough gas. To learn more about gas, check out ["the |
|
|
|
|
|
specification"](https://tendermint.com/docs/spec/abci/apps.html#gas). |
|
|
|
|
|
|
|
|
|
|
|
For the underlying key-value store we'll use |
|
|
|
|
|
[badger](https://github.com/dgraph-io/badger), which is an embeddable, |
|
|
|
|
|
persistent and fast key-value (KV) database. |
|
|
|
|
|
|
|
|
|
|
|
```go |
|
|
|
|
|
import "github.com/dgraph-io/badger" |
|
|
|
|
|
|
|
|
|
|
|
type KVStoreApplication struct { |
|
|
|
|
|
db *badger.DB |
|
|
|
|
|
currentBatch *badger.Txn |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
func NewKVStoreApplication(db *badger.DB) *KVStoreApplication { |
|
|
|
|
|
return &KVStoreApplication{ |
|
|
|
|
|
db: db, |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
``` |
|
|
|
|
|
|
|
|
|
|
|
### 1.3.2 BeginBlock -> DeliverTx -> EndBlock -> Commit |
|
|
|
|
|
|
|
|
|
|
|
When Tendermint Core has decided on the block, it's transfered to the |
|
|
|
|
|
application in 3 parts: `BeginBlock`, one `DeliverTx` per transaction and |
|
|
|
|
|
`EndBlock` in the end. DeliverTx are being transfered asynchronously, but the |
|
|
|
|
|
responses are expected to come in order. |
|
|
|
|
|
|
|
|
|
|
|
``` |
|
|
|
|
|
func (app *KVStoreApplication) BeginBlock(req abcitypes.RequestBeginBlock) abcitypes.ResponseBeginBlock { |
|
|
|
|
|
app.currentBatch = app.db.NewTransaction(true) |
|
|
|
|
|
return abcitypes.ResponseBeginBlock{} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
``` |
|
|
|
|
|
|
|
|
|
|
|
Here we create a batch, which will store block's transactions. |
|
|
|
|
|
|
|
|
|
|
|
```go |
|
|
|
|
|
func (app *KVStoreApplication) DeliverTx(req abcitypes.RequestDeliverTx) abcitypes.ResponseDeliverTx { |
|
|
|
|
|
code := app.isValid(req.Tx) |
|
|
|
|
|
if code != 0 { |
|
|
|
|
|
return abcitypes.ResponseDeliverTx{Code: code} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
parts := bytes.Split(req.Tx, []byte("=")) |
|
|
|
|
|
key, value := parts[0], parts[1] |
|
|
|
|
|
|
|
|
|
|
|
err := app.currentBatch.Set(key, value) |
|
|
|
|
|
if err != nil { |
|
|
|
|
|
panic(err) |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
return abcitypes.ResponseDeliverTx{Code: 0} |
|
|
|
|
|
} |
|
|
|
|
|
``` |
|
|
|
|
|
|
|
|
|
|
|
If the transaction is badly formatted or the same key=value already exist, we |
|
|
|
|
|
again return the non-zero code. Otherwise, we add it to the current batch. |
|
|
|
|
|
|
|
|
|
|
|
In the current design, a block can include incorrect transactions (those who |
|
|
|
|
|
passed CheckTx, but failed DeliverTx or transactions included by the proposer |
|
|
|
|
|
directly). This is done for performance reasons. |
|
|
|
|
|
|
|
|
|
|
|
Note we can't commit transactions inside the `DeliverTx` because in such case |
|
|
|
|
|
`Query`, which may be called in parallel, will return inconsistent data (i.e. |
|
|
|
|
|
it will report that some value already exist even when the actual block was not |
|
|
|
|
|
yet committed). |
|
|
|
|
|
|
|
|
|
|
|
`Commit` instructs the application to persist the new state. |
|
|
|
|
|
|
|
|
|
|
|
```go |
|
|
|
|
|
func (app *KVStoreApplication) Commit() abcitypes.ResponseCommit { |
|
|
|
|
|
app.currentBatch.Commit() |
|
|
|
|
|
return abcitypes.ResponseCommit{Data: []byte{}} |
|
|
|
|
|
} |
|
|
|
|
|
``` |
|
|
|
|
|
|
|
|
|
|
|
### 1.3.3 Query |
|
|
|
|
|
|
|
|
|
|
|
Now, when the client wants to know whenever a particular key/value exist, it |
|
|
|
|
|
will call Tendermint Core RPC `/abci_query` endpoint, which in turn will call |
|
|
|
|
|
the application's `Query` method. |
|
|
|
|
|
|
|
|
|
|
|
Applications are free to provide their own APIs. But by using Tendermint Core |
|
|
|
|
|
as a proxy, clients (including [light client |
|
|
|
|
|
package](https://godoc.org/github.com/tendermint/tendermint/lite)) can leverage |
|
|
|
|
|
the unified API across different applications. Plus they won't have to call the |
|
|
|
|
|
otherwise separate Tendermint Core API for additional proofs. |
|
|
|
|
|
|
|
|
|
|
|
Note we don't include a proof here. |
|
|
|
|
|
|
|
|
|
|
|
```go |
|
|
|
|
|
func (app *KVStoreApplication) Query(reqQuery abcitypes.RequestQuery) (resQuery abcitypes.ResponseQuery) { |
|
|
|
|
|
resQuery.Key = reqQuery.Data |
|
|
|
|
|
err := app.db.View(func(txn *badger.Txn) error { |
|
|
|
|
|
item, err := txn.Get(reqQuery.Data) |
|
|
|
|
|
if err != nil && err != badger.ErrKeyNotFound { |
|
|
|
|
|
return err |
|
|
|
|
|
} |
|
|
|
|
|
if err == badger.ErrKeyNotFound { |
|
|
|
|
|
resQuery.Log = "does not exist" |
|
|
|
|
|
} else { |
|
|
|
|
|
return item.Value(func(val []byte) error { |
|
|
|
|
|
resQuery.Log = "exists" |
|
|
|
|
|
resQuery.Value = val |
|
|
|
|
|
return nil |
|
|
|
|
|
}) |
|
|
|
|
|
} |
|
|
|
|
|
return nil |
|
|
|
|
|
}) |
|
|
|
|
|
if err != nil { |
|
|
|
|
|
panic(err) |
|
|
|
|
|
} |
|
|
|
|
|
return |
|
|
|
|
|
} |
|
|
|
|
|
``` |
|
|
|
|
|
|
|
|
|
|
|
The complete specification can be found |
|
|
|
|
|
[here](https://tendermint.com/docs/spec/abci/). |
|
|
|
|
|
|
|
|
|
|
|
## 1.4 Starting an application and a Tendermint Core instance in the same process |
|
|
|
|
|
|
|
|
|
|
|
Put the following code into the "main.go" file: |
|
|
|
|
|
|
|
|
|
|
|
```go |
|
|
|
|
|
package main |
|
|
|
|
|
|
|
|
|
|
|
import ( |
|
|
|
|
|
"flag" |
|
|
|
|
|
"fmt" |
|
|
|
|
|
"os" |
|
|
|
|
|
"os/signal" |
|
|
|
|
|
"path/filepath" |
|
|
|
|
|
"syscall" |
|
|
|
|
|
|
|
|
|
|
|
"github.com/dgraph-io/badger" |
|
|
|
|
|
"github.com/pkg/errors" |
|
|
|
|
|
"github.com/spf13/viper" |
|
|
|
|
|
|
|
|
|
|
|
abci "github.com/tendermint/tendermint/abci/types" |
|
|
|
|
|
cfg "github.com/tendermint/tendermint/config" |
|
|
|
|
|
tmflags "github.com/tendermint/tendermint/libs/cli/flags" |
|
|
|
|
|
"github.com/tendermint/tendermint/libs/log" |
|
|
|
|
|
nm "github.com/tendermint/tendermint/node" |
|
|
|
|
|
"github.com/tendermint/tendermint/p2p" |
|
|
|
|
|
"github.com/tendermint/tendermint/privval" |
|
|
|
|
|
"github.com/tendermint/tendermint/proxy" |
|
|
|
|
|
) |
|
|
|
|
|
|
|
|
|
|
|
var configFile string |
|
|
|
|
|
|
|
|
|
|
|
func init() { |
|
|
|
|
|
flag.StringVar(&configFile, "config", "$HOME/.tendermint/config/config.toml", "Path to config.toml") |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
func main() { |
|
|
|
|
|
db, err := badger.Open(badger.DefaultOptions("/tmp/badger")) |
|
|
|
|
|
if err != nil { |
|
|
|
|
|
fmt.Fprintf(os.Stderr, "failed to open badger db: %v", err) |
|
|
|
|
|
os.Exit(1) |
|
|
|
|
|
} |
|
|
|
|
|
defer db.Close() |
|
|
|
|
|
app := NewKVStoreApplication(db) |
|
|
|
|
|
|
|
|
|
|
|
flag.Parse() |
|
|
|
|
|
|
|
|
|
|
|
node, err := newTendermint(app, configFile) |
|
|
|
|
|
if err != nil { |
|
|
|
|
|
fmt.Fprintf(os.Stderr, "%v", err) |
|
|
|
|
|
os.Exit(2) |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
node.Start() |
|
|
|
|
|
defer func() { |
|
|
|
|
|
node.Stop() |
|
|
|
|
|
node.Wait() |
|
|
|
|
|
}() |
|
|
|
|
|
|
|
|
|
|
|
c := make(chan os.Signal, 1) |
|
|
|
|
|
signal.Notify(c, os.Interrupt, syscall.SIGTERM) |
|
|
|
|
|
<-c |
|
|
|
|
|
os.Exit(0) |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
func newTendermint(app abci.Application, configFile string) (*nm.Node, error) { |
|
|
|
|
|
// read config |
|
|
|
|
|
config := cfg.DefaultConfig() |
|
|
|
|
|
config.RootDir = filepath.Dir(filepath.Dir(configFile)) |
|
|
|
|
|
viper.SetConfigFile(configFile) |
|
|
|
|
|
if err := viper.ReadInConfig(); err != nil { |
|
|
|
|
|
return nil, errors.Wrap(err, "viper failed to read config file") |
|
|
|
|
|
} |
|
|
|
|
|
if err := viper.Unmarshal(config); err != nil { |
|
|
|
|
|
return nil, errors.Wrap(err, "viper failed to unmarshal config") |
|
|
|
|
|
} |
|
|
|
|
|
if err := config.ValidateBasic(); err != nil { |
|
|
|
|
|
return nil, errors.Wrap(err, "config is invalid") |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// create logger |
|
|
|
|
|
logger := log.NewTMLogger(log.NewSyncWriter(os.Stdout)) |
|
|
|
|
|
var err error |
|
|
|
|
|
logger, err = tmflags.ParseLogLevel(config.LogLevel, logger, cfg.DefaultLogLevel()) |
|
|
|
|
|
if err != nil { |
|
|
|
|
|
return nil, errors.Wrap(err, "failed to parse log level") |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// read private validator |
|
|
|
|
|
pv := privval.LoadFilePV( |
|
|
|
|
|
config.PrivValidatorKeyFile(), |
|
|
|
|
|
config.PrivValidatorStateFile(), |
|
|
|
|
|
) |
|
|
|
|
|
|
|
|
|
|
|
// read node key |
|
|
|
|
|
nodeKey, err := p2p.LoadNodeKey(config.NodeKeyFile()) |
|
|
|
|
|
if err != nil { |
|
|
|
|
|
return nil, errors.Wrap(err, "failed to load node's key") |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// create node |
|
|
|
|
|
node, err := nm.NewNode( |
|
|
|
|
|
config, |
|
|
|
|
|
pv, |
|
|
|
|
|
nodeKey, |
|
|
|
|
|
proxy.NewLocalClientCreator(app), |
|
|
|
|
|
nm.DefaultGenesisDocProviderFunc(config), |
|
|
|
|
|
nm.DefaultDBProvider, |
|
|
|
|
|
nm.DefaultMetricsProvider(config.Instrumentation), |
|
|
|
|
|
logger) |
|
|
|
|
|
if err != nil { |
|
|
|
|
|
return nil, errors.Wrap(err, "failed to create new Tendermint node") |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
return node, nil |
|
|
|
|
|
} |
|
|
|
|
|
``` |
|
|
|
|
|
|
|
|
|
|
|
This is a huge blob of code, so let's break it down into pieces. |
|
|
|
|
|
|
|
|
|
|
|
First, we initialize the Badger database and create an app instance: |
|
|
|
|
|
|
|
|
|
|
|
```go |
|
|
|
|
|
db, err := badger.Open(badger.DefaultOptions("/tmp/badger")) |
|
|
|
|
|
if err != nil { |
|
|
|
|
|
fmt.Fprintf(os.Stderr, "failed to open badger db: %v", err) |
|
|
|
|
|
os.Exit(1) |
|
|
|
|
|
} |
|
|
|
|
|
defer db.Close() |
|
|
|
|
|
app := NewKVStoreApplication(db) |
|
|
|
|
|
``` |
|
|
|
|
|
|
|
|
|
|
|
Then we use it to create a Tendermint Core `Node` instance: |
|
|
|
|
|
|
|
|
|
|
|
```go |
|
|
|
|
|
flag.Parse() |
|
|
|
|
|
|
|
|
|
|
|
node, err := newTendermint(app, configFile) |
|
|
|
|
|
if err != nil { |
|
|
|
|
|
fmt.Fprintf(os.Stderr, "%v", err) |
|
|
|
|
|
os.Exit(2) |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
... |
|
|
|
|
|
|
|
|
|
|
|
// create node |
|
|
|
|
|
node, err := nm.NewNode( |
|
|
|
|
|
config, |
|
|
|
|
|
pv, |
|
|
|
|
|
nodeKey, |
|
|
|
|
|
proxy.NewLocalClientCreator(app), |
|
|
|
|
|
nm.DefaultGenesisDocProviderFunc(config), |
|
|
|
|
|
nm.DefaultDBProvider, |
|
|
|
|
|
nm.DefaultMetricsProvider(config.Instrumentation), |
|
|
|
|
|
logger) |
|
|
|
|
|
if err != nil { |
|
|
|
|
|
return nil, errors.Wrap(err, "failed to create new Tendermint node") |
|
|
|
|
|
} |
|
|
|
|
|
``` |
|
|
|
|
|
|
|
|
|
|
|
`NewNode` requires a few things including a configuration file, a private |
|
|
|
|
|
validator, a node key and a few others in order to construct the full node. |
|
|
|
|
|
|
|
|
|
|
|
Note we use `proxy.NewLocalClientCreator` here to create a local client instead |
|
|
|
|
|
of one communicating through a socket or gRPC. |
|
|
|
|
|
|
|
|
|
|
|
[viper](https://github.com/spf13/viper) is being used for reading the config, |
|
|
|
|
|
which we will generate later using the `tendermint init` command. |
|
|
|
|
|
|
|
|
|
|
|
```go |
|
|
|
|
|
config := cfg.DefaultConfig() |
|
|
|
|
|
config.RootDir = filepath.Dir(filepath.Dir(configFile)) |
|
|
|
|
|
viper.SetConfigFile(configFile) |
|
|
|
|
|
if err := viper.ReadInConfig(); err != nil { |
|
|
|
|
|
return nil, errors.Wrap(err, "viper failed to read config file") |
|
|
|
|
|
} |
|
|
|
|
|
if err := viper.Unmarshal(config); err != nil { |
|
|
|
|
|
return nil, errors.Wrap(err, "viper failed to unmarshal config") |
|
|
|
|
|
} |
|
|
|
|
|
if err := config.ValidateBasic(); err != nil { |
|
|
|
|
|
return nil, errors.Wrap(err, "config is invalid") |
|
|
|
|
|
} |
|
|
|
|
|
``` |
|
|
|
|
|
|
|
|
|
|
|
We use `FilePV`, which is a private validator (i.e. thing which signs consensus |
|
|
|
|
|
messages). Normally, you would use `SignerRemote` to connect to an external |
|
|
|
|
|
[HSM](https://kb.certus.one/hsm.html). |
|
|
|
|
|
|
|
|
|
|
|
```go |
|
|
|
|
|
pv := privval.LoadFilePV( |
|
|
|
|
|
config.PrivValidatorKeyFile(), |
|
|
|
|
|
config.PrivValidatorStateFile(), |
|
|
|
|
|
) |
|
|
|
|
|
|
|
|
|
|
|
``` |
|
|
|
|
|
|
|
|
|
|
|
`nodeKey` is needed to identify the node in a p2p network. |
|
|
|
|
|
|
|
|
|
|
|
```go |
|
|
|
|
|
nodeKey, err := p2p.LoadNodeKey(config.NodeKeyFile()) |
|
|
|
|
|
if err != nil { |
|
|
|
|
|
return nil, errors.Wrap(err, "failed to load node's key") |
|
|
|
|
|
} |
|
|
|
|
|
``` |
|
|
|
|
|
|
|
|
|
|
|
As for the logger, we use the build-in library, which provides a nice |
|
|
|
|
|
abstraction over [go-kit's |
|
|
|
|
|
logger](https://github.com/go-kit/kit/tree/master/log). |
|
|
|
|
|
|
|
|
|
|
|
```go |
|
|
|
|
|
logger := log.NewTMLogger(log.NewSyncWriter(os.Stdout)) |
|
|
|
|
|
var err error |
|
|
|
|
|
logger, err = tmflags.ParseLogLevel(config.LogLevel, logger, cfg.DefaultLogLevel()) |
|
|
|
|
|
if err != nil { |
|
|
|
|
|
return nil, errors.Wrap(err, "failed to parse log level") |
|
|
|
|
|
} |
|
|
|
|
|
``` |
|
|
|
|
|
|
|
|
|
|
|
Finally, we start the node and add some signal handling to gracefully stop it |
|
|
|
|
|
upon receiving SIGTERM or Ctrl-C. |
|
|
|
|
|
|
|
|
|
|
|
```go |
|
|
|
|
|
node.Start() |
|
|
|
|
|
defer func() { |
|
|
|
|
|
node.Stop() |
|
|
|
|
|
node.Wait() |
|
|
|
|
|
}() |
|
|
|
|
|
|
|
|
|
|
|
c := make(chan os.Signal, 1) |
|
|
|
|
|
signal.Notify(c, os.Interrupt, syscall.SIGTERM) |
|
|
|
|
|
<-c |
|
|
|
|
|
os.Exit(0) |
|
|
|
|
|
``` |
|
|
|
|
|
|
|
|
|
|
|
## 1.5 Getting Up and Running |
|
|
|
|
|
|
|
|
|
|
|
We are going to use [Go modules](https://github.com/golang/go/wiki/Modules) for |
|
|
|
|
|
dependency management. |
|
|
|
|
|
|
|
|
|
|
|
```sh |
|
|
|
|
|
$ export GO111MODULE=on |
|
|
|
|
|
$ go mod init github.com/me/example |
|
|
|
|
|
$ go build |
|
|
|
|
|
``` |
|
|
|
|
|
|
|
|
|
|
|
This should build the binary. |
|
|
|
|
|
|
|
|
|
|
|
To create a default configuration, nodeKey and private validator files, let's |
|
|
|
|
|
execute `tendermint init`. But before we do that, we will need to install |
|
|
|
|
|
Tendermint Core. |
|
|
|
|
|
|
|
|
|
|
|
```sh |
|
|
|
|
|
$ rm -rf /tmp/example |
|
|
|
|
|
$ cd $GOPATH/src/github.com/tendermint/tendermint |
|
|
|
|
|
$ make install |
|
|
|
|
|
$ TMHOME="/tmp/example" tendermint init |
|
|
|
|
|
|
|
|
|
|
|
I[2019-07-16|18:40:36.480] Generated private validator module=main keyFile=/tmp/example/config/priv_validator_key.json stateFile=/tmp/example2/data/priv_validator_state.json |
|
|
|
|
|
I[2019-07-16|18:40:36.481] Generated node key module=main path=/tmp/example/config/node_key.json |
|
|
|
|
|
I[2019-07-16|18:40:36.482] Generated genesis file module=main path=/tmp/example/config/genesis.json |
|
|
|
|
|
``` |
|
|
|
|
|
|
|
|
|
|
|
We are ready to start our application: |
|
|
|
|
|
|
|
|
|
|
|
```sh |
|
|
|
|
|
$ ./example -config "/tmp/example/config/config.toml" |
|
|
|
|
|
|
|
|
|
|
|
badger 2019/07/16 18:42:25 INFO: All 0 tables opened in 0s |
|
|
|
|
|
badger 2019/07/16 18:42:25 INFO: Replaying file id: 0 at offset: 0 |
|
|
|
|
|
badger 2019/07/16 18:42:25 INFO: Replay took: 695.227s |
|
|
|
|
|
E[2019-07-16|18:42:25.818] Couldn't connect to any seeds module=p2p |
|
|
|
|
|
I[2019-07-16|18:42:26.853] Executed block module=state height=1 validTxs=0 invalidTxs=0 |
|
|
|
|
|
I[2019-07-16|18:42:26.865] Committed state module=state height=1 txs=0 appHash= |
|
|
|
|
|
``` |
|
|
|
|
|
|
|
|
|
|
|
Now open another tab in your terminal and try sending a transaction: |
|
|
|
|
|
|
|
|
|
|
|
```sh |
|
|
|
|
|
$ curl -s 'localhost:26657/broadcast_tx_commit?tx="tendermint=rocks"' |
|
|
|
|
|
{ |
|
|
|
|
|
"jsonrpc": "2.0", |
|
|
|
|
|
"id": "", |
|
|
|
|
|
"result": { |
|
|
|
|
|
"check_tx": { |
|
|
|
|
|
"gasWanted": "1" |
|
|
|
|
|
}, |
|
|
|
|
|
"deliver_tx": {}, |
|
|
|
|
|
"hash": "1B3C5A1093DB952C331B1749A21DCCBB0F6C7F4E0055CD04D16346472FC60EC6", |
|
|
|
|
|
"height": "128" |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
``` |
|
|
|
|
|
|
|
|
|
|
|
Response should contain the height where this transaction was committed. |
|
|
|
|
|
|
|
|
|
|
|
Now let's check if the given key now exists and its value: |
|
|
|
|
|
|
|
|
|
|
|
``` |
|
|
|
|
|
$ curl -s 'localhost:26657/abci_query?data="tendermint"' |
|
|
|
|
|
{ |
|
|
|
|
|
"jsonrpc": "2.0", |
|
|
|
|
|
"id": "", |
|
|
|
|
|
"result": { |
|
|
|
|
|
"response": { |
|
|
|
|
|
"log": "exists", |
|
|
|
|
|
"key": "dGVuZGVybWludA==", |
|
|
|
|
|
"value": "cm9ja3M=" |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
``` |
|
|
|
|
|
|
|
|
|
|
|
"dGVuZGVybWludA==" and "cm9ja3M=" are the base64-encoding of the ASCII of |
|
|
|
|
|
"tendermint" and "rocks" accordingly. |