mirror of
https://github.com/fluencelabs/tendermint
synced 2025-07-31 12:11:58 +00:00
Compare commits
15 Commits
NonMalleab
...
v0.32.1
Author | SHA1 | Date | |
---|---|---|---|
|
dbf4062acd | ||
|
79e924b34f | ||
|
0c9a284f8d | ||
|
245e1c9ef7 | ||
|
470f1efcc8 | ||
|
4e3b6bfb26 | ||
|
7a86e49312 | ||
|
744d65f173 | ||
|
1b69c6b56b | ||
|
e5084a4787 | ||
|
0787b79347 | ||
|
823d916a11 | ||
|
e8926867d8 | ||
|
0f076e5fbe | ||
|
ac232caef3 |
@@ -18,6 +18,5 @@ program](https://hackerone.com/tendermint).
|
||||
### FEATURES:
|
||||
|
||||
### IMPROVEMENTS:
|
||||
- [abci] \#3809 Recover from application panics in `server/socket_server.go` to allow socket cleanup (@ruseinov)
|
||||
|
||||
### BUG FIXES:
|
||||
|
@@ -228,22 +228,18 @@ func (cli *grpcClient) finishAsyncCall(req *types.Request, res *types.Response)
|
||||
reqres.Done() // Release waiters
|
||||
reqres.SetDone() // so reqRes.SetCallback will run the callback
|
||||
|
||||
// goroutine for callbacks
|
||||
// go routine for callbacks
|
||||
go func() {
|
||||
cli.mtx.Lock()
|
||||
defer cli.mtx.Unlock()
|
||||
// Notify reqRes listener if set
|
||||
if cb := reqres.GetCallback(); cb != nil {
|
||||
cb(res)
|
||||
}
|
||||
|
||||
// Notify client listener if set
|
||||
if cli.resCb != nil {
|
||||
cli.resCb(reqres.Request, res)
|
||||
}
|
||||
|
||||
// Notify reqRes listener if set
|
||||
if cb := reqres.GetCallback(); cb != nil {
|
||||
cb(res)
|
||||
}
|
||||
}()
|
||||
|
||||
return reqres
|
||||
}
|
||||
|
||||
|
@@ -146,16 +146,6 @@ func (s *SocketServer) waitForClose(closeConn chan error, connID int) {
|
||||
func (s *SocketServer) handleRequests(closeConn chan error, conn net.Conn, responses chan<- *types.Response) {
|
||||
var count int
|
||||
var bufReader = bufio.NewReader(conn)
|
||||
|
||||
defer func() {
|
||||
// make sure to recover from any app-related panics to allow proper socket cleanup
|
||||
r := recover()
|
||||
if r != nil {
|
||||
closeConn <- fmt.Errorf("recovered from panic: %v", r)
|
||||
s.appMtx.Unlock()
|
||||
}
|
||||
}()
|
||||
|
||||
for {
|
||||
|
||||
var req = &types.Request{}
|
||||
@@ -164,7 +154,7 @@ func (s *SocketServer) handleRequests(closeConn chan error, conn net.Conn, respo
|
||||
if err == io.EOF {
|
||||
closeConn <- err
|
||||
} else {
|
||||
closeConn <- fmt.Errorf("error reading message: %v", err)
|
||||
closeConn <- fmt.Errorf("Error reading message: %v", err.Error())
|
||||
}
|
||||
return
|
||||
}
|
||||
|
@@ -1,630 +0,0 @@
|
||||
# 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.
|
@@ -1,514 +0,0 @@
|
||||
# 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 an application in Go
|
||||
|
||||
To get maximum performance it is better to run your application alongside
|
||||
Tendermint Core. [Cosmos SDK](https://github.com/cosmos/cosmos-sdk) is written
|
||||
this way. Please refer to [Writing a built-in Tendermint Core application in
|
||||
Go](./go-built-in.md) guide for details.
|
||||
|
||||
Having a separate application might give you better security guarantees as two
|
||||
processes would be communicating via established binary protocol. Tendermint
|
||||
Core will not have access to application's state.
|
||||
|
||||
## 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 instances
|
||||
|
||||
Put the following code into the "main.go" file:
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
|
||||
"github.com/dgraph-io/badger"
|
||||
|
||||
abciserver "github.com/tendermint/tendermint/abci/server"
|
||||
"github.com/tendermint/tendermint/libs/log"
|
||||
)
|
||||
|
||||
var socketAddr string
|
||||
|
||||
func init() {
|
||||
flag.StringVar(&socketAddr, "socket-addr", "unix://example.sock", "Unix domain socket address")
|
||||
}
|
||||
|
||||
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()
|
||||
|
||||
logger := log.NewTMLogger(log.NewSyncWriter(os.Stdout))
|
||||
|
||||
server := abciserver.NewSocketServer(socketAddr, app)
|
||||
server.SetLogger(logger)
|
||||
if err := server.Start(); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "error starting socket server: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
defer server.Stop()
|
||||
|
||||
c := make(chan os.Signal, 1)
|
||||
signal.Notify(c, os.Interrupt, syscall.SIGTERM)
|
||||
<-c
|
||||
os.Exit(0)
|
||||
}
|
||||
```
|
||||
|
||||
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 start the ABCI server and add some signal handling to gracefully stop
|
||||
it upon receiving SIGTERM or Ctrl-C. Tendermint Core will act as a client,
|
||||
which connects to our server and send us transactions and other messages.
|
||||
|
||||
```go
|
||||
server := abciserver.NewSocketServer(socketAddr, app)
|
||||
server.SetLogger(logger)
|
||||
if err := server.Start(); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "error starting socket server: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
defer server.Stop()
|
||||
|
||||
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:20: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:20:36.481] Generated node key module=main path=/tmp/example/config/node_key.json
|
||||
I[2019-07-16|18:20:36.482] Generated genesis file module=main path=/tmp/example/config/genesis.json
|
||||
```
|
||||
|
||||
Feel free to explore the generated files, which can be found at
|
||||
`/tmp/example/config` directory. Documentation on the config can be found
|
||||
[here](https://tendermint.com/docs/tendermint-core/configuration.html).
|
||||
|
||||
We are ready to start our application:
|
||||
|
||||
```sh
|
||||
$ rm example.sock
|
||||
$ ./example
|
||||
|
||||
badger 2019/07/16 18:25:11 INFO: All 0 tables opened in 0s
|
||||
badger 2019/07/16 18:25:11 INFO: Replaying file id: 0 at offset: 0
|
||||
badger 2019/07/16 18:25:11 INFO: Replay took: 300.4s
|
||||
I[2019-07-16|18:25:11.523] Starting ABCIServer impl=ABCIServ
|
||||
```
|
||||
|
||||
Then we need to start Tendermint Core and point it to our application. Staying
|
||||
within the application directory execute:
|
||||
|
||||
```sh
|
||||
$ TMHOME="/tmp/example" tendermint node --proxy_app=unix://example.sock
|
||||
|
||||
I[2019-07-16|18:26:20.362] Version info module=main software=0.32.1 block=10 p2p=7
|
||||
I[2019-07-16|18:26:20.383] Starting Node module=main impl=Node
|
||||
E[2019-07-16|18:26:20.392] Couldn't connect to any seeds module=p2p
|
||||
I[2019-07-16|18:26:20.394] Started node module=main nodeInfo="{ProtocolVersion:{P2P:7 Block:10 App:0} ID_:8dab80770ae8e295d4ce905d86af78c4ff634b79 ListenAddr:tcp://0.0.0.0:26656 Network:test-chain-nIO96P Version:0.32.1 Channels:4020212223303800 Moniker:app48.fun-box.ru Other:{TxIndex:on RPCAddress:tcp://127.0.0.1:26657}}"
|
||||
I[2019-07-16|18:26:21.440] Executed block module=state height=1 validTxs=0 invalidTxs=0
|
||||
I[2019-07-16|18:26:21.446] Committed state module=state height=1 txs=0 appHash=
|
||||
```
|
||||
|
||||
This should start the full node and connect to our ABCI application.
|
||||
|
||||
```
|
||||
I[2019-07-16|18:25:11.525] Waiting for new connection...
|
||||
I[2019-07-16|18:26:20.329] Accepted a new connection
|
||||
I[2019-07-16|18:26:20.329] Waiting for new connection...
|
||||
I[2019-07-16|18:26:20.330] Accepted a new connection
|
||||
I[2019-07-16|18:26:20.330] Waiting for new connection...
|
||||
I[2019-07-16|18:26:20.330] Accepted a new connection
|
||||
```
|
||||
|
||||
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": "CDD3C6DFA0A08CAEDF546F9938A2EEC232209C24AA0E4201194E0AFB78A2C2BB",
|
||||
"height": "33"
|
||||
}
|
||||
```
|
||||
|
||||
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": "cm9ja3My"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
"dGVuZGVybWludA==" and "cm9ja3M=" are the base64-encoding of the ASCII of
|
||||
"tendermint" and "rocks" accordingly.
|
@@ -73,11 +73,11 @@ parameters over each successive round.
|
||||
|(When +2/3 Precommits for block found) |
|
||||
v |
|
||||
+--------------------------------------------------------------------+
|
||||
| Commit |
|
||||
| |
|
||||
| * Set CommitTime = now; |
|
||||
| * Wait for block, then stage/save/commit block; |
|
||||
+--------------------------------------------------------------------+
|
||||
| Commit |
|
||||
| |
|
||||
| * Set CommitTime = now; |
|
||||
| * Wait for block, then stage/save/commit block; |
|
||||
+--------------------------------------------------------------------+
|
||||
```
|
||||
|
||||
# Background Gossip
|
||||
@@ -131,15 +131,13 @@ liveness property.
|
||||
|
||||
### Propose Step (height:H,round:R)
|
||||
|
||||
Upon entering `Propose`:
|
||||
- The designated proposer proposes a block at `(H,R)`.
|
||||
Upon entering `Propose`: - The designated proposer proposes a block at
|
||||
`(H,R)`.
|
||||
|
||||
The `Propose` step ends:
|
||||
- After `timeoutProposeR` after entering `Propose`. --> goto
|
||||
`Prevote(H,R)`
|
||||
- After receiving proposal block and all prevotes at `PoLC-Round`. -->
|
||||
goto `Prevote(H,R)`
|
||||
- After [common exit conditions](#common-exit-conditions)
|
||||
The `Propose` step ends: - After `timeoutProposeR` after entering
|
||||
`Propose`. --> goto `Prevote(H,R)` - After receiving proposal block
|
||||
and all prevotes at `PoLC-Round`. --> goto `Prevote(H,R)` - After
|
||||
[common exit conditions](#common-exit-conditions)
|
||||
|
||||
### Prevote Step (height:H,round:R)
|
||||
|
||||
@@ -154,12 +152,10 @@ Upon entering `Prevote`, each validator broadcasts its prevote vote.
|
||||
- Else, if the proposal is invalid or wasn't received on time, it
|
||||
prevotes `<nil>`.
|
||||
|
||||
The `Prevote` step ends:
|
||||
- After +2/3 prevotes for a particular block or `<nil>`. -->; goto
|
||||
`Precommit(H,R)`
|
||||
- After `timeoutPrevote` after receiving any +2/3 prevotes. --> goto
|
||||
`Precommit(H,R)`
|
||||
- After [common exit conditions](#common-exit-conditions)
|
||||
The `Prevote` step ends: - After +2/3 prevotes for a particular block or
|
||||
`<nil>`. -->; goto `Precommit(H,R)` - After `timeoutPrevote` after
|
||||
receiving any +2/3 prevotes. --> goto `Precommit(H,R)` - After
|
||||
[common exit conditions](#common-exit-conditions)
|
||||
|
||||
### Precommit Step (height:H,round:R)
|
||||
|
||||
@@ -167,19 +163,17 @@ Upon entering `Precommit`, each validator broadcasts its precommit vote.
|
||||
|
||||
- If the validator has a PoLC at `(H,R)` for a particular block `B`, it
|
||||
(re)locks (or changes lock to) and precommits `B` and sets
|
||||
`LastLockRound = R`.
|
||||
- Else, if the validator has a PoLC at `(H,R)` for `<nil>`, it unlocks
|
||||
and precommits `<nil>`.
|
||||
- Else, it keeps the lock unchanged and precommits `<nil>`.
|
||||
`LastLockRound = R`. - Else, if the validator has a PoLC at `(H,R)` for
|
||||
`<nil>`, it unlocks and precommits `<nil>`. - Else, it keeps the lock
|
||||
unchanged and precommits `<nil>`.
|
||||
|
||||
A precommit for `<nil>` means "I didn’t see a PoLC for this round, but I
|
||||
did get +2/3 prevotes and waited a bit".
|
||||
|
||||
The Precommit step ends:
|
||||
- After +2/3 precommits for `<nil>`. --> goto `Propose(H,R+1)`
|
||||
- After `timeoutPrecommit` after receiving any +2/3 precommits. --> goto
|
||||
`Propose(H,R+1)`
|
||||
- After [common exit conditions](#common-exit-conditions)
|
||||
The Precommit step ends: - After +2/3 precommits for `<nil>`. -->
|
||||
goto `Propose(H,R+1)` - After `timeoutPrecommit` after receiving any
|
||||
+2/3 precommits. --> goto `Propose(H,R+1)` - After [common exit
|
||||
conditions](#common-exit-conditions)
|
||||
|
||||
### Common exit conditions
|
||||
|
||||
|
@@ -73,11 +73,6 @@ type SecretConnection struct {
|
||||
// Caller should call conn.Close()
|
||||
// See docs/sts-final.pdf for more information.
|
||||
func MakeSecretConnection(conn io.ReadWriteCloser, locPrivKey crypto.PrivKey) (*SecretConnection, error) {
|
||||
|
||||
hash := sha256.New
|
||||
hdkf_state := new([32]byte)
|
||||
hkdfInit := hkdf.New(hash, []byte("INIT_HANDSHAKE"), nil, []byte("TENDERMINT_SECRET_CONNECTION_TRANSCRIPT_HASH"))
|
||||
|
||||
locPubKey := locPrivKey.PubKey()
|
||||
|
||||
// Generate ephemeral keys for perfect forward secrecy.
|
||||
@@ -92,19 +87,7 @@ func MakeSecretConnection(conn io.ReadWriteCloser, locPrivKey crypto.PrivKey) (*
|
||||
}
|
||||
|
||||
// Sort by lexical order.
|
||||
loEphPub, hiEphPub := sort32(locEphPub, remEphPub)
|
||||
|
||||
if _, err := io.ReadFull(hkdfInit, hdkf_state[:]); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
hkdfLoEphPub := hkdf.New(hash, loEphPub[:], hdkf_state[:], []byte("TENDERMINT_SECRET_CONNECTION_TRANSCRIPT_HASH"))
|
||||
|
||||
if _, err := io.ReadFull(hkdfLoEphPub, hdkf_state[:]); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
hkdfHiEphPub := hkdf.New(hash, hiEphPub[:], hdkf_state[:], []byte("TENDERMINT_SECRET_CONNECTION_TRANSCRIPT_HASH"))
|
||||
loEphPub, _ := sort32(locEphPub, remEphPub)
|
||||
|
||||
// Check if the local ephemeral public key
|
||||
// was the least, lexicographically sorted.
|
||||
@@ -115,19 +98,9 @@ func MakeSecretConnection(conn io.ReadWriteCloser, locPrivKey crypto.PrivKey) (*
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if _, err := io.ReadFull(hkdfHiEphPub, hdkf_state[:]); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
hkdfSecret := hkdf.New(hash, dhSecret[:], hdkf_state[:], []byte("TENDERMINT_SECRET_CONNECTION_TRANSCRIPT_HASH"))
|
||||
|
||||
if _, err := io.ReadFull(hkdfSecret, hdkf_state[:]); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// generate the secret used for receiving, sending, challenge via HKDF-SHA2
|
||||
// on the transcript state (which itself also uses HKDF-SHA2 to derive a key from the dhSecret)
|
||||
recvSecret, sendSecret, challenge := deriveSecretAndChallenge(hdkf_state, locIsLeast)
|
||||
// generate the secret used for receiving, sending, challenge via hkdf-sha2 on dhSecret
|
||||
recvSecret, sendSecret, challenge := deriveSecretAndChallenge(dhSecret, locIsLeast)
|
||||
|
||||
// Construct SecretConnection.
|
||||
sc := &SecretConnection{
|
||||
@@ -291,7 +264,9 @@ func shareEphPubKey(conn io.ReadWriteCloser, locEphPub *[32]byte) (remEphPub *[3
|
||||
if err2 != nil {
|
||||
return nil, err2, true // abort
|
||||
}
|
||||
|
||||
if hasSmallOrder(_remEphPub) {
|
||||
return nil, ErrSmallOrderRemotePubKey, true
|
||||
}
|
||||
return _remEphPub, nil, false
|
||||
},
|
||||
)
|
||||
@@ -307,6 +282,52 @@ func shareEphPubKey(conn io.ReadWriteCloser, locEphPub *[32]byte) (remEphPub *[3
|
||||
return &_remEphPub, nil
|
||||
}
|
||||
|
||||
// use the samne blacklist as lib sodium (see https://eprint.iacr.org/2017/806.pdf for reference):
|
||||
// https://github.com/jedisct1/libsodium/blob/536ed00d2c5e0c65ac01e29141d69a30455f2038/src/libsodium/crypto_scalarmult/curve25519/ref10/x25519_ref10.c#L11-L17
|
||||
var blacklist = [][32]byte{
|
||||
// 0 (order 4)
|
||||
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00},
|
||||
// 1 (order 1)
|
||||
{0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00},
|
||||
// 325606250916557431795983626356110631294008115727848805560023387167927233504
|
||||
// (order 8)
|
||||
{0xe0, 0xeb, 0x7a, 0x7c, 0x3b, 0x41, 0xb8, 0xae, 0x16, 0x56, 0xe3,
|
||||
0xfa, 0xf1, 0x9f, 0xc4, 0x6a, 0xda, 0x09, 0x8d, 0xeb, 0x9c, 0x32,
|
||||
0xb1, 0xfd, 0x86, 0x62, 0x05, 0x16, 0x5f, 0x49, 0xb8, 0x00},
|
||||
// 39382357235489614581723060781553021112529911719440698176882885853963445705823
|
||||
// (order 8)
|
||||
{0x5f, 0x9c, 0x95, 0xbc, 0xa3, 0x50, 0x8c, 0x24, 0xb1, 0xd0, 0xb1,
|
||||
0x55, 0x9c, 0x83, 0xef, 0x5b, 0x04, 0x44, 0x5c, 0xc4, 0x58, 0x1c,
|
||||
0x8e, 0x86, 0xd8, 0x22, 0x4e, 0xdd, 0xd0, 0x9f, 0x11, 0x57},
|
||||
// p-1 (order 2)
|
||||
{0xec, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
|
||||
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
|
||||
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x7f},
|
||||
// p (=0, order 4)
|
||||
{0xed, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
|
||||
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
|
||||
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x7f},
|
||||
// p+1 (=1, order 1)
|
||||
{0xee, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
|
||||
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
|
||||
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x7f},
|
||||
}
|
||||
|
||||
func hasSmallOrder(pubKey [32]byte) bool {
|
||||
isSmallOrderPoint := false
|
||||
for _, bl := range blacklist {
|
||||
if subtle.ConstantTimeCompare(pubKey[:], bl[:]) == 1 {
|
||||
isSmallOrderPoint = true
|
||||
break
|
||||
}
|
||||
}
|
||||
return isSmallOrderPoint
|
||||
}
|
||||
|
||||
func deriveSecretAndChallenge(dhSecret *[32]byte, locIsLeast bool) (recvSecret, sendSecret *[aeadKeySize]byte, challenge *[32]byte) {
|
||||
hash := sha256.New
|
||||
hkdf := hkdf.New(hash, dhSecret[:], nil, []byte("TENDERMINT_SECRET_CONNECTION_KEY_AND_CHALLENGE_GEN"))
|
||||
|
@@ -100,6 +100,49 @@ func TestSecretConnectionHandshake(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// Test that shareEphPubKey rejects lower order public keys based on an
|
||||
// (incomplete) blacklist.
|
||||
func TestShareLowOrderPubkey(t *testing.T) {
|
||||
var fooConn, barConn = makeKVStoreConnPair()
|
||||
defer fooConn.Close()
|
||||
defer barConn.Close()
|
||||
locEphPub, _ := genEphKeys()
|
||||
|
||||
// all blacklisted low order points:
|
||||
for _, remLowOrderPubKey := range blacklist {
|
||||
_, _ = cmn.Parallel(
|
||||
func(_ int) (val interface{}, err error, abort bool) {
|
||||
_, err = shareEphPubKey(fooConn, locEphPub)
|
||||
|
||||
require.Error(t, err)
|
||||
require.Equal(t, err, ErrSmallOrderRemotePubKey)
|
||||
|
||||
return nil, nil, false
|
||||
},
|
||||
func(_ int) (val interface{}, err error, abort bool) {
|
||||
readRemKey, err := shareEphPubKey(barConn, &remLowOrderPubKey)
|
||||
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, locEphPub, readRemKey)
|
||||
|
||||
return nil, nil, false
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Test that additionally that the Diffie-Hellman shared secret is non-zero.
|
||||
// The shared secret would be zero for lower order pub-keys (but tested against the blacklist only).
|
||||
func TestComputeDHFailsOnLowOrder(t *testing.T) {
|
||||
_, locPrivKey := genEphKeys()
|
||||
for _, remLowOrderPubKey := range blacklist {
|
||||
shared, err := computeDHSecret(&remLowOrderPubKey, locPrivKey)
|
||||
assert.Error(t, err)
|
||||
|
||||
assert.Equal(t, err, ErrSharedSecretIsZero)
|
||||
assert.Empty(t, shared)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConcurrentWrite(t *testing.T) {
|
||||
fooSecConn, barSecConn := makeSecretConnPair(t)
|
||||
fooWriteText := cmn.RandStr(dataMaxSize)
|
||||
|
Reference in New Issue
Block a user