diff --git a/.circleci/config.yml b/.circleci/config.yml index ad4ca5a16..69ba3c4cc 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -136,6 +136,24 @@ jobs: export PATH="$GOBIN:$PATH" make test_sim_gaia_fast + test_sim_gaia_multi_seed: + <<: *defaults + parallelism: 1 + steps: + - attach_workspace: + at: /tmp/workspace + - checkout + - run: + name: dependencies + command: | + export PATH="$GOBIN:$PATH" + make get_vendor_deps + - run: + name: Test multi-seed Gaia simulation + command: | + export PATH="$GOBIN:$PATH" + make test_sim_gaia_multi_seed + test_cover: <<: *defaults parallelism: 4 @@ -240,6 +258,9 @@ workflows: - test_sim_gaia_fast: requires: - setup_dependencies + - test_sim_gaia_multi_seed: + requires: + - setup_dependencies - test_cover: requires: - setup_dependencies diff --git a/Makefile b/Makefile index acf81fc97..5225076fa 100644 --- a/Makefile +++ b/Makefile @@ -162,9 +162,9 @@ test_sim_gaia_fast: @echo "Running quick Gaia simulation. This may take several minutes..." @go test ./cmd/gaia/app -run TestFullGaiaSimulation -SimulationEnabled=true -SimulationNumBlocks=400 -SimulationBlockSize=200 -SimulationCommit=true -v -timeout 24h -test_sim_gaia_full: - @echo "Running full multi-seed Gaia simulation. This may take awhile!" - @sh scripts/multisim.sh +test_sim_gaia_multi_seed: + @echo "Running multi-seed Gaia simulation. This may take awhile!" + @bash scripts/multisim.sh 10 SIM_NUM_BLOCKS ?= 210 SIM_BLOCK_SIZE ?= 200 @@ -241,4 +241,4 @@ localnet-stop: check_tools check_dev_tools get_tools get_dev_tools get_vendor_deps draw_deps test test_cli test_unit \ test_cover test_lint benchmark devdoc_init devdoc devdoc_save devdoc_update \ build-linux build-docker-gaiadnode localnet-start localnet-stop \ -format check-ledger test_sim_gaia_nondeterminism test_sim_modules test_sim_gaia_fast test_sim_gaia_slow update_tools update_dev_tools +format check-ledger test_sim_gaia_nondeterminism test_sim_modules test_sim_gaia_fast test_sim_gaia_multi_seed update_tools update_dev_tools diff --git a/PENDING.md b/PENDING.md index ed15ac7f8..dbae0cc2c 100644 --- a/PENDING.md +++ b/PENDING.md @@ -28,6 +28,7 @@ BREAKING CHANGES * [x/stake, x/slashing] [#1305](https://github.com/cosmos/cosmos-sdk/issues/1305) - Rename "revoked" to "jailed" * [x/stake] [#1676] Revoked and jailed validators put into the unbonding state * [x/stake] [#1877] Redelegations/unbonding-delegation from unbonding validator have reduced time + * [x/slashing] \#1789 Slashing changes for Tendermint validator set offset (NextValSet) * [x/stake] [\#2040](https://github.com/cosmos/cosmos-sdk/issues/2040) Validator operator type has now changed to `sdk.ValAddress` * [x/stake] [\#2221](https://github.com/cosmos/cosmos-sdk/issues/2221) New @@ -41,6 +42,7 @@ BREAKING CHANGES * [x/gov] [#2195] Governance uses BFT Time * [x/gov] \#2256 Removed slashing for governance non-voting validators * [simulation] \#2162 Added back correct supply invariants + * [x/slashing] \#2430 Simulate more slashes, check if validator is jailed before jailing * [x/stake] \#2393 Removed `CompleteUnbonding` and `CompleteRedelegation` Msg types, and instead added unbonding/redelegation queues to endblocker * SDK @@ -88,7 +90,7 @@ FEATURES * [gaia-lite] [\#966](https://github.com/cosmos/cosmos-sdk/issues/966) Add support for `generate_only=true` query argument to generate offline unsigned transactions * [gaia-lite] [\#1953](https://github.com/cosmos/cosmos-sdk/issues/1953) Add /sign endpoint to sign transactions generated with `generate_only=true`. * [gaia-lite] [\#1954](https://github.com/cosmos/cosmos-sdk/issues/1954) Add /broadcast endpoint to broadcast transactions signed by the /sign endpoint. - * [gaia-lite] [\#2113](https://github.com/cosmos/cosmos-sdk/issues/2113) Rename `/accounts/{address}/send` to `/bank/accounts/{address}/transfers` + * [gaia-lite] [\#2113](https://github.com/cosmos/cosmos-sdk/issues/2113) Rename `/accounts/{address}/send` to `/bank/accounts/{address}/transfers`, rename `/accounts/{address}` to `/auth/accounts/{address}` * Gaia CLI (`gaiacli`) * [cli] Cmds to query staking pool and params diff --git a/client/keys/add.go b/client/keys/add.go index be2825a55..21a372e2f 100644 --- a/client/keys/add.go +++ b/client/keys/add.go @@ -8,7 +8,6 @@ import ( "github.com/cosmos/cosmos-sdk/client" "github.com/gorilla/mux" - "github.com/pkg/errors" "github.com/spf13/cobra" "github.com/spf13/viper" @@ -61,7 +60,7 @@ func runAddCmd(cmd *cobra.Command, args []string) error { name = "inmemorykey" } else { if len(args) != 1 || len(args[0]) == 0 { - return errors.New("you must provide a name for the key") + return errMissingName() } name = args[0] kb, err = GetKeyBase() @@ -144,11 +143,16 @@ func printCreate(info keys.Info, seed string) { if !viper.GetBool(flagNoBackup) { out.Seed = seed } - json, err := MarshalJSON(out) + var jsonString []byte + if viper.GetBool(client.FlagIndentResponse) { + jsonString, err = cdc.MarshalJSONIndent(out, "", " ") + } else { + jsonString, err = cdc.MarshalJSON(out) + } if err != nil { panic(err) // really shouldn't happen... } - fmt.Println(string(json)) + fmt.Println(string(jsonString)) default: panic(fmt.Sprintf("I can't speak: %s", output)) } @@ -165,75 +169,77 @@ type NewKeyBody struct { } // add new key REST handler -func AddNewKeyRequestHandler(w http.ResponseWriter, r *http.Request) { - var kb keys.Keybase - var m NewKeyBody +func AddNewKeyRequestHandler(indent bool) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var kb keys.Keybase + var m NewKeyBody - kb, err := GetKeyBase() - if err != nil { - w.WriteHeader(500) - w.Write([]byte(err.Error())) - return - } - - body, err := ioutil.ReadAll(r.Body) - err = json.Unmarshal(body, &m) - - if err != nil { - w.WriteHeader(http.StatusBadRequest) - w.Write([]byte(err.Error())) - return - } - if m.Name == "" { - w.WriteHeader(http.StatusBadRequest) - w.Write([]byte("You have to specify a name for the locally stored account.")) - return - } - if m.Password == "" { - w.WriteHeader(http.StatusBadRequest) - w.Write([]byte("You have to specify a password for the locally stored account.")) - return - } - - // check if already exists - infos, err := kb.List() - for _, i := range infos { - if i.GetName() == m.Name { - w.WriteHeader(http.StatusConflict) - w.Write([]byte(fmt.Sprintf("Account with name %s already exists.", m.Name))) + kb, err := GetKeyBase() + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte(err.Error())) return } - } - // create account - seed := m.Seed - if seed == "" { - seed = getSeed(keys.Secp256k1) - } - info, err := kb.CreateKey(m.Name, seed, m.Password) - if err != nil { - w.WriteHeader(http.StatusInternalServerError) - w.Write([]byte(err.Error())) - return - } + body, err := ioutil.ReadAll(r.Body) + if err != nil { + w.WriteHeader(http.StatusBadRequest) + w.Write([]byte(err.Error())) + return + } + err = json.Unmarshal(body, &m) + if err != nil { + w.WriteHeader(http.StatusBadRequest) + w.Write([]byte(err.Error())) + return + } + if m.Name == "" { + w.WriteHeader(http.StatusBadRequest) + err = errMissingName() + w.Write([]byte(err.Error())) + return + } + if m.Password == "" { + w.WriteHeader(http.StatusBadRequest) + err = errMissingPassword() + w.Write([]byte(err.Error())) + return + } - keyOutput, err := Bech32KeyOutput(info) - if err != nil { - w.WriteHeader(http.StatusInternalServerError) - w.Write([]byte(err.Error())) - return + // check if already exists + infos, err := kb.List() + for _, info := range infos { + if info.GetName() == m.Name { + w.WriteHeader(http.StatusConflict) + err = errKeyNameConflict(m.Name) + w.Write([]byte(err.Error())) + return + } + } + + // create account + seed := m.Seed + if seed == "" { + seed = getSeed(keys.Secp256k1) + } + info, err := kb.CreateKey(m.Name, seed, m.Password) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte(err.Error())) + return + } + + keyOutput, err := Bech32KeyOutput(info) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte(err.Error())) + return + } + + keyOutput.Seed = seed + + PostProcessResponse(w, cdc, keyOutput, indent) } - - keyOutput.Seed = seed - - bz, err := json.Marshal(keyOutput) - if err != nil { - w.WriteHeader(http.StatusInternalServerError) - w.Write([]byte(err.Error())) - return - } - - w.Write(bz) } // function to just a new seed to display in the UI before actually persisting it in the keybase @@ -258,3 +264,82 @@ func SeedRequestHandler(w http.ResponseWriter, r *http.Request) { seed := getSeed(algo) w.Write([]byte(seed)) } + +// RecoverKeyBody is recover key request REST body +type RecoverKeyBody struct { + Password string `json:"password"` + Seed string `json:"seed"` +} + +// RecoverRequestHandler performs key recover request +func RecoverRequestHandler(indent bool) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + name := vars["name"] + var m RecoverKeyBody + body, err := ioutil.ReadAll(r.Body) + if err != nil { + w.WriteHeader(http.StatusBadRequest) + w.Write([]byte(err.Error())) + return + } + err = cdc.UnmarshalJSON(body, &m) + if err != nil { + w.WriteHeader(http.StatusBadRequest) + w.Write([]byte(err.Error())) + return + } + + if name == "" { + w.WriteHeader(http.StatusBadRequest) + err = errMissingName() + w.Write([]byte(err.Error())) + return + } + if m.Password == "" { + w.WriteHeader(http.StatusBadRequest) + err = errMissingPassword() + w.Write([]byte(err.Error())) + return + } + if m.Seed == "" { + w.WriteHeader(http.StatusBadRequest) + err = errMissingSeed() + w.Write([]byte(err.Error())) + return + } + + kb, err := GetKeyBase() + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte(err.Error())) + return + } + // check if already exists + infos, err := kb.List() + for _, info := range infos { + if info.GetName() == name { + w.WriteHeader(http.StatusConflict) + err = errKeyNameConflict(name) + w.Write([]byte(err.Error())) + return + } + } + + info, err := kb.CreateKey(name, m.Seed, m.Password) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte(err.Error())) + return + } + + keyOutput, err := Bech32KeyOutput(info) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte(err.Error())) + return + } + + PostProcessResponse(w, cdc, keyOutput, indent) + } +} diff --git a/client/keys/delete.go b/client/keys/delete.go index 944feb4b1..23fc41ffd 100644 --- a/client/keys/delete.go +++ b/client/keys/delete.go @@ -68,14 +68,14 @@ func DeleteKeyRequestHandler(w http.ResponseWriter, r *http.Request) { decoder := json.NewDecoder(r.Body) err := decoder.Decode(&m) if err != nil { - w.WriteHeader(400) + w.WriteHeader(http.StatusBadRequest) w.Write([]byte(err.Error())) return } kb, err = GetKeyBase() if err != nil { - w.WriteHeader(500) + w.WriteHeader(http.StatusInternalServerError) w.Write([]byte(err.Error())) return } @@ -83,10 +83,10 @@ func DeleteKeyRequestHandler(w http.ResponseWriter, r *http.Request) { // TODO handle error if key is not available or pass is wrong err = kb.Delete(name, m.Password) if err != nil { - w.WriteHeader(500) + w.WriteHeader(http.StatusInternalServerError) w.Write([]byte(err.Error())) return } - w.WriteHeader(200) + w.WriteHeader(http.StatusOK) } diff --git a/client/keys/errors.go b/client/keys/errors.go new file mode 100644 index 000000000..9c6139d7a --- /dev/null +++ b/client/keys/errors.go @@ -0,0 +1,19 @@ +package keys + +import "fmt" + +func errKeyNameConflict(name string) error { + return fmt.Errorf("acount with name %s already exists", name) +} + +func errMissingName() error { + return fmt.Errorf("you have to specify a name for the locally stored account") +} + +func errMissingPassword() error { + return fmt.Errorf("you have to specify a password for the locally stored account") +} + +func errMissingSeed() error { + return fmt.Errorf("you have to specify seed for key recover") +} diff --git a/client/keys/list.go b/client/keys/list.go index 22f163f1d..f232fccff 100644 --- a/client/keys/list.go +++ b/client/keys/list.go @@ -1,7 +1,6 @@ package keys import ( - "encoding/json" "net/http" "github.com/spf13/cobra" @@ -35,35 +34,31 @@ func runListCmd(cmd *cobra.Command, args []string) error { // REST // query key list REST handler -func QueryKeysRequestHandler(w http.ResponseWriter, r *http.Request) { - kb, err := GetKeyBase() - if err != nil { - w.WriteHeader(500) - w.Write([]byte(err.Error())) - return +func QueryKeysRequestHandler(indent bool) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + kb, err := GetKeyBase() + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte(err.Error())) + return + } + infos, err := kb.List() + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte(err.Error())) + return + } + // an empty list will be JSONized as null, but we want to keep the empty list + if len(infos) == 0 { + PostProcessResponse(w, cdc, "[]", indent) + return + } + keysOutput, err := Bech32KeysOutput(infos) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte(err.Error())) + return + } + PostProcessResponse(w, cdc, keysOutput, indent) } - infos, err := kb.List() - if err != nil { - w.WriteHeader(500) - w.Write([]byte(err.Error())) - return - } - // an empty list will be JSONized as null, but we want to keep the empty list - if len(infos) == 0 { - w.Write([]byte("[]")) - return - } - keysOutput, err := Bech32KeysOutput(infos) - if err != nil { - w.WriteHeader(500) - w.Write([]byte(err.Error())) - return - } - output, err := json.MarshalIndent(keysOutput, "", " ") - if err != nil { - w.WriteHeader(500) - w.Write([]byte(err.Error())) - return - } - w.Write(output) } diff --git a/client/keys/root.go b/client/keys/root.go index f2a27dc0c..a7a7d2e6f 100644 --- a/client/keys/root.go +++ b/client/keys/root.go @@ -30,11 +30,12 @@ func Commands() *cobra.Command { } // resgister REST routes -func RegisterRoutes(r *mux.Router) { - r.HandleFunc("/keys", QueryKeysRequestHandler).Methods("GET") - r.HandleFunc("/keys", AddNewKeyRequestHandler).Methods("POST") +func RegisterRoutes(r *mux.Router, indent bool) { + r.HandleFunc("/keys", QueryKeysRequestHandler(indent)).Methods("GET") + r.HandleFunc("/keys", AddNewKeyRequestHandler(indent)).Methods("POST") r.HandleFunc("/keys/seed", SeedRequestHandler).Methods("GET") - r.HandleFunc("/keys/{name}", GetKeyRequestHandler).Methods("GET") + r.HandleFunc("/keys/{name}/recover", RecoverRequestHandler(indent)).Methods("POST") + r.HandleFunc("/keys/{name}", GetKeyRequestHandler(indent)).Methods("GET") r.HandleFunc("/keys/{name}", UpdateKeyRequestHandler).Methods("PUT") r.HandleFunc("/keys/{name}", DeleteKeyRequestHandler).Methods("DELETE") } diff --git a/client/keys/show.go b/client/keys/show.go index 82c6f9883..b567daf12 100644 --- a/client/keys/show.go +++ b/client/keys/show.go @@ -1,7 +1,6 @@ package keys import ( - "encoding/json" "fmt" "net/http" @@ -91,44 +90,39 @@ func getBechKeyOut(bechPrefix string) (bechKeyOutFn, error) { // REST // get key REST handler -func GetKeyRequestHandler(w http.ResponseWriter, r *http.Request) { - vars := mux.Vars(r) - name := vars["name"] - bechPrefix := r.URL.Query().Get(FlagBechPrefix) +func GetKeyRequestHandler(indent bool) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + name := vars["name"] + bechPrefix := r.URL.Query().Get(FlagBechPrefix) - if bechPrefix == "" { - bechPrefix = "acc" + if bechPrefix == "" { + bechPrefix = "acc" + } + + bechKeyOut, err := getBechKeyOut(bechPrefix) + if err != nil { + w.WriteHeader(http.StatusBadRequest) + w.Write([]byte(err.Error())) + return + } + + info, err := GetKeyInfo(name) + // TODO: check for the error if key actually does not exist, instead of + // assuming this as the reason + if err != nil { + w.WriteHeader(http.StatusNotFound) + w.Write([]byte(err.Error())) + return + } + + keyOutput, err := bechKeyOut(info) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte(err.Error())) + return + } + + PostProcessResponse(w, cdc, keyOutput, indent) } - - bechKeyOut, err := getBechKeyOut(bechPrefix) - if err != nil { - w.WriteHeader(400) - w.Write([]byte(err.Error())) - return - } - - info, err := GetKeyInfo(name) - // TODO: check for the error if key actually does not exist, instead of - // assuming this as the reason - if err != nil { - w.WriteHeader(404) - w.Write([]byte(err.Error())) - return - } - - keyOutput, err := bechKeyOut(info) - if err != nil { - w.WriteHeader(500) - w.Write([]byte(err.Error())) - return - } - - output, err := json.MarshalIndent(keyOutput, "", " ") - if err != nil { - w.WriteHeader(500) - w.Write([]byte(err.Error())) - return - } - - w.Write(output) } diff --git a/client/keys/update.go b/client/keys/update.go index 78a81bf0e..18a18be58 100644 --- a/client/keys/update.go +++ b/client/keys/update.go @@ -69,14 +69,14 @@ func UpdateKeyRequestHandler(w http.ResponseWriter, r *http.Request) { decoder := json.NewDecoder(r.Body) err := decoder.Decode(&m) if err != nil { - w.WriteHeader(400) + w.WriteHeader(http.StatusBadRequest) w.Write([]byte(err.Error())) return } kb, err = GetKeyBase() if err != nil { - w.WriteHeader(500) + w.WriteHeader(http.StatusInternalServerError) w.Write([]byte(err.Error())) return } @@ -86,10 +86,10 @@ func UpdateKeyRequestHandler(w http.ResponseWriter, r *http.Request) { // TODO check if account exists and if password is correct err = kb.Update(name, m.OldPassword, getNewpass) if err != nil { - w.WriteHeader(401) + w.WriteHeader(http.StatusUnauthorized) w.Write([]byte(err.Error())) return } - w.WriteHeader(200) + w.WriteHeader(http.StatusOK) } diff --git a/client/keys/utils.go b/client/keys/utils.go index 95fc83ed6..4ca8fc8f4 100644 --- a/client/keys/utils.go +++ b/client/keys/utils.go @@ -12,7 +12,9 @@ import ( "github.com/cosmos/cosmos-sdk/client" + "github.com/cosmos/cosmos-sdk/codec" sdk "github.com/cosmos/cosmos-sdk/types" + "net/http" ) // KeyDBName is the directory under root where we store the keys @@ -231,3 +233,26 @@ func printPubKey(info keys.Info, bechKeyOut bechKeyOutFn) { fmt.Println(ko.PubKey) } + +// PostProcessResponse performs post process for rest response +func PostProcessResponse(w http.ResponseWriter, cdc *codec.Codec, response interface{}, indent bool) { + var output []byte + switch response.(type) { + default: + var err error + if indent { + output, err = cdc.MarshalJSONIndent(response, "", " ") + } else { + output, err = cdc.MarshalJSON(response) + } + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte(err.Error())) + return + } + case []byte: + output = response.([]byte) + } + w.Header().Set("Content-Type", "application/json") + w.Write(output) +} diff --git a/client/lcd/lcd_test.go b/client/lcd/lcd_test.go index 485c5a2f6..6eef4afd0 100644 --- a/client/lcd/lcd_test.go +++ b/client/lcd/lcd_test.go @@ -54,9 +54,13 @@ func TestKeys(t *testing.T) { match := reg.MatchString(seed) require.True(t, match, "Returned seed has wrong format", seed) + // recover key + recoverName := "test_recovername" + recoverPassword := "1234567890" + doRecoverKey(t, port, recoverName, recoverPassword, seed) + newName := "test_newname" newPassword := "0987654321" - // add key jsonStr := []byte(fmt.Sprintf(`{"name":"%s", "password":"%s", "seed":"%s"}`, newName, newPassword, seed)) res, body = Request(t, port, "POST", "/keys", jsonStr) @@ -78,7 +82,7 @@ func TestKeys(t *testing.T) { // existing keys res, body = Request(t, port, "GET", "/keys", nil) require.Equal(t, http.StatusOK, res.StatusCode, body) - var m [2]keys.KeyOutput + var m [3]keys.KeyOutput err = cdc.UnmarshalJSON([]byte(body), &m) require.Nil(t, err) @@ -243,7 +247,7 @@ func TestCoinSend(t *testing.T) { someFakeAddr := sdk.AccAddress(bz) // query empty - res, body := Request(t, port, "GET", fmt.Sprintf("/accounts/%s", someFakeAddr), nil) + res, body := Request(t, port, "GET", fmt.Sprintf("/auth/accounts/%s", someFakeAddr), nil) require.Equal(t, http.StatusNoContent, res.StatusCode, body) acc := getAccount(t, port, addr) @@ -518,9 +522,12 @@ func TestBonding(t *testing.T) { name, password, denom := "test", "1234567890", "steak" addr, seed := CreateAddr(t, name, password, GetKeyBase(t)) - cleanup, _, operAddrs, port := InitializeTestLCD(t, 1, []sdk.AccAddress{addr}) + cleanup, valPubKeys, operAddrs, port := InitializeTestLCD(t, 2, []sdk.AccAddress{addr}) defer cleanup() + require.Equal(t, 2, len(valPubKeys)) + require.Equal(t, 2, len(operAddrs)) + amt := sdk.NewDec(60) validator := getValidator(t, port, operAddrs[0]) @@ -802,7 +809,7 @@ func TestProposalsQuery(t *testing.T) { //_____________________________________________________________________________ // get the account to get the sequence func getAccount(t *testing.T, port string, addr sdk.AccAddress) auth.Account { - res, body := Request(t, port, "GET", fmt.Sprintf("/accounts/%s", addr), nil) + res, body := Request(t, port, "GET", fmt.Sprintf("/auth/accounts/%s", addr), nil) require.Equal(t, http.StatusOK, res.StatusCode, body) var acc auth.Account err := cdc.UnmarshalJSON([]byte(body), &acc) @@ -856,6 +863,20 @@ func doSendWithGas(t *testing.T, port, seed, name, password string, addr sdk.Acc return } +func doRecoverKey(t *testing.T, port, recoverName, recoverPassword, seed string) { + jsonStr := []byte(fmt.Sprintf(`{"password":"%s", "seed":"%s"}`, recoverPassword, seed)) + res, body := Request(t, port, "POST", fmt.Sprintf("/keys/%s/recover", recoverName), jsonStr) + + require.Equal(t, http.StatusOK, res.StatusCode, body) + var resp keys.KeyOutput + err := codec.Cdc.UnmarshalJSON([]byte(body), &resp) + require.Nil(t, err, body) + + addr1Bech32 := resp.Address + _, err = sdk.AccAddressFromBech32(addr1Bech32) + require.NoError(t, err, "Failed to return a correct bech32 address") +} + func doSend(t *testing.T, port, seed, name, password string, addr sdk.AccAddress) (receiveAddr sdk.AccAddress, resultTx ctypes.ResultBroadcastTxCommit) { res, body, receiveAddr := doSendWithGas(t, port, seed, name, password, addr, "", 0, "") require.Equal(t, http.StatusOK, res.StatusCode, body) diff --git a/client/lcd/root.go b/client/lcd/root.go index ef690ed55..475186ed0 100644 --- a/client/lcd/root.go +++ b/client/lcd/root.go @@ -145,7 +145,7 @@ func createHandler(cdc *codec.Codec) *mux.Router { r.HandleFunc("/version", CLIVersionRequestHandler).Methods("GET") r.HandleFunc("/node_version", NodeVersionRequestHandler(cliCtx)).Methods("GET") - keys.RegisterRoutes(r) + keys.RegisterRoutes(r, cliCtx.Indent) rpc.RegisterRoutes(cliCtx, r) tx.RegisterRoutes(cliCtx, r, cdc) auth.RegisterRoutes(cliCtx, r, cdc, "acc") diff --git a/client/lcd/swagger-ui/swagger.yaml b/client/lcd/swagger-ui/swagger.yaml index 9c4246394..b7fb70ff3 100644 --- a/client/lcd/swagger-ui/swagger.yaml +++ b/client/lcd/swagger-ui/swagger.yaml @@ -346,6 +346,184 @@ paths: $ref: "#/definitions/BroadcastTxCommitResult" 400: description: The Tx was malformated + /keys: + get: + summary: List of accounts stored locally + tags: + - ICS1 + produces: + - application/json + responses: + 200: + description: Array of accounts + schema: + type: array + items: + $ref: '#/definitions/Account' + post: + summary: Create a new account locally + tags: + - ICS1 + consumes: + - application/json + produces: + - application/json + parameters: + - in: body + name: account + description: The account to create + schema: + type: object + required: + - name + - password + - seed + properties: + name: + type: string + password: + type: string + seed: + type: string + responses: + 200: + description: Returns account information of the created key + schema: + $ref: "#/definitions/Account" + /keys/seed: + get: + summary: Create a new seed to create a new account with + tags: + - ICS1 + responses: + 200: + description: 16 word Seed + schema: + type: string + /keys/{name}/recover: + post: + summary: Recover a account from a seed + tags: + - ICS1 + consumes: + - application/json + produces: + - application/json + parameters: + - in: path + name: name + description: Account name + required: true + type: string + - in: body + name: pwdAndSeed + description: Provide password and seed to recover a key + schema: + type: object + required: + - password + - seed + properties: + password: + type: string + seed: + type: string + responses: + 200: + description: Returns account information of the recovered key + schema: + $ref: "#/definitions/Account" + /keys/{name}: + parameters: + - in: path + name: name + description: Account name + required: true + type: string + get: + summary: Get a certain locally stored account + tags: + - ICS1 + produces: + - application/json + responses: + 200: + description: Locally stored account + schema: + $ref: "#/definitions/Account" + 404: + description: Account is not available + put: + summary: Update the password for this account in the KMS + tags: + - ICS1 + consumes: + - application/json + parameters: + - in: body + name: account + description: The new and old password + schema: + type: object + required: + - new_password + - old_password + properties: + new_password: + type: string + old_password: + type: string + responses: + 200: + description: Updated password + 401: + description: Password is wrong + 404: + description: Account is not available + delete: + summary: Remove an account + tags: + - ICS1 + consumes: + - application/json + parameters: + - in: body + name: account + description: The password of the account to remove from the KMS + schema: + type: object + required: + - password + properties: + password: + type: string + responses: + 200: + description: Removed account + 401: + description: Password is wrong + 404: + description: Account is not available + /auth/accounts/{address}: + get: + summary: Get the account information on blockchain + tags: + - ICS1 + produces: + - application/json + parameters: + - in: path + name: address + description: Account address + required: true + type: string + responses: + 200: + description: Account information on the blockchain + schema: + $ref: "#/definitions/AccountQueryResponse" + 404: + description: Account is not available definitions: CheckTxResult: @@ -561,7 +739,35 @@ definitions: address: $ref: "#/definitions/Address" pub_key: - $ref: "#/definitions/PubKey" + type: string + example: "cosmospub1addwnpepqfgv3pakxazq2fgs8tmmhmzsrs94fptl7kyztyxprjpf0mkus3h7cxqe70s" + type: + type: string + example: local + seed: + type: string + AccountInfo: + type: object + properties: + account_number: + type: string + address: + type: string + coins: + type: array + items: + $ref: "#/definitions/Coin" + public_key: + type: string + sequence: + type: string + AccountQueryResponse: + type: object + properties: + type: + type: string + value: + $ref: "#/definitions/AccountInfo" BlockID: type: object properties: diff --git a/client/lcd/test_helpers.go b/client/lcd/test_helpers.go index 83c570913..cd48b89ea 100644 --- a/client/lcd/test_helpers.go +++ b/client/lcd/test_helpers.go @@ -146,7 +146,7 @@ func InitializeTestLCD( // append initial (proposing) validator genDoc.Validators[0] = tmtypes.GenesisValidator{ PubKey: privVal.GetPubKey(), - Power: 999999, // create enough power to enable 2/3 voting power + Power: 100, // create enough power to enable 2/3 voting power Name: "validator-1", } @@ -176,7 +176,7 @@ func InitializeTestLCD( appGenTxs = append(appGenTxs, appGenTx) } - genesisState, err := gapp.GaiaAppGenState(cdc, appGenTxs[:]) + genesisState, err := gapp.NewTestGaiaAppGenState(cdc, appGenTxs[:], genDoc.Validators, valOperAddrs) require.NoError(t, err) // add some tokens to init accounts diff --git a/client/rpc/validators.go b/client/rpc/validators.go index f937ac4c8..fc37881cb 100644 --- a/client/rpc/validators.go +++ b/client/rpc/validators.go @@ -11,10 +11,10 @@ import ( "github.com/cosmos/cosmos-sdk/client" "github.com/cosmos/cosmos-sdk/client/context" + "github.com/cosmos/cosmos-sdk/client/utils" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/spf13/viper" tmtypes "github.com/tendermint/tendermint/types" - "github.com/cosmos/cosmos-sdk/client/utils" ) // TODO these next two functions feel kinda hacky based on their placement diff --git a/cmd/gaia/app/genesis.go b/cmd/gaia/app/genesis.go index 872857194..62a3cae79 100644 --- a/cmd/gaia/app/genesis.go +++ b/cmd/gaia/app/genesis.go @@ -162,7 +162,6 @@ func GaiaAppGenTxNF(cdc *codec.Codec, pk crypto.PubKey, addr sdk.AccAddress, nam // Create the core parameters for genesis initialization for gaia // note that the pubkey input is this machines pubkey func GaiaAppGenState(cdc *codec.Codec, appGenTxs []json.RawMessage) (genesisState GenesisState, err error) { - if len(appGenTxs) == 0 { err = errors.New("must provide at least genesis transaction") return @@ -198,6 +197,7 @@ func GaiaAppGenState(cdc *codec.Codec, appGenTxs []json.RawMessage) (genesisStat DistrData: distr.DefaultGenesisState(), GovData: gov.DefaultGenesisState(), } + return } diff --git a/cmd/gaia/app/test_utils.go b/cmd/gaia/app/test_utils.go new file mode 100644 index 000000000..d793e5bfc --- /dev/null +++ b/cmd/gaia/app/test_utils.go @@ -0,0 +1,76 @@ +package app + +import ( + "encoding/json" + "errors" + + "github.com/cosmos/cosmos-sdk/codec" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/x/gov" + "github.com/cosmos/cosmos-sdk/x/stake" + tmtypes "github.com/tendermint/tendermint/types" +) + +// NewTestGaiaAppGenState creates the core parameters for a test genesis +// initialization given a set of genesis txs, TM validators and their respective +// operating addresses. +func NewTestGaiaAppGenState( + cdc *codec.Codec, appGenTxs []json.RawMessage, tmVals []tmtypes.GenesisValidator, valOperAddrs []sdk.ValAddress, +) (GenesisState, error) { + + switch { + case len(appGenTxs) == 0: + return GenesisState{}, errors.New("must provide at least genesis transaction") + case len(tmVals) != len(valOperAddrs): + return GenesisState{}, errors.New("number of TM validators does not match number of operator addresses") + } + + // start with the default staking genesis state + stakeData := stake.DefaultGenesisState() + + // get genesis account information + genAccs := make([]GenesisAccount, len(appGenTxs)) + for i, appGenTx := range appGenTxs { + + var genTx GaiaGenTx + if err := cdc.UnmarshalJSON(appGenTx, &genTx); err != nil { + return GenesisState{}, err + } + + stakeData.Pool.LooseTokens = stakeData.Pool.LooseTokens.Add(sdk.NewDecFromInt(freeFermionsAcc)) + + // create the genesis account for the given genesis tx + genAccs[i] = genesisAccountFromGenTx(genTx) + } + + for i, tmVal := range tmVals { + var issuedDelShares sdk.Dec + + // increase total supply by validator's power + power := sdk.NewInt(tmVal.Power) + stakeData.Pool.LooseTokens = stakeData.Pool.LooseTokens.Add(sdk.NewDecFromInt(power)) + + // add the validator + desc := stake.NewDescription(tmVal.Name, "", "", "") + validator := stake.NewValidator(valOperAddrs[i], tmVal.PubKey, desc) + + validator, stakeData.Pool, issuedDelShares = validator.AddTokensFromDel(stakeData.Pool, power) + stakeData.Validators = append(stakeData.Validators, validator) + + // create the self-delegation from the issuedDelShares + selfDel := stake.Delegation{ + DelegatorAddr: sdk.AccAddress(validator.OperatorAddr), + ValidatorAddr: validator.OperatorAddr, + Shares: issuedDelShares, + Height: 0, + } + + stakeData.Bonds = append(stakeData.Bonds, selfDel) + } + + return GenesisState{ + Accounts: genAccs, + StakeData: stakeData, + GovData: gov.DefaultGenesisState(), + }, nil +} diff --git a/cmd/gaia/cmd/gaiadebug/main.go b/cmd/gaia/cmd/gaiadebug/main.go index a6db38a21..73840537b 100644 --- a/cmd/gaia/cmd/gaiadebug/main.go +++ b/cmd/gaia/cmd/gaiadebug/main.go @@ -138,11 +138,17 @@ func runPubKeyCmd(cmd *cobra.Command, args []string) error { if err != nil { return err } + + consenusPub, err := sdk.Bech32ifyConsPub(pubKey) + if err != nil { + return err + } fmt.Println("Address:", pubKey.Address()) fmt.Printf("Hex: %X\n", pubkeyBytes) fmt.Println("JSON (base64):", string(pubKeyJSONBytes)) fmt.Println("Bech32 Acc:", accPub) - fmt.Println("Bech32 Val:", valPub) + fmt.Println("Bech32 Validator Operator:", valPub) + fmt.Println("Bech32 Validator Consensus:", consenusPub) return nil } diff --git a/scripts/multisim.sh b/scripts/multisim.sh index be59b0f1a..8ffa338b8 100755 --- a/scripts/multisim.sh +++ b/scripts/multisim.sh @@ -1,12 +1,14 @@ #!/bin/bash -seeds=(1 2 4 7 9 20 32 123 4728 37827 981928 87821 891823782 989182 89182391) +seeds=(1 2 4 7 9 20 32 123 124 582 1893 2989 3012 4728 37827 981928 87821 891823782 989182 89182391) +blocks=$1 -echo "Running multi-seed simulation with seeds: ${seeds[@]}" +echo "Running multi-seed simulation with seeds ${seeds[@]}" +echo "Running $blocks blocks per seed" echo "Edit scripts/multisim.sh to add new seeds. Keeping parameters in the file makes failures easy to reproduce." -echo "This script will kill all sub-simulations on SIGINT/SIGTERM/EXIT (i.e. Ctrl-C)." +echo "This script will kill all sub-simulations on SIGINT/SIGTERM (i.e. Ctrl-C)." -trap 'kill $(jobs -pr)' SIGINT SIGTERM EXIT +trap 'kill $(jobs -pr)' SIGINT SIGTERM tmpdir=$(mktemp -d) echo "Using temporary log directory: $tmpdir" @@ -16,7 +18,7 @@ sim() { echo "Running full Gaia simulation with seed $seed. This may take awhile!" file="$tmpdir/gaia-simulation-seed-$seed-date-$(date -Iseconds -u).stdout" echo "Writing stdout to $file..." - go test ./cmd/gaia/app -run TestFullGaiaSimulation -SimulationEnabled=true -SimulationNumBlocks=1000 \ + go test ./cmd/gaia/app -run TestFullGaiaSimulation -SimulationEnabled=true -SimulationNumBlocks=$blocks \ -SimulationVerbose=true -SimulationCommit=true -SimulationSeed=$seed -v -timeout 24h > $file } @@ -26,7 +28,7 @@ for seed in ${seeds[@]}; do sim $seed & pids[${i}]=$! i=$(($i+1)) - sleep 0.1 # start in order, nicer logs + sleep 10 # start in order, nicer logs done echo "Simulation processes spawned, waiting for completion..." @@ -37,10 +39,13 @@ i=0 for pid in ${pids[*]}; do wait $pid last=$? - if [ $last -ne 0 ]; then - seed=${seeds[${i}]} + seed=${seeds[${i}]} + if [ $last -ne 0 ] + then echo "Simulation with seed $seed failed!" code=1 + else + echo "Simulation with seed $seed OK" fi i=$(($i+1)) done diff --git a/x/auth/client/rest/query.go b/x/auth/client/rest/query.go index ccd3a4bf7..5d47eb692 100644 --- a/x/auth/client/rest/query.go +++ b/x/auth/client/rest/query.go @@ -17,7 +17,7 @@ import ( // register REST routes func RegisterRoutes(cliCtx context.CLIContext, r *mux.Router, cdc *codec.Codec, storeName string) { r.HandleFunc( - "/accounts/{address}", + "/auth/accounts/{address}", QueryAccountRequestHandlerFn(storeName, cdc, authcmd.GetAccountDecoder(cdc), cliCtx), ).Methods("GET") r.HandleFunc( @@ -36,7 +36,6 @@ func QueryAccountRequestHandlerFn( decoder auth.AccountDecoder, cliCtx context.CLIContext, ) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "application/json") vars := mux.Vars(r) bech32addr := vars["address"] diff --git a/x/distribution/abci_app.go b/x/distribution/abci_app.go index bea9824c0..9cced9ab8 100644 --- a/x/distribution/abci_app.go +++ b/x/distribution/abci_app.go @@ -1,6 +1,8 @@ package distribution import ( + "fmt" + abci "github.com/tendermint/tendermint/abci/types" sdk "github.com/cosmos/cosmos-sdk/types" @@ -10,6 +12,7 @@ import ( // set the proposer for determining distribution during endblock func BeginBlocker(ctx sdk.Context, req abci.RequestBeginBlock, k keeper.Keeper) { consAddr := sdk.ConsAddress(req.Header.ProposerAddress) + fmt.Printf("debug consAddr: %v\n", consAddr) k.SetProposerConsAddr(ctx, consAddr) } diff --git a/x/distribution/types/validator_info.go b/x/distribution/types/validator_info.go index 18aef8bde..c8a02569c 100644 --- a/x/distribution/types/validator_info.go +++ b/x/distribution/types/validator_info.go @@ -19,9 +19,9 @@ func NewValidatorDistInfo(operatorAddr sdk.ValAddress, currentHeight int64) Vali return ValidatorDistInfo{ OperatorAddr: operatorAddr, FeePoolWithdrawalHeight: currentHeight, - Pool: DecCoins{}, - PoolCommission: DecCoins{}, - DelAccum: NewTotalAccum(currentHeight), + Pool: DecCoins{}, + PoolCommission: DecCoins{}, + DelAccum: NewTotalAccum(currentHeight), } } diff --git a/x/mock/simulation/constants.go b/x/mock/simulation/constants.go index 544da50e3..a96d4541f 100644 --- a/x/mock/simulation/constants.go +++ b/x/mock/simulation/constants.go @@ -14,7 +14,7 @@ const ( numKeys int = 250 // Chance that double-signing evidence is found on a given block - evidenceFraction float64 = 0.01 + evidenceFraction float64 = 0.5 // TODO Remove in favor of binary search for invariant violation onOperation bool = false diff --git a/x/mock/simulation/random_simulate_blocks.go b/x/mock/simulation/random_simulate_blocks.go index 51705cbef..0fc9c21a9 100644 --- a/x/mock/simulation/random_simulate_blocks.go +++ b/x/mock/simulation/random_simulate_blocks.go @@ -77,6 +77,9 @@ func SimulateFromSeed(tb testing.TB, app *baseapp.BaseApp, } validators := initChain(r, accs, setups, app, appStateFn) + // Second variable to keep pending validator set (delayed one block since TM 0.24) + // Initially this is the same as the initial validator set + nextValidators := validators header := abci.Header{Height: 0, Time: timestamp} opCount := 0 @@ -160,8 +163,9 @@ func SimulateFromSeed(tb testing.TB, app *baseapp.BaseApp, // Generate a random RequestBeginBlock with the current validator set for the next block request = RandomRequestBeginBlock(r, validators, livenessTransitionMatrix, evidenceFraction, pastTimes, pastVoteInfos, event, header) - // Update the validator set - validators = updateValidators(tb, r, validators, res.ValidatorUpdates, event) + // Update the validator set, which will be reflected in the application on the next block + validators = nextValidators + nextValidators = updateValidators(tb, r, validators, res.ValidatorUpdates, event) } if stopEarly { DisplayEvents(events) diff --git a/x/slashing/keeper.go b/x/slashing/keeper.go index 0c1c70b36..94dd9f0d0 100644 --- a/x/slashing/keeper.go +++ b/x/slashing/keeper.go @@ -9,6 +9,7 @@ import ( "github.com/cosmos/cosmos-sdk/codec" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/x/params" + stake "github.com/cosmos/cosmos-sdk/x/stake/types" abci "github.com/tendermint/tendermint/abci/types" "github.com/tendermint/tendermint/crypto" ) @@ -56,19 +57,28 @@ func (k Keeper) handleDoubleSign(ctx sdk.Context, addr crypto.Address, infractio // Double sign confirmed logger.Info(fmt.Sprintf("Confirmed double sign from %s at height %d, age of %d less than max age of %d", pubkey.Address(), infractionHeight, age, maxEvidenceAge)) + // We need to retrieve the stake distribution which signed the block, so we subtract ValidatorUpdateDelay from the evidence height. + // Note that this *can* result in a negative "distributionHeight", up to -ValidatorUpdateDelay, + // i.e. at the end of the pre-genesis block (none) = at the beginning of the genesis block. + // That's fine since this is just used to filter unbonding delegations & redelegations. + distributionHeight := infractionHeight - stake.ValidatorUpdateDelay + // Cap the amount slashed to the penalty for the worst infraction // within the slashing period when this infraction was committed fraction := k.SlashFractionDoubleSign(ctx) - revisedFraction := k.capBySlashingPeriod(ctx, consAddr, fraction, infractionHeight) + revisedFraction := k.capBySlashingPeriod(ctx, consAddr, fraction, distributionHeight) logger.Info(fmt.Sprintf("Fraction slashed capped by slashing period from %v to %v", fraction, revisedFraction)) // Slash validator - k.validatorSet.Slash(ctx, consAddr, infractionHeight, power, revisedFraction) + k.validatorSet.Slash(ctx, consAddr, distributionHeight, power, revisedFraction) - // Jail validator - k.validatorSet.Jail(ctx, consAddr) + // Jail validator if not already jailed + validator := k.validatorSet.ValidatorByConsAddr(ctx, consAddr) + if !validator.GetJailed() { + k.validatorSet.Jail(ctx, consAddr) + } - // Set validator jail duration + // Set or updated validator jail duration signInfo, found := k.getValidatorSigningInfo(ctx, consAddr) if !found { panic(fmt.Sprintf("Expected signing info for validator %s but not found", consAddr)) @@ -123,7 +133,13 @@ func (k Keeper) handleValidatorSignature(ctx sdk.Context, addr crypto.Address, p // Downtime confirmed: slash and jail the validator logger.Info(fmt.Sprintf("Validator %s past min height of %d and below signed blocks threshold of %d", pubkey.Address(), minHeight, k.MinSignedPerWindow(ctx))) - k.validatorSet.Slash(ctx, consAddr, height, power, k.SlashFractionDowntime(ctx)) + // We need to retrieve the stake distribution which signed the block, so we subtract ValidatorUpdateDelay from the evidence height, + // and subtract an additional 1 since this is the LastCommit. + // Note that this *can* result in a negative "distributionHeight" up to -ValidatorUpdateDelay-1, + // i.e. at the end of the pre-genesis block (none) = at the beginning of the genesis block. + // That's fine since this is just used to filter unbonding delegations & redelegations. + distributionHeight := height - stake.ValidatorUpdateDelay - 1 + k.validatorSet.Slash(ctx, consAddr, distributionHeight, power, k.SlashFractionDowntime(ctx)) k.validatorSet.Jail(ctx, consAddr) signInfo.JailedUntil = ctx.BlockHeader().Time.Add(k.DowntimeUnbondDuration(ctx)) } else { diff --git a/x/slashing/keeper_test.go b/x/slashing/keeper_test.go index bd207b241..8f1eaa3f5 100644 --- a/x/slashing/keeper_test.go +++ b/x/slashing/keeper_test.go @@ -22,20 +22,21 @@ func init() { // Test that a validator is slashed correctly // when we discover evidence of infraction -// TODO fix this test to not be using the same pubkey/address for signing and operating, it's confusing func TestHandleDoubleSign(t *testing.T) { // initial setup ctx, ck, sk, _, keeper := createTestInput(t) + // validator added pre-genesis + ctx = ctx.WithBlockHeight(-1) sk = sk.WithHooks(keeper.Hooks()) amtInt := int64(100) - addr, val, amt := addrs[0], pks[0], sdk.NewInt(amtInt) - got := stake.NewHandler(sk)(ctx, NewTestMsgCreateValidator(addr, val, amt)) + operatorAddr, val, amt := addrs[0], pks[0], sdk.NewInt(amtInt) + got := stake.NewHandler(sk)(ctx, newTestMsgCreateValidator(operatorAddr, val, amt)) require.True(t, got.IsOK()) validatorUpdates := stake.EndBlocker(ctx, sk) keeper.AddValidators(ctx, validatorUpdates) - require.Equal(t, ck.GetCoins(ctx, sdk.AccAddress(addr)), sdk.Coins{{sk.GetParams(ctx).BondDenom, initCoins.Sub(amt)}}) - require.True(t, sdk.NewDecFromInt(amt).Equal(sk.Validator(ctx, addr).GetPower())) + require.Equal(t, ck.GetCoins(ctx, sdk.AccAddress(operatorAddr)), sdk.Coins{{sk.GetParams(ctx).BondDenom, initCoins.Sub(amt)}}) + require.True(t, sdk.NewDecFromInt(amt).Equal(sk.Validator(ctx, operatorAddr).GetPower())) // handle a signature to set signing info keeper.handleValidatorSignature(ctx, val.Address(), amtInt, true) @@ -44,13 +45,13 @@ func TestHandleDoubleSign(t *testing.T) { keeper.handleDoubleSign(ctx, val.Address(), 0, time.Unix(0, 0), amtInt) // should be jailed - require.True(t, sk.Validator(ctx, addr).GetJailed()) + require.True(t, sk.Validator(ctx, operatorAddr).GetJailed()) // unjail to measure power - sk.Unjail(ctx, sdk.ConsAddress(addr)) // TODO distinguish cons address + sk.Unjail(ctx, sdk.ConsAddress(val.Address())) // power should be reduced require.Equal( t, sdk.NewDecFromInt(amt).Mul(sdk.NewDec(19).Quo(sdk.NewDec(20))), - sk.Validator(ctx, addr).GetPower(), + sk.Validator(ctx, operatorAddr).GetPower(), ) ctx = ctx.WithBlockHeader(abci.Header{Time: time.Unix(1, 0).Add(keeper.MaxEvidenceAge(ctx))}) @@ -58,74 +59,74 @@ func TestHandleDoubleSign(t *testing.T) { keeper.handleDoubleSign(ctx, val.Address(), 0, time.Unix(0, 0), amtInt) require.Equal( t, sdk.NewDecFromInt(amt).Mul(sdk.NewDec(19).Quo(sdk.NewDec(20))), - sk.Validator(ctx, addr).GetPower(), + sk.Validator(ctx, operatorAddr).GetPower(), ) } // Test that the amount a validator is slashed for multiple double signs // is correctly capped by the slashing period in which they were committed -// TODO properly distinguish between consensus and operator address is variable names func TestSlashingPeriodCap(t *testing.T) { // initial setup ctx, ck, sk, _, keeper := createTestInput(t) sk = sk.WithHooks(keeper.Hooks()) amtInt := int64(100) - addr, amt := addrs[0], sdk.NewInt(amtInt) - valConsPubKey, valConsAddr := pks[0], sdk.ConsAddress(pks[0].Address()) - got := stake.NewHandler(sk)(ctx, NewTestMsgCreateValidator(addr, valConsPubKey, amt)) + operatorAddr, amt := addrs[0], sdk.NewInt(amtInt) + valConsPubKey, valConsAddr := pks[0], pks[0].Address() + got := stake.NewHandler(sk)(ctx, newTestMsgCreateValidator(operatorAddr, valConsPubKey, amt)) require.True(t, got.IsOK()) validatorUpdates := stake.EndBlocker(ctx, sk) + ctx = ctx.WithBlockHeight(ctx.BlockHeight() + 1) keeper.AddValidators(ctx, validatorUpdates) - require.Equal(t, ck.GetCoins(ctx, sdk.AccAddress(addr)), sdk.Coins{{sk.GetParams(ctx).BondDenom, initCoins.Sub(amt)}}) - require.True(t, sdk.NewDecFromInt(amt).Equal(sk.Validator(ctx, addr).GetPower())) + require.Equal(t, ck.GetCoins(ctx, sdk.AccAddress(operatorAddr)), sdk.Coins{{sk.GetParams(ctx).BondDenom, initCoins.Sub(amt)}}) + require.True(t, sdk.NewDecFromInt(amt).Equal(sk.Validator(ctx, operatorAddr).GetPower())) // handle a signature to set signing info - keeper.handleValidatorSignature(ctx, valConsPubKey.Address(), amtInt, true) + keeper.handleValidatorSignature(ctx, valConsAddr, amtInt, true) // double sign less than max age - keeper.handleDoubleSign(ctx, valConsPubKey.Address(), 0, time.Unix(0, 0), amtInt) + keeper.handleDoubleSign(ctx, valConsAddr, 1, time.Unix(0, 0), amtInt) // should be jailed - require.True(t, sk.Validator(ctx, addr).GetJailed()) - // end block - stake.EndBlocker(ctx, sk) - // update block height - ctx = ctx.WithBlockHeight(int64(1)) - // unjail to measure power - sk.Unjail(ctx, valConsAddr) - // end block - stake.EndBlocker(ctx, sk) - // power should be reduced - expectedPower := sdk.NewDecFromInt(amt).Mul(sdk.NewDec(19).Quo(sdk.NewDec(20))) - require.Equal(t, expectedPower, sk.Validator(ctx, addr).GetPower()) - - // double sign again, same slashing period - keeper.handleDoubleSign(ctx, valConsPubKey.Address(), 0, time.Unix(0, 0), amtInt) - // should be jailed - require.True(t, sk.Validator(ctx, addr).GetJailed()) + require.True(t, sk.Validator(ctx, operatorAddr).GetJailed()) // end block stake.EndBlocker(ctx, sk) // update block height ctx = ctx.WithBlockHeight(int64(2)) // unjail to measure power - sk.Unjail(ctx, valConsAddr) + sk.Unjail(ctx, sdk.ConsAddress(valConsAddr)) + // end block + stake.EndBlocker(ctx, sk) + // power should be reduced + expectedPower := sdk.NewDecFromInt(amt).Mul(sdk.NewDec(19).Quo(sdk.NewDec(20))) + require.Equal(t, expectedPower, sk.Validator(ctx, operatorAddr).GetPower()) + + // double sign again, same slashing period + keeper.handleDoubleSign(ctx, valConsAddr, 1, time.Unix(0, 0), amtInt) + // should be jailed + require.True(t, sk.Validator(ctx, operatorAddr).GetJailed()) + // end block + stake.EndBlocker(ctx, sk) + // update block height + ctx = ctx.WithBlockHeight(int64(3)) + // unjail to measure power + sk.Unjail(ctx, sdk.ConsAddress(valConsAddr)) // end block stake.EndBlocker(ctx, sk) // power should be equal, no more should have been slashed expectedPower = sdk.NewDecFromInt(amt).Mul(sdk.NewDec(19).Quo(sdk.NewDec(20))) - require.Equal(t, expectedPower, sk.Validator(ctx, addr).GetPower()) + require.Equal(t, expectedPower, sk.Validator(ctx, operatorAddr).GetPower()) // double sign again, new slashing period - keeper.handleDoubleSign(ctx, valConsPubKey.Address(), 2, time.Unix(0, 0), amtInt) + keeper.handleDoubleSign(ctx, valConsAddr, 3, time.Unix(0, 0), amtInt) // should be jailed - require.True(t, sk.Validator(ctx, addr).GetJailed()) + require.True(t, sk.Validator(ctx, operatorAddr).GetJailed()) // unjail to measure power - sk.Unjail(ctx, valConsAddr) + sk.Unjail(ctx, sdk.ConsAddress(valConsAddr)) // end block stake.EndBlocker(ctx, sk) // power should be reduced expectedPower = sdk.NewDecFromInt(amt).Mul(sdk.NewDec(18).Quo(sdk.NewDec(20))) - require.Equal(t, expectedPower, sk.Validator(ctx, addr).GetPower()) + require.Equal(t, expectedPower, sk.Validator(ctx, operatorAddr).GetPower()) } // Test a validator through uptime, downtime, revocation, @@ -196,6 +197,35 @@ func TestHandleAbsentValidator(t *testing.T) { validator, _ = sk.GetValidatorByConsAddr(ctx, sdk.GetConsAddress(val)) require.Equal(t, sdk.Unbonding, validator.GetStatus()) + slashAmt := sdk.NewDec(amtInt).Mul(keeper.SlashFractionDowntime(ctx)).RoundInt64() + + // validator should have been slashed + require.Equal(t, amtInt-slashAmt, validator.GetTokens().RoundInt64()) + + // 502nd block *also* missed (since the LastCommit would have still included the just-unbonded validator) + height++ + ctx = ctx.WithBlockHeight(height) + keeper.handleValidatorSignature(ctx, val.Address(), amtInt, false) + info, found = keeper.getValidatorSigningInfo(ctx, sdk.ConsAddress(val.Address())) + require.True(t, found) + require.Equal(t, int64(0), info.StartHeight) + require.Equal(t, keeper.SignedBlocksWindow(ctx)-keeper.MinSignedPerWindow(ctx)-2, info.SignedBlocksCounter) + + // end block + stake.EndBlocker(ctx, sk) + + // validator should not have been slashed any more, since it was already jailed + validator, _ = sk.GetValidatorByConsAddr(ctx, sdk.GetConsAddress(val)) + require.Equal(t, amtInt-slashAmt, validator.GetTokens().RoundInt64()) + + // 502nd block *double signed* (oh no!) + keeper.handleDoubleSign(ctx, val.Address(), height, ctx.BlockHeader().Time, amtInt) + + // validator should have been slashed + validator, _ = sk.GetValidatorByConsAddr(ctx, sdk.GetConsAddress(val)) + secondSlashAmt := sdk.NewDec(amtInt).Mul(keeper.SlashFractionDoubleSign(ctx)).RoundInt64() + require.Equal(t, amtInt-slashAmt-secondSlashAmt, validator.GetTokens().RoundInt64()) + // unrevocation should fail prior to jail expiration got = slh(ctx, NewMsgUnjail(addr)) require.False(t, got.IsOK()) @@ -214,14 +244,14 @@ func TestHandleAbsentValidator(t *testing.T) { // validator should have been slashed pool = sk.GetPool(ctx) - slashAmt := sdk.NewDec(amtInt).Mul(keeper.SlashFractionDowntime(ctx)).RoundInt64() - require.Equal(t, amtInt-slashAmt, pool.BondedTokens.RoundInt64()) + require.Equal(t, amtInt-slashAmt-secondSlashAmt, pool.BondedTokens.RoundInt64()) // validator start height should have been changed info, found = keeper.getValidatorSigningInfo(ctx, sdk.ConsAddress(val.Address())) require.True(t, found) require.Equal(t, height, info.StartHeight) - require.Equal(t, keeper.SignedBlocksWindow(ctx)-keeper.MinSignedPerWindow(ctx)-1, info.SignedBlocksCounter) + // we've missed 2 blocks more than the maximum + require.Equal(t, keeper.SignedBlocksWindow(ctx)-keeper.MinSignedPerWindow(ctx)-2, info.SignedBlocksCounter) // validator should not be immediately jailed again height++ diff --git a/x/slashing/keys.go b/x/slashing/keys.go index 1f84a285d..ec453cedc 100644 --- a/x/slashing/keys.go +++ b/x/slashing/keys.go @@ -4,6 +4,7 @@ import ( "encoding/binary" sdk "github.com/cosmos/cosmos-sdk/types" + stake "github.com/cosmos/cosmos-sdk/x/stake/types" ) // key prefix bytes @@ -34,7 +35,8 @@ func GetValidatorSlashingPeriodPrefix(v sdk.ConsAddress) []byte { // stored by *Tendermint* address (not operator address) followed by start height func GetValidatorSlashingPeriodKey(v sdk.ConsAddress, startHeight int64) []byte { b := make([]byte, 8) - binary.LittleEndian.PutUint64(b, uint64(startHeight)) + // this needs to be height + ValidatorUpdateDelay because the slashing period for genesis validators starts at height -ValidatorUpdateDelay + binary.BigEndian.PutUint64(b, uint64(startHeight+stake.ValidatorUpdateDelay)) return append(GetValidatorSlashingPeriodPrefix(v), b...) } diff --git a/x/slashing/slashing_period.go b/x/slashing/slashing_period.go index fc57e663a..0595d5eeb 100644 --- a/x/slashing/slashing_period.go +++ b/x/slashing/slashing_period.go @@ -5,6 +5,7 @@ import ( "fmt" sdk "github.com/cosmos/cosmos-sdk/types" + stake "github.com/cosmos/cosmos-sdk/x/stake/types" ) // Cap an infraction's slash amount by the slashing period in which it was committed @@ -15,7 +16,7 @@ func (k Keeper) capBySlashingPeriod(ctx sdk.Context, address sdk.ConsAddress, fr // Sanity check if slashingPeriod.EndHeight > 0 && slashingPeriod.EndHeight < infractionHeight { - panic(fmt.Sprintf("slashing period ended before infraction: infraction height %d, slashing period ended at %d", infractionHeight, slashingPeriod.EndHeight)) + panic(fmt.Sprintf("slashing period ended before infraction: validator %s, infraction height %d, slashing period ended at %d", address, infractionHeight, slashingPeriod.EndHeight)) } // Calculate the updated total slash amount @@ -43,7 +44,7 @@ func (k Keeper) getValidatorSlashingPeriodForHeight(ctx sdk.Context, address sdk end := sdk.PrefixEndBytes(GetValidatorSlashingPeriodKey(address, height)) iterator := store.ReverseIterator(start, end) if !iterator.Valid() { - panic("expected to find slashing period, but none was found") + panic(fmt.Sprintf("expected to find slashing period for validator %s before height %d, but none was found", address, height)) } slashingPeriod = k.unmarshalSlashingPeriodKeyValue(iterator.Key(), iterator.Value()) return @@ -68,7 +69,7 @@ func (k Keeper) unmarshalSlashingPeriodKeyValue(key []byte, value []byte) Valida var slashingPeriodValue ValidatorSlashingPeriodValue k.cdc.MustUnmarshalBinary(value, &slashingPeriodValue) address := sdk.ConsAddress(key[1 : 1+sdk.AddrLen]) - startHeight := int64(binary.LittleEndian.Uint64(key[1+sdk.AddrLen : 1+sdk.AddrLen+8])) + startHeight := int64(binary.BigEndian.Uint64(key[1+sdk.AddrLen:1+sdk.AddrLen+8]) - uint64(stake.ValidatorUpdateDelay)) return ValidatorSlashingPeriod{ ValidatorAddr: address, StartHeight: startHeight, diff --git a/x/stake/genesis.go b/x/stake/genesis.go index 6b916701e..6e1e055f2 100644 --- a/x/stake/genesis.go +++ b/x/stake/genesis.go @@ -18,6 +18,12 @@ import ( // the bonded validators. // Returns final validator set after applying all declaration and delegations func InitGenesis(ctx sdk.Context, keeper Keeper, data types.GenesisState) (res []abci.ValidatorUpdate, err error) { + + // We need to pretend to be "n blocks before genesis", where "n" is the validator update delay, + // so that e.g. slashing periods are correctly initialized for the validator set + // e.g. with a one-block offset - the first TM block is at height 0, so state updates applied from genesis.json are in block -1. + ctx = ctx.WithBlockHeight(-types.ValidatorUpdateDelay) + keeper.SetPool(ctx, data.Pool) keeper.SetParams(ctx, data.Params) keeper.InitIntraTxCounter(ctx) diff --git a/x/stake/keeper/slash_test.go b/x/stake/keeper/slash_test.go index 7dcf8ccc9..aab97b811 100644 --- a/x/stake/keeper/slash_test.go +++ b/x/stake/keeper/slash_test.go @@ -191,6 +191,34 @@ func TestSlashAtFutureHeight(t *testing.T) { require.Panics(t, func() { keeper.Slash(ctx, consAddr, 1, 10, fraction) }) } +// test slash at a negative height +// this just represents pre-genesis and should have the same effect as slashing at height 0 +func TestSlashAtNegativeHeight(t *testing.T) { + ctx, keeper, _ := setupHelper(t, 10) + consAddr := sdk.ConsAddress(PKs[0].Address()) + fraction := sdk.NewDecWithPrec(5, 1) + + oldPool := keeper.GetPool(ctx) + validator, found := keeper.GetValidatorByConsAddr(ctx, consAddr) + require.True(t, found) + keeper.Slash(ctx, consAddr, -2, 10, fraction) + + // read updated state + validator, found = keeper.GetValidatorByConsAddr(ctx, consAddr) + require.True(t, found) + newPool := keeper.GetPool(ctx) + + // end block + updates := keeper.ApplyAndReturnValidatorSetUpdates(ctx) + require.Equal(t, 1, len(updates), "cons addr: %v, updates: %v", []byte(consAddr), updates) + + validator = keeper.mustGetValidator(ctx, validator.OperatorAddr) + // power decreased + require.Equal(t, sdk.NewDec(5), validator.GetPower()) + // pool bonded shares decreased + require.Equal(t, sdk.NewDec(5).RoundInt64(), oldPool.BondedTokens.Sub(newPool.BondedTokens).RoundInt64()) +} + // tests Slash at the current height func TestSlashValidatorAtCurrentHeight(t *testing.T) { ctx, keeper, _ := setupHelper(t, 10) diff --git a/x/stake/types/params.go b/x/stake/types/params.go index 4dcc3782a..c3685f7c3 100644 --- a/x/stake/types/params.go +++ b/x/stake/types/params.go @@ -9,9 +9,17 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" ) -// defaultUnbondingTime reflects three weeks in seconds as the default -// unbonding time. -const defaultUnbondingTime time.Duration = 60 * 60 * 24 * 3 * time.Second +const ( + // defaultUnbondingTime reflects three weeks in seconds as the default + // unbonding time. + defaultUnbondingTime time.Duration = 60 * 60 * 24 * 3 * time.Second + + // Delay, in blocks, between when validator updates are returned to Tendermint and when they are applied + // For example, if this is 0, the validator set at the end of a block will sign the next block, or + // if this is 1, the validator set at the end of a block will sign the block after the next. + // Constant as this should not change without a hard fork. + ValidatorUpdateDelay int64 = 1 +) // Params defines the high level settings for staking type Params struct {