Skip to content

Commit a58454e

Browse files
committedOct 22, 2020

31 files changed

+2722
-5
lines changed
 

‎.dockerignore

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
build
2+
test/e2e/build
3+
test/e2e/networks
4+
test/logs
5+
test/p2p/data

‎.gitignore

+2
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ remote_dump
1010
.revision
1111
vendor
1212
.vagrant
13+
test/e2e/build
14+
test/e2e/networks/*/
1315
test/p2p/data/
1416
test/logs
1517
coverage.txt

‎go.mod

+1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ module github.com/tendermint/tendermint
33
go 1.14
44

55
require (
6+
github.com/BurntSushi/toml v0.3.1
67
github.com/ChainSafe/go-schnorrkel v0.0.0-20200405005733-88cbf1b4c40d
78
github.com/Workiva/go-datastructures v1.0.52
89
github.com/btcsuite/btcd v0.21.0-beta

‎privval/file.go

+8-5
Original file line numberDiff line numberDiff line change
@@ -152,11 +152,8 @@ type FilePV struct {
152152
LastSignState FilePVLastSignState
153153
}
154154

155-
// GenFilePV generates a new validator with randomly generated private key
156-
// and sets the filePaths, but does not call Save().
157-
func GenFilePV(keyFilePath, stateFilePath string) *FilePV {
158-
privKey := ed25519.GenPrivKey()
159-
155+
// NewFilePV generates a new validator from the given key and paths.
156+
func NewFilePV(privKey crypto.PrivKey, keyFilePath, stateFilePath string) *FilePV {
160157
return &FilePV{
161158
Key: FilePVKey{
162159
Address: privKey.PubKey().Address(),
@@ -171,6 +168,12 @@ func GenFilePV(keyFilePath, stateFilePath string) *FilePV {
171168
}
172169
}
173170

171+
// GenFilePV generates a new validator with randomly generated private key
172+
// and sets the filePaths, but does not call Save().
173+
func GenFilePV(keyFilePath, stateFilePath string) *FilePV {
174+
return NewFilePV(ed25519.GenPrivKey(), keyFilePath, stateFilePath)
175+
}
176+
174177
// LoadFilePV loads a FilePV from the filePaths. The FilePV handles double
175178
// signing prevention by persisting data to the stateFilePath. If either file path
176179
// does not exist, the program will exit.

‎test/e2e/Makefile

+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
docker:
2+
docker build --tag tendermint/e2e-node -f docker/Dockerfile ../..
3+
4+
ci: runner
5+
./build/runner -f networks/ci.toml
6+
7+
# We need to build support for database backends into the app in
8+
# order to build a binary with a Tendermint node in it (for built-in
9+
# ABCI testing).
10+
app:
11+
go build -o build/app -tags badgerdb,boltdb,cleveldb,rocksdb ./app
12+
13+
runner:
14+
go build -o build/runner ./runner
15+
16+
.PHONY: app ci docker runner

‎test/e2e/README.md

+78
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
# End-to-End Tests
2+
3+
Spins up and tests Tendermint networks in Docker Compose based on a testnet manifest. To run the CI testnet:
4+
5+
```sh
6+
make docker
7+
make runner
8+
./build/runner -f networks/ci.toml
9+
```
10+
11+
This creates and runs a testnet named `ci` under `networks/ci/` (determined by the manifest filename).
12+
13+
## Testnet Manifests
14+
15+
Testnets are specified as TOML manifests. For an example see [`networks/ci.toml`](networks/ci.toml), and for documentation see [`pkg/manifest.go`](pkg/manifest.go).
16+
17+
## Test Stages
18+
19+
The test runner has the following stages, which can also be executed explicitly by running `./build/runner -f <manifest> <stage>`:
20+
21+
* `setup`: generates configuration files.
22+
23+
* `start`: starts Docker containers.
24+
25+
* `load`: generates a transaction load against the testnet nodes.
26+
27+
* `perturb`: runs any requested perturbations (e.g. node restarts or network disconnects).
28+
29+
* `wait`: waits for a few blocks to be produced, and for all nodes to catch up to it.
30+
31+
* `test`: runs test cases in `tests/` against all nodes in a running testnet.
32+
33+
* `stop`: stops Docker containers.
34+
35+
* `cleanup`: removes configuration files and Docker containers/networks.
36+
37+
* `logs`: outputs all node logs.
38+
39+
## Tests
40+
41+
Test cases are written as normal Go tests in `tests/`. They use a `testNode()` helper which executes each test as a parallel subtest for each node in the network.
42+
43+
### Running Manual Tests
44+
45+
To run tests manually, set the `E2E_MANIFEST` environment variable to the path of the testnet manifest (e.g. `networks/ci.toml`) and run them as normal, e.g.:
46+
47+
```sh
48+
./build/runner -f networks/ci.toml start
49+
E2E_MANIFEST=networks/ci.toml go test -v ./tests/...
50+
```
51+
52+
Optionally, `E2E_NODE` specifies the name of a single testnet node to test.
53+
54+
These environment variables can also be specified in `tests/e2e_test.go` to run tests from an editor or IDE:
55+
56+
```go
57+
func init() {
58+
// This can be used to manually specify a testnet manifest and/or node to
59+
// run tests against. The testnet must have been started by the runner first.
60+
os.Setenv("E2E_MANIFEST", "networks/ci.toml")
61+
os.Setenv("E2E_NODE", "validator01")
62+
}
63+
```
64+
65+
### Debugging Failures
66+
67+
If a command or test fails, the runner simply exits with an error message and non-zero status code. The testnet is left running with data in the testnet directory, and can be inspected with e.g. `docker ps`, `docker logs`, or `./build/runner -f <manifest> logs` or `tail`. To shut down and remove the testnet, run `./build/runner -f <manifest> cleanup`.
68+
69+
## Enabling IPv6
70+
71+
Docker does not enable IPv6 by default. To do so, enter the following in `daemon.json` (or in the Docker for Mac UI under Preferences → Docker Engine):
72+
73+
```json
74+
{
75+
"ipv6": true,
76+
"fixed-cidr-v6": "2001:db8:1::/64"
77+
}
78+
```

‎test/e2e/app/app.go

+217
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,217 @@
1+
package main
2+
3+
import (
4+
"bytes"
5+
"encoding/base64"
6+
"errors"
7+
"fmt"
8+
"os"
9+
"path/filepath"
10+
11+
"github.com/tendermint/tendermint/abci/example/code"
12+
abci "github.com/tendermint/tendermint/abci/types"
13+
"github.com/tendermint/tendermint/libs/log"
14+
"github.com/tendermint/tendermint/version"
15+
)
16+
17+
// Application is an ABCI application for use by end-to-end tests. It is a
18+
// simple key/value store for strings, storing data in memory and persisting
19+
// to disk as JSON, taking state sync snapshots if requested.
20+
type Application struct {
21+
abci.BaseApplication
22+
logger log.Logger
23+
state *State
24+
snapshots *SnapshotStore
25+
cfg *Config
26+
restoreSnapshot *abci.Snapshot
27+
restoreChunks [][]byte
28+
}
29+
30+
// NewApplication creates the application.
31+
func NewApplication(cfg *Config) (*Application, error) {
32+
state, err := NewState(filepath.Join(cfg.Dir, "state.json"), cfg.PersistInterval)
33+
if err != nil {
34+
return nil, err
35+
}
36+
snapshots, err := NewSnapshotStore(filepath.Join(cfg.Dir, "snapshots"))
37+
if err != nil {
38+
return nil, err
39+
}
40+
return &Application{
41+
logger: log.NewTMLogger(log.NewSyncWriter(os.Stdout)),
42+
state: state,
43+
snapshots: snapshots,
44+
cfg: cfg,
45+
}, nil
46+
}
47+
48+
// Info implements ABCI.
49+
func (app *Application) Info(req abci.RequestInfo) abci.ResponseInfo {
50+
return abci.ResponseInfo{
51+
Version: version.ABCIVersion,
52+
AppVersion: 1,
53+
LastBlockHeight: int64(app.state.Height),
54+
LastBlockAppHash: app.state.Hash,
55+
}
56+
}
57+
58+
// Info implements ABCI.
59+
func (app *Application) InitChain(req abci.RequestInitChain) abci.ResponseInitChain {
60+
var err error
61+
app.state.initialHeight = uint64(req.InitialHeight)
62+
if len(req.AppStateBytes) > 0 {
63+
err = app.state.Import(0, req.AppStateBytes)
64+
if err != nil {
65+
panic(err)
66+
}
67+
}
68+
resp := abci.ResponseInitChain{
69+
AppHash: app.state.Hash,
70+
}
71+
if resp.Validators, err = app.validatorUpdates(0); err != nil {
72+
panic(err)
73+
}
74+
return resp
75+
}
76+
77+
// CheckTx implements ABCI.
78+
func (app *Application) CheckTx(req abci.RequestCheckTx) abci.ResponseCheckTx {
79+
_, _, err := parseTx(req.Tx)
80+
if err != nil {
81+
return abci.ResponseCheckTx{
82+
Code: code.CodeTypeEncodingError,
83+
Log: err.Error(),
84+
}
85+
}
86+
return abci.ResponseCheckTx{Code: code.CodeTypeOK, GasWanted: 1}
87+
}
88+
89+
// DeliverTx implements ABCI.
90+
func (app *Application) DeliverTx(req abci.RequestDeliverTx) abci.ResponseDeliverTx {
91+
key, value, err := parseTx(req.Tx)
92+
if err != nil {
93+
panic(err) // shouldn't happen since we verified it in CheckTx
94+
}
95+
app.state.Set(key, value)
96+
return abci.ResponseDeliverTx{Code: code.CodeTypeOK}
97+
}
98+
99+
// EndBlock implements ABCI.
100+
func (app *Application) EndBlock(req abci.RequestEndBlock) abci.ResponseEndBlock {
101+
var err error
102+
resp := abci.ResponseEndBlock{}
103+
if resp.ValidatorUpdates, err = app.validatorUpdates(uint64(req.Height)); err != nil {
104+
panic(err)
105+
}
106+
return resp
107+
}
108+
109+
// Commit implements ABCI.
110+
func (app *Application) Commit() abci.ResponseCommit {
111+
height, hash, err := app.state.Commit()
112+
if err != nil {
113+
panic(err)
114+
}
115+
if app.cfg.SnapshotInterval > 0 && height%app.cfg.SnapshotInterval == 0 {
116+
snapshot, err := app.snapshots.Create(app.state)
117+
if err != nil {
118+
panic(err)
119+
}
120+
logger.Info("Created state sync snapshot", "height", snapshot.Height)
121+
}
122+
retainHeight := int64(0)
123+
if app.cfg.RetainBlocks > 0 {
124+
retainHeight = int64(height - app.cfg.RetainBlocks + 1)
125+
}
126+
return abci.ResponseCommit{
127+
Data: hash,
128+
RetainHeight: retainHeight,
129+
}
130+
}
131+
132+
// Query implements ABCI.
133+
func (app *Application) Query(req abci.RequestQuery) abci.ResponseQuery {
134+
return abci.ResponseQuery{
135+
Height: int64(app.state.Height),
136+
Key: req.Data,
137+
Value: []byte(app.state.Get(string(req.Data))),
138+
}
139+
}
140+
141+
// ListSnapshots implements ABCI.
142+
func (app *Application) ListSnapshots(req abci.RequestListSnapshots) abci.ResponseListSnapshots {
143+
snapshots, err := app.snapshots.List()
144+
if err != nil {
145+
panic(err)
146+
}
147+
return abci.ResponseListSnapshots{Snapshots: snapshots}
148+
}
149+
150+
// LoadSnapshotChunk implements ABCI.
151+
func (app *Application) LoadSnapshotChunk(req abci.RequestLoadSnapshotChunk) abci.ResponseLoadSnapshotChunk {
152+
chunk, err := app.snapshots.LoadChunk(req.Height, req.Format, req.Chunk)
153+
if err != nil {
154+
panic(err)
155+
}
156+
return abci.ResponseLoadSnapshotChunk{Chunk: chunk}
157+
}
158+
159+
// OfferSnapshot implements ABCI.
160+
func (app *Application) OfferSnapshot(req abci.RequestOfferSnapshot) abci.ResponseOfferSnapshot {
161+
if app.restoreSnapshot != nil {
162+
panic("A snapshot is already being restored")
163+
}
164+
app.restoreSnapshot = req.Snapshot
165+
app.restoreChunks = [][]byte{}
166+
return abci.ResponseOfferSnapshot{Result: abci.ResponseOfferSnapshot_ACCEPT}
167+
}
168+
169+
// ApplySnapshotChunk implements ABCI.
170+
func (app *Application) ApplySnapshotChunk(req abci.RequestApplySnapshotChunk) abci.ResponseApplySnapshotChunk {
171+
if app.restoreSnapshot == nil {
172+
panic("No restore in progress")
173+
}
174+
app.restoreChunks = append(app.restoreChunks, req.Chunk)
175+
if len(app.restoreChunks) == int(app.restoreSnapshot.Chunks) {
176+
bz := []byte{}
177+
for _, chunk := range app.restoreChunks {
178+
bz = append(bz, chunk...)
179+
}
180+
err := app.state.Import(app.restoreSnapshot.Height, bz)
181+
if err != nil {
182+
panic(err)
183+
}
184+
app.restoreSnapshot = nil
185+
app.restoreChunks = nil
186+
}
187+
return abci.ResponseApplySnapshotChunk{Result: abci.ResponseApplySnapshotChunk_ACCEPT}
188+
}
189+
190+
// validatorUpdates generates a validator set update.
191+
func (app *Application) validatorUpdates(height uint64) (abci.ValidatorUpdates, error) {
192+
updates := app.cfg.ValidatorUpdates[fmt.Sprintf("%v", height)]
193+
if len(updates) == 0 {
194+
return nil, nil
195+
}
196+
valUpdates := abci.ValidatorUpdates{}
197+
for keyString, power := range updates {
198+
keyBytes, err := base64.StdEncoding.DecodeString(keyString)
199+
if err != nil {
200+
return nil, fmt.Errorf("invalid base64 pubkey value %q: %w", keyString, err)
201+
}
202+
valUpdates = append(valUpdates, abci.Ed25519ValidatorUpdate(keyBytes, int64(power)))
203+
}
204+
return valUpdates, nil
205+
}
206+
207+
// parseTx parses a tx in 'key=value' format into a key and value.
208+
func parseTx(tx []byte) (string, string, error) {
209+
parts := bytes.Split(tx, []byte("="))
210+
if len(parts) != 2 {
211+
return "", "", fmt.Errorf("invalid tx format: %q", string(tx))
212+
}
213+
if len(parts[0]) == 0 {
214+
return "", "", errors.New("key cannot be empty")
215+
}
216+
return string(parts[0]), string(parts[1]), nil
217+
}

‎test/e2e/app/config.go

+50
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
package main
2+
3+
import (
4+
"errors"
5+
"fmt"
6+
7+
"github.com/BurntSushi/toml"
8+
)
9+
10+
// Config is the application configuration.
11+
type Config struct {
12+
ChainID string `toml:"chain_id"`
13+
Listen string
14+
Protocol string
15+
Dir string
16+
PersistInterval uint64 `toml:"persist_interval"`
17+
SnapshotInterval uint64 `toml:"snapshot_interval"`
18+
RetainBlocks uint64 `toml:"retain_blocks"`
19+
ValidatorUpdates map[string]map[string]uint8 `toml:"validator_update"`
20+
PrivValServer string `toml:"privval_server"`
21+
PrivValKey string `toml:"privval_key"`
22+
PrivValState string `toml:"privval_state"`
23+
}
24+
25+
// LoadConfig loads the configuration from disk.
26+
func LoadConfig(file string) (*Config, error) {
27+
cfg := &Config{
28+
Listen: "unix:///var/run/app.sock",
29+
Protocol: "socket",
30+
PersistInterval: 1,
31+
}
32+
_, err := toml.DecodeFile(file, &cfg)
33+
if err != nil {
34+
return nil, fmt.Errorf("failed to load config from %q: %w", file, err)
35+
}
36+
return cfg, cfg.Validate()
37+
}
38+
39+
// Validate validates the configuration. We don't do exhaustive config
40+
// validation here, instead relying on Testnet.Validate() to handle it.
41+
func (cfg Config) Validate() error {
42+
switch {
43+
case cfg.ChainID == "":
44+
return errors.New("chain_id parameter is required")
45+
case cfg.Listen == "" && cfg.Protocol != "builtin":
46+
return errors.New("listen parameter is required")
47+
default:
48+
return nil
49+
}
50+
}

‎test/e2e/app/main.go

+173
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
package main
2+
3+
import (
4+
"errors"
5+
"fmt"
6+
"os"
7+
"path/filepath"
8+
"time"
9+
10+
"github.com/spf13/viper"
11+
"github.com/tendermint/tendermint/abci/server"
12+
"github.com/tendermint/tendermint/config"
13+
tmflags "github.com/tendermint/tendermint/libs/cli/flags"
14+
"github.com/tendermint/tendermint/libs/log"
15+
tmnet "github.com/tendermint/tendermint/libs/net"
16+
"github.com/tendermint/tendermint/node"
17+
"github.com/tendermint/tendermint/p2p"
18+
"github.com/tendermint/tendermint/privval"
19+
"github.com/tendermint/tendermint/proxy"
20+
)
21+
22+
var logger = log.NewTMLogger(log.NewSyncWriter(os.Stdout))
23+
24+
// main is the binary entrypoint.
25+
func main() {
26+
if len(os.Args) != 2 {
27+
fmt.Printf("Usage: %v <configfile>", os.Args[0])
28+
return
29+
}
30+
configFile := ""
31+
if len(os.Args) == 2 {
32+
configFile = os.Args[1]
33+
}
34+
35+
if err := run(configFile); err != nil {
36+
logger.Error(err.Error())
37+
os.Exit(1)
38+
}
39+
}
40+
41+
// run runs the application - basically like main() with error handling.
42+
func run(configFile string) error {
43+
cfg, err := LoadConfig(configFile)
44+
if err != nil {
45+
return err
46+
}
47+
48+
switch cfg.Protocol {
49+
case "socket", "grpc":
50+
err = startApp(cfg)
51+
case "builtin":
52+
err = startNode(cfg)
53+
default:
54+
err = fmt.Errorf("invalid protocol %q", cfg.Protocol)
55+
}
56+
if err != nil {
57+
return err
58+
}
59+
60+
// Start remote signer
61+
if cfg.PrivValServer != "" {
62+
if err = startSigner(cfg); err != nil {
63+
return err
64+
}
65+
}
66+
67+
// Apparently there's no way to wait for the server, so we just sleep
68+
for {
69+
time.Sleep(1 * time.Hour)
70+
}
71+
}
72+
73+
// startApp starts the application server, listening for connections from Tendermint.
74+
func startApp(cfg *Config) error {
75+
app, err := NewApplication(cfg)
76+
if err != nil {
77+
return err
78+
}
79+
server, err := server.NewServer(cfg.Listen, cfg.Protocol, app)
80+
if err != nil {
81+
return err
82+
}
83+
err = server.Start()
84+
if err != nil {
85+
return err
86+
}
87+
logger.Info(fmt.Sprintf("Server listening on %v (%v protocol)", cfg.Listen, cfg.Protocol))
88+
return nil
89+
}
90+
91+
// startNode starts a Tendermint node running the application directly. It assumes the Tendermint
92+
// configuration is in $TMHOME/config/tendermint.toml.
93+
//
94+
// FIXME There is no way to simply load the configuration from a file, so we need to pull in Viper.
95+
func startNode(cfg *Config) error {
96+
app, err := NewApplication(cfg)
97+
if err != nil {
98+
return err
99+
}
100+
101+
home := os.Getenv("TMHOME")
102+
if home == "" {
103+
return errors.New("TMHOME not set")
104+
}
105+
viper.AddConfigPath(filepath.Join(home, "config"))
106+
viper.SetConfigName("config")
107+
err = viper.ReadInConfig()
108+
if err != nil {
109+
return err
110+
}
111+
tmcfg := config.DefaultConfig()
112+
err = viper.Unmarshal(tmcfg)
113+
if err != nil {
114+
return err
115+
}
116+
tmcfg.SetRoot(home)
117+
if err = tmcfg.ValidateBasic(); err != nil {
118+
return fmt.Errorf("error in config file: %v", err)
119+
}
120+
if tmcfg.LogFormat == config.LogFormatJSON {
121+
logger = log.NewTMJSONLogger(log.NewSyncWriter(os.Stdout))
122+
}
123+
logger, err = tmflags.ParseLogLevel(tmcfg.LogLevel, logger, config.DefaultLogLevel())
124+
if err != nil {
125+
return err
126+
}
127+
logger = logger.With("module", "main")
128+
129+
nodeKey, err := p2p.LoadOrGenNodeKey(tmcfg.NodeKeyFile())
130+
if err != nil {
131+
return fmt.Errorf("failed to load or gen node key %s: %w", tmcfg.NodeKeyFile(), err)
132+
}
133+
134+
n, err := node.NewNode(tmcfg,
135+
privval.LoadOrGenFilePV(tmcfg.PrivValidatorKeyFile(), tmcfg.PrivValidatorStateFile()),
136+
nodeKey,
137+
proxy.NewLocalClientCreator(app),
138+
node.DefaultGenesisDocProviderFunc(tmcfg),
139+
node.DefaultDBProvider,
140+
node.DefaultMetricsProvider(tmcfg.Instrumentation),
141+
logger,
142+
)
143+
if err != nil {
144+
return err
145+
}
146+
return n.Start()
147+
}
148+
149+
// startSigner starts a signer server connecting to the given endpoint.
150+
func startSigner(cfg *Config) error {
151+
filePV := privval.LoadFilePV(cfg.PrivValKey, cfg.PrivValState)
152+
153+
protocol, address := tmnet.ProtocolAndAddress(cfg.PrivValServer)
154+
var dialFn privval.SocketDialer
155+
switch protocol {
156+
case "tcp":
157+
dialFn = privval.DialTCPFn(address, 3*time.Second, filePV.Key.PrivKey)
158+
case "unix":
159+
dialFn = privval.DialUnixFn(address)
160+
default:
161+
return fmt.Errorf("invalid privval protocol %q", protocol)
162+
}
163+
164+
endpoint := privval.NewSignerDialerEndpoint(logger, dialFn,
165+
privval.SignerDialerEndpointRetryWaitInterval(1*time.Second),
166+
privval.SignerDialerEndpointConnRetries(100))
167+
err := privval.NewSignerServer(endpoint, cfg.ChainID, filePV).Start()
168+
if err != nil {
169+
return err
170+
}
171+
logger.Info(fmt.Sprintf("Remote signer connecting to %v", cfg.PrivValServer))
172+
return nil
173+
}

‎test/e2e/app/snapshots.go

+155
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
// nolint: gosec
2+
package main
3+
4+
import (
5+
"crypto/sha256"
6+
"encoding/json"
7+
"errors"
8+
"fmt"
9+
"io/ioutil"
10+
"math"
11+
"os"
12+
"path/filepath"
13+
"sync"
14+
15+
abci "github.com/tendermint/tendermint/abci/types"
16+
)
17+
18+
const (
19+
snapshotChunkSize = 1e6
20+
)
21+
22+
// SnapshotStore stores state sync snapshots. Snapshots are stored simply as
23+
// JSON files, and chunks are generated on-the-fly by splitting the JSON data
24+
// into fixed-size chunks.
25+
type SnapshotStore struct {
26+
sync.RWMutex
27+
dir string
28+
metadata []abci.Snapshot
29+
}
30+
31+
// NewSnapshotStore creates a new snapshot store.
32+
func NewSnapshotStore(dir string) (*SnapshotStore, error) {
33+
store := &SnapshotStore{dir: dir}
34+
if err := os.MkdirAll(dir, 0755); err != nil {
35+
return nil, err
36+
}
37+
if err := store.loadMetadata(); err != nil {
38+
return nil, err
39+
}
40+
return store, nil
41+
}
42+
43+
// loadMetadata loads snapshot metadata. Does not take out locks, since it's
44+
// called internally on construction.
45+
func (s *SnapshotStore) loadMetadata() error {
46+
file := filepath.Join(s.dir, "metadata.json")
47+
metadata := []abci.Snapshot{}
48+
49+
bz, err := ioutil.ReadFile(file)
50+
switch {
51+
case errors.Is(err, os.ErrNotExist):
52+
case err != nil:
53+
return fmt.Errorf("failed to load snapshot metadata from %q: %w", file, err)
54+
}
55+
if len(bz) != 0 {
56+
err = json.Unmarshal(bz, &metadata)
57+
if err != nil {
58+
return fmt.Errorf("invalid snapshot data in %q: %w", file, err)
59+
}
60+
}
61+
s.metadata = metadata
62+
return nil
63+
}
64+
65+
// saveMetadata saves snapshot metadata. Does not take out locks, since it's
66+
// called internally from e.g. Create().
67+
func (s *SnapshotStore) saveMetadata() error {
68+
bz, err := json.Marshal(s.metadata)
69+
if err != nil {
70+
return err
71+
}
72+
73+
// save the file to a new file and move it to make saving atomic.
74+
newFile := filepath.Join(s.dir, "metadata.json.new")
75+
file := filepath.Join(s.dir, "metadata.json")
76+
err = ioutil.WriteFile(newFile, bz, 0644) // nolint: gosec
77+
if err != nil {
78+
return err
79+
}
80+
return os.Rename(newFile, file)
81+
}
82+
83+
// Create creates a snapshot of the given application state's key/value pairs.
84+
func (s *SnapshotStore) Create(state *State) (abci.Snapshot, error) {
85+
s.Lock()
86+
defer s.Unlock()
87+
bz, err := state.Export()
88+
if err != nil {
89+
return abci.Snapshot{}, err
90+
}
91+
hash := sha256.Sum256(bz)
92+
snapshot := abci.Snapshot{
93+
Height: state.Height,
94+
Format: 1,
95+
Hash: hash[:],
96+
Chunks: byteChunks(bz),
97+
}
98+
err = ioutil.WriteFile(filepath.Join(s.dir, fmt.Sprintf("%v.json", state.Height)), bz, 0644)
99+
if err != nil {
100+
return abci.Snapshot{}, err
101+
}
102+
s.metadata = append(s.metadata, snapshot)
103+
err = s.saveMetadata()
104+
if err != nil {
105+
return abci.Snapshot{}, err
106+
}
107+
return snapshot, nil
108+
}
109+
110+
// List lists available snapshots.
111+
func (s *SnapshotStore) List() ([]*abci.Snapshot, error) {
112+
s.RLock()
113+
defer s.RUnlock()
114+
snapshots := []*abci.Snapshot{}
115+
for _, snapshot := range s.metadata {
116+
s := snapshot // copy to avoid pointer to range variable
117+
snapshots = append(snapshots, &s)
118+
}
119+
return snapshots, nil
120+
}
121+
122+
// LoadChunk loads a snapshot chunk.
123+
func (s *SnapshotStore) LoadChunk(height uint64, format uint32, chunk uint32) ([]byte, error) {
124+
s.RLock()
125+
defer s.RUnlock()
126+
for _, snapshot := range s.metadata {
127+
if snapshot.Height == height && snapshot.Format == format {
128+
bz, err := ioutil.ReadFile(filepath.Join(s.dir, fmt.Sprintf("%v.json", height)))
129+
if err != nil {
130+
return nil, err
131+
}
132+
return byteChunk(bz, chunk), nil
133+
}
134+
}
135+
return nil, nil
136+
}
137+
138+
// byteChunk returns the chunk at a given index from the full byte slice.
139+
func byteChunk(bz []byte, index uint32) []byte {
140+
start := int(index * snapshotChunkSize)
141+
end := int((index + 1) * snapshotChunkSize)
142+
switch {
143+
case start >= len(bz):
144+
return nil
145+
case end >= len(bz):
146+
return bz[start:]
147+
default:
148+
return bz[start:end]
149+
}
150+
}
151+
152+
// byteChunks calculates the number of chunks in the byte slice.
153+
func byteChunks(bz []byte) uint32 {
154+
return uint32(math.Ceil(float64(len(bz)) / snapshotChunkSize))
155+
}

‎test/e2e/app/state.go

+155
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
//nolint: gosec
2+
package main
3+
4+
import (
5+
"crypto/sha256"
6+
"encoding/json"
7+
"errors"
8+
"fmt"
9+
"io/ioutil"
10+
"os"
11+
"sort"
12+
"sync"
13+
)
14+
15+
// State is the application state.
16+
type State struct {
17+
sync.RWMutex
18+
Height uint64
19+
Values map[string]string
20+
Hash []byte
21+
22+
// private fields aren't marshalled to disk.
23+
file string
24+
persistInterval uint64
25+
initialHeight uint64
26+
}
27+
28+
// NewState creates a new state.
29+
func NewState(file string, persistInterval uint64) (*State, error) {
30+
state := &State{
31+
Values: make(map[string]string),
32+
file: file,
33+
persistInterval: persistInterval,
34+
}
35+
state.Hash = hashItems(state.Values)
36+
err := state.load()
37+
switch {
38+
case errors.Is(err, os.ErrNotExist):
39+
case err != nil:
40+
return nil, err
41+
}
42+
return state, nil
43+
}
44+
45+
// load loads state from disk. It does not take out a lock, since it is called
46+
// during construction.
47+
func (s *State) load() error {
48+
bz, err := ioutil.ReadFile(s.file)
49+
if err != nil {
50+
return fmt.Errorf("failed to read state from %q: %w", s.file, err)
51+
}
52+
err = json.Unmarshal(bz, s)
53+
if err != nil {
54+
return fmt.Errorf("invalid state data in %q: %w", s.file, err)
55+
}
56+
return nil
57+
}
58+
59+
// save saves the state to disk. It does not take out a lock since it is called
60+
// internally by Commit which does lock.
61+
func (s *State) save() error {
62+
bz, err := json.Marshal(s)
63+
if err != nil {
64+
return fmt.Errorf("failed to marshal state: %w", err)
65+
}
66+
// We write the state to a separate file and move it to the destination, to
67+
// make it atomic.
68+
newFile := fmt.Sprintf("%v.new", s.file)
69+
err = ioutil.WriteFile(newFile, bz, 0644)
70+
if err != nil {
71+
return fmt.Errorf("failed to write state to %q: %w", s.file, err)
72+
}
73+
return os.Rename(newFile, s.file)
74+
}
75+
76+
// Export exports key/value pairs as JSON, used for state sync snapshots.
77+
func (s *State) Export() ([]byte, error) {
78+
s.RLock()
79+
defer s.RUnlock()
80+
return json.Marshal(s.Values)
81+
}
82+
83+
// Import imports key/value pairs from JSON bytes, used for InitChain.AppStateBytes and
84+
// state sync snapshots. It also saves the state once imported.
85+
func (s *State) Import(height uint64, jsonBytes []byte) error {
86+
s.Lock()
87+
defer s.Unlock()
88+
values := map[string]string{}
89+
err := json.Unmarshal(jsonBytes, &values)
90+
if err != nil {
91+
return fmt.Errorf("failed to decode imported JSON data: %w", err)
92+
}
93+
s.Height = height
94+
s.Values = values
95+
s.Hash = hashItems(values)
96+
return s.save()
97+
}
98+
99+
// Get fetches a value. A missing value is returned as an empty string.
100+
func (s *State) Get(key string) string {
101+
s.RLock()
102+
defer s.RUnlock()
103+
return s.Values[key]
104+
}
105+
106+
// Set sets a value. Setting an empty value is equivalent to deleting it.
107+
func (s *State) Set(key, value string) {
108+
s.Lock()
109+
defer s.Unlock()
110+
if value == "" {
111+
delete(s.Values, key)
112+
} else {
113+
s.Values[key] = value
114+
}
115+
}
116+
117+
// Commit commits the current state.
118+
func (s *State) Commit() (uint64, []byte, error) {
119+
s.Lock()
120+
defer s.Unlock()
121+
s.Hash = hashItems(s.Values)
122+
switch {
123+
case s.Height > 0:
124+
s.Height++
125+
case s.initialHeight > 0:
126+
s.Height = s.initialHeight
127+
default:
128+
s.Height = 1
129+
}
130+
if s.persistInterval > 0 && s.Height%s.persistInterval == 0 {
131+
err := s.save()
132+
if err != nil {
133+
return 0, nil, err
134+
}
135+
}
136+
return s.Height, s.Hash, nil
137+
}
138+
139+
// hashItems hashes a set of key/value items.
140+
func hashItems(items map[string]string) []byte {
141+
keys := make([]string, 0, len(items))
142+
for key := range items {
143+
keys = append(keys, key)
144+
}
145+
sort.Strings(keys)
146+
147+
hasher := sha256.New()
148+
for _, key := range keys {
149+
_, _ = hasher.Write([]byte(key))
150+
_, _ = hasher.Write([]byte{0})
151+
_, _ = hasher.Write([]byte(items[key]))
152+
_, _ = hasher.Write([]byte{0})
153+
}
154+
return hasher.Sum(nil)
155+
}

‎test/e2e/docker/Dockerfile

+32
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
# We need to build in a Linux environment to support C libraries, e.g. RocksDB.
2+
# We use Debian instead of Alpine, so that we can use binary database packages
3+
# instead of spending time compiling them.
4+
FROM golang:1.15
5+
6+
RUN apt-get update -y && apt-get upgrade -y
7+
RUN apt-get install -y libleveldb-dev librocksdb-dev
8+
9+
# Set up build directory /src/tendermint
10+
ENV TENDERMINT_BUILD_OPTIONS badgerdb,boltdb,cleveldb,rocksdb
11+
WORKDIR /src/tendermint
12+
13+
# Fetch dependencies separately (for layer caching)
14+
COPY go.mod go.sum ./
15+
RUN go mod download
16+
17+
# Build Tendermint and install into /usr/bin/tendermint
18+
COPY . .
19+
RUN make build && cp build/tendermint /usr/bin/tendermint
20+
COPY test/e2e/docker/entrypoint* /usr/bin/
21+
RUN cd test/e2e && make app && cp build/app /usr/bin/app
22+
23+
# Set up runtime directory. We don't use a separate runtime image since we need
24+
# e.g. leveldb and rocksdb which are already installed in the build image.
25+
WORKDIR /tendermint
26+
VOLUME /tendermint
27+
ENV TMHOME=/tendermint
28+
29+
EXPOSE 26656 26657 26660
30+
ENTRYPOINT ["/usr/bin/entrypoint"]
31+
CMD ["node"]
32+
STOPSIGNAL SIGTERM

‎test/e2e/docker/entrypoint

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
#!/usr/bin/env bash
2+
3+
# Forcibly remove any stray UNIX sockets left behind from previous runs
4+
rm -rf /var/run/privval.sock /var/run/app.sock
5+
6+
/usr/bin/app /tendermint/config/app.toml &
7+
8+
sleep 1
9+
10+
/usr/bin/tendermint "$@"

‎test/e2e/docker/entrypoint-builtin

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
#!/usr/bin/env bash
2+
3+
# Forcibly remove any stray UNIX sockets left behind from previous runs
4+
rm -rf /var/run/privval.sock /var/run/app.sock
5+
6+
/usr/bin/app /tendermint/config/app.toml

‎test/e2e/networks/ci.toml

+97
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
# This testnet is (will be) run by CI, and attempts to cover a broad range of
2+
# functionality with a single network.
3+
4+
initial_height = 1000
5+
initial_state = { initial01 = "a", initial02 = "b", initial03 = "c" }
6+
7+
[validators]
8+
validator01 = 100
9+
10+
[validator_update.0]
11+
validator01 = 10
12+
validator02 = 20
13+
validator03 = 30
14+
validator04 = 40
15+
16+
[validator_update.1010]
17+
validator05 = 50
18+
19+
# validator03 gets killed and validator05 has lots of perturbations, so weight them low.
20+
[validator_update.1020]
21+
validator01 = 100
22+
validator02 = 100
23+
validator03 = 50
24+
validator04 = 100
25+
validator05 = 50
26+
27+
[node.seed01]
28+
mode = "seed"
29+
30+
[node.validator01]
31+
seeds = ["seed01"]
32+
snapshot_interval = 5
33+
perturb = ["disconnect"]
34+
35+
[node.validator02]
36+
seeds = ["seed01"]
37+
database = "boltdb"
38+
abci_protocol = "tcp"
39+
privval_protocol = "tcp"
40+
persist_interval = 0
41+
# FIXME The WAL gets corrupted when restarted
42+
# https://github.com/tendermint/tendermint/issues/5422
43+
#perturb = ["restart"]
44+
45+
[node.validator03]
46+
seeds = ["seed01"]
47+
database = "badgerdb"
48+
# FIXME Should use grpc, but it has race conditions
49+
# https://github.com/tendermint/tendermint/issues/5439
50+
abci_protocol = "unix"
51+
privval_protocol = "unix"
52+
persist_interval = 3
53+
retain_blocks = 3
54+
# FIXME The WAL gets corrupted when killed
55+
# https://github.com/tendermint/tendermint/issues/5422
56+
#perturb = ["kill"]
57+
58+
[node.validator04]
59+
persistent_peers = ["validator01"]
60+
database = "rocksdb"
61+
abci_protocol = "builtin"
62+
retain_blocks = 1
63+
perturb = ["pause"]
64+
65+
[node.validator05]
66+
start_at = 1005 # Becomes part of the validator set at 1010
67+
seeds = ["seed01"]
68+
database = "cleveldb"
69+
fast_sync = "v0"
70+
# FIXME Should use grpc, but it has race conditions
71+
# https://github.com/tendermint/tendermint/issues/5439
72+
abci_protocol = "tcp"
73+
privval_protocol = "tcp"
74+
# FIXME The WAL gets corrupted when killed
75+
# https://github.com/tendermint/tendermint/issues/5422
76+
#perturb = ["kill", "pause", "disconnect", "restart"]
77+
78+
[node.full01]
79+
start_at = 1010
80+
mode = "full"
81+
# FIXME Should use v1, but it won't catch up since some nodes don't have all blocks
82+
# https://github.com/tendermint/tendermint/issues/5444
83+
fast_sync = "v2"
84+
persistent_peers = ["validator01", "validator02", "validator03", "validator04", "validator05"]
85+
# FIXME The WAL gets corrupted when restarted
86+
# https://github.com/tendermint/tendermint/issues/5422
87+
#perturb = ["restart"]
88+
89+
[node.full02]
90+
start_at = 1015
91+
mode = "full"
92+
fast_sync = "v2"
93+
state_sync = true
94+
seeds = ["seed01"]
95+
# FIXME The WAL gets corrupted when restarted
96+
# https://github.com/tendermint/tendermint/issues/5422
97+
#perturb = ["restart"]

‎test/e2e/networks/simple.toml

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
[node.validator01]
2+
[node.validator02]
3+
[node.validator03]
4+
[node.validator04]

‎test/e2e/networks/single.toml

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
[node.validator]

‎test/e2e/pkg/manifest.go

+127
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
package e2e
2+
3+
import (
4+
"fmt"
5+
6+
"github.com/BurntSushi/toml"
7+
)
8+
9+
// Manifest represents a TOML testnet manifest.
10+
type Manifest struct {
11+
// IPv6 uses IPv6 networking instead of IPv4. Defaults to IPv4.
12+
IPv6 bool `toml:"ipv6"`
13+
14+
// InitialHeight specifies the initial block height, set in genesis. Defaults to 1.
15+
InitialHeight int64 `toml:"initial_height"`
16+
17+
// InitialState is an initial set of key/value pairs for the application,
18+
// set in genesis. Defaults to nothing.
19+
InitialState map[string]string `toml:"initial_state"`
20+
21+
// Validators is the initial validator set in genesis, given as node names
22+
// and power:
23+
//
24+
// validators = { validator01 = 10; validator02 = 20; validator03 = 30 }
25+
//
26+
// Defaults to all nodes that have mode=validator at power 100. Explicitly
27+
// specifying an empty set will start with no validators in genesis, and
28+
// the application must return the validator set in InitChain via the
29+
// setting validator_update.0 (see below).
30+
Validators *map[string]int64
31+
32+
// ValidatorUpdates is a map of heights to validator names and their power,
33+
// and will be returned by the ABCI application. For example, the following
34+
// changes the power of validator01 and validator02 at height 1000:
35+
//
36+
// [validator_update.1000]
37+
// validator01 = 20
38+
// validator02 = 10
39+
//
40+
// Specifying height 0 returns the validator update during InitChain. The
41+
// application returns the validator updates as-is, i.e. removing a
42+
// validator must be done by returning it with power 0, and any validators
43+
// not specified are not changed.
44+
ValidatorUpdates map[string]map[string]int64 `toml:"validator_update"`
45+
46+
// Nodes specifies the network nodes. At least one node must be given.
47+
Nodes map[string]ManifestNode `toml:"node"`
48+
}
49+
50+
// ManifestNode represents a node in a testnet manifest.
51+
type ManifestNode struct {
52+
// Mode specifies the type of node: "validator", "full", or "seed". Defaults to
53+
// "validator". Full nodes do not get a signing key (a dummy key is generated),
54+
// and seed nodes run in seed mode with the PEX reactor enabled.
55+
Mode string
56+
57+
// Seeds is the list of node names to use as P2P seed nodes. Defaults to none.
58+
Seeds []string
59+
60+
// PersistentPeers is a list of node names to maintain persistent P2P
61+
// connections to. If neither seeds nor persistent peers are specified,
62+
// this defaults to all other nodes in the network.
63+
PersistentPeers []string `toml:"persistent_peers"`
64+
65+
// Database specifies the database backend: "goleveldb", "cleveldb",
66+
// "rocksdb", "boltdb", or "badgerdb". Defaults to goleveldb.
67+
Database string
68+
69+
// ABCIProtocol specifies the protocol used to communicate with the ABCI
70+
// application: "unix", "tcp", "grpc", or "builtin". Defaults to unix.
71+
// builtin will build a complete Tendermint node into the application and
72+
// launch it instead of launching a separate Tendermint process.
73+
ABCIProtocol string `toml:"abci_protocol"`
74+
75+
// PrivvalProtocol specifies the protocol used to sign consensus messages:
76+
// "file", "unix", or "tcp". Defaults to "file". For unix and tcp, the ABCI
77+
// application will launch a remote signer client in a separate goroutine.
78+
// Only nodes with mode=validator will actually make use of this.
79+
PrivvalProtocol string `toml:"privval_protocol"`
80+
81+
// StartAt specifies the block height at which the node will be started. The
82+
// runner will wait for the network to reach at least this block height.
83+
StartAt int64 `toml:"start_at"`
84+
85+
// FastSync specifies the fast sync mode: "" (disable), "v0", "v1", or "v2".
86+
// Defaults to disabled.
87+
FastSync string `toml:"fast_sync"`
88+
89+
// StateSync enables state sync. The runner automatically configures trusted
90+
// block hashes and RPC servers. At least one node in the network must have
91+
// SnapshotInterval set to non-zero, and the state syncing node must have
92+
// StartAt set to an appropriate height where a snapshot is available.
93+
StateSync bool `toml:"state_sync"`
94+
95+
// PersistInterval specifies the height interval at which the application
96+
// will persist state to disk. Defaults to 1 (every height), setting this to
97+
// 0 disables state persistence.
98+
PersistInterval *uint64 `toml:"persist_interval"`
99+
100+
// SnapshotInterval specifies the height interval at which the application
101+
// will take state sync snapshots. Defaults to 0 (disabled).
102+
SnapshotInterval uint64 `toml:"snapshot_interval"`
103+
104+
// RetainBlocks specifies the number of recent blocks to retain. Defaults to
105+
// 0, which retains all blocks. Must be greater that PersistInterval and
106+
// SnapshotInterval.
107+
RetainBlocks uint64 `toml:"retain_blocks"`
108+
109+
// Perturb lists perturbations to apply to the node after it has been
110+
// started and synced with the network:
111+
//
112+
// disconnect: temporarily disconnects the node from the network
113+
// kill: kills the node with SIGKILL then restarts it
114+
// pause: temporarily pauses (freezes) the node
115+
// restart: restarts the node, shutting it down with SIGTERM
116+
Perturb []string
117+
}
118+
119+
// LoadManifest loads a testnet manifest from a file.
120+
func LoadManifest(file string) (Manifest, error) {
121+
manifest := Manifest{}
122+
_, err := toml.DecodeFile(file, &manifest)
123+
if err != nil {
124+
return manifest, fmt.Errorf("failed to load testnet manifest %q: %w", file, err)
125+
}
126+
return manifest, nil
127+
}

‎test/e2e/pkg/testnet.go

+470
Large diffs are not rendered by default.

‎test/e2e/runner/cleanup.go

+35
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package main
2+
3+
import (
4+
"errors"
5+
"fmt"
6+
"os"
7+
8+
e2e "github.com/tendermint/tendermint/test/e2e/pkg"
9+
)
10+
11+
// Cleanup removes the Docker Compose containers and testnet directory.
12+
func Cleanup(testnet *e2e.Testnet) error {
13+
if testnet.Dir == "" {
14+
return errors.New("no directory set")
15+
}
16+
_, err := os.Stat(testnet.Dir)
17+
if os.IsNotExist(err) {
18+
return nil
19+
} else if err != nil {
20+
return err
21+
}
22+
23+
logger.Info("Removing Docker containers and networks")
24+
err = execCompose(testnet.Dir, "down")
25+
if err != nil {
26+
return err
27+
}
28+
29+
logger.Info(fmt.Sprintf("Removing testnet directory %q", testnet.Dir))
30+
err = os.RemoveAll(testnet.Dir)
31+
if err != nil {
32+
return err
33+
}
34+
return nil
35+
}

‎test/e2e/runner/exec.go

+50
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
//nolint: gosec
2+
package main
3+
4+
import (
5+
"fmt"
6+
"os"
7+
osexec "os/exec"
8+
"path/filepath"
9+
)
10+
11+
// execute executes a shell command.
12+
func exec(args ...string) error {
13+
cmd := osexec.Command(args[0], args[1:]...)
14+
out, err := cmd.CombinedOutput()
15+
switch err := err.(type) {
16+
case nil:
17+
return nil
18+
case *osexec.ExitError:
19+
return fmt.Errorf("failed to run %q:\n%v", args, string(out))
20+
default:
21+
return err
22+
}
23+
}
24+
25+
// execVerbose executes a shell command while displaying its output.
26+
func execVerbose(args ...string) error {
27+
cmd := osexec.Command(args[0], args[1:]...)
28+
cmd.Stdout = os.Stdout
29+
cmd.Stderr = os.Stderr
30+
return cmd.Run()
31+
}
32+
33+
// execCompose runs a Docker Compose command for a testnet.
34+
func execCompose(dir string, args ...string) error {
35+
return exec(append(
36+
[]string{"docker-compose", "-f", filepath.Join(dir, "docker-compose.yml")},
37+
args...)...)
38+
}
39+
40+
// execComposeVerbose runs a Docker Compose command for a testnet and displays its output.
41+
func execComposeVerbose(dir string, args ...string) error {
42+
return execVerbose(append(
43+
[]string{"docker-compose", "-f", filepath.Join(dir, "docker-compose.yml")},
44+
args...)...)
45+
}
46+
47+
// execDocker runs a Docker command.
48+
func execDocker(args ...string) error {
49+
return exec(append([]string{"docker"}, args...)...)
50+
}

‎test/e2e/runner/load.go

+106
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
package main
2+
3+
import (
4+
"context"
5+
"crypto/rand"
6+
"errors"
7+
"fmt"
8+
"math"
9+
"time"
10+
11+
rpchttp "github.com/tendermint/tendermint/rpc/client/http"
12+
e2e "github.com/tendermint/tendermint/test/e2e/pkg"
13+
"github.com/tendermint/tendermint/types"
14+
)
15+
16+
// Load generates transactions against the network until the given
17+
// context is cancelled.
18+
func Load(ctx context.Context, testnet *e2e.Testnet) error {
19+
concurrency := 50
20+
initialTimeout := 1 * time.Minute
21+
stallTimeout := 15 * time.Second
22+
23+
chTx := make(chan types.Tx)
24+
chSuccess := make(chan types.Tx)
25+
ctx, cancel := context.WithCancel(ctx)
26+
defer cancel()
27+
28+
// Spawn job generator and processors.
29+
logger.Info("Starting transaction load...")
30+
started := time.Now()
31+
32+
go loadGenerate(ctx, chTx)
33+
34+
for w := 0; w < concurrency; w++ {
35+
go loadProcess(ctx, testnet, chTx, chSuccess)
36+
}
37+
38+
// Monitor successful transactions, and abort on stalls.
39+
success := 0
40+
timeout := initialTimeout
41+
for {
42+
select {
43+
case <-chSuccess:
44+
success++
45+
timeout = stallTimeout
46+
case <-time.After(timeout):
47+
return fmt.Errorf("unable to submit transactions for %v", timeout)
48+
case <-ctx.Done():
49+
if success == 0 {
50+
return errors.New("failed to submit any transactions")
51+
}
52+
logger.Info(fmt.Sprintf("Ending transaction load after %v txs (%.1f tx/s)...",
53+
success, float64(success)/time.Since(started).Seconds()))
54+
return nil
55+
}
56+
}
57+
}
58+
59+
// loadGenerate generates jobs until the context is cancelled
60+
func loadGenerate(ctx context.Context, chTx chan<- types.Tx) {
61+
for i := 0; i < math.MaxInt64; i++ {
62+
// We keep generating the same 1000 keys over and over, with different values.
63+
// This gives a reasonable load without putting too much data in the app.
64+
id := i % 1000
65+
66+
bz := make([]byte, 2048) // 4kb hex-encoded
67+
_, err := rand.Read(bz)
68+
if err != nil {
69+
panic(fmt.Sprintf("Failed to read random bytes: %v", err))
70+
}
71+
tx := types.Tx(fmt.Sprintf("load-%X=%x", id, bz))
72+
73+
select {
74+
case chTx <- tx:
75+
time.Sleep(10 * time.Millisecond)
76+
case <-ctx.Done():
77+
close(chTx)
78+
return
79+
}
80+
}
81+
}
82+
83+
// loadProcess processes transactions
84+
func loadProcess(ctx context.Context, testnet *e2e.Testnet, chTx <-chan types.Tx, chSuccess chan<- types.Tx) {
85+
// Each worker gets its own client to each node, which allows for some
86+
// concurrency while still bounding it.
87+
clients := map[string]*rpchttp.HTTP{}
88+
89+
var err error
90+
for tx := range chTx {
91+
node := testnet.RandomNode()
92+
client, ok := clients[node.Name]
93+
if !ok {
94+
client, err = node.Client()
95+
if err != nil {
96+
continue
97+
}
98+
clients[node.Name] = client
99+
}
100+
_, err = client.BroadcastTxCommit(ctx, tx)
101+
if err != nil {
102+
continue
103+
}
104+
chSuccess <- tx
105+
}
106+
}

‎test/e2e/runner/main.go

+184
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
package main
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"os"
7+
8+
"github.com/spf13/cobra"
9+
"github.com/tendermint/tendermint/libs/log"
10+
e2e "github.com/tendermint/tendermint/test/e2e/pkg"
11+
)
12+
13+
var logger = log.NewTMLogger(log.NewSyncWriter(os.Stdout))
14+
15+
func main() {
16+
NewCLI().Run()
17+
}
18+
19+
// CLI is the Cobra-based command-line interface.
20+
type CLI struct {
21+
root *cobra.Command
22+
testnet *e2e.Testnet
23+
}
24+
25+
// NewCLI sets up the CLI.
26+
func NewCLI() *CLI {
27+
cli := &CLI{}
28+
cli.root = &cobra.Command{
29+
Use: "runner",
30+
Short: "End-to-end test runner",
31+
SilenceUsage: true,
32+
SilenceErrors: true, // we'll output them ourselves in Run()
33+
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
34+
file, err := cmd.Flags().GetString("file")
35+
if err != nil {
36+
return err
37+
}
38+
testnet, err := e2e.LoadTestnet(file)
39+
if err != nil {
40+
return err
41+
}
42+
43+
cli.testnet = testnet
44+
return nil
45+
},
46+
RunE: func(cmd *cobra.Command, args []string) error {
47+
if err := Cleanup(cli.testnet); err != nil {
48+
return err
49+
}
50+
if err := Setup(cli.testnet); err != nil {
51+
return err
52+
}
53+
54+
chLoadResult := make(chan error)
55+
ctx, loadCancel := context.WithCancel(context.Background())
56+
defer loadCancel()
57+
go func() {
58+
err := Load(ctx, cli.testnet)
59+
if err != nil {
60+
logger.Error(fmt.Sprintf("Transaction load failed: %v", err.Error()))
61+
}
62+
chLoadResult <- err
63+
}()
64+
65+
if err := Start(cli.testnet); err != nil {
66+
return err
67+
}
68+
if err := Perturb(cli.testnet); err != nil {
69+
return err
70+
}
71+
if err := Wait(cli.testnet, 5); err != nil { // wait for network to settle
72+
return err
73+
}
74+
75+
loadCancel()
76+
if err := <-chLoadResult; err != nil {
77+
return err
78+
}
79+
if err := Wait(cli.testnet, 3); err != nil { // wait for last txs to commit
80+
return err
81+
}
82+
if err := Test(cli.testnet); err != nil {
83+
return err
84+
}
85+
if err := Cleanup(cli.testnet); err != nil {
86+
return err
87+
}
88+
return nil
89+
},
90+
}
91+
92+
cli.root.PersistentFlags().StringP("file", "f", "", "Testnet TOML manifest")
93+
_ = cli.root.MarkPersistentFlagRequired("file")
94+
95+
cli.root.AddCommand(&cobra.Command{
96+
Use: "setup",
97+
Short: "Generates the testnet directory and configuration",
98+
RunE: func(cmd *cobra.Command, args []string) error {
99+
return Setup(cli.testnet)
100+
},
101+
})
102+
103+
cli.root.AddCommand(&cobra.Command{
104+
Use: "start",
105+
Short: "Starts the Docker testnet, waiting for nodes to become available",
106+
RunE: func(cmd *cobra.Command, args []string) error {
107+
_, err := os.Stat(cli.testnet.Dir)
108+
if os.IsNotExist(err) {
109+
err = Setup(cli.testnet)
110+
}
111+
if err != nil {
112+
return err
113+
}
114+
return Start(cli.testnet)
115+
},
116+
})
117+
118+
cli.root.AddCommand(&cobra.Command{
119+
Use: "perturb",
120+
Short: "Perturbs the Docker testnet, e.g. by restarting or disconnecting nodes",
121+
RunE: func(cmd *cobra.Command, args []string) error {
122+
return Perturb(cli.testnet)
123+
},
124+
})
125+
126+
cli.root.AddCommand(&cobra.Command{
127+
Use: "wait",
128+
Short: "Waits for a few blocks to be produced and all nodes to catch up",
129+
RunE: func(cmd *cobra.Command, args []string) error {
130+
return Wait(cli.testnet, 5)
131+
},
132+
})
133+
134+
cli.root.AddCommand(&cobra.Command{
135+
Use: "stop",
136+
Short: "Stops the Docker testnet",
137+
RunE: func(cmd *cobra.Command, args []string) error {
138+
logger.Info("Stopping testnet")
139+
return execCompose(cli.testnet.Dir, "down")
140+
},
141+
})
142+
143+
cli.root.AddCommand(&cobra.Command{
144+
Use: "load",
145+
Short: "Generates transaction load until the command is cancelled",
146+
RunE: func(cmd *cobra.Command, args []string) error {
147+
return Load(context.Background(), cli.testnet)
148+
},
149+
})
150+
151+
cli.root.AddCommand(&cobra.Command{
152+
Use: "test",
153+
Short: "Runs test cases against a running testnet",
154+
RunE: func(cmd *cobra.Command, args []string) error {
155+
return Test(cli.testnet)
156+
},
157+
})
158+
159+
cli.root.AddCommand(&cobra.Command{
160+
Use: "cleanup",
161+
Short: "Removes the testnet directory",
162+
RunE: func(cmd *cobra.Command, args []string) error {
163+
return Cleanup(cli.testnet)
164+
},
165+
})
166+
167+
cli.root.AddCommand(&cobra.Command{
168+
Use: "logs",
169+
Short: "Shows the testnet logs",
170+
RunE: func(cmd *cobra.Command, args []string) error {
171+
return execComposeVerbose(cli.testnet.Dir, "logs", "--follow")
172+
},
173+
})
174+
175+
return cli
176+
}
177+
178+
// Run runs the CLI.
179+
func (cli *CLI) Run() {
180+
if err := cli.root.Execute(); err != nil {
181+
logger.Error(err.Error())
182+
os.Exit(1)
183+
}
184+
}

‎test/e2e/runner/perturb.go

+75
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
package main
2+
3+
import (
4+
"fmt"
5+
"time"
6+
7+
rpctypes "github.com/tendermint/tendermint/rpc/core/types"
8+
e2e "github.com/tendermint/tendermint/test/e2e/pkg"
9+
)
10+
11+
// Perturbs a running testnet.
12+
func Perturb(testnet *e2e.Testnet) error {
13+
for _, node := range testnet.Nodes {
14+
for _, perturbation := range node.Perturbations {
15+
_, err := PerturbNode(node, perturbation)
16+
if err != nil {
17+
return err
18+
}
19+
time.Sleep(3 * time.Second) // give network some time to recover between each
20+
}
21+
}
22+
return nil
23+
}
24+
25+
// PerturbNode perturbs a node with a given perturbation, returning its status
26+
// after recovering.
27+
func PerturbNode(node *e2e.Node, perturbation e2e.Perturbation) (*rpctypes.ResultStatus, error) {
28+
testnet := node.Testnet
29+
switch perturbation {
30+
case e2e.PerturbationDisconnect:
31+
logger.Info(fmt.Sprintf("Disconnecting node %v...", node.Name))
32+
if err := execDocker("network", "disconnect", testnet.Name+"_"+testnet.Name, node.Name); err != nil {
33+
return nil, err
34+
}
35+
time.Sleep(10 * time.Second)
36+
if err := execDocker("network", "connect", testnet.Name+"_"+testnet.Name, node.Name); err != nil {
37+
return nil, err
38+
}
39+
40+
case e2e.PerturbationKill:
41+
logger.Info(fmt.Sprintf("Killing node %v...", node.Name))
42+
if err := execCompose(testnet.Dir, "kill", "-s", "SIGKILL", node.Name); err != nil {
43+
return nil, err
44+
}
45+
if err := execCompose(testnet.Dir, "start", node.Name); err != nil {
46+
return nil, err
47+
}
48+
49+
case e2e.PerturbationPause:
50+
logger.Info(fmt.Sprintf("Pausing node %v...", node.Name))
51+
if err := execCompose(testnet.Dir, "pause", node.Name); err != nil {
52+
return nil, err
53+
}
54+
time.Sleep(10 * time.Second)
55+
if err := execCompose(testnet.Dir, "unpause", node.Name); err != nil {
56+
return nil, err
57+
}
58+
59+
case e2e.PerturbationRestart:
60+
logger.Info(fmt.Sprintf("Restarting node %v...", node.Name))
61+
if err := execCompose(testnet.Dir, "restart", node.Name); err != nil {
62+
return nil, err
63+
}
64+
65+
default:
66+
return nil, fmt.Errorf("unexpected perturbation %q", perturbation)
67+
}
68+
69+
status, err := waitForNode(node, 0, 10*time.Second)
70+
if err != nil {
71+
return nil, err
72+
}
73+
logger.Info(fmt.Sprintf("Node %v recovered at height %v", node.Name, status.SyncInfo.LatestBlockHeight))
74+
return status, nil
75+
}

‎test/e2e/runner/rpc.go

+107
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
package main
2+
3+
import (
4+
"context"
5+
"errors"
6+
"fmt"
7+
"time"
8+
9+
rpchttp "github.com/tendermint/tendermint/rpc/client/http"
10+
rpctypes "github.com/tendermint/tendermint/rpc/core/types"
11+
e2e "github.com/tendermint/tendermint/test/e2e/pkg"
12+
"github.com/tendermint/tendermint/types"
13+
)
14+
15+
// waitForHeight waits for the network to reach a certain height (or above),
16+
// returning the highest height seen. Errors if the network is not making
17+
// progress at all.
18+
func waitForHeight(testnet *e2e.Testnet, height int64) (*types.Block, *types.BlockID, error) {
19+
var (
20+
err error
21+
maxResult *rpctypes.ResultBlock
22+
clients = map[string]*rpchttp.HTTP{}
23+
lastIncrease = time.Now()
24+
)
25+
26+
for {
27+
for _, node := range testnet.Nodes {
28+
if node.Mode == e2e.ModeSeed {
29+
continue
30+
}
31+
client, ok := clients[node.Name]
32+
if !ok {
33+
client, err = node.Client()
34+
if err != nil {
35+
continue
36+
}
37+
clients[node.Name] = client
38+
}
39+
40+
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
41+
defer cancel()
42+
result, err := client.Block(ctx, nil)
43+
if err != nil {
44+
continue
45+
}
46+
if result.Block != nil && (maxResult == nil || result.Block.Height >= maxResult.Block.Height) {
47+
maxResult = result
48+
lastIncrease = time.Now()
49+
}
50+
if maxResult != nil && maxResult.Block.Height >= height {
51+
return maxResult.Block, &maxResult.BlockID, nil
52+
}
53+
}
54+
55+
if len(clients) == 0 {
56+
return nil, nil, errors.New("unable to connect to any network nodes")
57+
}
58+
if time.Since(lastIncrease) >= 10*time.Second {
59+
if maxResult == nil {
60+
return nil, nil, errors.New("chain stalled at unknown height")
61+
}
62+
return nil, nil, fmt.Errorf("chain stalled at height %v", maxResult.Block.Height)
63+
}
64+
time.Sleep(1 * time.Second)
65+
}
66+
}
67+
68+
// waitForNode waits for a node to become available and catch up to the given block height.
69+
func waitForNode(node *e2e.Node, height int64, timeout time.Duration) (*rpctypes.ResultStatus, error) {
70+
client, err := node.Client()
71+
if err != nil {
72+
return nil, err
73+
}
74+
ctx, cancel := context.WithTimeout(context.Background(), timeout)
75+
defer cancel()
76+
for {
77+
status, err := client.Status(ctx)
78+
switch {
79+
case errors.Is(err, context.DeadlineExceeded):
80+
return nil, fmt.Errorf("timed out waiting for %v to reach height %v", node.Name, height)
81+
case errors.Is(err, context.Canceled):
82+
return nil, err
83+
case err == nil && status.SyncInfo.LatestBlockHeight >= height:
84+
return status, nil
85+
}
86+
87+
time.Sleep(200 * time.Millisecond)
88+
}
89+
}
90+
91+
// waitForAllNodes waits for all nodes to become available and catch up to the given block height.
92+
func waitForAllNodes(testnet *e2e.Testnet, height int64, timeout time.Duration) (int64, error) {
93+
lastHeight := int64(0)
94+
for _, node := range testnet.Nodes {
95+
if node.Mode == e2e.ModeSeed {
96+
continue
97+
}
98+
status, err := waitForNode(node, height, 20*time.Second)
99+
if err != nil {
100+
return 0, err
101+
}
102+
if status.SyncInfo.LatestBlockHeight > lastHeight {
103+
lastHeight = status.SyncInfo.LatestBlockHeight
104+
}
105+
}
106+
return lastHeight, nil
107+
}

‎test/e2e/runner/setup.go

+360
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,360 @@
1+
// nolint: gosec
2+
package main
3+
4+
import (
5+
"bytes"
6+
"encoding/base64"
7+
"encoding/json"
8+
"errors"
9+
"fmt"
10+
"io/ioutil"
11+
"os"
12+
"path/filepath"
13+
"regexp"
14+
"sort"
15+
"strings"
16+
"text/template"
17+
"time"
18+
19+
"github.com/BurntSushi/toml"
20+
"github.com/tendermint/tendermint/config"
21+
"github.com/tendermint/tendermint/crypto/ed25519"
22+
"github.com/tendermint/tendermint/p2p"
23+
"github.com/tendermint/tendermint/privval"
24+
e2e "github.com/tendermint/tendermint/test/e2e/pkg"
25+
"github.com/tendermint/tendermint/types"
26+
)
27+
28+
const (
29+
AppAddressTCP = "tcp://127.0.0.1:30000"
30+
AppAddressUNIX = "unix:///var/run/app.sock"
31+
32+
PrivvalAddressTCP = "tcp://0.0.0.0:27559"
33+
PrivvalAddressUNIX = "unix:///var/run/privval.sock"
34+
PrivvalKeyFile = "config/priv_validator_key.json"
35+
PrivvalStateFile = "data/priv_validator_state.json"
36+
PrivvalDummyKeyFile = "config/dummy_validator_key.json"
37+
PrivvalDummyStateFile = "data/dummy_validator_state.json"
38+
)
39+
40+
// Setup sets up the testnet configuration.
41+
func Setup(testnet *e2e.Testnet) error {
42+
logger.Info(fmt.Sprintf("Generating testnet files in %q", testnet.Dir))
43+
44+
err := os.MkdirAll(testnet.Dir, os.ModePerm)
45+
if err != nil {
46+
return err
47+
}
48+
49+
compose, err := MakeDockerCompose(testnet)
50+
if err != nil {
51+
return err
52+
}
53+
err = ioutil.WriteFile(filepath.Join(testnet.Dir, "docker-compose.yml"), compose, 0644)
54+
if err != nil {
55+
return err
56+
}
57+
58+
genesis, err := MakeGenesis(testnet)
59+
if err != nil {
60+
return err
61+
}
62+
63+
for _, node := range testnet.Nodes {
64+
nodeDir := filepath.Join(testnet.Dir, node.Name)
65+
dirs := []string{
66+
filepath.Join(nodeDir, "config"),
67+
filepath.Join(nodeDir, "data"),
68+
filepath.Join(nodeDir, "data", "app"),
69+
}
70+
for _, dir := range dirs {
71+
err := os.MkdirAll(dir, 0755)
72+
if err != nil {
73+
return err
74+
}
75+
}
76+
77+
err = genesis.SaveAs(filepath.Join(nodeDir, "config", "genesis.json"))
78+
if err != nil {
79+
return err
80+
}
81+
82+
cfg, err := MakeConfig(node)
83+
if err != nil {
84+
return err
85+
}
86+
config.WriteConfigFile(filepath.Join(nodeDir, "config", "config.toml"), cfg) // panics
87+
88+
appCfg, err := MakeAppConfig(node)
89+
if err != nil {
90+
return err
91+
}
92+
err = ioutil.WriteFile(filepath.Join(nodeDir, "config", "app.toml"), appCfg, 0644)
93+
if err != nil {
94+
return err
95+
}
96+
97+
err = (&p2p.NodeKey{PrivKey: node.Key}).SaveAs(filepath.Join(nodeDir, "config", "node_key.json"))
98+
if err != nil {
99+
return err
100+
}
101+
102+
(privval.NewFilePV(node.Key,
103+
filepath.Join(nodeDir, PrivvalKeyFile),
104+
filepath.Join(nodeDir, PrivvalStateFile),
105+
)).Save()
106+
107+
// Set up a dummy validator. Tendermint requires a file PV even when not used, so we
108+
// give it a dummy such that it will fail if it actually tries to use it.
109+
(privval.NewFilePV(ed25519.GenPrivKey(),
110+
filepath.Join(nodeDir, PrivvalDummyKeyFile),
111+
filepath.Join(nodeDir, PrivvalDummyStateFile),
112+
)).Save()
113+
}
114+
115+
return nil
116+
}
117+
118+
// MakeDockerCompose generates a Docker Compose config for a testnet.
119+
func MakeDockerCompose(testnet *e2e.Testnet) ([]byte, error) {
120+
// Must use version 2 Docker Compose format, to support IPv6.
121+
tmpl, err := template.New("docker-compose").Parse(`version: '2.4'
122+
123+
networks:
124+
{{ .Name }}:
125+
driver: bridge
126+
{{- if .IPv6 }}
127+
enable_ipv6: true
128+
{{- end }}
129+
ipam:
130+
driver: default
131+
config:
132+
- subnet: {{ .IP }}
133+
134+
services:
135+
{{- range .Nodes }}
136+
{{ .Name }}:
137+
container_name: {{ .Name }}
138+
image: tendermint/e2e-node
139+
{{- if eq .ABCIProtocol "builtin" }}
140+
entrypoint: /usr/bin/entrypoint-builtin
141+
{{- end }}
142+
init: true
143+
ports:
144+
- 26656
145+
- {{ if .ProxyPort }}{{ .ProxyPort }}:{{ end }}26657
146+
volumes:
147+
- ./{{ .Name }}:/tendermint
148+
networks:
149+
{{ $.Name }}:
150+
ipv{{ if $.IPv6 }}6{{ else }}4{{ end}}_address: {{ .IP }}
151+
152+
{{end}}`)
153+
if err != nil {
154+
return nil, err
155+
}
156+
var buf bytes.Buffer
157+
err = tmpl.Execute(&buf, testnet)
158+
if err != nil {
159+
return nil, err
160+
}
161+
return buf.Bytes(), nil
162+
}
163+
164+
// MakeGenesis generates a genesis document.
165+
func MakeGenesis(testnet *e2e.Testnet) (types.GenesisDoc, error) {
166+
genesis := types.GenesisDoc{
167+
GenesisTime: time.Now(),
168+
ChainID: testnet.Name,
169+
ConsensusParams: types.DefaultConsensusParams(),
170+
InitialHeight: testnet.InitialHeight,
171+
}
172+
for validator, power := range testnet.Validators {
173+
genesis.Validators = append(genesis.Validators, types.GenesisValidator{
174+
Name: validator.Name,
175+
Address: validator.Key.PubKey().Address(),
176+
PubKey: validator.Key.PubKey(),
177+
Power: power,
178+
})
179+
}
180+
// The validator set will be sorted internally by Tendermint ranked by power,
181+
// but we sort it here as well so that all genesis files are identical.
182+
sort.Slice(genesis.Validators, func(i, j int) bool {
183+
return strings.Compare(genesis.Validators[i].Name, genesis.Validators[j].Name) == -1
184+
})
185+
if len(testnet.InitialState) > 0 {
186+
appState, err := json.Marshal(testnet.InitialState)
187+
if err != nil {
188+
return genesis, err
189+
}
190+
genesis.AppState = appState
191+
}
192+
return genesis, genesis.ValidateAndComplete()
193+
}
194+
195+
// MakeConfig generates a Tendermint config for a node.
196+
func MakeConfig(node *e2e.Node) (*config.Config, error) {
197+
cfg := config.DefaultConfig()
198+
cfg.Moniker = node.Name
199+
cfg.ProxyApp = AppAddressTCP
200+
cfg.RPC.ListenAddress = "tcp://0.0.0.0:26657"
201+
cfg.P2P.ExternalAddress = fmt.Sprintf("tcp://%v", node.AddressP2P(false))
202+
cfg.P2P.AddrBookStrict = false
203+
cfg.DBBackend = node.Database
204+
cfg.StateSync.DiscoveryTime = 5 * time.Second
205+
206+
switch node.ABCIProtocol {
207+
case e2e.ProtocolUNIX:
208+
cfg.ProxyApp = AppAddressUNIX
209+
case e2e.ProtocolTCP:
210+
cfg.ProxyApp = AppAddressTCP
211+
case e2e.ProtocolGRPC:
212+
cfg.ProxyApp = AppAddressTCP
213+
cfg.ABCI = "grpc"
214+
case e2e.ProtocolBuiltin:
215+
cfg.ProxyApp = ""
216+
cfg.ABCI = ""
217+
default:
218+
return nil, fmt.Errorf("unexpected ABCI protocol setting %q", node.ABCIProtocol)
219+
}
220+
221+
// Tendermint errors if it does not have a privval key set up, regardless of whether
222+
// it's actually needed (e.g. for remote KMS or non-validators). We set up a dummy
223+
// key here by default, and use the real key for actual validators that should use
224+
// the file privval.
225+
cfg.PrivValidatorListenAddr = ""
226+
cfg.PrivValidatorKey = PrivvalDummyKeyFile
227+
cfg.PrivValidatorState = PrivvalDummyStateFile
228+
229+
switch node.Mode {
230+
case e2e.ModeValidator:
231+
switch node.PrivvalProtocol {
232+
case e2e.ProtocolFile:
233+
cfg.PrivValidatorKey = PrivvalKeyFile
234+
cfg.PrivValidatorState = PrivvalStateFile
235+
case e2e.ProtocolUNIX:
236+
cfg.PrivValidatorListenAddr = PrivvalAddressUNIX
237+
case e2e.ProtocolTCP:
238+
cfg.PrivValidatorListenAddr = PrivvalAddressTCP
239+
default:
240+
return nil, fmt.Errorf("invalid privval protocol setting %q", node.PrivvalProtocol)
241+
}
242+
case e2e.ModeSeed:
243+
cfg.P2P.SeedMode = true
244+
cfg.P2P.PexReactor = true
245+
case e2e.ModeFull:
246+
// Don't need to do anything, since we're using a dummy privval key by default.
247+
default:
248+
return nil, fmt.Errorf("unexpected mode %q", node.Mode)
249+
}
250+
251+
if node.FastSync == "" {
252+
cfg.FastSyncMode = false
253+
} else {
254+
cfg.FastSync.Version = node.FastSync
255+
}
256+
257+
if node.StateSync {
258+
cfg.StateSync.Enable = true
259+
cfg.StateSync.RPCServers = []string{}
260+
for _, peer := range node.Testnet.ArchiveNodes() {
261+
if peer.Name == node.Name {
262+
continue
263+
}
264+
cfg.StateSync.RPCServers = append(cfg.StateSync.RPCServers, peer.AddressRPC())
265+
}
266+
if len(cfg.StateSync.RPCServers) < 2 {
267+
return nil, errors.New("unable to find 2 suitable state sync RPC servers")
268+
}
269+
}
270+
271+
cfg.P2P.Seeds = ""
272+
for _, seed := range node.Seeds {
273+
if len(cfg.P2P.Seeds) > 0 {
274+
cfg.P2P.Seeds += ","
275+
}
276+
cfg.P2P.Seeds += seed.AddressP2P(true)
277+
}
278+
cfg.P2P.PersistentPeers = ""
279+
for _, peer := range node.PersistentPeers {
280+
if len(cfg.P2P.PersistentPeers) > 0 {
281+
cfg.P2P.PersistentPeers += ","
282+
}
283+
cfg.P2P.PersistentPeers += peer.AddressP2P(true)
284+
}
285+
return cfg, nil
286+
}
287+
288+
// MakeAppConfig generates an ABCI application config for a node.
289+
func MakeAppConfig(node *e2e.Node) ([]byte, error) {
290+
cfg := map[string]interface{}{
291+
"chain_id": node.Testnet.Name,
292+
"dir": "data/app",
293+
"listen": AppAddressUNIX,
294+
"protocol": "socket",
295+
"persist_interval": node.PersistInterval,
296+
"snapshot_interval": node.SnapshotInterval,
297+
"retain_blocks": node.RetainBlocks,
298+
}
299+
switch node.ABCIProtocol {
300+
case e2e.ProtocolUNIX:
301+
cfg["listen"] = AppAddressUNIX
302+
case e2e.ProtocolTCP:
303+
cfg["listen"] = AppAddressTCP
304+
case e2e.ProtocolGRPC:
305+
cfg["listen"] = AppAddressTCP
306+
cfg["protocol"] = "grpc"
307+
case e2e.ProtocolBuiltin:
308+
delete(cfg, "listen")
309+
cfg["protocol"] = "builtin"
310+
default:
311+
return nil, fmt.Errorf("unexpected ABCI protocol setting %q", node.ABCIProtocol)
312+
}
313+
switch node.PrivvalProtocol {
314+
case e2e.ProtocolFile:
315+
case e2e.ProtocolTCP:
316+
cfg["privval_server"] = PrivvalAddressTCP
317+
cfg["privval_key"] = PrivvalKeyFile
318+
cfg["privval_state"] = PrivvalStateFile
319+
case e2e.ProtocolUNIX:
320+
cfg["privval_server"] = PrivvalAddressUNIX
321+
cfg["privval_key"] = PrivvalKeyFile
322+
cfg["privval_state"] = PrivvalStateFile
323+
default:
324+
return nil, fmt.Errorf("unexpected privval protocol setting %q", node.PrivvalProtocol)
325+
}
326+
327+
if len(node.Testnet.ValidatorUpdates) > 0 {
328+
validatorUpdates := map[string]map[string]int64{}
329+
for height, validators := range node.Testnet.ValidatorUpdates {
330+
updateVals := map[string]int64{}
331+
for node, power := range validators {
332+
updateVals[base64.StdEncoding.EncodeToString(node.Key.PubKey().Bytes())] = power
333+
}
334+
validatorUpdates[fmt.Sprintf("%v", height)] = updateVals
335+
}
336+
cfg["validator_update"] = validatorUpdates
337+
}
338+
339+
var buf bytes.Buffer
340+
err := toml.NewEncoder(&buf).Encode(cfg)
341+
if err != nil {
342+
return nil, fmt.Errorf("failed to generate app config: %w", err)
343+
}
344+
return buf.Bytes(), nil
345+
}
346+
347+
// UpdateConfigStateSync updates the state sync config for a node.
348+
func UpdateConfigStateSync(node *e2e.Node, height int64, hash []byte) error {
349+
cfgPath := filepath.Join(node.Testnet.Dir, node.Name, "config", "config.toml")
350+
351+
// FIXME Apparently there's no function to simply load a config file without
352+
// involving the entire Viper apparatus, so we'll just resort to regexps.
353+
bz, err := ioutil.ReadFile(cfgPath)
354+
if err != nil {
355+
return err
356+
}
357+
bz = regexp.MustCompile(`(?m)^trust_height =.*`).ReplaceAll(bz, []byte(fmt.Sprintf(`trust_height = %v`, height)))
358+
bz = regexp.MustCompile(`(?m)^trust_hash =.*`).ReplaceAll(bz, []byte(fmt.Sprintf(`trust_hash = "%X"`, hash)))
359+
return ioutil.WriteFile(cfgPath, bz, 0644)
360+
}

‎test/e2e/runner/start.go

+68
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
package main
2+
3+
import (
4+
"fmt"
5+
"sort"
6+
"time"
7+
8+
e2e "github.com/tendermint/tendermint/test/e2e/pkg"
9+
)
10+
11+
func Start(testnet *e2e.Testnet) error {
12+
13+
// Sort nodes by starting order
14+
nodeQueue := testnet.Nodes
15+
sort.SliceStable(nodeQueue, func(i, j int) bool {
16+
return nodeQueue[i].StartAt < nodeQueue[j].StartAt
17+
})
18+
19+
// Start initial nodes (StartAt: 0)
20+
logger.Info("Starting initial network nodes...")
21+
for len(nodeQueue) > 0 && nodeQueue[0].StartAt == 0 {
22+
node := nodeQueue[0]
23+
nodeQueue = nodeQueue[1:]
24+
if err := execCompose(testnet.Dir, "up", "-d", node.Name); err != nil {
25+
return err
26+
}
27+
if _, err := waitForNode(node, 0, 10*time.Second); err != nil {
28+
return err
29+
}
30+
logger.Info(fmt.Sprintf("Node %v up on http://127.0.0.1:%v", node.Name, node.ProxyPort))
31+
}
32+
33+
// Wait for initial height
34+
logger.Info(fmt.Sprintf("Waiting for initial height %v...", testnet.InitialHeight))
35+
block, blockID, err := waitForHeight(testnet, testnet.InitialHeight)
36+
if err != nil {
37+
return err
38+
}
39+
40+
// Update any state sync nodes with a trusted height and hash
41+
for _, node := range nodeQueue {
42+
if node.StateSync {
43+
err = UpdateConfigStateSync(node, block.Height, blockID.Hash.Bytes())
44+
if err != nil {
45+
return err
46+
}
47+
}
48+
}
49+
50+
// Start up remaining nodes
51+
for _, node := range nodeQueue {
52+
logger.Info(fmt.Sprintf("Starting node %v at height %v...", node.Name, node.StartAt))
53+
if _, _, err := waitForHeight(testnet, node.StartAt); err != nil {
54+
return err
55+
}
56+
if err := execCompose(testnet.Dir, "up", "-d", node.Name); err != nil {
57+
return err
58+
}
59+
status, err := waitForNode(node, node.StartAt, 30*time.Second)
60+
if err != nil {
61+
return err
62+
}
63+
logger.Info(fmt.Sprintf("Node %v up on http://127.0.0.1:%v at height %v",
64+
node.Name, node.ProxyPort, status.SyncInfo.LatestBlockHeight))
65+
}
66+
67+
return nil
68+
}

‎test/e2e/runner/test.go

+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package main
2+
3+
import (
4+
"os"
5+
6+
e2e "github.com/tendermint/tendermint/test/e2e/pkg"
7+
)
8+
9+
// Test runs test cases under tests/
10+
func Test(testnet *e2e.Testnet) error {
11+
logger.Info("Running tests in ./tests/...")
12+
13+
err := os.Setenv("E2E_MANIFEST", testnet.File)
14+
if err != nil {
15+
return err
16+
}
17+
18+
return execVerbose("go", "test", "-count", "1", "./tests/...")
19+
}

‎test/e2e/runner/wait.go

+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package main
2+
3+
import (
4+
"fmt"
5+
"time"
6+
7+
e2e "github.com/tendermint/tendermint/test/e2e/pkg"
8+
)
9+
10+
// Wait waits for a number of blocks to be produced, and for all nodes to catch
11+
// up with it.
12+
func Wait(testnet *e2e.Testnet, blocks int64) error {
13+
block, _, err := waitForHeight(testnet, 0)
14+
if err != nil {
15+
return err
16+
}
17+
waitFor := block.Height + blocks
18+
logger.Info(fmt.Sprintf("Waiting for all nodes to reach height %v...", waitFor))
19+
_, err = waitForAllNodes(testnet, waitFor, 20*time.Second)
20+
if err != nil {
21+
return err
22+
}
23+
return nil
24+
}

‎test/e2e/tests/app_test.go

+30
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package e2e_test
2+
3+
import (
4+
"testing"
5+
6+
"github.com/stretchr/testify/assert"
7+
"github.com/stretchr/testify/require"
8+
e2e "github.com/tendermint/tendermint/test/e2e/pkg"
9+
)
10+
11+
// Tests that any initial state given in genesis has made it into the app.
12+
func TestApp_InitialState(t *testing.T) {
13+
testNode(t, func(t *testing.T, node e2e.Node) {
14+
switch {
15+
case node.Mode == e2e.ModeSeed:
16+
return
17+
case len(node.Testnet.InitialState) == 0:
18+
return
19+
}
20+
21+
client, err := node.Client()
22+
require.NoError(t, err)
23+
for k, v := range node.Testnet.InitialState {
24+
resp, err := client.ABCIQuery(ctx, "", []byte(k))
25+
require.NoError(t, err)
26+
assert.Equal(t, k, string(resp.Response.Key))
27+
assert.Equal(t, v, string(resp.Response.Value))
28+
}
29+
})
30+
}

‎test/e2e/tests/e2e_test.go

+57
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
package e2e_test
2+
3+
import (
4+
"context"
5+
"os"
6+
"path/filepath"
7+
"testing"
8+
9+
"github.com/stretchr/testify/require"
10+
e2e "github.com/tendermint/tendermint/test/e2e/pkg"
11+
)
12+
13+
func init() {
14+
// This can be used to manually specify a testnet manifest and/or node to
15+
// run tests against. The testnet must have been started by the runner first.
16+
//os.Setenv("E2E_MANIFEST", "networks/simple.toml")
17+
//os.Setenv("E2E_NODE", "validator01")
18+
}
19+
20+
var (
21+
ctx = context.Background()
22+
)
23+
24+
// testNode runs tests for testnet nodes. The callback function is given a
25+
// single node to test, running as a subtest in parallel with other subtests.
26+
//
27+
// The testnet manifest must be given as the envvar E2E_MANIFEST. If not set,
28+
// these tests are skipped so that they're not picked up during normal unit
29+
// test runs. If E2E_NODE is also set, only the specified node is tested,
30+
// otherwise all nodes are tested.
31+
func testNode(t *testing.T, testFunc func(*testing.T, e2e.Node)) {
32+
manifest := os.Getenv("E2E_MANIFEST")
33+
if manifest == "" {
34+
t.Skip("E2E_MANIFEST not set, not an end-to-end test run")
35+
}
36+
if !filepath.IsAbs(manifest) {
37+
manifest = filepath.Join("..", manifest)
38+
}
39+
40+
testnet, err := e2e.LoadTestnet(manifest)
41+
require.NoError(t, err)
42+
nodes := testnet.Nodes
43+
44+
if name := os.Getenv("E2E_NODE"); name != "" {
45+
node := testnet.LookupNode(name)
46+
require.NotNil(t, node, "node %q not found in testnet %q", name, testnet.Name)
47+
nodes = []*e2e.Node{node}
48+
}
49+
50+
for _, node := range nodes {
51+
node := *node
52+
t.Run(node.Name, func(t *testing.T) {
53+
t.Parallel()
54+
testFunc(t, node)
55+
})
56+
}
57+
}

0 commit comments

Comments
 (0)
Please sign in to comment.