From 9dddf9fe5d3759f29b02bff5b6a940c019437026 Mon Sep 17 00:00:00 2001
From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com>
Date: Mon, 27 Nov 2023 16:06:40 +0100
Subject: [PATCH] Add fine-grained control of time (backport #88) (#91)
* Add fine-grained control of time (#88)
* Start implementing auto-include-tx flag
* Split testnet initialization and run
* Add script to restart testnet from beginning
* Start implementing auto-include-tx flag
* Delete backup directory before taking backup
* Uncomment killing processes with app binary
* Rewrite TxQueue
* Fix fire events method
* Implement tx filtering and rechecks
* Start implementing basic test for Tx effect
* Ensure the script is not blocking
* Rename Dockerfile-test to Dockerfile.test
* Add Contains method
* Remove initial setup from Dockerfile
* Remove sleep and add -p 1
* Use big.Int for community pool size
* Use testnet restart in tests instead of a new setup each time
* Add cometmock args to be taken by startup script
* Pull time handling into TimeHandler class
* Fix CheckTx and time
* Add test cases for AutoTx and block production
* Add time control to README
* Fix auto-tx flag
* Add test for starting timestamp
* Add test for starting time=system time
(cherry picked from commit 7edb4c16410d9e6f5c3d26c1b62fb4a1031a4442)
# Conflicts:
# Dockerfile-test
# cometmock/abci_client/client.go
# cometmock/main.go
# cometmock/rpc_server/routes.go
# e2e-tests/local-testnet-singlechain.sh
# e2e-tests/main_test.go
* Fix merge
* Fix isConnected behaviour and start addressing test failures
* Adjust block queries to simd 47
* Modify Dockerfile to use old simd
---------
Co-authored-by: Philip Offtermatt <57488781+p-offtermatt@users.noreply.github.com>
Co-authored-by: Philip Offtermatt
---
.gitignore | 1 +
Dockerfile.test | 48 +++
Makefile | 6 +-
README.md | 12 +-
cometmock/abci_client/client.go | 195 +++++-----
cometmock/abci_client/time_handler.go | 119 +++++++
cometmock/main.go | 91 ++++-
cometmock/rpc_server/routes.go | 39 +-
cometmock/utils/txs.go | 17 +
e2e-tests/.gitignore | 4 +
.../local-testnet-singlechain-restart.sh | 27 ++
e2e-tests/local-testnet-singlechain-setup.sh | 228 ++++++++++++
e2e-tests/local-testnet-singlechain-start.sh | 9 +
e2e-tests/local-testnet-singlechain.sh | 21 +-
e2e-tests/main_test.go | 332 +++++++++++++++---
e2e-tests/test_utils.go | 166 +++++++++
go.mod | 6 +-
go.sum | 12 +-
18 files changed, 1154 insertions(+), 179 deletions(-)
create mode 100644 Dockerfile.test
create mode 100644 cometmock/abci_client/time_handler.go
create mode 100644 cometmock/utils/txs.go
create mode 100644 e2e-tests/.gitignore
create mode 100755 e2e-tests/local-testnet-singlechain-restart.sh
create mode 100755 e2e-tests/local-testnet-singlechain-setup.sh
create mode 100755 e2e-tests/local-testnet-singlechain-start.sh
create mode 100644 e2e-tests/test_utils.go
diff --git a/.gitignore b/.gitignore
index 4061428..e71a340 100644
--- a/.gitignore
+++ b/.gitignore
@@ -10,6 +10,7 @@
# Test binary, built with `go test -c`
*.test
+!Dockerfile.test
# Output of the go coverage tool, specifically when used with LiteIDE
*.out
diff --git a/Dockerfile.test b/Dockerfile.test
new file mode 100644
index 0000000..cc2c773
--- /dev/null
+++ b/Dockerfile.test
@@ -0,0 +1,48 @@
+# import simd from ibc-go
+FROM ghcr.io/cosmos/simapp:v0.47 AS simapp-builder
+
+FROM golang:1.20-alpine as cometmock-builder
+
+ENV PACKAGES curl make git libc-dev bash gcc linux-headers
+RUN apk add --no-cache $PACKAGES
+
+ENV CGO_ENABLED=0
+ENV GOOS=linux
+ENV GOFLAGS="-buildvcs=false"
+
+# cache gomodules for cometmock
+ADD ./go.mod /go.mod
+ADD ./go.sum /go.sum
+RUN go mod download
+
+# Add CometMock and install it
+ADD . /CometMock
+WORKDIR /CometMock
+RUN go build -o /usr/local/bin/cometmock ./cometmock
+
+RUN apk update
+RUN apk add --no-cache which iputils procps-ng tmux net-tools htop jq gcompat
+
+FROM golang:1.21-alpine as test-env
+
+ENV PACKAGES curl make git libc-dev bash gcc linux-headers
+RUN apk add --no-cache $PACKAGES
+RUN apk update
+RUN apk add --no-cache which iputils procps-ng tmux net-tools htop jq gcompat
+
+ENV CGO_ENABLED=0
+ENV GOOS=linux
+ENV GOFLAGS="-buildvcs=false"
+
+ADD ./go.mod /go.mod
+ADD ./go.sum /go.sum
+RUN go mod download
+
+ADD ./e2e-tests /CometMock/e2e-tests
+
+COPY --from=simapp-builder /usr/bin/simd /usr/local/bin/simd
+
+WORKDIR /CometMock/e2e-tests
+RUN /CometMock/e2e-tests/local-testnet-singlechain-setup.sh simd ""
+
+COPY --from=cometmock-builder /usr/local/bin/cometmock /usr/local/bin/cometmock
\ No newline at end of file
diff --git a/Makefile b/Makefile
index 343ad1d..45ecc32 100644
--- a/Makefile
+++ b/Makefile
@@ -2,12 +2,12 @@ install:
go install ./cometmock
test-locally:
- go test -timeout 600s ./e2e-tests -test.v
+ go test -timeout 600s -p 1 ./e2e-tests -test.v
test-docker:
# Build the Docker image
- docker build -f Dockerfile-test -t cometmock-test .
+ docker build -f Dockerfile.test -t cometmock-test .
# Start a container and execute the test command inside
docker rm cometmock-test-instance || true
- docker run --name cometmock-test-instance --workdir /CometMock cometmock-test go test -timeout 600s ./e2e-tests -test.v
\ No newline at end of file
+ docker run --name cometmock-test-instance --workdir /CometMock cometmock-test go test -p 1 -timeout 600s ./e2e-tests -test.v
\ No newline at end of file
diff --git a/README.md b/README.md
index 2d088c5..f426856 100644
--- a/README.md
+++ b/README.md
@@ -27,11 +27,19 @@ CometMock was tested with `go version go1.20.3 darwin/arm64`.
To run CometMock, start your (cosmos-sdk) application instances with the flags ```--with-tendermint=false, --transport=grpc```.
After the applications started, start CometMock like this
```
-cometmock [--block-time=XXX] {app_address1,app_address2,...} {genesis_file} {cometmock_listen_address} {home_folder1,home_folder2,...} {connection_mode}
+cometmock [--block-time=value] [--auto-tx=] [--block-production-interval=] [--starting-timestamp=] [--starting-timestamp-from-genesis=] {app_address1,app_address2,...} {genesis_file} {cometmock_listen_address} {home_folder1,home_folder2,...} {connection_mode}
```
where:
-* The `--block-time` flag is optional and specifies the time in milliseconds between blocks. The default is 1000ms(=1s). Values <= 0 mean that automatic block production is disabled. In this case, blocks can be produced by calling the `advance_blocks` endpoint or by broadcasting transactions (each transaction will be included in a fresh block). Note that all flags have to come before positional arguments.
+* The `--block-time` flag is optional and specifies the time in milliseconds between the timestamps of consecutive blocks.
+Values <= 0 mean that the timestamps are taken from the system time. The default value is -1.
+* The `--auto-tx` flag is optional. If it is set to true, when a transaction is broadcasted, it will be automatically included in the next block. The default value is false.
+* The `--block-production-interval` flag is optional and specifies the time (in milliseconds) to sleep between the production of consecutive blocks.
+This does not mean that blocks are produced this fast, just that CometMock will sleep by this amount between producing two blocks.
+The default value is 1000ms=1s.
+* The `--starting-timestamp` flag is optional and specifies the starting timestamp of the blockchain. If not specified, the starting timestamp is taken from the system time.
+* The `--starting-timestamp-from-genesis` flag is optional and can be used to override the starting timestamp of the blockchain with the timestamp of the genesis file.
+In that case, the first block will have a timestamp of Genesis timestamp + block time or, if block time is <= 0, Genesis timestamp + some small, unspecified amount depending on system time.
* The `app_addresses` are the `--address` flags of the applications. This is by default `"tcp://0.0.0.0:26658"`
* The `genesis_file` is the genesis json that is also used by apps.
* The `cometmock_listen_address` can be freely chosen and will be the address that requests that would normally go to CometBFT rpc endpoints need to be directed to.
diff --git a/cometmock/abci_client/client.go b/cometmock/abci_client/client.go
index 883dc63..f9e83a3 100644
--- a/cometmock/abci_client/client.go
+++ b/cometmock/abci_client/client.go
@@ -14,12 +14,13 @@ import (
cometlog "github.com/cometbft/cometbft/libs/log"
cmtmath "github.com/cometbft/cometbft/libs/math"
cmtstate "github.com/cometbft/cometbft/proto/tendermint/state"
- cmttypes "github.com/cometbft/cometbft/proto/tendermint/types"
+ cmtproto "github.com/cometbft/cometbft/proto/tendermint/types"
"github.com/cometbft/cometbft/state"
blockindexkv "github.com/cometbft/cometbft/state/indexer/block/kv"
"github.com/cometbft/cometbft/state/txindex"
indexerkv "github.com/cometbft/cometbft/state/txindex/kv"
"github.com/cometbft/cometbft/types"
+ cmttypes "github.com/cometbft/cometbft/types"
"github.com/informalsystems/CometMock/cometmock/storage"
"github.com/informalsystems/CometMock/cometmock/utils"
)
@@ -43,6 +44,10 @@ const (
Equivocation
)
+// hardcode max data bytes to the maximal value since we do not utilize a mempool
+// to pick evidence/txs out of
+const maxDataBytes = cmttypes.MaxBlockSizeBytes
+
// AbciClient facilitates calls to the ABCI interface of multiple nodes.
// It also tracks the current state and a common logger.
type AbciClient struct {
@@ -68,24 +73,29 @@ type AbciClient struct {
signingStatus map[string]bool
signingStatusMutex sync.RWMutex
- // time offset. whenever we qury the time, we add this offset to it
- // this means after modifying this, blocks will have the timestamp offset by this value.
- // this will look to the app like one block took a long time to be produced.
- timeOffset time.Duration
+ // The TimeHandler that will be queried
+ // to obtain the block timestamp for each block.
+ TimeHandler TimeHandler
+
+ // If this is true, then when broadcastTx is called,
+ // a block will automatically be produced immediately.
+ // If not, the transaction will be added to the TxQueue
+ // and consumed when the next block is created.
+ AutoIncludeTx bool
+
+ // A list of transactions that will be included in the next block that is created.
+ TxQueue []types.Tx
}
-func (a *AbciClient) GetTimeOffset() time.Duration {
- return a.timeOffset
+func (a *AbciClient) QueueTx(tx types.Tx) {
+ // lock the block mutex so txs are not queued while a block is being run
+ blockMutex.Lock()
+ a.TxQueue = append(a.TxQueue, tx)
+ blockMutex.Unlock()
}
-func (a *AbciClient) IncrementTimeOffset(additionalOffset time.Duration) error {
- if additionalOffset < 0 {
- a.Logger.Error("time offset cannot be decremented, please provide a positive offset")
- return fmt.Errorf("time offset cannot be decremented, please provide a positive offset")
- }
- a.Logger.Debug("Incrementing time offset", "additionalOffset", additionalOffset.String())
- a.timeOffset = a.timeOffset + additionalOffset
- return nil
+func (a *AbciClient) ClearTxs() {
+ a.TxQueue = make([]types.Tx, 0)
}
func (a *AbciClient) CauseLightClientAttack(address string, misbehaviourType string) error {
@@ -109,7 +119,7 @@ func (a *AbciClient) CauseLightClientAttack(address string, misbehaviourType str
return fmt.Errorf("unknown misbehaviour type %s, possible types are: Equivocation, Lunatic, Amnesia", misbehaviourType)
}
- _, _, _, _, _, err = a.RunBlockWithEvidence(nil, map[*types.Validator]MisbehaviourType{validator: misbehaviour})
+ err = a.RunBlockWithEvidence(map[*types.Validator]MisbehaviourType{validator: misbehaviour})
return err
}
@@ -121,8 +131,7 @@ func (a *AbciClient) CauseDoubleSign(address string) error {
return err
}
- _, _, _, _, _, err = a.RunBlockWithEvidence(nil, map[*types.Validator]MisbehaviourType{validator: DuplicateVote})
- return err
+ return a.RunBlockWithEvidence(map[*types.Validator]MisbehaviourType{validator: DuplicateVote})
}
func (a *AbciClient) GetValidatorFromAddress(address string) (*types.Validator, error) {
@@ -192,7 +201,17 @@ func CreateAndStartIndexerService(eventBus *types.EventBus, logger cometlog.Logg
return indexerService, txIndexer, blockIndexer, indexerService.Start()
}
-func NewAbciClient(clients []AbciCounterpartyClient, logger cometlog.Logger, curState state.State, lastBlock *types.Block, lastCommit *types.Commit, storage storage.Storage, privValidators map[string]types.PrivValidator, errorOnUnequalResponses bool) *AbciClient {
+func NewAbciClient(
+ clients []AbciCounterpartyClient,
+ logger cometlog.Logger,
+ curState state.State,
+ lastBlock *types.Block,
+ lastCommit *types.Commit,
+ storage storage.Storage,
+ timeHandler TimeHandler,
+ privValidators map[string]types.PrivValidator,
+ errorOnUnequalResponses bool,
+) *AbciClient {
signingStatus := make(map[string]bool)
for addr := range privValidators {
signingStatus[addr] = true
@@ -222,8 +241,10 @@ func NewAbciClient(clients []AbciCounterpartyClient, logger cometlog.Logger, cur
IndexerService: indexerService,
TxIndex: txIndex,
BlockIndex: blockIndex,
+ TimeHandler: timeHandler,
ErrorOnUnequalResponses: errorOnUnequalResponses,
signingStatus: signingStatus,
+ TxQueue: make([]types.Tx, 0),
}
}
@@ -541,10 +562,11 @@ func (a *AbciClient) SendCommit() (*abcitypes.ResponseCommit, error) {
return responses[0].(*abcitypes.ResponseCommit), nil
}
-func (a *AbciClient) SendCheckTx(tx *[]byte) (*abcitypes.ResponseCheckTx, error) {
+func (a *AbciClient) SendCheckTx(checkType abcitypes.CheckTxType, tx *[]byte) (*abcitypes.ResponseCheckTx, error) {
// build the CheckTx request
checkTxRequest := abcitypes.RequestCheckTx{
- Tx: *tx,
+ Tx: *tx,
+ Type: checkType,
}
// send CheckTx to all clients and collect the responses
@@ -631,7 +653,7 @@ func (a *AbciClient) SendAbciQuery(data []byte, path string, height int64, prove
// RunEmptyBlocks runs a specified number of empty blocks through ABCI.
func (a *AbciClient) RunEmptyBlocks(numBlocks int) error {
for i := 0; i < numBlocks; i++ {
- _, _, _, _, _, err := a.RunBlock(nil)
+ err := a.RunBlock()
if err != nil {
return err
}
@@ -641,14 +663,20 @@ func (a *AbciClient) RunEmptyBlocks(numBlocks int) error {
// RunBlock runs a block with a specified transaction through the ABCI application.
// It calls RunBlockWithTimeAndProposer with the current time and the LastValidators.Proposer.
-func (a *AbciClient) RunBlock(tx *[]byte) (*abcitypes.ResponseBeginBlock, *abcitypes.ResponseCheckTx, *abcitypes.ResponseDeliverTx, *abcitypes.ResponseEndBlock, *abcitypes.ResponseCommit, error) {
- return a.RunBlockWithTimeAndProposer(tx, time.Now().Add(a.timeOffset), a.CurState.LastValidators.Proposer, make(map[*types.Validator]MisbehaviourType, 0))
+func (a *AbciClient) RunBlock() error {
+ blockTime := a.TimeHandler.GetBlockTime(a.LastBlock.Time)
+ return a.RunBlockWithTimeAndProposer(blockTime, a.CurState.LastValidators.Proposer, make(map[*types.Validator]MisbehaviourType, 0))
+}
+
+func (a *AbciClient) RunBlockWithTime(t time.Time) error {
+ return a.RunBlockWithTimeAndProposer(t, a.CurState.LastValidators.Proposer, make(map[*types.Validator]MisbehaviourType, 0))
}
// RunBlockWithEvidence runs a block with a specified transaction through the ABCI application.
// It also produces the specified evidence for the specified misbehaving validators.
-func (a *AbciClient) RunBlockWithEvidence(tx *[]byte, misbehavingValidators map[*types.Validator]MisbehaviourType) (*abcitypes.ResponseBeginBlock, *abcitypes.ResponseCheckTx, *abcitypes.ResponseDeliverTx, *abcitypes.ResponseEndBlock, *abcitypes.ResponseCommit, error) {
- return a.RunBlockWithTimeAndProposer(tx, time.Now().Add(a.timeOffset), a.CurState.LastValidators.Proposer, misbehavingValidators)
+func (a *AbciClient) RunBlockWithEvidence(misbehavingValidators map[*types.Validator]MisbehaviourType) error {
+ blockTime := a.TimeHandler.GetBlockTime(a.LastBlock.Time)
+ return a.RunBlockWithTimeAndProposer(blockTime, a.CurState.LastValidators.Proposer, misbehavingValidators)
}
func (a *AbciClient) ConstructDuplicateVoteEvidence(v *types.Validator) (*types.DuplicateVoteEvidence, error) {
@@ -668,24 +696,24 @@ func (a *AbciClient) ConstructDuplicateVoteEvidence(v *types.Validator) (*types.
index, valInLastState := lastState.Validators.GetByAddress(v.Address)
// produce vote A.
- voteA := &cmttypes.Vote{
+ voteA := &cmtproto.Vote{
ValidatorAddress: v.Address,
ValidatorIndex: int32(index),
Height: lastBlock.Height,
Round: 1,
- Timestamp: time.Now().Add(a.timeOffset),
- Type: cmttypes.PrecommitType,
+ Timestamp: lastBlock.Time,
+ Type: cmtproto.PrecommitType,
BlockID: blockId.ToProto(),
}
// produce vote B, which just has a different round.
- voteB := &cmttypes.Vote{
+ voteB := &cmtproto.Vote{
ValidatorAddress: v.Address,
ValidatorIndex: int32(index),
Height: lastBlock.Height,
Round: 2, // this is what differentiates the votes
- Timestamp: time.Now().Add(a.timeOffset),
- Type: cmttypes.PrecommitType,
+ Timestamp: lastBlock.Time,
+ Type: cmtproto.PrecommitType,
BlockID: blockId.ToProto(),
}
@@ -774,23 +802,13 @@ func (a *AbciClient) ConstructLightClientAttackEvidence(
}, nil
}
-// RunBlock runs a block with a specified transaction through the ABCI application.
-// It calls BeginBlock, DeliverTx, EndBlock, Commit and then
-// updates the state.
-// RunBlock is safe for use by multiple goroutines simultaneously.
-func (a *AbciClient) RunBlockWithTimeAndProposer(
- tx *[]byte,
+// internal method that runs a block.
+// Should only be used after locking the blockMutex.
+func (a *AbciClient) runBlock_helper(
blockTime time.Time,
proposer *types.Validator,
misbehavingValidators map[*types.Validator]MisbehaviourType,
-) (*abcitypes.ResponseBeginBlock, *abcitypes.ResponseCheckTx, *abcitypes.ResponseDeliverTx, *abcitypes.ResponseEndBlock, *abcitypes.ResponseCommit, error) {
- // lock mutex to avoid running two blocks at the same time
- a.Logger.Debug("Locking mutex")
- blockMutex.Lock()
-
- defer blockMutex.Unlock()
- defer a.Logger.Debug("Unlocking mutex")
-
+) error {
a.Logger.Info("Running block")
if verbose {
a.Logger.Info("State at start of block", "state", a.CurState)
@@ -798,19 +816,10 @@ func (a *AbciClient) RunBlockWithTimeAndProposer(
newHeight := a.CurState.LastBlockHeight + 1
- txs := make([]types.Tx, 0)
- if tx != nil {
- txs = append(txs, *tx)
- }
-
- var resCheckTx *abcitypes.ResponseCheckTx
var err error
- if tx != nil {
- resCheckTx, err = a.SendCheckTx(tx)
- if err != nil {
- return nil, nil, nil, nil, nil, err
- }
- }
+
+ // filter all empty txs from the queues
+ txs := cmttypes.Txs(a.TxQueue)
// TODO: handle special case where proposer is nil
var proposerAddress types.Address
@@ -832,7 +841,7 @@ func (a *AbciClient) RunBlockWithTimeAndProposer(
}
if err != nil {
- return nil, nil, nil, nil, nil, err
+ return fmt.Errorf("error constructing evidence: %v", err)
}
evidences = append(evidences, evidence)
@@ -843,9 +852,11 @@ func (a *AbciClient) RunBlockWithTimeAndProposer(
block.Time = blockTime
blockId, err := utils.GetBlockIdFromBlock(block)
if err != nil {
- return nil, nil, nil, nil, nil, err
+ return err
}
+ a.ClearTxs()
+
commitSigs := []types.CommitSig{}
for index, val := range a.CurState.Validators.Validators {
@@ -853,29 +864,29 @@ func (a *AbciClient) RunBlockWithTimeAndProposer(
shouldSign, err := a.GetSigningStatus(val.Address.String())
if err != nil {
- return nil, nil, nil, nil, nil, err
+ return err
}
if shouldSign {
// create and sign a precommit
- vote := &cmttypes.Vote{
+ vote := &cmtproto.Vote{
ValidatorAddress: val.Address,
ValidatorIndex: int32(index),
Height: block.Height,
Round: 1,
- Timestamp: time.Now().Add(a.timeOffset),
- Type: cmttypes.PrecommitType,
+ Timestamp: blockTime,
+ Type: cmtproto.PrecommitType,
BlockID: blockId.ToProto(),
}
err = privVal.SignVote(a.CurState.ChainID, vote)
if err != nil {
- return nil, nil, nil, nil, nil, err
+ return err
}
convertedVote, err := types.VoteFromProto(vote)
if err != nil {
- return nil, nil, nil, nil, nil, err
+ return err
}
commitSig := convertedVote.CommitSig()
@@ -896,7 +907,7 @@ func (a *AbciClient) RunBlockWithTimeAndProposer(
// sanity check that the commit is signed correctly
err = a.CurState.Validators.VerifyCommitLightTrusting(a.CurState.ChainID, a.LastCommit, cmtmath.Fraction{Numerator: 1, Denominator: 3})
if err != nil {
- return nil, nil, nil, nil, nil, err
+ return fmt.Errorf("error verifying commit %v: %v", a.LastCommit.StringIndented("\t"), err)
}
// sanity check that the commit makes a proper light block
@@ -913,32 +924,28 @@ func (a *AbciClient) RunBlockWithTimeAndProposer(
err = lightBlock.ValidateBasic(a.CurState.ChainID)
if err != nil {
a.Logger.Error("Light block validation failed", "err", err)
- return nil, nil, nil, nil, nil, err
+ return err
}
resBeginBlock, err := a.SendBeginBlock(block)
if err != nil {
- return nil, nil, nil, nil, nil, err
+ return fmt.Errorf("error from BeginBlock for block %v: %v", block.String(), err)
}
- var resDeliverTx *abcitypes.ResponseDeliverTx
- if tx != nil {
- resDeliverTx, err = a.SendDeliverTx(tx)
+ var deliverTxResponses []*abcitypes.ResponseDeliverTx
+ for _, tx := range txs {
+ txBytes := []byte(tx)
+ a.Logger.Info("Sending DeliverTx", "tx", tx)
+ resDeliverTx, err := a.SendDeliverTx(&txBytes)
if err != nil {
- return nil, nil, nil, nil, nil, err
+ return err
}
- } else {
- resDeliverTx = nil
+ deliverTxResponses = append(deliverTxResponses, resDeliverTx)
}
resEndBlock, err := a.SendEndBlock()
if err != nil {
- return nil, nil, nil, nil, nil, err
- }
-
- deliverTxResponses := []*abcitypes.ResponseDeliverTx{}
- if tx != nil {
- deliverTxResponses = append(deliverTxResponses, resDeliverTx)
+ return err
}
// lock the state update mutex while the stores are updated to avoid
@@ -959,24 +966,42 @@ func (a *AbciClient) RunBlockWithTimeAndProposer(
// insert entries into the storage
err = a.Storage.UpdateStores(newHeight, block, a.LastCommit, &state, &abciResponses)
if err != nil {
- return nil, nil, nil, nil, nil, err
+ return fmt.Errorf("error getting block id from block %v: %v", block.String(), err)
}
// updates state as a side effect. returns an error if the state update fails
err = a.UpdateStateFromBlock(blockId, block, abciResponses)
if err != nil {
- return nil, nil, nil, nil, nil, err
+ return fmt.Errorf("error updating state for result %v, block %v: %v", abciResponses.String(), block.String(), err)
}
// unlock the state mutex, since we are done updating state
a.Storage.UnlockAfterStateUpdate()
resCommit, err := a.SendCommit()
if err != nil {
- return nil, nil, nil, nil, nil, err
+ return fmt.Errorf("error from Commit for block %v: %v", block.String(), err)
}
a.CurState.AppHash = resCommit.Data
- return resBeginBlock, resCheckTx, resDeliverTx, resEndBlock, resCommit, nil
+ return nil
+}
+
+// RunBlock RunBlockWithTimeAndProposer runs a block through the ABCI application.
+// RunBlock is safe for use by multiple goroutines simultaneously.
+func (a *AbciClient) RunBlockWithTimeAndProposer(
+ blockTime time.Time,
+ proposer *types.Validator,
+ misbehavingValidators map[*types.Validator]MisbehaviourType,
+) error {
+ // lock mutex to avoid running two blocks at the same time
+ a.Logger.Debug("Locking mutex")
+ blockMutex.Lock()
+
+ err := a.runBlock_helper(blockTime, proposer, misbehavingValidators)
+
+ blockMutex.Unlock()
+ a.Logger.Debug("Unlocking mutex")
+ return err
}
// UpdateStateFromBlock updates the AbciClients state
diff --git a/cometmock/abci_client/time_handler.go b/cometmock/abci_client/time_handler.go
new file mode 100644
index 0000000..e11a1a9
--- /dev/null
+++ b/cometmock/abci_client/time_handler.go
@@ -0,0 +1,119 @@
+package abci_client
+
+import (
+ "sync"
+ "time"
+)
+
+// A TimeHandler is responsible for
+// deciding the timestamps of blocks.
+// It will be called by AbciClient.RunBlock
+// to decide on a block time.
+// It may decide the time based on any number of factors,
+// and the parameters of its methods might expand over time as needed.
+// The TimeHandler does not have a way to decide the time of the first block,
+// which is expected to be done externally, e.g. from the Genesis.
+type TimeHandler interface {
+ // CONTRACT: TimeHandler.GetBlockTime will be called
+ // precisely once for each block after the first.
+ // It returns the timestamp of the next block.
+ GetBlockTime(lastBlockTimestamp time.Time) time.Time
+
+ // AdvanceTime advances the timestamp of all following blocks by
+ // the given duration.
+ // The duration needs to be non-negative.
+ // It returns the timestamp that the next block would have if it
+ // was produced now.
+ AdvanceTime(duration time.Duration) time.Time
+}
+
+// The SystemClockTimeHandler uses the system clock
+// to decide the timestamps of blocks.
+// It will return the system time + offset for each block.
+// The offset is calculated by the initial timestamp
+// + the sum of all durations passed to AdvanceTime.
+type SystemClockTimeHandler struct {
+ // The offset to add to the system time.
+ curOffset time.Duration
+
+ // A mutex that ensures that there are no concurrent calls
+ // to AdvanceTime
+ mutex sync.Mutex
+}
+
+func NewSystemClockTimeHandler(initialTimestamp time.Time) *SystemClockTimeHandler {
+ return &SystemClockTimeHandler{
+ curOffset: time.Since(initialTimestamp),
+ }
+}
+
+func (s *SystemClockTimeHandler) GetBlockTime(lastBlockTimestamp time.Time) time.Time {
+ return time.Now().Add(s.curOffset)
+}
+
+func (s *SystemClockTimeHandler) AdvanceTime(duration time.Duration) time.Time {
+ s.mutex.Lock()
+ defer s.mutex.Unlock()
+
+ s.curOffset += duration
+ return time.Now().Add(s.curOffset)
+}
+
+var _ TimeHandler = (*SystemClockTimeHandler)(nil)
+
+// The FixedBlockTimeHandler uses a fixed duration
+// to advance the timestamp of a block compared to the previous block.
+// The block timestamps therefore do not at all depend on the system time,
+// but on the time of the previous block.
+type FixedBlockTimeHandler struct {
+ // The fixed duration to add to the last block time
+ // when deciding the next block timestamp.
+ blockTime time.Duration
+
+ // The offset to add to the last block time.
+ // This will be cleared after each block,
+ // but since the block time of the next block depends
+ // on the last block,
+ // this will shift the timestamps of all future blocks.
+ curBlockOffset time.Duration
+
+ // A mutex that ensures that GetBlockTime and AdvanceTime
+ // are not called concurrently.
+ // Otherwise, the block offset might be put into a broken state.
+ mutex sync.Mutex
+
+ // The timestamp of the last block we produced.
+ // If this is used before the first block is produced,
+ // it will be the zero time.
+ lastBlockTimestamp time.Time
+}
+
+func NewFixedBlockTimeHandler(blockTime time.Duration) *FixedBlockTimeHandler {
+ return &FixedBlockTimeHandler{
+ blockTime: blockTime,
+ curBlockOffset: 0,
+ }
+}
+
+func (f *FixedBlockTimeHandler) GetBlockTime(lastBlockTimestamp time.Time) time.Time {
+ f.mutex.Lock()
+ defer f.mutex.Unlock()
+
+ res := lastBlockTimestamp.Add(f.blockTime + f.curBlockOffset)
+ f.curBlockOffset = 0
+ f.lastBlockTimestamp = res
+ return res
+}
+
+// FixedBlockTimeHandler.AdvanceTime will only return the correct next block time
+// after GetBlockTime has been called once, but it will
+// still advance the time correctly before that - only the output will be wrong.
+func (f *FixedBlockTimeHandler) AdvanceTime(duration time.Duration) time.Time {
+ f.mutex.Lock()
+ defer f.mutex.Unlock()
+
+ f.curBlockOffset += duration
+ return f.lastBlockTimestamp.Add(f.blockTime + f.curBlockOffset)
+}
+
+var _ TimeHandler = (*FixedBlockTimeHandler)(nil)
diff --git a/cometmock/main.go b/cometmock/main.go
index ce6e249..96cae2c 100644
--- a/cometmock/main.go
+++ b/cometmock/main.go
@@ -40,7 +40,7 @@ func GetMockPVsFromNodeHomes(nodeHomes []string) []types.PrivValidator {
func main() {
logger := cometlog.NewTMLogger(cometlog.NewSyncWriter(os.Stdout))
- argumentString := "[--block-time=value] "
+ argumentString := "[--block-time=value] [--auto-tx=] [--block-production-interval=] [--starting-timestamp=] [--starting-timestamp-from-genesis=] "
app := &cli.App{
Name: "cometmock",
@@ -59,15 +59,51 @@ func main() {
&cli.Int64Flag{
Name: "block-time",
Usage: `
-Time between blocks in milliseconds.
+The number of milliseconds by which the block timestamp should advance from one block to the next.
+If this is <0, block timestamps will advance with the system time between the block productions.
+Even then, it is still possible to shift the block time from the system time, e.g. by setting an initial timestamp
+or by using the 'advance_time' endpoint.`,
+ Value: -1,
+ },
+ &cli.BoolFlag{
+ Name: "auto-tx",
+ Usage: `
+If this is true, transactions are included immediately
+after they are received via broadcast_tx, i.e. a new block
+is created when a BroadcastTx endpoint is hit.
+If this is false, transactions are still included
+upon creation of new blocks, but CometMock will not specifically produce
+a new block when a transaction is broadcast.`,
+ Value: true,
+ },
+ &cli.Int64Flag{
+ Name: "block-production-interval",
+ Usage: `
+Time to sleep between blocks in milliseconds.
To disable block production, set to 0.
This will not necessarily mean block production is this fast
- it is just the sleep time between blocks.
-Setting this to a value <= 0 disables automatic block production.
+Setting this to a value < 0 disables automatic block production.
In this case, blocks are only produced when instructed explicitly either by
advancing blocks or broadcasting transactions.`,
Value: 1000,
},
+ &cli.Int64Flag{
+ Name: "starting-timestamp",
+ Usage: `
+The timestamp to use for the first block, given in milliseconds since the unix epoch.
+If this is < 0, the current system time is used.
+If this is >= 0, the system time is ignored and this timestamp is used for the first block instead.`,
+ Value: -1,
+ },
+ &cli.BoolFlag{
+ Name: "starting-timestamp-from-genesis",
+ Usage: `
+If this is true, it overrides the starting-timestamp, and instead
+bases the time for the first block on the genesis time, incremented by the block time
+or the system time between creating the genesis request and producing the first block.`,
+ Value: false,
+ },
},
ArgsUsage: argumentString,
Action: func(c *cli.Context) error {
@@ -85,8 +121,8 @@ advancing blocks or broadcasting transactions.`,
return cli.Exit(fmt.Sprintf("Invalid connection mode: %s. Connection mode must be either 'socket' or 'grpc'.\nUsage: %s", connectionMode, argumentString), 1)
}
- blockTime := c.Int("block-time")
- fmt.Printf("Block time: %d\n", blockTime)
+ blockProductionInterval := c.Int("block-production-interval")
+ fmt.Printf("Block production interval: %d\n", blockProductionInterval)
// read node homes from args
nodeHomes := strings.Split(nodeHomesString, ",")
@@ -106,6 +142,25 @@ advancing blocks or broadcasting transactions.`,
clients := []abci_client.AbciCounterpartyClient{}
privValsMap := make(map[string]types.PrivValidator)
+ // read starting timestamp from args
+ // if starting timestamp should be taken from genesis,
+ // read it from there
+ var startingTime time.Time
+ if c.Bool("starting-timestamp-from-genesis") {
+ startingTime = genesisDoc.GenesisTime
+ } else {
+ if c.Int64("starting-timestamp") < 0 {
+ startingTime = time.Now()
+ } else {
+ dur := time.Duration(c.Int64("starting-timestamp")) * time.Millisecond
+ startingTime = time.Unix(0, 0).Add(dur)
+ }
+ }
+ fmt.Printf("Starting time: %s\n", startingTime.Format(time.RFC3339))
+
+ // read block time from args
+ blockTime := time.Duration(c.Int64("block-time")) * time.Millisecond
+ fmt.Printf("Block time: %d\n", blockTime.Milliseconds())
for i, appAddress := range appAddresses {
logger.Info("Connecting to client at %v", appAddress)
@@ -147,6 +202,13 @@ advancing blocks or broadcasting transactions.`,
privValsMap[addr.String()] = privVal
}
+ var timeHandler abci_client.TimeHandler
+ if blockTime < 0 {
+ timeHandler = abci_client.NewSystemClockTimeHandler(startingTime)
+ } else {
+ timeHandler = abci_client.NewFixedBlockTimeHandler(blockTime)
+ }
+
abci_client.GlobalClient = abci_client.NewAbciClient(
clients,
logger,
@@ -154,10 +216,14 @@ advancing blocks or broadcasting transactions.`,
&types.Block{},
&types.Commit{},
&storage.MapStorage{},
+ timeHandler,
privValsMap,
true,
)
+ abci_client.GlobalClient.AutoIncludeTx = c.Bool("auto-tx")
+ fmt.Printf("Auto include tx: %t\n", abci_client.GlobalClient.AutoIncludeTx)
+
// connect to clients
abci_client.GlobalClient.RetryDisconnectedClients()
@@ -168,8 +234,15 @@ advancing blocks or broadcasting transactions.`,
panic(err)
}
+ var firstBlockTime time.Time
+ if blockTime < 0 {
+ firstBlockTime = startingTime
+ } else {
+ firstBlockTime = startingTime.Add(blockTime)
+ }
+
// run an empty block
- _, _, _, _, _, err = abci_client.GlobalClient.RunBlock(nil)
+ err = abci_client.GlobalClient.RunBlockWithTime(firstBlockTime)
if err != nil {
logger.Error(err.Error())
panic(err)
@@ -177,15 +250,15 @@ advancing blocks or broadcasting transactions.`,
go rpc_server.StartRPCServerWithDefaultConfig(cometMockListenAddress, logger)
- if blockTime > 0 {
+ if blockProductionInterval > 0 {
// produce blocks according to blockTime
for {
- _, _, _, _, _, err := abci_client.GlobalClient.RunBlock(nil)
+ err := abci_client.GlobalClient.RunBlock()
if err != nil {
logger.Error(err.Error())
panic(err)
}
- time.Sleep(time.Millisecond * time.Duration(blockTime))
+ time.Sleep(time.Millisecond * time.Duration(blockProductionInterval))
}
} else {
// wait forever
diff --git a/cometmock/rpc_server/routes.go b/cometmock/rpc_server/routes.go
index 9374634..7a62090 100644
--- a/cometmock/rpc_server/routes.go
+++ b/cometmock/rpc_server/routes.go
@@ -11,6 +11,8 @@ import (
cmtquery "github.com/cometbft/cometbft/libs/pubsub/query"
"github.com/cometbft/cometbft/p2p"
cometp2p "github.com/cometbft/cometbft/p2p"
+
+ abcitypes "github.com/cometbft/cometbft/abci/types"
ctypes "github.com/cometbft/cometbft/rpc/core/types"
rpc "github.com/cometbft/cometbft/rpc/jsonrpc/server"
rpctypes "github.com/cometbft/cometbft/rpc/jsonrpc/types"
@@ -86,8 +88,8 @@ func AdvanceTime(ctx *rpctypes.Context, duration_in_seconds time.Duration) (*Res
return nil, errors.New("duration to advance time by must be greater than 0")
}
- abci_client.GlobalClient.IncrementTimeOffset(duration_in_seconds * time.Second)
- return &ResultAdvanceTime{time.Now().Add(abci_client.GlobalClient.GetTimeOffset())}, nil
+ res := abci_client.GlobalClient.TimeHandler.AdvanceTime(duration_in_seconds * time.Second)
+ return &ResultAdvanceTime{res}, nil
}
type ResultSetSigningStatus struct {
@@ -433,21 +435,17 @@ func Health(ctx *rpctypes.Context) (*ctypes.ResultHealth, error) {
return &ctypes.ResultHealth{}, nil
}
+// CURRENTLY UNSUPPORTED - THIS IS BECAUSE IT IS DISCOURAGED TO USE THIS BY COMETBFT
+// needs some major changes to work with ABCI++
// BroadcastTxCommit broadcasts a transaction,
// and wait until it is included in a block and and comitted.
// In our case, this means running a block with just the the transition,
// then return.
func BroadcastTxCommit(ctx *rpctypes.Context, tx types.Tx) (*ctypes.ResultBroadcastTxCommit, error) {
- abci_client.GlobalClient.Logger.Info(
- "BroadcastTxCommit called", "tx", tx)
-
- res, err := BroadcastTx(&tx)
- return res, err
+ return nil, errors.New("BroadcastTxCommit is currently not supported. Try BroadcastTxSync or BroadcastTxAsync instead")
}
-// BroadcastTxSync would normally broadcast a transaction and wait until it gets the result from CheckTx.
-// In our case, we run a block with just the transition in it,
-// then return.
+// BroadcastTxSync broadcasts a transaction and waits until it gets the result from CheckTx.
func BroadcastTxSync(ctx *rpctypes.Context, tx types.Tx) (*ctypes.ResultBroadcastTx, error) {
abci_client.GlobalClient.Logger.Info(
"BroadcastTxSync called", "tx", tx)
@@ -481,25 +479,26 @@ func BroadcastTxAsync(ctx *rpctypes.Context, tx types.Tx) (*ctypes.ResultBroadca
return &ctypes.ResultBroadcastTx{}, nil
}
-// BroadcastTx delivers a transaction to the ABCI client, includes it in the next block, then returns.
func BroadcastTx(tx *types.Tx) (*ctypes.ResultBroadcastTxCommit, error) {
abci_client.GlobalClient.Logger.Info(
"BroadcastTxs called", "tx", tx)
- byteTx := []byte(*tx)
-
- _, responseCheckTx, responseDeliverTx, _, _, err := abci_client.GlobalClient.RunBlock(&byteTx)
+ txBytes := []byte(*tx)
+ checkTxResponse, err := abci_client.GlobalClient.SendCheckTx(abcitypes.CheckTxType_New, &txBytes)
if err != nil {
return nil, err
}
+ abci_client.GlobalClient.QueueTx(*tx)
+
+ if abci_client.GlobalClient.AutoIncludeTx {
+ go abci_client.GlobalClient.RunBlock()
+ }
- // TODO: fill the return value if necessary
return &ctypes.ResultBroadcastTxCommit{
- CheckTx: *responseCheckTx,
- DeliverTx: *responseDeliverTx,
- Height: abci_client.GlobalClient.LastBlock.Height,
- Hash: tx.Hash(),
- }, nil
+ CheckTx: *checkTxResponse,
+ Hash: tx.Hash(),
+ Height: abci_client.GlobalClient.CurState.LastBlockHeight,
+ }, err
}
func ABCIInfo(ctx *rpctypes.Context) (*ctypes.ResultABCIInfo, error) {
diff --git a/cometmock/utils/txs.go b/cometmock/utils/txs.go
new file mode 100644
index 0000000..92601e3
--- /dev/null
+++ b/cometmock/utils/txs.go
@@ -0,0 +1,17 @@
+package utils
+
+import (
+ "bytes"
+
+ cmttypes "github.com/cometbft/cometbft/types"
+)
+
+// Contains returns true if txs contains tx, false otherwise.
+func Contains(txs cmttypes.Txs, tx cmttypes.Tx) bool {
+ for _, ttx := range txs {
+ if bytes.Equal([]byte(ttx), []byte(tx)) {
+ return true
+ }
+ }
+ return false
+}
diff --git a/e2e-tests/.gitignore b/e2e-tests/.gitignore
new file mode 100644
index 0000000..219563d
--- /dev/null
+++ b/e2e-tests/.gitignore
@@ -0,0 +1,4 @@
+# Scripts that are generated by the testnet setup
+start_apps.sh
+start_cometmock.sh
+cometmock_log
\ No newline at end of file
diff --git a/e2e-tests/local-testnet-singlechain-restart.sh b/e2e-tests/local-testnet-singlechain-restart.sh
new file mode 100755
index 0000000..7edba3a
--- /dev/null
+++ b/e2e-tests/local-testnet-singlechain-restart.sh
@@ -0,0 +1,27 @@
+#!/bin/bash
+
+# After the testnet was started, this script can restart it.
+# It does so by killing the existing testnet,
+# overwriting the node home directories with backups made
+# right after initializatio, and then starting the testnet again.
+
+
+BINARY_NAME=$1
+
+set -eux
+
+ROOT_DIR=${HOME}/nodes/provider
+BACKUP_DIR=${ROOT_DIR}_bkup
+
+if [ -z "$BINARY_NAME" ]; then
+ echo "Usage: $0 [cometmock_args]"
+ exit 1
+fi
+
+# Kill the testnet
+pkill -f ^$BINARY_NAME &> /dev/null || true
+pkill -f ^cometmock &> /dev/null || true
+
+# Restore the backup
+rm -rf ${ROOT_DIR}
+cp -r ${BACKUP_DIR} ${ROOT_DIR}
\ No newline at end of file
diff --git a/e2e-tests/local-testnet-singlechain-setup.sh b/e2e-tests/local-testnet-singlechain-setup.sh
new file mode 100755
index 0000000..eb89c09
--- /dev/null
+++ b/e2e-tests/local-testnet-singlechain-setup.sh
@@ -0,0 +1,228 @@
+#!/bin/bash
+## This script sets up the environment to run the single chain local testnet.
+## Importantly, it does not actually start nodes (or cometmock) - instead,
+## it will produce two scripts, start_apps.sh and start_cometmock.sh.
+## After this script is done setting up, simply run these two scripts to run the
+## testnet.
+## The reason for this is that we want to be able to make the testnet setup
+## differentiated from the actual run to allow for better caching in Docker.
+
+set -eux
+
+BINARY_NAME=$1
+
+# User balance of stake tokens
+USER_COINS="100000000000stake"
+# Amount of stake tokens staked
+STAKE="100000000stake"
+# Node IP address
+NODE_IP="127.0.0.1"
+
+# Home directory
+HOME_DIR=$HOME
+
+rm -rf ./start_apps.sh
+rm -rf ./start_cometmock.sh
+
+# Validator moniker
+MONIKERS=("coordinator" "alice" "bob")
+LEAD_VALIDATOR_MONIKER="coordinator"
+
+PROV_NODES_ROOT_DIR=${HOME_DIR}/nodes/provider
+CONS_NODES_ROOT_DIR=${HOME_DIR}/nodes/consumer
+
+# Base port. Ports assigned after these ports sequentially by nodes.
+RPC_LADDR_BASEPORT=29170
+P2P_LADDR_BASEPORT=29180
+GRPC_LADDR_BASEPORT=29190
+NODE_ADDRESS_BASEPORT=29200
+PPROF_LADDR_BASEPORT=29210
+CLIENT_BASEPORT=29220
+
+# keeps a comma separated list of node addresses for provider and consumer
+PROVIDER_NODE_LISTEN_ADDR_STR=""
+CONSUMER_NODE_LISTEN_ADDR_STR=""
+
+# Strings that keep the homes of provider nodes and homes of consumer nodes
+PROV_NODES_HOME_STR=""
+CONS_NODES_HOME_STR=""
+
+PROVIDER_COMETMOCK_ADDR=tcp://$NODE_IP:22331
+CONSUMER_COMETMOCK_ADDR=tcp://$NODE_IP:22332
+
+# Clean start
+pkill -f ^$BINARY_NAME &> /dev/null || true
+pkill -f ^cometmock &> /dev/null || true
+sleep 1
+rm -rf ${PROV_NODES_ROOT_DIR}
+rm -rf ${CONS_NODES_ROOT_DIR}
+
+# Let lead validator create genesis file
+LEAD_VALIDATOR_PROV_DIR=${PROV_NODES_ROOT_DIR}/provider-${LEAD_VALIDATOR_MONIKER}
+LEAD_VALIDATOR_CONS_DIR=${CONS_NODES_ROOT_DIR}/consumer-${LEAD_VALIDATOR_MONIKER}
+LEAD_PROV_KEY=${LEAD_VALIDATOR_MONIKER}-key
+LEAD_PROV_LISTEN_ADDR=tcp://${NODE_IP}:${RPC_LADDR_BASEPORT}
+
+for index in "${!MONIKERS[@]}"
+do
+ MONIKER=${MONIKERS[$index]}
+ # validator key
+ PROV_KEY=${MONIKER}-key
+
+ # home directory of this validator on provider
+ PROV_NODE_DIR=${PROV_NODES_ROOT_DIR}/provider-${MONIKER}
+
+ # home directory of this validator on consumer
+ CONS_NODE_DIR=${CONS_NODES_ROOT_DIR}/consumer-${MONIKER}
+
+ # Build genesis file and node directory structure
+ $BINARY_NAME init $MONIKER --chain-id provider --home ${PROV_NODE_DIR}
+ jq ".app_state.gov.params.voting_period = \"100000s\" | .app_state.staking.params.unbonding_time = \"86400s\" | .app_state.slashing.params.signed_blocks_window=\"1000\" " \
+ ${PROV_NODE_DIR}/config/genesis.json > \
+ ${PROV_NODE_DIR}/edited_genesis.json && mv ${PROV_NODE_DIR}/edited_genesis.json ${PROV_NODE_DIR}/config/genesis.json
+
+
+ sleep 1
+
+ # Create account keypair
+ $BINARY_NAME keys add $PROV_KEY --home ${PROV_NODE_DIR} --keyring-backend test --output json > ${PROV_NODE_DIR}/${PROV_KEY}.json 2>&1
+ sleep 1
+
+ # copy genesis in, unless this validator is the lead validator
+ if [ $MONIKER != $LEAD_VALIDATOR_MONIKER ]; then
+ cp ${LEAD_VALIDATOR_PROV_DIR}/config/genesis.json ${PROV_NODE_DIR}/config/genesis.json
+ fi
+
+ # Add stake to user
+ PROV_ACCOUNT_ADDR=$(jq -r '.address' ${PROV_NODE_DIR}/${PROV_KEY}.json)
+ $BINARY_NAME genesis add-genesis-account $PROV_ACCOUNT_ADDR $USER_COINS --home ${PROV_NODE_DIR} --keyring-backend test
+ sleep 1
+
+ # copy genesis out, unless this validator is the lead validator
+ if [ $MONIKER != $LEAD_VALIDATOR_MONIKER ]; then
+ cp ${PROV_NODE_DIR}/config/genesis.json ${LEAD_VALIDATOR_PROV_DIR}/config/genesis.json
+ fi
+
+ PPROF_LADDR=${NODE_IP}:$(($PPROF_LADDR_BASEPORT + $index))
+ P2P_LADDR_PORT=$(($P2P_LADDR_BASEPORT + $index))
+
+ # adjust configs of this node
+ sed -i -r 's/timeout_commit = "5s"/timeout_commit = "3s"/g' ${PROV_NODE_DIR}/config/config.toml
+ sed -i -r 's/timeout_propose = "3s"/timeout_propose = "1s"/g' ${PROV_NODE_DIR}/config/config.toml
+
+ # make address book non-strict. necessary for this setup
+ sed -i -r 's/addr_book_strict = true/addr_book_strict = false/g' ${PROV_NODE_DIR}/config/config.toml
+
+ # avoid port double binding
+ sed -i -r "s/pprof_laddr = \"localhost:6060\"/pprof_laddr = \"${PPROF_LADDR}\"/g" ${PROV_NODE_DIR}/config/config.toml
+
+ # allow duplicate IP addresses (all nodes are on the same machine)
+ sed -i -r 's/allow_duplicate_ip = false/allow_duplicate_ip = true/g' ${PROV_NODE_DIR}/config/config.toml
+done
+
+for MONIKER in "${MONIKERS[@]}"
+do
+ # validator key
+ PROV_KEY=${MONIKER}-key
+
+ # home directory of this validator on provider
+ PROV_NODE_DIR=${PROV_NODES_ROOT_DIR}/provider-${MONIKER}
+
+ # copy genesis in, unless this validator is the lead validator
+ if [ $MONIKER != $LEAD_VALIDATOR_MONIKER ]; then
+ cp ${LEAD_VALIDATOR_PROV_DIR}/config/genesis.json* ${PROV_NODE_DIR}/config/genesis.json
+ fi
+
+ # Stake 1/1000 user's coins
+ $BINARY_NAME genesis gentx $PROV_KEY $STAKE --chain-id provider --home ${PROV_NODE_DIR} --keyring-backend test --moniker $MONIKER
+ sleep 1
+
+ # Copy gentxs to the lead validator for possible future collection.
+ # Obviously we don't need to copy the first validator's gentx to itself
+ if [ $MONIKER != $LEAD_VALIDATOR_MONIKER ]; then
+ cp ${PROV_NODE_DIR}/config/gentx/* ${LEAD_VALIDATOR_PROV_DIR}/config/gentx/
+ fi
+done
+
+# Collect genesis transactions with lead validator
+$BINARY_NAME genesis collect-gentxs --home ${LEAD_VALIDATOR_PROV_DIR} --gentx-dir ${LEAD_VALIDATOR_PROV_DIR}/config/gentx/
+
+sleep 1
+
+START_COMMANDS=""
+for index in "${!MONIKERS[@]}"
+do
+ MONIKER=${MONIKERS[$index]}
+
+ PERSISTENT_PEERS=""
+
+ for peer_index in "${!MONIKERS[@]}"
+ do
+ if [ $index == $peer_index ]; then
+ continue
+ fi
+ PEER_MONIKER=${MONIKERS[$peer_index]}
+
+ PEER_PROV_NODE_DIR=${PROV_NODES_ROOT_DIR}/provider-${PEER_MONIKER}
+
+ PEER_NODE_ID=$($BINARY_NAME tendermint show-node-id --home ${PEER_PROV_NODE_DIR})
+
+ PEER_P2P_LADDR_PORT=$(($P2P_LADDR_BASEPORT + $peer_index))
+ PERSISTENT_PEERS="$PERSISTENT_PEERS,$PEER_NODE_ID@${NODE_IP}:${PEER_P2P_LADDR_PORT}"
+ done
+
+ # remove trailing comma from persistent peers
+ PERSISTENT_PEERS=${PERSISTENT_PEERS:1}
+
+ # validator key
+ PROV_KEY=${MONIKER}-key
+
+ # home directory of this validator on provider
+ PROV_NODE_DIR=${PROV_NODES_ROOT_DIR}/provider-${MONIKER}
+
+ # home directory of this validator on consumer
+ CONS_NODE_DIR=${PROV_NODES_ROOT_DIR}/consumer-${MONIKER}
+
+ # copy genesis in, unless this validator is already the lead validator and thus it already has its genesis
+ if [ $MONIKER != $LEAD_VALIDATOR_MONIKER ]; then
+ cp ${LEAD_VALIDATOR_PROV_DIR}/config/genesis.json ${PROV_NODE_DIR}/config/genesis.json
+ fi
+
+ # enable vote extensions by setting .consesnsus.params.abci.vote_extensions_enable_height to 1, but 1 does not work currently - set it to 2 instead. see https://github.com/cosmos/cosmos-sdk/issues/18029#issuecomment-1754598598
+ jq ".consensus.params.abci.vote_extensions_enable_height = \"2\"" ${PROV_NODE_DIR}/config/genesis.json > ${PROV_NODE_DIR}/edited_genesis.json && mv ${PROV_NODE_DIR}/edited_genesis.json ${PROV_NODE_DIR}/config/genesis.json
+
+ RPC_LADDR_PORT=$(($RPC_LADDR_BASEPORT + $index))
+ P2P_LADDR_PORT=$(($P2P_LADDR_BASEPORT + $index))
+ GRPC_LADDR_PORT=$(($GRPC_LADDR_BASEPORT + $index))
+ NODE_ADDRESS_PORT=$(($NODE_ADDRESS_BASEPORT + $index))
+
+ PROVIDER_NODE_LISTEN_ADDR_STR="${NODE_IP}:${NODE_ADDRESS_PORT},$PROVIDER_NODE_LISTEN_ADDR_STR"
+ PROV_NODES_HOME_STR="${PROV_NODE_DIR},$PROV_NODES_HOME_STR"
+
+ rm -rf ${PROV_NODES_ROOT_DIR}_bkup
+ cp -r ${PROV_NODES_ROOT_DIR} ${PROV_NODES_ROOT_DIR}_bkup
+
+ # Start gaia
+ echo $BINARY_NAME start \
+ --home ${PROV_NODE_DIR} \
+ --transport=grpc --with-tendermint=false \
+ --p2p.persistent_peers ${PERSISTENT_PEERS} \
+ --rpc.laddr tcp://${NODE_IP}:${RPC_LADDR_PORT} \
+ --grpc.address ${NODE_IP}:${GRPC_LADDR_PORT} \
+ --address tcp://${NODE_IP}:${NODE_ADDRESS_PORT} \
+ --p2p.laddr tcp://${NODE_IP}:${P2P_LADDR_PORT} \
+ --grpc-web.enable=false "&> ${PROV_NODE_DIR}/logs &" | tee -a start_apps.sh
+
+ sleep 5
+done
+
+PROVIDER_NODE_LISTEN_ADDR_STR=${PROVIDER_NODE_LISTEN_ADDR_STR::${#PROVIDER_NODE_LISTEN_ADDR_STR}-1}
+PROV_NODES_HOME_STR=${PROV_NODES_HOME_STR::${#PROV_NODES_HOME_STR}-1}
+
+echo "Testnet applications are set up! Starting CometMock..."
+echo cometmock \$1 $PROVIDER_NODE_LISTEN_ADDR_STR ${LEAD_VALIDATOR_PROV_DIR}/config/genesis.json $PROVIDER_COMETMOCK_ADDR $PROV_NODES_HOME_STR grpc "&> ${LEAD_VALIDATOR_PROV_DIR}/cometmock_log &" | tee -a start_cometmock.sh
+
+chmod +x start_apps.sh
+chmod +x start_cometmock.sh
+
+# cometmock $PROVIDER_NODE_LISTEN_ADDR_STR ${LEAD_VALIDATOR_PROV_DIR}/config/genesis.json $PROVIDER_COMETMOCK_ADDR $PROV_NODES_HOME_STR grpc $COMETMOCK_ARGS &> ${LEAD_VALIDATOR_PROV_DIR}/cometmock_log &
\ No newline at end of file
diff --git a/e2e-tests/local-testnet-singlechain-start.sh b/e2e-tests/local-testnet-singlechain-start.sh
new file mode 100755
index 0000000..bad0414
--- /dev/null
+++ b/e2e-tests/local-testnet-singlechain-start.sh
@@ -0,0 +1,9 @@
+#!/bin/bash
+
+## Starts the testnet, assuming that the scripts generated by local-testnet-singlechain-setup.sh
+## are already present in the current directory.
+
+COMETMOCK_ARGS=$1
+
+./start_apps.sh
+./start_cometmock.sh "$1"
\ No newline at end of file
diff --git a/e2e-tests/local-testnet-singlechain.sh b/e2e-tests/local-testnet-singlechain.sh
index 8c17489..f22f8c9 100755
--- a/e2e-tests/local-testnet-singlechain.sh
+++ b/e2e-tests/local-testnet-singlechain.sh
@@ -1,15 +1,17 @@
#!/bin/bash
+
+## This script sets up the local testnet and starts it.
+## To run this, both the application binary and cometmock must be installed.
set -eux
+parent_path=$( cd "$(dirname "${BASH_SOURCE[0]}")" ; pwd -P )
+pushd "$parent_path"
+
BINARY_NAME=$1
-# User balance of stake tokens
-USER_COINS="100000000000stake"
-# Amount of stake tokens staked
-STAKE="100000000stake"
-# Node IP address
-NODE_IP="127.0.0.1"
+COMETMOCK_ARGS=$2
+<<<<<<< HEAD
# Home directory
HOME_DIR=$HOME
@@ -205,4 +207,9 @@ PROV_NODES_HOME_STR=${PROV_NODES_HOME_STR::${#PROV_NODES_HOME_STR}-1}
echo "Testnet applications are set up! Starting CometMock..."
cometmock $PROVIDER_NODE_LISTEN_ADDR_STR ${LEAD_VALIDATOR_PROV_DIR}/config/genesis.json $PROVIDER_COMETMOCK_ADDR $PROV_NODES_HOME_STR grpc &> ${LEAD_VALIDATOR_PROV_DIR}/cometmock_log &
-sleep 5
\ No newline at end of file
+sleep 5
+=======
+# set up the net
+./local-testnet-singlechain-setup.sh $BINARY_NAME "$COMETMOCK_ARGS"
+./local-testnet-singlechain-start.sh
+>>>>>>> 7edb4c1 (Add fine-grained control of time (#88))
diff --git a/e2e-tests/main_test.go b/e2e-tests/main_test.go
index d0f5ad2..4d20d3e 100644
--- a/e2e-tests/main_test.go
+++ b/e2e-tests/main_test.go
@@ -1,85 +1,74 @@
package main
import (
- "bytes"
"encoding/json"
"fmt"
"os/exec"
"strconv"
+ "strings"
"testing"
"time"
-)
-
-func runCommandWithOutput(cmd *exec.Cmd) (string, error) {
- var stdout, stderr bytes.Buffer
- cmd.Stdout = &stdout
- cmd.Stderr = &stderr
-
- err := cmd.Run()
- if err != nil {
- return "", fmt.Errorf("error running command: %v\nstdout: %s\nstderr: %s", err, stdout.String(), stderr.String())
- }
-
- return stdout.String(), nil
-}
-
-// From the output of the AbciInfo command, extract the latest block height.
-// The json bytes should look e.g. like this:
-// {"jsonrpc":"2.0","id":1,"result":{"response":{"data":"interchain-security-p","last_block_height":"2566","last_block_app_hash":"R4Q3Si7+t7TIidl2oTHcQRDNEz+lP0IDWhU5OI89psg="}}}
-func extractHeightFromInfo(jsonBytes []byte) (int, error) {
- // Use a generic map to represent the JSON structure
- var data map[string]interface{}
-
- if err := json.Unmarshal(jsonBytes, &data); err != nil {
- return -1, fmt.Errorf("Failed to unmarshal JSON %s \n error was %v", string(jsonBytes), err)
- }
-
- // Navigate the map and use type assertions to get the last_block_height
- result, ok := data["result"].(map[string]interface{})
- if !ok {
- return -1, fmt.Errorf("Failed to navigate abci_info output structure trying to access result: json was %s", string(jsonBytes))
- }
- response, ok := result["response"].(map[string]interface{})
- if !ok {
- return -1, fmt.Errorf("Failed to navigate abci_info output structure trying to access response: json was %s", string(jsonBytes))
- }
-
- lastBlockHeight, ok := response["last_block_height"].(string)
- if !ok {
- return -1, fmt.Errorf("Failed to navigate abci_info output structure trying to access last_block_height: json was %s", string(jsonBytes))
- }
-
- return strconv.Atoi(lastBlockHeight)
-}
+ "github.com/stretchr/testify/require"
+)
-// Tests happy path functionality for Abci Info.
-func TestAbciInfo(t *testing.T) {
+func StartChain(
+ t *testing.T,
+ cometmockArgs string,
+) error {
// execute the local-testnet-singlechain.sh script
t.Log("Running local-testnet-singlechain.sh")
- cmd := exec.Command("./local-testnet-singlechain.sh", "simd")
+ cmd := exec.Command("./local-testnet-singlechain-restart.sh", "simd")
_, err := runCommandWithOutput(cmd)
if err != nil {
- t.Fatalf("Error running local-testnet-singlechain.sh: %v", err)
+ return fmt.Errorf("Error running local-testnet-singlechain.sh: %v", err)
+ }
+
+ cmd = exec.Command("./local-testnet-singlechain-start.sh", cometmockArgs)
+ _, err = runCommandWithOutput(cmd)
+ if err != nil {
+ return fmt.Errorf("Error running local-testnet-singlechain.sh: %v", err)
}
t.Log("Done starting testnet")
// wait until we are producing blocks
for {
+ // --type height 0 gets the latest height
out, err := exec.Command("bash", "-c", "simd q block --node tcp://127.0.0.1:22331 | jq -r '.block.header.height'").Output()
- if err == nil {
+ if err != nil {
+ t.Log("Error running query command: ", err)
+ continue
+ }
+
+ height, err := strconv.Atoi(strings.TrimSpace(string(out)))
+ if err != nil {
+ t.Log("Could not parse height: ", string(out))
+ }
+
+ if err == nil && height > 0 {
t.Log("We are producing blocks: ", string(out))
break
}
t.Log("Waiting for blocks to be produced, latest output: ", string(out))
time.Sleep(1 * time.Second)
}
+ time.Sleep(5 * time.Second)
+ return nil
+}
+
+// Tests happy path functionality for Abci Info.
+func TestAbciInfo(t *testing.T) {
+ // start the chain
+ err := StartChain(t, "")
+ if err != nil {
+ t.Fatalf("Error starting chain: %v", err)
+ }
// call the abci_info command by calling curl on the REST endpoint
// curl -H 'Content-Type: application/json' -H 'Accept:application/json' --data '{"jsonrpc":"2.0","method":"abci_info","id":1}' 127.0.0.1:22331
args := []string{"bash", "-c", "curl -H 'Content-Type: application/json' -H 'Accept:application/json' --data '{\"jsonrpc\":\"2.0\",\"method\":\"abci_info\",\"id\":1}' 127.0.0.1:22331"}
- cmd = exec.Command(args[0], args[1:]...)
+ cmd := exec.Command(args[0], args[1:]...)
out, err := runCommandWithOutput(cmd)
if err != nil {
t.Fatalf("Error running curl\ncommand: %v\noutput: %v\nerror: %v", cmd, string(out), err)
@@ -112,3 +101,246 @@ func TestAbciInfo(t *testing.T) {
t.Fatalf("Expected block height to increase, but it did not. First height was %v, second height was %v", height, height2)
}
}
+
+func TestAbciQuery(t *testing.T) {
+ // start the chain
+ err := StartChain(t, "")
+ if err != nil {
+ t.Fatalf("Error starting chain: %v", err)
+ }
+
+ // call the abci_query command by submitting a query that hits the AbciQuery endpoint
+ // for simplicity, we query for the staking params here - any query would work,
+ // but ones without arguments are easier to construct
+ args := []string{"bash", "-c", "simd q staking params --node tcp://127.0.0.1:22331 --output json"}
+ cmd := exec.Command(args[0], args[1:]...)
+ out, err := runCommandWithOutput(cmd)
+ if err != nil {
+ t.Fatalf("Error running command: %v\noutput: %v\nerror: %v", cmd, string(out), err)
+ }
+
+ // check that the output is valid JSON
+ var data map[string]interface{}
+ if err := json.Unmarshal([]byte(out), &data); err != nil {
+ t.Fatalf("Failed to unmarshal JSON %s \n error was %v", string(out), err)
+ }
+
+ // check that the output contains the expected unbonding_time field. its contents are not important
+ _, ok := data["unbonding_time"]
+ if !ok {
+ t.Fatalf("Expected output to contain unbonding_time field, but it did not. Output was %s", string(out))
+ }
+}
+
+func TestTx(t *testing.T) {
+ err := StartChain(t, "")
+ if err != nil {
+ t.Fatalf("Error starting chain: %v", err)
+ }
+
+ // check the current amount in the community pool
+ communityPoolSize, err := getCommunityPoolSize()
+ require.NoError(t, err)
+
+ // send some tokens to the community pool
+ err = sendToCommunityPool(500000, "coordinator")
+ require.NoError(t, err)
+
+ // check that the amount in the community pool has increased
+ communityPoolSize2, err := getCommunityPoolSize()
+ require.NoError(t, err)
+
+ // cannot check for equality because the community pool gets dust over time
+ require.True(t, communityPoolSize2 > communityPoolSize+500000)
+}
+
+// TestBlockTime checks that the basic behaviour with a specified block-time is as expected,
+// i.e. the time increases by the specified block time for each block.
+func TestBlockTime(t *testing.T) {
+ err := StartChain(t, "--block-time=5000")
+ if err != nil {
+ t.Fatalf("Error starting chain: %v", err)
+ }
+
+ // get a block with height+time
+ blockString, err := QueryBlock()
+ require.NoError(t, err)
+
+ // get the height and time from the block
+ height, err := GetHeightFromBlock(blockString)
+ require.NoError(t, err)
+
+ blockTime, err := GetTimeFromBlock(blockString)
+ require.NoError(t, err)
+
+ // wait for a couple of blocks to be produced
+ time.Sleep(10 * time.Second)
+
+ // get the new height and time
+ blockString2, err := QueryBlock()
+ require.NoError(t, err)
+
+ height2, err := GetHeightFromBlock(blockString2)
+ require.NoError(t, err)
+
+ blockTime2, err := GetTimeFromBlock(blockString2)
+ require.NoError(t, err)
+
+ blockDifference := height2 - height
+ // we expect that at least one block was produced, otherwise there is a problem
+ require.True(t, blockDifference >= 1)
+
+ // get the expected time diff between blocks, as block time was set to 5000 millis = 5 seconds
+ expectedTimeDifference := time.Duration(blockDifference) * 5 * time.Second
+
+ timeDifference := blockTime2.Sub(blockTime)
+
+ require.Equal(t, expectedTimeDifference, timeDifference)
+}
+
+// TestAutoBlockProductionOff checks that the basic behaviour with
+// block-production-interval is as expected, i.e. blocks only
+// appear when it is manually instructed.
+func TestNoAutoBlockProduction(t *testing.T) {
+ err := StartChain(t, "--block-production-interval=-1 --block-time=0")
+ if err != nil {
+ t.Fatalf("Error starting chain: %v", err)
+ }
+
+ height, blockTime, err := GetHeightAndTime()
+ require.NoError(t, err)
+
+ // wait a few seconds to detect it blocks are produced automatically
+ time.Sleep(10 * time.Second)
+
+ // get the new height and time
+ height2, blockTime2, err := GetHeightAndTime()
+ require.NoError(t, err)
+
+ // no blocks should have been produced
+ require.Equal(t, height, height2)
+ require.Equal(t, blockTime, blockTime2)
+
+ // advance time by 5 seconds
+ err = AdvanceTime(5 * time.Second)
+ require.NoError(t, err)
+
+ // get the height and time again, they should not have changed yet
+ height3, blockTime3, err := GetHeightAndTime()
+ require.NoError(t, err)
+
+ require.Equal(t, height, height3)
+ require.Equal(t, blockTime, blockTime3)
+
+ // produce a block
+ err = AdvanceBlocks(1)
+ require.NoError(t, err)
+
+ // get the height and time again, they should have changed
+ height4, blockTime4, err := GetHeightAndTime()
+ require.NoError(t, err)
+
+ require.Equal(t, height+1, height4)
+ require.Equal(t, blockTime.Add(5*time.Second), blockTime4)
+}
+
+// TestNoAutoTx checks that without auto-tx, transactions are not included
+// in blocks automatically.
+func TestNoAutoTx(t *testing.T) {
+ err := StartChain(t, "--block-production-interval=-1 --auto-tx=false")
+ if err != nil {
+ t.Fatalf("Error starting chain: %v", err)
+ }
+
+ // produce a couple of blocks to initialize the community pool
+ err = AdvanceBlocks(10)
+ require.NoError(t, err)
+
+ height, blockTime, err := GetHeightAndTime()
+ require.NoError(t, err)
+
+ communityPoolBefore, err := getCommunityPoolSize()
+ require.NoError(t, err)
+
+ // broadcast txs
+ err = sendToCommunityPool(50000000000, "coordinator")
+ require.NoError(t, err)
+ err = sendToCommunityPool(50000000000, "bob")
+ require.NoError(t, err)
+
+ // get the new height and time
+ height2, blockTime2, err := GetHeightAndTime()
+ require.NoError(t, err)
+
+ // no blocks should have been produced
+ require.Equal(t, height, height2)
+ require.Equal(t, blockTime, blockTime2)
+
+ // produce a block
+ err = AdvanceBlocks(1)
+ require.NoError(t, err)
+
+ // get the height and time again, they should have changed
+ height3, blockTime3, err := GetHeightAndTime()
+ require.NoError(t, err)
+
+ require.Equal(t, height+1, height3)
+ // exact time does not matter, just that it is after the previous block
+ require.True(t, blockTime.Before(blockTime3))
+
+ // check that the community pool was increased
+ communityPoolAfter, err := getCommunityPoolSize()
+ require.NoError(t, err)
+
+ // cannot check for equality because the community pool gets dust over time
+ require.True(t, communityPoolAfter > communityPoolBefore+500000000)
+}
+
+func TestStartingTimestamp(t *testing.T) {
+ err := StartChain(t, "--block-production-interval=-1 --auto-tx=false --starting-timestamp=0 --block-time=1")
+ if err != nil {
+ t.Fatalf("Error starting chain: %v", err)
+ }
+
+ // produce a couple of blocks
+ err = AdvanceBlocks(10)
+ require.NoError(t, err)
+
+ // get the time
+ _, blockTime, err := GetHeightAndTime()
+ require.NoError(t, err)
+
+ // the time should be starting-timestamp + 10 * blockTime + 1 (for the first block needed after Genesis)
+ startingTimestamp := time.Unix(0, 0)
+ expectedTime := startingTimestamp.Add(11 * time.Millisecond)
+
+ require.True(t, expectedTime.Compare(blockTime) == 0, "expectedTime: %v, blockTime: %v", expectedTime, blockTime)
+}
+
+func TestSystemStartingTime(t *testing.T) {
+ err := StartChain(t, "--block-production-interval=-1 --auto-tx=false --starting-timestamp=-1 --block-time=1")
+ if err != nil {
+ t.Fatalf("Error starting chain: %v", err)
+ }
+ startingTime := time.Now()
+
+ // produce a couple of blocks
+ err = AdvanceBlocks(10)
+ require.NoError(t, err)
+
+ // get the time
+ _, blockTime, err := GetHeightAndTime()
+ require.NoError(t, err)
+
+ // the time should be starting-timestamp + 10 * blockTime + 1 (for the first block needed after Genesis)
+ expectedTime := startingTime.Add(11 * time.Millisecond)
+
+ // since the starting timestamp is taken from the system time,
+ // we can only check that the time is close to the expected time
+ // since the chain startup is hard to time exactly
+ delta := 30 * time.Second
+
+ diff := expectedTime.Sub(blockTime).Abs()
+
+ require.True(t, diff <= delta, "expectedTime: %v, blockTime: %v", expectedTime, blockTime)
+}
diff --git a/e2e-tests/test_utils.go b/e2e-tests/test_utils.go
new file mode 100644
index 0000000..c268151
--- /dev/null
+++ b/e2e-tests/test_utils.go
@@ -0,0 +1,166 @@
+package main
+
+import (
+ "bytes"
+ "encoding/json"
+ "fmt"
+ "os/exec"
+ "strconv"
+ "strings"
+ "time"
+)
+
+// From the output of the AbciInfo command, extract the latest block height.
+// The json bytes should look e.g. like this:
+// {"jsonrpc":"2.0","id":1,"result":{"response":{"data":"interchain-security-p","last_block_height":"2566","last_block_app_hash":"R4Q3Si7+t7TIidl2oTHcQRDNEz+lP0IDWhU5OI89psg="}}}
+func extractHeightFromInfo(jsonBytes []byte) (int, error) {
+ // Use a generic map to represent the JSON structure
+ var data map[string]interface{}
+
+ if err := json.Unmarshal(jsonBytes, &data); err != nil {
+ return -1, fmt.Errorf("failed to unmarshal JSON %s \n error was %v", string(jsonBytes), err)
+ }
+
+ // Navigate the map and use type assertions to get the last_block_height
+ result, ok := data["result"].(map[string]interface{})
+ if !ok {
+ return -1, fmt.Errorf("failed to navigate abci_info output structure trying to access result: json was %s", string(jsonBytes))
+ }
+
+ response, ok := result["response"].(map[string]interface{})
+ if !ok {
+ return -1, fmt.Errorf("failed to navigate abci_info output structure trying to access response: json was %s", string(jsonBytes))
+ }
+
+ lastBlockHeight, ok := response["last_block_height"].(string)
+ if !ok {
+ return -1, fmt.Errorf("failed to navigate abci_info output structure trying to access last_block_height: json was %s", string(jsonBytes))
+ }
+
+ return strconv.Atoi(lastBlockHeight)
+}
+
+// Queries simd for the latest block.
+func QueryBlock() (string, error) {
+ // execute the query command
+ cmd := exec.Command("bash", "-c", "simd q block --node tcp://127.0.0.1:22331")
+ out, err := runCommandWithOutput(cmd)
+ if err != nil {
+ return "", fmt.Errorf("error running query command: %v", err)
+ }
+
+ return out, nil
+}
+
+type BlockInfo struct {
+ Block struct {
+ Header struct {
+ Height string `json:"height"`
+ Time string `json:"time"`
+ } `json:"header"`
+ } `json:"block"`
+}
+
+func GetHeightFromBlock(blockString string) (int, error) {
+ var block BlockInfo
+ err := json.Unmarshal([]byte(blockString), &block)
+ if err != nil {
+ return 0, err
+ }
+
+ res, err := strconv.Atoi(block.Block.Header.Height)
+ if err != nil {
+ return 0, err
+ }
+
+ return res, nil
+}
+
+func GetTimeFromBlock(blockBytes string) (time.Time, error) {
+ var block BlockInfo
+ err := json.Unmarshal([]byte(blockBytes), &block)
+ if err != nil {
+ return time.Time{}, err
+ }
+
+ res, err := time.Parse(time.RFC3339, block.Block.Header.Time)
+ if err != nil {
+ return time.Time{}, err
+ }
+
+ return res, nil
+}
+
+func GetHeightAndTime() (int, time.Time, error) {
+ blockBytes, err := QueryBlock()
+ if err != nil {
+ return 0, time.Time{}, err
+ }
+
+ height, err := GetHeightFromBlock(blockBytes)
+ if err != nil {
+ return 0, time.Time{}, err
+ }
+
+ timestamp, err := GetTimeFromBlock(blockBytes)
+ if err != nil {
+ return 0, time.Time{}, err
+ }
+
+ return height, timestamp, nil
+}
+
+// Queries the size of the community pool.
+// For this, it will just check the number of tokens of the first denom in the community pool.
+func getCommunityPoolSize() (int64, error) {
+ // execute the query command
+ cmd := exec.Command("bash", "-c", "simd q distribution community-pool --output json --node tcp://127.0.0.1:22331 | jq -r '.pool[0].amount'")
+ out, err := runCommandWithOutput(cmd)
+ if err != nil {
+ return -1, fmt.Errorf("error running query command: %v", err)
+ }
+
+ res, err := strconv.ParseFloat(strings.TrimSpace(out), 64)
+ if err != nil {
+ return -1, fmt.Errorf("error parsing community pool size: %v, input was %v", err, strings.TrimSpace(out))
+ }
+
+ return int64(res), nil
+}
+
+func sendToCommunityPool(amount int, sender string) error {
+ // execute the tx command
+ stringCmd := fmt.Sprintf("simd tx distribution fund-community-pool %vstake --chain-id provider --from %v-key --keyring-backend test --node tcp://127.0.0.1:22331 --home ~/nodes/provider/provider-%v -y", amount, sender, sender)
+ cmd := exec.Command("bash", "-c", stringCmd)
+ _, err := runCommandWithOutput(cmd)
+ return err
+}
+
+func runCommandWithOutput(cmd *exec.Cmd) (string, error) {
+ var stdout, stderr bytes.Buffer
+ cmd.Stdout = &stdout
+ cmd.Stderr = &stderr
+
+ err := cmd.Run()
+ if err != nil {
+ return "", fmt.Errorf("error running command: %v\nstdout: %s\nstderr: %s", err, stdout.String(), stderr.String())
+ }
+
+ return stdout.String(), nil
+}
+
+func AdvanceTime(duration time.Duration) error {
+ stringCmd := fmt.Sprintf("curl -H 'Content-Type: application/json' -H 'Accept:application/json' --data '{\"jsonrpc\":\"2.0\",\"method\":\"advance_time\",\"params\":{\"duration_in_seconds\": \"%v\"},\"id\":1}' 127.0.0.1:22331", duration.Seconds())
+
+ cmd := exec.Command("bash", "-c", stringCmd)
+ _, err := runCommandWithOutput(cmd)
+ return err
+}
+
+func AdvanceBlocks(numBlocks int) error {
+ stringCmd := fmt.Sprintf("curl -H 'Content-Type: application/json' -H 'Accept:application/json' --data '{\"jsonrpc\":\"2.0\",\"method\":\"advance_blocks\",\"params\":{\"num_blocks\": \"%v\"},\"id\":1}' 127.0.0.1:22331", numBlocks)
+
+ cmd := exec.Command("bash", "-c", stringCmd)
+ _, err := runCommandWithOutput(cmd)
+ return err
+}
diff --git a/go.mod b/go.mod
index bda249d..49c17de 100644
--- a/go.mod
+++ b/go.mod
@@ -6,6 +6,8 @@ require (
github.com/barkimedes/go-deepcopy v0.0.0-20220514131651-17c30cfc62df
github.com/cometbft/cometbft v0.37.2
github.com/cometbft/cometbft-db v0.7.0
+ github.com/stretchr/testify v1.8.1
+ github.com/urfave/cli/v2 v2.25.7
)
require (
@@ -15,6 +17,7 @@ require (
github.com/cespare/xxhash/v2 v2.1.2 // indirect
github.com/cosmos/gogoproto v1.4.1 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
+ github.com/davecgh/go-spew v1.1.1 // indirect
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 // indirect
github.com/dgraph-io/badger/v2 v2.2007.4 // indirect
github.com/dgraph-io/ristretto v0.0.3-0.20200630154024-f66de99634de // indirect
@@ -38,6 +41,7 @@ require (
github.com/onsi/gomega v1.19.0 // indirect
github.com/petermattis/goid v0.0.0-20180202154549-b0b1615b78e5 // indirect
github.com/pkg/errors v0.9.1 // indirect
+ github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/prometheus/client_golang v1.14.0 // indirect
github.com/prometheus/client_model v0.3.0 // indirect
github.com/prometheus/common v0.37.0 // indirect
@@ -46,7 +50,6 @@ require (
github.com/sasha-s/go-deadlock v0.3.1 // indirect
github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 // indirect
github.com/tecbot/gorocksdb v0.0.0-20191217155057-f0fad39f321c // indirect
- github.com/urfave/cli/v2 v2.25.7 // indirect
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect
go.etcd.io/bbolt v1.3.6 // indirect
golang.org/x/crypto v0.5.0 // indirect
@@ -56,4 +59,5 @@ require (
google.golang.org/genproto v0.0.0-20221118155620-16455021b5e6 // indirect
google.golang.org/grpc v1.52.0 // indirect
google.golang.org/protobuf v1.28.2-0.20220831092852-f930b1dc76e8 // indirect
+ gopkg.in/yaml.v3 v3.0.1 // indirect
)
diff --git a/go.sum b/go.sum
index c6d46f9..ab4d7be 100644
--- a/go.sum
+++ b/go.sum
@@ -74,7 +74,6 @@ github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3Ee
github.com/cosmos/go-bip39 v0.0.0-20180819234021-555e2067c45d h1:49RLWk1j44Xu4fjHb6JFYmeUnDORVwHNkDxaQ0ctCVU=
github.com/cosmos/gogoproto v1.4.1 h1:WoyH+0/jbCTzpKNvyav5FL1ZTWsp1im1MxEpJEzKUB8=
github.com/cosmos/gogoproto v1.4.1/go.mod h1:Ac9lzL4vFpBMcptJROQ6dQ4M3pOEK5Z/l0Q9p+LoCr4=
-github.com/cpuguy83/go-md2man v1.0.10 h1:BSKMNlYxDvnunlTymqtgONjNnaRV1sTpcovwwjF22jk=
github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w=
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
@@ -211,8 +210,10 @@ github.com/klauspost/compress v1.15.11/go.mod h1:QPwzmACJjUTFsnSHH934V6woptycfrD
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
+github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
+github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/libp2p/go-buffer-pool v0.1.0 h1:oK4mSFcQz7cTQIfqbe4MIj9gLW+mnanjyFtc6cdF0Y8=
github.com/libp2p/go-buffer-pool v0.1.0/go.mod h1:N+vh8gMqimBzdKkSMVuydVDq+UV5QTWy5HSiZacSbPg=
@@ -279,7 +280,6 @@ github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1
github.com/prometheus/procfs v0.8.0 h1:ODq8ZFEaYeCaZOJlZZdJA2AbQR98dSHSM1KW/You5mo=
github.com/prometheus/procfs v0.8.0/go.mod h1:z7EfXMXOkbkqb9IINtpCn86r/to3BnA0uaxHdg830/4=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
-github.com/russross/blackfriday v1.5.2 h1:HyvC0ARfnZBqnXwABFeSZHpKvJHJJfPz81GNueLj0oo=
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
@@ -299,11 +299,16 @@ github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnIn
github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c=
+github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
+github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
+github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 h1:epCh84lMvA70Z7CTTCmYQn2CKbY8j86K7/FAIr141uY=
github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc=
github.com/tecbot/gorocksdb v0.0.0-20191217155057-f0fad39f321c h1:g+WoO5jjkqGAzHWCjJB1zZfXPIAaDpzXIEJ0eS6B5Ok=
@@ -606,6 +611,7 @@ google.golang.org/protobuf v1.28.2-0.20220831092852-f930b1dc76e8/go.mod h1:HV8QO
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
@@ -618,7 +624,9 @@ gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=