diff --git a/e2e-tests/local-testnet-singlechain-setup.sh b/e2e-tests/local-testnet-singlechain-setup.sh index 2ff8c33..eb89c09 100755 --- a/e2e-tests/local-testnet-singlechain-setup.sh +++ b/e2e-tests/local-testnet-singlechain-setup.sh @@ -220,7 +220,7 @@ PROVIDER_NODE_LISTEN_ADDR_STR=${PROVIDER_NODE_LISTEN_ADDR_STR::${#PROVIDER_NODE_ PROV_NODES_HOME_STR=${PROV_NODES_HOME_STR::${#PROV_NODES_HOME_STR}-1} echo "Testnet applications are set up! Starting CometMock..." -echo cometmock $PROVIDER_NODE_LISTEN_ADDR_STR ${LEAD_VALIDATOR_PROV_DIR}/config/genesis.json $PROVIDER_COMETMOCK_ADDR $PROV_NODES_HOME_STR grpc \$1 "&> ${LEAD_VALIDATOR_PROV_DIR}/cometmock_log &" | tee -a start_cometmock.sh +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 diff --git a/e2e-tests/local-testnet-singlechain-start.sh b/e2e-tests/local-testnet-singlechain-start.sh index 2c22e2a..bad0414 100755 --- a/e2e-tests/local-testnet-singlechain-start.sh +++ b/e2e-tests/local-testnet-singlechain-start.sh @@ -6,4 +6,4 @@ COMETMOCK_ARGS=$1 ./start_apps.sh -./start_cometmock.sh $1 \ No newline at end of file +./start_cometmock.sh "$1" \ No newline at end of file diff --git a/e2e-tests/main_test.go b/e2e-tests/main_test.go index 67742e6..ed82f62 100644 --- a/e2e-tests/main_test.go +++ b/e2e-tests/main_test.go @@ -1,89 +1,16 @@ package main import ( - "bytes" "encoding/json" "fmt" "math/big" "os/exec" - "strconv" - "strings" "testing" "time" "github.com/stretchr/testify/require" ) -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) -} - -// 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() (*big.Int, 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 big.NewInt(-1), fmt.Errorf("Error running query command: %v", err) - } - - res := new(big.Int) - - res, ok := res.SetString(strings.TrimSpace(out), 10) - if !ok { - return big.NewInt(-1), fmt.Errorf("Error parsing community pool size: %v", err) - } - return res, err -} - -func sendToCommunityPool(amount int) error { - // execute the tx command - stringCmd := fmt.Sprintf("simd tx distribution fund-community-pool %vstake --chain-id provider --from coordinator-key --keyring-backend test --node tcp://127.0.0.1:22331 --home ~/nodes/provider/provider-coordinator -y", amount) - cmd := exec.Command("bash", "-c", stringCmd) - _, err := runCommandWithOutput(cmd) - return err -} - func StartChain( t *testing.T, cometmockArgs string, @@ -206,7 +133,7 @@ func TestTx(t *testing.T) { require.NoError(t, err) // send some tokens to the community pool - err = sendToCommunityPool(50000000000) + err = sendToCommunityPool(50000000000, "coordinator") require.NoError(t, err) // check that the amount in the community pool has increased @@ -217,9 +144,144 @@ func TestTx(t *testing.T) { require.True(t, communityPoolSize2.Cmp(communityPoolSize.Add(communityPoolSize, big.NewInt(50000000000))) == +1) } -func TestTxAutoIncludeOff(t *testing.T) { - err := StartChain(t, "") +// 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.Cmp(communityPoolBefore.Add(communityPoolBefore, big.NewInt(100000000000))) == +1) } diff --git a/e2e-tests/test_utils.go b/e2e-tests/test_utils.go new file mode 100644 index 0000000..0f92b56 --- /dev/null +++ b/e2e-tests/test_utils.go @@ -0,0 +1,166 @@ +package main + +import ( + "bytes" + "encoding/json" + "fmt" + "math/big" + "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 --type height 0 --output json --node tcp://127.0.0.1:22331 --output json") + out, err := runCommandWithOutput(cmd) + if err != nil { + return "", fmt.Errorf("error running query command: %v", err) + } + + return out, nil +} + +type BlockInfo struct { + Header struct { + Height string `json:"height"` + Time string `json:"time"` + } `json:"header"` +} + +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.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.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() (*big.Int, 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 big.NewInt(-1), fmt.Errorf("error running query command: %v", err) + } + + res := new(big.Int) + + res, ok := res.SetString(strings.TrimSpace(out), 10) + if !ok { + return big.NewInt(-1), fmt.Errorf("error parsing community pool size: %v", err) + } + return res, err +} + +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 +}