diff --git a/Gopkg.lock b/Gopkg.lock index fa6fafb99..5e40c17e6 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -34,7 +34,7 @@ [[projects]] branch = "master" - digest = "1:6aabc1566d6351115d561d038da82a4c19b46c3b6e17f4a0a2fa60260663dc79" + digest = "1:2c00f064ba355903866cbfbf3f7f4c0fe64af6638cc7d1b8bdcf3181bc67f1d8" name = "github.com/btcsuite/btcd" packages = ["btcec"] pruneopts = "UT" @@ -71,7 +71,7 @@ version = "v1.4.7" [[projects]] - digest = "1:fa30c0652956e159cdb97dcb2ef8b8db63ed668c02a5c3a40961c8f0641252fe" + digest = "1:fdf5169073fb0ad6dc12a70c249145e30f4058647bea25f0abd48b6d9f228a11" name = "github.com/go-kit/kit" packages = [ "log", @@ -103,7 +103,7 @@ version = "v1.7.0" [[projects]] - digest = "1:212285efb97b9ec2e20550d81f0446cb7897e57cbdfd7301b1363ab113d8be45" + digest = "1:35621fe20f140f05a0c4ef662c26c0ab4ee50bca78aa30fe87d33120bd28165e" name = "github.com/gogo/protobuf" packages = [ "gogoproto", @@ -118,7 +118,7 @@ version = "v1.1.1" [[projects]] - digest = "1:cb22af0ed7c72d495d8be1106233ee553898950f15fd3f5404406d44c2e86888" + digest = "1:17fe264ee908afc795734e8c4e63db2accabaf57326dbf21763a7d6b86096260" name = "github.com/golang/protobuf" packages = [ "proto", @@ -165,13 +165,12 @@ [[projects]] branch = "master" - digest = "1:ac64f01acc5eeea9dde40e326de6b6471e501392ec06524c3b51033aa50789bc" + digest = "1:12247a2e99a060cc692f6680e5272c8adf0b8f572e6bce0d7095e624c958a240" name = "github.com/hashicorp/hcl" packages = [ ".", "hcl/ast", "hcl/parser", - "hcl/printer", "hcl/scanner", "hcl/strconv", "hcl/token", @@ -263,7 +262,7 @@ version = "v1.0.0" [[projects]] - digest = "1:98225904b7abff96c052b669b25788f18225a36673fba022fb93514bb9a2a64e" + digest = "1:c1a04665f9613e082e1209cf288bf64f4068dcd6c87a64bf1c4ff006ad422ba0" name = "github.com/prometheus/client_golang" packages = [ "prometheus", @@ -274,7 +273,7 @@ [[projects]] branch = "master" - digest = "1:0f37e09b3e92aaeda5991581311f8dbf38944b36a3edec61cc2d1991f527554a" + digest = "1:2d5cd61daa5565187e1d96bae64dbbc6080dacf741448e9629c64fd93203b0d4" name = "github.com/prometheus/client_model" packages = ["go"] pruneopts = "UT" @@ -282,7 +281,7 @@ [[projects]] branch = "master" - digest = "1:dad2e5a2153ee7a6c9ab8fc13673a16ee4fb64434a7da980965a3741b0c981a3" + digest = "1:63b68062b8968092eb86bedc4e68894bd096ea6b24920faca8b9dcf451f54bb5" name = "github.com/prometheus/common" packages = [ "expfmt", @@ -294,7 +293,7 @@ [[projects]] branch = "master" - digest = "1:a37c98f4b7a66bb5c539c0539f0915a74ef1c8e0b3b6f45735289d94cae92bfd" + digest = "1:8c49953a1414305f2ff5465147ee576dd705487c35b15918fcd4efdc0cb7a290" name = "github.com/prometheus/procfs" packages = [ ".", @@ -313,7 +312,7 @@ revision = "e2704e165165ec55d062f5919b4b29494e9fa790" [[projects]] - digest = "1:37ace7f35375adec11634126944bdc45a673415e2fcc07382d03b75ec76ea94c" + digest = "1:bd1ae00087d17c5a748660b8e89e1043e1e5479d0fea743352cda2f8dd8c4f84" name = "github.com/spf13/afero" packages = [ ".", @@ -332,7 +331,7 @@ version = "v1.2.0" [[projects]] - digest = "1:627ab2f549a6a55c44f46fa24a4307f4d0da81bfc7934ed0473bf38b24051d26" + digest = "1:7ffc0983035bc7e297da3688d9fe19d60a420e9c38bef23f845c53788ed6a05e" name = "github.com/spf13/cobra" packages = ["."] pruneopts = "UT" @@ -364,7 +363,7 @@ version = "v1.0.0" [[projects]] - digest = "1:73697231b93fb74a73ebd8384b68b9a60c57ea6b13c56d2425414566a72c8e6d" + digest = "1:7e8d267900c7fa7f35129a2a37596e38ed0f11ca746d6d9ba727980ee138f9f6" name = "github.com/stretchr/testify" packages = [ "assert", @@ -376,7 +375,7 @@ [[projects]] branch = "master" - digest = "1:442d2ffa75ffae302ce8800bf4144696b92bef02917923ea132ce2d39efe7d65" + digest = "1:f2ffd421680b0a3f7887501b3c6974bcf19217ecd301d0e2c9b681940ec363d5" name = "github.com/syndtr/goleveldb" packages = [ "leveldb", @@ -397,7 +396,7 @@ [[projects]] branch = "master" - digest = "1:203b409c21115233a576f99e8f13d8e07ad82b25500491f7e1cca12588fb3232" + digest = "1:087aaa7920e5d0bf79586feb57ce01c35c830396ab4392798112e8aae8c47722" name = "github.com/tendermint/ed25519" packages = [ ".", @@ -424,7 +423,7 @@ version = "v0.9.2" [[projects]] - digest = "1:963f6c04345ce36f900c1d6367200eebc3cc2db6ee632ff865ea8dcf64b748a0" + digest = "1:4f15e95fe3888cc75dd34f407d6394cbc7fd3ff24920851b92b295f6a8b556e6" name = "github.com/tendermint/tendermint" packages = [ "abci/client", @@ -491,7 +490,7 @@ version = "v0.23.1-rc0" [[projects]] - digest = "1:ad879bb8c71020a3f92f0c61f414d93eae1d5dc2f37023b6abaa3cc84b00165e" + digest = "1:bf6d9a827ea3cad964c2f863302e4f6823170d0b5ed16f72cf1184a7c615067e" name = "github.com/tendermint/tmlibs" packages = ["cli"] pruneopts = "UT" @@ -507,7 +506,7 @@ [[projects]] branch = "master" - digest = "1:2a3ce1f08dcae8bac666deb6e4c88b5d7170c510da38fd746231144cac351704" + digest = "1:27507554c6d4f060d8d700c31c624a43d3a92baa634e178ddc044bdf7d13b44a" name = "golang.org/x/crypto" packages = [ "blowfish", @@ -529,7 +528,7 @@ revision = "614d502a4dac94afa3a6ce146bd1736da82514c6" [[projects]] - digest = "1:04dda8391c3e2397daf254ac68003f30141c069b228d06baec8324a5f81dc1e9" + digest = "1:d36f55a999540d29b6ea3c2ea29d71c76b1d9853fdcd3e5c5cb4836f2ba118f1" name = "golang.org/x/net" packages = [ "context", @@ -546,7 +545,7 @@ [[projects]] branch = "master" - digest = "1:c8baf78f0ac6eb27c645e264fe5e8a74d5a50db188ab41a7ff3b275c112e0735" + digest = "1:86171d21d59449dcf7cee0b7d2da83dff989dab9b9b69bfe0a3d59c3c1ca6081" name = "golang.org/x/sys" packages = [ "cpu", @@ -556,7 +555,7 @@ revision = "11551d06cbcc94edc80a0facaccbda56473c19c1" [[projects]] - digest = "1:7509ba4347d1f8de6ae9be8818b0cd1abc3deeffe28aeaf4be6d4b6b5178d9ca" + digest = "1:a2ab62866c75542dd18d2b069fec854577a20211d7c0ea6ae746072a1dccdd18" name = "golang.org/x/text" packages = [ "collate", @@ -587,7 +586,7 @@ revision = "c66870c02cf823ceb633bcd05be3c7cda29976f4" [[projects]] - digest = "1:4515e3030c440845b046354fd5d57671238428b820deebce2e9dabb5cd3c51ac" + digest = "1:2dab32a43451e320e49608ff4542fdfc653c95dcc35d0065ec9c6c3dd540ed74" name = "google.golang.org/grpc" packages = [ ".", @@ -664,6 +663,8 @@ "github.com/tendermint/tendermint/libs/common", "github.com/tendermint/tendermint/libs/db", "github.com/tendermint/tendermint/libs/log", + "github.com/tendermint/tendermint/lite", + "github.com/tendermint/tendermint/lite/proxy", "github.com/tendermint/tendermint/node", "github.com/tendermint/tendermint/p2p", "github.com/tendermint/tendermint/privval", diff --git a/Makefile b/Makefile index 983234dbc..e363891e2 100644 --- a/Makefile +++ b/Makefile @@ -163,6 +163,17 @@ test_sim_gaia_slow: @echo "Running full Gaia simulation. This may take awhile!" @go test ./cmd/gaia/app -run TestFullGaiaSimulation -SimulationEnabled=true -SimulationNumBlocks=1000 -SimulationVerbose=true -v -timeout 24h +SIM_NUM_BLOCKS ?= 210 +SIM_BLOCK_SIZE ?= 200 +SIM_COMMIT ?= true +test_sim_gaia_benchmark: + @echo "Running Gaia benchmark for numBlocks=$(SIM_NUM_BLOCKS), blockSize=$(SIM_BLOCK_SIZE). This may take awhile!" + @go test -benchmem -run=^$$ github.com/cosmos/cosmos-sdk/cmd/gaia/app -bench ^BenchmarkFullGaiaSimulation$$ -SimulationEnabled=true -SimulationNumBlocks=$(SIM_NUM_BLOCKS) -SimulationBlockSize=$(SIM_BLOCK_SIZE) -SimulationCommit=$(SIM_COMMIT) -timeout 24h + +test_sim_gaia_profile: + @echo "Running Gaia benchmark for numBlocks=$(SIM_NUM_BLOCKS), blockSize=$(SIM_BLOCK_SIZE). This may take awhile!" + @go test -benchmem -run=^$$ github.com/cosmos/cosmos-sdk/cmd/gaia/app -bench ^BenchmarkFullGaiaSimulation$$ -SimulationEnabled=true -SimulationNumBlocks=$(SIM_NUM_BLOCKS) -SimulationBlockSize=$(SIM_BLOCK_SIZE) -SimulationCommit=$(SIM_COMMIT) -timeout 24h -cpuprofile cpu.out -memprofile mem.out + test_cover: @bash tests/test_cover.sh diff --git a/PENDING.md b/PENDING.md index 4e9ac60fa..4bea690f3 100644 --- a/PENDING.md +++ b/PENDING.md @@ -23,6 +23,8 @@ BREAKING CHANGES * [x/stake] \#1901 Validator type's Owner field renamed to Operator; Validator's GetOwner() renamed accordingly to comply with the SDK's Validator interface. * [docs] [#2001](https://github.com/cosmos/cosmos-sdk/pull/2001) Update slashing spec for slashing period * [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/stake] \#2040 Validator operator type has now changed to `sdk.ValAddress` * A new bech32 prefix has been introduced for Tendermint signing keys and addresses, `cosmosconspub` and `cosmoscons` respectively. @@ -30,6 +32,7 @@ BREAKING CHANGES * SDK * [core] \#1807 Switch from use of rational to decimal * [types] \#1901 Validator interface's GetOwner() renamed to GetOperator() + * [x/slashing] [#2122](https://github.com/cosmos/cosmos-sdk/pull/2122) - Implement slashing period * [types] \#2119 Parsed error messages and ABCI log errors to make them more human readable. * [simulation] Rename TestAndRunTx to Operation [#2153](https://github.com/cosmos/cosmos-sdk/pull/2153) @@ -40,6 +43,7 @@ FEATURES * Gaia REST API (`gaiacli advanced rest-server`) * [lcd] Endpoints to query staking pool and params + * [lcd] \#2110 Add support for `simulate=true` requests query argument to endpoints that send txs to run simulations of transactions * Gaia CLI (`gaiacli`) * [cli] Cmds to query staking pool and params @@ -48,6 +52,7 @@ FEATURES provide desired Bech32 prefix encoding * [cli] \#2047 Setting the --gas flag value to 0 triggers a simulation of the tx before the actual execution. The gas estimate obtained via the simulation will be used as gas limit in the actual execution. * [cli] \#2047 The --gas-adjustment flag can be used to adjust the estimate obtained via the simulation triggered by --gas=0. + * [cli] \#2110 Add --dry-run flag to perform a simulation of a transaction without broadcasting it. The --gas flag is ignored as gas would be automatically estimated. * Gaia * [cli] #2170 added ability to show the node's address via `gaiad tendermint show-address` @@ -55,6 +60,7 @@ FEATURES * SDK * [querier] added custom querier functionality, so ABCI query requests can be handled by keepers * [simulation] \#1924 allow operations to specify future operations + * [simulation] \#1924 Add benchmarking capabilities, with makefile commands "test_sim_gaia_benchmark, test_sim_gaia_profile" * Tendermint @@ -73,7 +79,8 @@ IMPROVEMENTS * Gaia * [x/stake] [#2023](https://github.com/cosmos/cosmos-sdk/pull/2023) Terminate iteration loop in `UpdateBondedValidators` and `UpdateBondedValidatorsFull` when the first revoked validator is encountered and perform a sanity check. * [x/auth] Signature verification's gas cost now accounts for pubkey type. [#2046](https://github.com/tendermint/tendermint/pull/2046) - + * [x/stake] [x/slashing] Ensure delegation invariants to jailed validators [#1883](https://github.com/cosmos/cosmos-sdk/issues/1883). + * [x/stake] Improve speed of GetValidator, which was shown to be a performance bottleneck. [#2046](https://github.com/tendermint/tendermint/pull/2200) * SDK * [tools] Make get_vendor_deps deletes `.vendor-new` directories, in case scratch files are present. * [spec] Added simple piggy bank distribution spec diff --git a/client/context/context.go b/client/context/context.go index 743c92355..8e56cfa1a 100644 --- a/client/context/context.go +++ b/client/context/context.go @@ -1,14 +1,18 @@ package context import ( - "io" - + "bytes" + "fmt" "github.com/cosmos/cosmos-sdk/client" "github.com/cosmos/cosmos-sdk/wire" "github.com/cosmos/cosmos-sdk/x/auth" + "io" "github.com/spf13/viper" + "github.com/tendermint/tendermint/libs/cli" + tmlite "github.com/tendermint/tendermint/lite" + tmliteProxy "github.com/tendermint/tendermint/lite/proxy" rpcclient "github.com/tendermint/tendermint/rpc/client" ) @@ -32,6 +36,8 @@ type CLIContext struct { Async bool JSON bool PrintResponse bool + Certifier tmlite.Certifier + DryRun bool } // NewCLIContext returns a new initialized CLIContext with parameters from the @@ -57,9 +63,41 @@ func NewCLIContext() CLIContext { Async: viper.GetBool(client.FlagAsync), JSON: viper.GetBool(client.FlagJson), PrintResponse: viper.GetBool(client.FlagPrintResponse), + Certifier: createCertifier(), + DryRun: viper.GetBool(client.FlagDryRun), } } +func createCertifier() tmlite.Certifier { + trustNode := viper.GetBool(client.FlagTrustNode) + if trustNode { + return nil + } + chainID := viper.GetString(client.FlagChainID) + home := viper.GetString(cli.HomeFlag) + nodeURI := viper.GetString(client.FlagNode) + + var errMsg bytes.Buffer + if chainID == "" { + errMsg.WriteString("chain-id ") + } + if home == "" { + errMsg.WriteString("home ") + } + if nodeURI == "" { + errMsg.WriteString("node ") + } + // errMsg is not empty + if errMsg.Len() != 0 { + panic(fmt.Errorf("can't create certifier for distrust mode, empty values from these options: %s", errMsg.String())) + } + certifier, err := tmliteProxy.GetCertifier(chainID, home, nodeURI) + if err != nil { + panic(err) + } + return certifier +} + // WithCodec returns a copy of the context with an updated codec. func (ctx CLIContext) WithCodec(cdc *wire.Codec) CLIContext { ctx.Codec = cdc @@ -117,3 +155,15 @@ func (ctx CLIContext) WithUseLedger(useLedger bool) CLIContext { ctx.UseLedger = useLedger return ctx } + +// WithCertifier - return a copy of the context with an updated Certifier +func (ctx CLIContext) WithCertifier(certifier tmlite.Certifier) CLIContext { + ctx.Certifier = certifier + return ctx +} + +// WithGasAdjustment returns a copy of the context with an updated GasAdjustment flag. +func (ctx CLIContext) WithGasAdjustment(adjustment float64) CLIContext { + ctx.GasAdjustment = adjustment + return ctx +} diff --git a/client/context/query.go b/client/context/query.go index e526c0abb..4c1cad877 100644 --- a/client/context/query.go +++ b/client/context/query.go @@ -10,9 +10,14 @@ import ( "github.com/pkg/errors" + "github.com/cosmos/cosmos-sdk/store" + "github.com/cosmos/cosmos-sdk/wire" + abci "github.com/tendermint/tendermint/abci/types" cmn "github.com/tendermint/tendermint/libs/common" + tmliteProxy "github.com/tendermint/tendermint/lite/proxy" rpcclient "github.com/tendermint/tendermint/rpc/client" ctypes "github.com/tendermint/tendermint/rpc/core/types" + "strings" ) // GetNode returns an RPC client. If the context's client is not defined, an @@ -304,12 +309,77 @@ func (ctx CLIContext) query(path string, key cmn.HexBytes) (res []byte, err erro return res, errors.Errorf("query failed: (%d) %s", resp.Code, resp.Log) } + // Data from trusted node or subspace query doesn't need verification + if ctx.TrustNode || !isQueryStoreWithProof(path) { + return resp.Value, nil + } + + err = ctx.verifyProof(path, resp) + if err != nil { + return nil, err + } + return resp.Value, nil } +// verifyProof perform response proof verification +func (ctx CLIContext) verifyProof(path string, resp abci.ResponseQuery) error { + + if ctx.Certifier == nil { + return fmt.Errorf("missing valid certifier to verify data from untrusted node") + } + + node, err := ctx.GetNode() + if err != nil { + return err + } + + // AppHash for height H is in header H+1 + commit, err := tmliteProxy.GetCertifiedCommit(resp.Height+1, node, ctx.Certifier) + if err != nil { + return err + } + + var multiStoreProof store.MultiStoreProof + cdc := wire.NewCodec() + err = cdc.UnmarshalBinary(resp.Proof, &multiStoreProof) + if err != nil { + return errors.Wrap(err, "failed to unmarshalBinary rangeProof") + } + + // Verify the substore commit hash against trusted appHash + substoreCommitHash, err := store.VerifyMultiStoreCommitInfo(multiStoreProof.StoreName, + multiStoreProof.StoreInfos, commit.Header.AppHash) + if err != nil { + return errors.Wrap(err, "failed in verifying the proof against appHash") + } + err = store.VerifyRangeProof(resp.Key, resp.Value, substoreCommitHash, &multiStoreProof.RangeProof) + if err != nil { + return errors.Wrap(err, "failed in the range proof verification") + } + return nil +} + // queryStore performs a query from a Tendermint node with the provided a store // name and path. func (ctx CLIContext) queryStore(key cmn.HexBytes, storeName, endPath string) ([]byte, error) { path := fmt.Sprintf("/store/%s/%s", storeName, endPath) return ctx.query(path, key) } + +// isQueryStoreWithProof expects a format like /// +// queryType can be app or store +func isQueryStoreWithProof(path string) bool { + if !strings.HasPrefix(path, "/") { + return false + } + paths := strings.SplitN(path[1:], "/", 3) + if len(paths) != 3 { + return false + } + + if store.RequireProof("/" + paths[2]) { + return true + } + return false +} diff --git a/client/flags.go b/client/flags.go index 81e067067..a1d3c6e17 100644 --- a/client/flags.go +++ b/client/flags.go @@ -4,8 +4,11 @@ import "github.com/spf13/cobra" // nolint const ( + // DefaultGasAdjustment is applied to gas estimates to avoid tx + // execution failures due to state changes that might + // occur between the tx simulation and the actual run. + DefaultGasAdjustment = 1.0 DefaultGasLimit = 200000 - DefaultGasAdjustment = 1.2 FlagUseLedger = "ledger" FlagChainID = "chain-id" @@ -23,6 +26,7 @@ const ( FlagAsync = "async" FlagJson = "json" FlagPrintResponse = "print-response" + FlagDryRun = "dry-run" ) // LineBreak can be included in a command list to provide a blank line @@ -54,10 +58,12 @@ func PostCommands(cmds ...*cobra.Command) []*cobra.Command { c.Flags().String(FlagNode, "tcp://localhost:26657", ": to tendermint rpc interface for this chain") c.Flags().Bool(FlagUseLedger, false, "Use a connected Ledger device") c.Flags().Int64(FlagGas, DefaultGasLimit, "gas limit to set per-transaction; set to 0 to calculate required gas automatically") - c.Flags().Float64(FlagGasAdjustment, DefaultGasAdjustment, "adjustment factor to be multiplied against the estimate returned by the tx simulation") + c.Flags().Float64(FlagGasAdjustment, DefaultGasAdjustment, "adjustment factor to be multiplied against the estimate returned by the tx simulation; if the gas limit is set manually this flag is ignored ") c.Flags().Bool(FlagAsync, false, "broadcast transactions asynchronously") c.Flags().Bool(FlagJson, false, "return output in json format") c.Flags().Bool(FlagPrintResponse, true, "return tx response (only works with async = false)") + c.Flags().Bool(FlagTrustNode, true, "Don't verify proofs for query responses") + c.Flags().Bool(FlagDryRun, false, "ignore the --gas flag and perform a simulation of a transaction, but don't broadcast it") } return cmds } diff --git a/client/lcd/lcd_test.go b/client/lcd/lcd_test.go index d68ba278c..707cc21f7 100644 --- a/client/lcd/lcd_test.go +++ b/client/lcd/lcd_test.go @@ -2,6 +2,7 @@ package lcd import ( "encoding/hex" + "encoding/json" "fmt" "net/http" "regexp" @@ -265,11 +266,21 @@ func TestCoinSend(t *testing.T) { require.Equal(t, int64(1), mycoins.Amount.Int64()) // test failure with too little gas - res, body, _ = doSendWithGas(t, port, seed, name, password, addr, 100) + res, body, _ = doSendWithGas(t, port, seed, name, password, addr, 100, 0, "") require.Equal(t, http.StatusInternalServerError, res.StatusCode, body) - // test success with just enough gas - res, body, _ = doSendWithGas(t, port, seed, name, password, addr, 3000) + // test failure with wrong adjustment + res, body, _ = doSendWithGas(t, port, seed, name, password, addr, 0, 0.1, "") + require.Equal(t, http.StatusInternalServerError, res.StatusCode, body) + + // run simulation and test success with estimated gas + res, body, _ = doSendWithGas(t, port, seed, name, password, addr, 0, 0, "?simulate=true") + require.Equal(t, http.StatusOK, res.StatusCode, body) + var responseBody struct { + GasEstimate int64 `json:"gas_estimate"` + } + require.Nil(t, json.Unmarshal([]byte(body), &responseBody)) + res, body, _ = doSendWithGas(t, port, seed, name, password, addr, responseBody.GasEstimate, 0, "") require.Equal(t, http.StatusOK, res.StatusCode, body) } @@ -720,7 +731,7 @@ func getAccount(t *testing.T, port string, addr sdk.AccAddress) auth.Account { return acc } -func doSendWithGas(t *testing.T, port, seed, name, password string, addr sdk.AccAddress, gas int64) (res *http.Response, body string, receiveAddr sdk.AccAddress) { +func doSendWithGas(t *testing.T, port, seed, name, password string, addr sdk.AccAddress, gas int64, gasAdjustment float64, queryStr string) (res *http.Response, body string, receiveAddr sdk.AccAddress) { // create receive address kb := client.MockKeyBase() @@ -744,22 +755,28 @@ func doSendWithGas(t *testing.T, port, seed, name, password string, addr sdk.Acc "gas":"%v", `, gas) } + gasAdjustmentStr := "" + if gasAdjustment > 0 { + gasStr = fmt.Sprintf(` + "gas_adjustment":"%v", + `, gasAdjustment) + } jsonStr := []byte(fmt.Sprintf(`{ - %v + %v%v "name":"%s", "password":"%s", "account_number":"%d", "sequence":"%d", "amount":[%s], "chain_id":"%s" - }`, gasStr, name, password, accnum, sequence, coinbz, chainID)) + }`, gasStr, gasAdjustmentStr, name, password, accnum, sequence, coinbz, chainID)) - res, body = Request(t, port, "POST", fmt.Sprintf("/accounts/%s/send", receiveAddr), jsonStr) + res, body = Request(t, port, "POST", fmt.Sprintf("/accounts/%s/send%v", receiveAddr, queryStr), jsonStr) return } 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) + res, body, receiveAddr := doSendWithGas(t, port, seed, name, password, addr, 0, 0, "") require.Equal(t, http.StatusOK, res.StatusCode, body) err := cdc.UnmarshalJSON([]byte(body), &resultTx) diff --git a/client/lcd/root.go b/client/lcd/root.go index bfa62f1cf..f18ee5dfd 100644 --- a/client/lcd/root.go +++ b/client/lcd/root.go @@ -4,11 +4,11 @@ import ( "net/http" "os" - client "github.com/cosmos/cosmos-sdk/client" + "github.com/cosmos/cosmos-sdk/client" "github.com/cosmos/cosmos-sdk/client/context" - keys "github.com/cosmos/cosmos-sdk/client/keys" - rpc "github.com/cosmos/cosmos-sdk/client/rpc" - tx "github.com/cosmos/cosmos-sdk/client/tx" + "github.com/cosmos/cosmos-sdk/client/keys" + "github.com/cosmos/cosmos-sdk/client/rpc" + "github.com/cosmos/cosmos-sdk/client/tx" "github.com/cosmos/cosmos-sdk/wire" auth "github.com/cosmos/cosmos-sdk/x/auth/client/rest" bank "github.com/cosmos/cosmos-sdk/x/bank/client/rest" @@ -66,6 +66,7 @@ func ServeCommand(cdc *wire.Codec) *cobra.Command { cmd.Flags().String(client.FlagChainID, "", "The chain ID to connect to") cmd.Flags().String(client.FlagNode, "tcp://localhost:26657", "Address of the node to connect to") cmd.Flags().Int(flagMaxOpenConnections, 1000, "The number of maximum open connections") + cmd.Flags().Bool(client.FlagTrustNode, false, "Whether trust connected full node") return cmd } diff --git a/client/lcd/test_helpers.go b/client/lcd/test_helpers.go index 818eae1e8..7d9a46403 100644 --- a/client/lcd/test_helpers.go +++ b/client/lcd/test_helpers.go @@ -190,6 +190,7 @@ func InitializeTestLCD(t *testing.T, nValidators int, initAddrs []sdk.AccAddress node, err := startTM(config, logger, genDoc, privVal, app) require.NoError(t, err) + tests.WaitForNextHeightTM(tests.ExtractPortFromAddress(config.RPC.ListenAddress)) lcd, err := startLCD(logger, listenAddr, cdc) require.NoError(t, err) diff --git a/client/utils/rest.go b/client/utils/rest.go index 0e2640312..25e1c23d8 100644 --- a/client/utils/rest.go +++ b/client/utils/rest.go @@ -1,12 +1,45 @@ package utils import ( + "fmt" "net/http" + "strconv" +) + +const ( + queryArgDryRun = "simulate" ) // WriteErrorResponse prepares and writes a HTTP error // given a status code and an error message. -func WriteErrorResponse(w *http.ResponseWriter, status int, msg string) { - (*w).WriteHeader(status) - (*w).Write([]byte(msg)) +func WriteErrorResponse(w http.ResponseWriter, status int, msg string) { + w.WriteHeader(status) + w.Write([]byte(msg)) +} + +// WriteGasEstimateResponse prepares and writes an HTTP +// response for transactions simulations. +func WriteSimulationResponse(w http.ResponseWriter, gas int64) { + w.WriteHeader(http.StatusOK) + w.Write([]byte(fmt.Sprintf(`{"gas_estimate":%v}`, gas))) +} + +// HasDryRunArg returns true if the request's URL query contains +// the dry run argument and its value is set to "true". +func HasDryRunArg(r *http.Request) bool { + return r.URL.Query().Get(queryArgDryRun) == "true" +} + +// ParseFloat64OrReturnBadRequest converts s to a float64 value. It returns a default +// value if the string is empty. Write +func ParseFloat64OrReturnBadRequest(w http.ResponseWriter, s string, defaultIfEmpty float64) (n float64, ok bool) { + if len(s) == 0 { + return defaultIfEmpty, true + } + n, err := strconv.ParseFloat(s, 64) + if err != nil { + WriteErrorResponse(w, http.StatusBadRequest, err.Error()) + return n, false + } + return n, true } diff --git a/client/utils/utils.go b/client/utils/utils.go index fb5d61988..20b1c6bc5 100644 --- a/client/utils/utils.go +++ b/client/utils/utils.go @@ -12,46 +12,26 @@ import ( "github.com/tendermint/tendermint/libs/common" ) -// DefaultGasAdjustment is applied to gas estimates to avoid tx -// execution failures due to state changes that might -// occur between the tx simulation and the actual run. -const DefaultGasAdjustment = 1.2 - // SendTx implements a auxiliary handler that facilitates sending a series of // messages in a signed transaction given a TxContext and a QueryContext. It // ensures that the account exists, has a proper number and sequence set. In // addition, it builds and signs a transaction with the supplied messages. // Finally, it broadcasts the signed transaction to a node. func SendTx(txCtx authctx.TxContext, cliCtx context.CLIContext, msgs []sdk.Msg) error { - if err := cliCtx.EnsureAccountExists(); err != nil { - return err - } - - from, err := cliCtx.GetFromAddress() + txCtx, err := prepareTxContext(txCtx, cliCtx) if err != nil { return err } - - // TODO: (ref #1903) Allow for user supplied account number without - // automatically doing a manual lookup. - if txCtx.AccountNumber == 0 { - accNum, err := cliCtx.GetAccountNumber(from) + autogas := cliCtx.DryRun || (cliCtx.Gas == 0) + if autogas { + txCtx, err = EnrichCtxWithGas(txCtx, cliCtx, cliCtx.FromAddressName, msgs) if err != nil { return err } - - txCtx = txCtx.WithAccountNumber(accNum) + fmt.Fprintf(os.Stdout, "estimated gas = %v\n", txCtx.Gas) } - - // TODO: (ref #1903) Allow for user supplied account sequence without - // automatically doing a manual lookup. - if txCtx.Sequence == 0 { - accSeq, err := cliCtx.GetAccountSequence(from) - if err != nil { - return err - } - - txCtx = txCtx.WithSequence(accSeq) + if cliCtx.DryRun { + return nil } passphrase, err := keys.GetPassphrase(cliCtx.FromAddressName) @@ -59,13 +39,6 @@ func SendTx(txCtx authctx.TxContext, cliCtx context.CLIContext, msgs []sdk.Msg) return err } - if cliCtx.Gas == 0 { - txCtx, err = EnrichCtxWithGas(txCtx, cliCtx, cliCtx.FromAddressName, passphrase, msgs) - if err != nil { - return err - } - } - // build and sign the transaction txBytes, err := txCtx.BuildAndSign(cliCtx.FromAddressName, passphrase, msgs) if err != nil { @@ -75,24 +48,24 @@ func SendTx(txCtx authctx.TxContext, cliCtx context.CLIContext, msgs []sdk.Msg) return cliCtx.EnsureBroadcastTx(txBytes) } -// EnrichCtxWithGas calculates the gas estimate that would be consumed by the -// transaction and set the transaction's respective value accordingly. -func EnrichCtxWithGas(txCtx authctx.TxContext, cliCtx context.CLIContext, name, passphrase string, msgs []sdk.Msg) (authctx.TxContext, error) { - txBytes, err := BuildAndSignTxWithZeroGas(txCtx, name, passphrase, msgs) +// SimulateMsgs simulates the transaction and returns the gas estimate and the adjusted value. +func SimulateMsgs(txCtx authctx.TxContext, cliCtx context.CLIContext, name string, msgs []sdk.Msg, gas int64) (estimated, adjusted int64, err error) { + txBytes, err := txCtx.WithGas(gas).BuildWithPubKey(name, msgs) if err != nil { - return txCtx, err + return } - estimate, adjusted, err := CalculateGas(cliCtx.Query, cliCtx.Codec, txBytes, cliCtx.GasAdjustment) - if err != nil { - return txCtx, err - } - fmt.Fprintf(os.Stderr, "gas: [estimated = %v] [adjusted = %v]\n", estimate, adjusted) - return txCtx.WithGas(adjusted), nil + estimated, adjusted, err = CalculateGas(cliCtx.Query, cliCtx.Codec, txBytes, cliCtx.GasAdjustment) + return } -// BuildAndSignTxWithZeroGas builds transactions with GasWanted set to 0. -func BuildAndSignTxWithZeroGas(txCtx authctx.TxContext, name, passphrase string, msgs []sdk.Msg) ([]byte, error) { - return txCtx.WithGas(0).BuildAndSign(name, passphrase, msgs) +// EnrichCtxWithGas calculates the gas estimate that would be consumed by the +// transaction and set the transaction's respective value accordingly. +func EnrichCtxWithGas(txCtx authctx.TxContext, cliCtx context.CLIContext, name string, msgs []sdk.Msg) (authctx.TxContext, error) { + _, adjusted, err := SimulateMsgs(txCtx, cliCtx, name, msgs, 0) + if err != nil { + return txCtx, err + } + return txCtx.WithGas(adjusted), nil } // CalculateGas simulates the execution of a transaction and returns @@ -109,14 +82,10 @@ func CalculateGas(queryFunc func(string, common.HexBytes) ([]byte, error), cdc * return } adjusted = adjustGasEstimate(estimate, adjustment) - fmt.Fprintf(os.Stderr, "gas: [estimated = %v] [adjusted = %v]\n", estimate, adjusted) return } func adjustGasEstimate(estimate int64, adjustment float64) int64 { - if adjustment == 0 { - return int64(DefaultGasAdjustment * float64(estimate)) - } return int64(adjustment * float64(estimate)) } @@ -127,3 +96,35 @@ func parseQueryResponse(cdc *amino.Codec, rawRes []byte) (int64, error) { } return simulationResult.GasUsed, nil } + +func prepareTxContext(txCtx authctx.TxContext, cliCtx context.CLIContext) (authctx.TxContext, error) { + if err := cliCtx.EnsureAccountExists(); err != nil { + return txCtx, err + } + + from, err := cliCtx.GetFromAddress() + if err != nil { + return txCtx, err + } + + // TODO: (ref #1903) Allow for user supplied account number without + // automatically doing a manual lookup. + if txCtx.AccountNumber == 0 { + accNum, err := cliCtx.GetAccountNumber(from) + if err != nil { + return txCtx, err + } + txCtx = txCtx.WithAccountNumber(accNum) + } + + // TODO: (ref #1903) Allow for user supplied account sequence without + // automatically doing a manual lookup. + if txCtx.Sequence == 0 { + accSeq, err := cliCtx.GetAccountSequence(from) + if err != nil { + return txCtx, err + } + txCtx = txCtx.WithSequence(accSeq) + } + return txCtx, nil +} diff --git a/cmd/gaia/app/app.go b/cmd/gaia/app/app.go index 4ce6b2806..f1ca2a7b6 100644 --- a/cmd/gaia/app/app.go +++ b/cmd/gaia/app/app.go @@ -93,9 +93,10 @@ func NewGaiaApp(logger log.Logger, db dbm.DB, traceStore io.Writer, baseAppOptio app.ibcMapper = ibc.NewMapper(app.cdc, app.keyIBC, app.RegisterCodespace(ibc.DefaultCodespace)) app.paramsKeeper = params.NewKeeper(app.cdc, app.keyParams) app.stakeKeeper = stake.NewKeeper(app.cdc, app.keyStake, app.coinKeeper, app.RegisterCodespace(stake.DefaultCodespace)) + app.slashingKeeper = slashing.NewKeeper(app.cdc, app.keySlashing, app.stakeKeeper, app.paramsKeeper.Getter(), app.RegisterCodespace(slashing.DefaultCodespace)) + app.stakeKeeper = app.stakeKeeper.WithValidatorHooks(app.slashingKeeper.ValidatorHooks()) app.govKeeper = gov.NewKeeper(app.cdc, app.keyGov, app.paramsKeeper.Setter(), app.coinKeeper, app.stakeKeeper, app.RegisterCodespace(gov.DefaultCodespace)) app.feeCollectionKeeper = auth.NewFeeCollectionKeeper(app.cdc, app.keyFeeCollection) - app.slashingKeeper = slashing.NewKeeper(app.cdc, app.keySlashing, app.stakeKeeper, app.paramsKeeper.Getter(), app.RegisterCodespace(slashing.DefaultCodespace)) // register message routes app.Router(). diff --git a/cmd/gaia/app/sim_test.go b/cmd/gaia/app/sim_test.go index 559b8b4b9..3878166b3 100644 --- a/cmd/gaia/app/sim_test.go +++ b/cmd/gaia/app/sim_test.go @@ -3,7 +3,9 @@ package app import ( "encoding/json" "flag" + "fmt" "math/rand" + "os" "testing" "github.com/stretchr/testify/require" @@ -15,6 +17,7 @@ import ( "github.com/cosmos/cosmos-sdk/baseapp" sdk "github.com/cosmos/cosmos-sdk/types" banksim "github.com/cosmos/cosmos-sdk/x/bank/simulation" + "github.com/cosmos/cosmos-sdk/x/gov" govsim "github.com/cosmos/cosmos-sdk/x/gov/simulation" "github.com/cosmos/cosmos-sdk/x/mock/simulation" slashingsim "github.com/cosmos/cosmos-sdk/x/slashing/simulation" @@ -28,6 +31,7 @@ var ( blockSize int enabled bool verbose bool + commit bool ) func init() { @@ -36,6 +40,7 @@ func init() { flag.IntVar(&blockSize, "SimulationBlockSize", 200, "Operations per block") flag.BoolVar(&enabled, "SimulationEnabled", false, "Enable the simulation") flag.BoolVar(&verbose, "SimulationVerbose", false, "Verbose log output") + flag.BoolVar(&commit, "SimulationCommit", false, "Have the simulation commit") } func appStateFn(r *rand.Rand, keys []crypto.PrivKey, accs []sdk.AccAddress) json.RawMessage { @@ -49,7 +54,7 @@ func appStateFn(r *rand.Rand, keys []crypto.PrivKey, accs []sdk.AccAddress) json Coins: coins, }) } - + govGenesis := gov.DefaultGenesisState() // Default genesis state stakeGenesis := stake.DefaultGenesisState() var validators []stake.Validator @@ -73,6 +78,7 @@ func appStateFn(r *rand.Rand, keys []crypto.PrivKey, accs []sdk.AccAddress) json genesis := GenesisState{ Accounts: genesisAccounts, StakeData: stakeGenesis, + GovData: govGenesis, } // Marshal genesis @@ -112,6 +118,39 @@ func invariants(app *GaiaApp) []simulation.Invariant { } } +// Profile with: +// /usr/local/go/bin/go test -benchmem -run=^$ github.com/cosmos/cosmos-sdk/cmd/gaia/app -bench ^BenchmarkFullGaiaSimulation$ -SimulationCommit=true -cpuprofile cpu.out +func BenchmarkFullGaiaSimulation(b *testing.B) { + // Setup Gaia application + var logger log.Logger + logger = log.NewNopLogger() + var db dbm.DB + dir := os.TempDir() + db, _ = dbm.NewGoLevelDB("Simulation", dir) + defer func() { + db.Close() + os.RemoveAll(dir) + }() + app := NewGaiaApp(logger, db, nil) + + // Run randomized simulation + // TODO parameterize numbers, save for a later PR + simulation.SimulateFromSeed( + b, app.BaseApp, appStateFn, seed, + testAndRunTxs(app), + []simulation.RandSetup{}, + invariants(app), // these shouldn't get ran + numBlocks, + blockSize, + commit, + ) + if commit { + fmt.Println("GoLevelDB Stats") + fmt.Println(db.Stats()["leveldb.stats"]) + fmt.Println("GoLevelDB cached block size", db.Stats()["leveldb.cachedblock"]) + } +} + func TestFullGaiaSimulation(t *testing.T) { if !enabled { t.Skip("Skipping Gaia simulation") @@ -136,9 +175,11 @@ func TestFullGaiaSimulation(t *testing.T) { invariants(app), numBlocks, blockSize, - false, + commit, ) - + if commit { + fmt.Println("Database Size", db.Stats()["database.size"]) + } } // TODO: Make another test for the fuzzer itself, which just has noOp txs diff --git a/cmd/gaia/cli_test/cli_test.go b/cmd/gaia/cli_test/cli_test.go index b91b5a45d..696c0fc6a 100644 --- a/cmd/gaia/cli_test/cli_test.go +++ b/cmd/gaia/cli_test/cli_test.go @@ -58,6 +58,13 @@ func TestGaiaCLISend(t *testing.T) { fooAcc = executeGetAccount(t, fmt.Sprintf("gaiacli account %s %v", fooAddr, flags)) require.Equal(t, int64(40), fooAcc.GetCoins().AmountOf("steak").Int64()) + // Test --dry-run + success := executeWrite(t, fmt.Sprintf("gaiacli send %v --amount=10steak --to=%s --from=foo --dry-run", flags, barAddr), app.DefaultKeyPass) + require.True(t, success) + // Check state didn't change + fooAcc = executeGetAccount(t, fmt.Sprintf("gaiacli account %s %v", fooAddr, flags)) + require.Equal(t, int64(40), fooAcc.GetCoins().AmountOf("steak").Int64()) + // test autosequencing executeWrite(t, fmt.Sprintf("gaiacli send %v --amount=10steak --to=%s --from=foo", flags, barAddr), app.DefaultKeyPass) tests.WaitForNextNBlocksTM(2, port) @@ -148,6 +155,10 @@ func TestGaiaCLICreateValidator(t *testing.T) { initialPool.BondedTokens = initialPool.BondedTokens.Add(sdk.NewDec(1)) + // Test --dry-run + success := executeWrite(t, cvStr+" --dry-run", app.DefaultKeyPass) + require.True(t, success) + executeWrite(t, cvStr, app.DefaultKeyPass) tests.WaitForNextNBlocksTM(2, port) @@ -164,7 +175,7 @@ func TestGaiaCLICreateValidator(t *testing.T) { unbondStr += fmt.Sprintf(" --validator=%s", sdk.ValAddress(barAddr)) unbondStr += fmt.Sprintf(" --shares-amount=%v", "1") - success := executeWrite(t, unbondStr, app.DefaultKeyPass) + success = executeWrite(t, unbondStr, app.DefaultKeyPass) require.True(t, success) tests.WaitForNextNBlocksTM(2, port) @@ -211,6 +222,10 @@ func TestGaiaCLISubmitProposal(t *testing.T) { spStr += fmt.Sprintf(" --title=%s", "Test") spStr += fmt.Sprintf(" --description=%s", "test") + // Test --dry-run + success := executeWrite(t, spStr+" --dry-run", app.DefaultKeyPass) + require.True(t, success) + executeWrite(t, spStr, app.DefaultKeyPass) tests.WaitForNextNBlocksTM(2, port) diff --git a/docs/sdk/clients.md b/docs/sdk/clients.md index 7a23ba28a..62d79bbac 100644 --- a/docs/sdk/clients.md +++ b/docs/sdk/clients.md @@ -95,6 +95,8 @@ When you query an account balance with zero tokens, you will get this error: `No ### Send Tokens +The following command could be used to send coins from one account to another: + ```bash gaiacli send \ --amount=10faucetToken \ @@ -110,7 +112,7 @@ The `--amount` flag accepts the format `--amount=`. ::: tip Note You may want to cap the maximum gas that can be consumed by the transaction via the `--gas` flag. If set to 0, the gas limit will be automatically estimated. -Gas estimate might be inaccurate as state changes could occur in between the end of the simulation and the actual execution of a transaction, thus an adjustment is applied on top of the original estimate in order to ensure the transaction is broadcasted successfully. The adjustment can be controlled via the `--gas-adjustment` flag, whose default value is 1.2. +Gas estimate might be inaccurate as state changes could occur in between the end of the simulation and the actual execution of a transaction, thus an adjustment is applied on top of the original estimate in order to ensure the transaction is broadcasted successfully. The adjustment can be controlled via the `--gas-adjustment` flag, whose default value is 1.0. ::: Now, view the updated balances of the origin and destination accounts: @@ -126,6 +128,17 @@ You can also check your balance at a given block by using the `--block` flag: gaiacli account --block= ``` +You can simulate a transaction without actually broadcasting it by appending the `--dry-run` flag to the command line: + +```bash +gaiacli send \ + --amount=10faucetToken \ + --chain-id= \ + --name= \ + --to= \ + --dry-run +``` + ### Staking #### Set up a Validator diff --git a/examples/democoin/mock/validator.go b/examples/democoin/mock/validator.go index f937e45dc..9f84786ad 100644 --- a/examples/democoin/mock/validator.go +++ b/examples/democoin/mock/validator.go @@ -135,3 +135,8 @@ func (vs *ValidatorSet) Jail(ctx sdk.Context, pubkey crypto.PubKey) { func (vs *ValidatorSet) Unjail(ctx sdk.Context, pubkey crypto.PubKey) { panic("not implemented") } + +// Implements sdk.ValidatorSet +func (vs *ValidatorSet) Delegation(ctx sdk.Context, addrDel sdk.AccAddress, addrVal sdk.ValAddress) sdk.Delegation { + panic("not implemented") +} diff --git a/examples/democoin/x/cool/app_test.go b/examples/democoin/x/cool/app_test.go index 71b4202bc..c0453a58e 100644 --- a/examples/democoin/x/cool/app_test.go +++ b/examples/democoin/x/cool/app_test.go @@ -88,17 +88,17 @@ func TestMsgQuiz(t *testing.T) { require.Equal(t, acc1, res1) // Set the trend, submit a really cool quiz and check for reward - mock.SignCheckDeliver(t, mapp.BaseApp, []sdk.Msg{setTrendMsg1}, []int64{0}, []int64{0}, true, priv1) - mock.SignCheckDeliver(t, mapp.BaseApp, []sdk.Msg{quizMsg1}, []int64{0}, []int64{1}, true, priv1) + mock.SignCheckDeliver(t, mapp.BaseApp, []sdk.Msg{setTrendMsg1}, []int64{0}, []int64{0}, true, true, priv1) + mock.SignCheckDeliver(t, mapp.BaseApp, []sdk.Msg{quizMsg1}, []int64{0}, []int64{1}, true, true, priv1) mock.CheckBalance(t, mapp, addr1, sdk.Coins{{"icecold", sdk.NewInt(69)}}) - mock.SignCheckDeliver(t, mapp.BaseApp, []sdk.Msg{quizMsg2}, []int64{0}, []int64{2}, false, priv1) // result without reward + mock.SignCheckDeliver(t, mapp.BaseApp, []sdk.Msg{quizMsg2}, []int64{0}, []int64{2}, false, false, priv1) // result without reward mock.CheckBalance(t, mapp, addr1, sdk.Coins{{"icecold", sdk.NewInt(69)}}) - mock.SignCheckDeliver(t, mapp.BaseApp, []sdk.Msg{quizMsg1}, []int64{0}, []int64{3}, true, priv1) + mock.SignCheckDeliver(t, mapp.BaseApp, []sdk.Msg{quizMsg1}, []int64{0}, []int64{3}, true, true, priv1) mock.CheckBalance(t, mapp, addr1, sdk.Coins{{"icecold", sdk.NewInt(138)}}) - mock.SignCheckDeliver(t, mapp.BaseApp, []sdk.Msg{setTrendMsg2}, []int64{0}, []int64{4}, true, priv1) // reset the trend - mock.SignCheckDeliver(t, mapp.BaseApp, []sdk.Msg{quizMsg1}, []int64{0}, []int64{5}, false, priv1) // the same answer will nolonger do! + mock.SignCheckDeliver(t, mapp.BaseApp, []sdk.Msg{setTrendMsg2}, []int64{0}, []int64{4}, true, true, priv1) // reset the trend + mock.SignCheckDeliver(t, mapp.BaseApp, []sdk.Msg{quizMsg1}, []int64{0}, []int64{5}, false, false, priv1) // the same answer will nolonger do! mock.CheckBalance(t, mapp, addr1, sdk.Coins{{"icecold", sdk.NewInt(138)}}) - mock.SignCheckDeliver(t, mapp.BaseApp, []sdk.Msg{quizMsg2}, []int64{0}, []int64{6}, true, priv1) // earlier answer now relevant again + mock.SignCheckDeliver(t, mapp.BaseApp, []sdk.Msg{quizMsg2}, []int64{0}, []int64{6}, true, true, priv1) // earlier answer now relevant again mock.CheckBalance(t, mapp, addr1, sdk.Coins{{"badvibesonly", sdk.NewInt(69)}, {"icecold", sdk.NewInt(138)}}) - mock.SignCheckDeliver(t, mapp.BaseApp, []sdk.Msg{setTrendMsg3}, []int64{0}, []int64{7}, false, priv1) // expect to fail to set the trend to something which is not cool + mock.SignCheckDeliver(t, mapp.BaseApp, []sdk.Msg{setTrendMsg3}, []int64{0}, []int64{7}, false, false, priv1) // expect to fail to set the trend to something which is not cool } diff --git a/examples/democoin/x/pow/app_test.go b/examples/democoin/x/pow/app_test.go index dc53d1d99..76f062e3b 100644 --- a/examples/democoin/x/pow/app_test.go +++ b/examples/democoin/x/pow/app_test.go @@ -74,13 +74,13 @@ func TestMsgMine(t *testing.T) { // Mine and check for reward mineMsg1 := GenerateMsgMine(addr1, 1, 2) - mock.SignCheckDeliver(t, mapp.BaseApp, []sdk.Msg{mineMsg1}, []int64{0}, []int64{0}, true, priv1) + mock.SignCheckDeliver(t, mapp.BaseApp, []sdk.Msg{mineMsg1}, []int64{0}, []int64{0}, true, true, priv1) mock.CheckBalance(t, mapp, addr1, sdk.Coins{{"pow", sdk.NewInt(1)}}) // Mine again and check for reward mineMsg2 := GenerateMsgMine(addr1, 2, 3) - mock.SignCheckDeliver(t, mapp.BaseApp, []sdk.Msg{mineMsg2}, []int64{0}, []int64{1}, true, priv1) + mock.SignCheckDeliver(t, mapp.BaseApp, []sdk.Msg{mineMsg2}, []int64{0}, []int64{1}, true, true, priv1) mock.CheckBalance(t, mapp, addr1, sdk.Coins{{"pow", sdk.NewInt(2)}}) // Mine again - should be invalid - mock.SignCheckDeliver(t, mapp.BaseApp, []sdk.Msg{mineMsg2}, []int64{0}, []int64{1}, false, priv1) + mock.SignCheckDeliver(t, mapp.BaseApp, []sdk.Msg{mineMsg2}, []int64{0}, []int64{1}, false, false, priv1) mock.CheckBalance(t, mapp, addr1, sdk.Coins{{"pow", sdk.NewInt(2)}}) } diff --git a/server/export_test.go b/server/export_test.go index 358f72cf6..488c55bbf 100644 --- a/server/export_test.go +++ b/server/export_test.go @@ -1,16 +1,16 @@ package server import ( - "testing" - "github.com/stretchr/testify/require" - "github.com/cosmos/cosmos-sdk/wire" - "github.com/tendermint/tendermint/libs/log" - tcmd "github.com/tendermint/tendermint/cmd/tendermint/commands" - "os" "bytes" + "github.com/cosmos/cosmos-sdk/server/mock" + "github.com/cosmos/cosmos-sdk/wire" + "github.com/stretchr/testify/require" + tcmd "github.com/tendermint/tendermint/cmd/tendermint/commands" + "github.com/tendermint/tendermint/libs/log" "io" - "github.com/cosmos/cosmos-sdk/server/mock" - ) + "os" + "testing" +) func TestEmptyState(t *testing.T) { defer setupViper(t)() diff --git a/server/mock/app.go b/server/mock/app.go index eb2dfc3cc..3c6ad3ec2 100644 --- a/server/mock/app.go +++ b/server/mock/app.go @@ -129,7 +129,7 @@ func AppGenStateEmpty(_ *wire.Codec, _ []json.RawMessage) (appState json.RawMess // Return a validator, not much else func AppGenTx(_ *wire.Codec, pk crypto.PubKey, genTxConfig gc.GenTx) ( - appGenTx, cliPrint json.RawMessage, validator tmtypes.GenesisValidator, err error) { + appGenTx, cliPrint json.RawMessage, validator tmtypes.GenesisValidator, err error) { validator = tmtypes.GenesisValidator{ PubKey: pk, diff --git a/server/tm_cmds.go b/server/tm_cmds.go index b6daf0775..bf208a5be 100644 --- a/server/tm_cmds.go +++ b/server/tm_cmds.go @@ -7,11 +7,11 @@ import ( "github.com/spf13/cobra" "github.com/spf13/viper" + "github.com/cosmos/cosmos-sdk/client" sdk "github.com/cosmos/cosmos-sdk/types" tcmd "github.com/tendermint/tendermint/cmd/tendermint/commands" "github.com/tendermint/tendermint/p2p" pvm "github.com/tendermint/tendermint/privval" - "github.com/cosmos/cosmos-sdk/client" ) // ShowNodeIDCmd - ported from Tendermint, dump node ID to stdout diff --git a/store/multistoreproof.go b/store/multistoreproof.go new file mode 100644 index 000000000..e25f1cc1f --- /dev/null +++ b/store/multistoreproof.go @@ -0,0 +1,91 @@ +package store + +import ( + "bytes" + "github.com/pkg/errors" + "github.com/tendermint/iavl" + cmn "github.com/tendermint/tendermint/libs/common" +) + +// MultiStoreProof defines a collection of store proofs in a multi-store +type MultiStoreProof struct { + StoreInfos []storeInfo + StoreName string + RangeProof iavl.RangeProof +} + +// buildMultiStoreProof build MultiStoreProof based on iavl proof and storeInfos +func buildMultiStoreProof(iavlProof []byte, storeName string, storeInfos []storeInfo) []byte { + var rangeProof iavl.RangeProof + cdc.MustUnmarshalBinary(iavlProof, &rangeProof) + + msp := MultiStoreProof{ + StoreInfos: storeInfos, + StoreName: storeName, + RangeProof: rangeProof, + } + + proof := cdc.MustMarshalBinary(msp) + return proof +} + +// VerifyMultiStoreCommitInfo verify multiStoreCommitInfo against appHash +func VerifyMultiStoreCommitInfo(storeName string, storeInfos []storeInfo, appHash []byte) ([]byte, error) { + var substoreCommitHash []byte + var height int64 + for _, storeInfo := range storeInfos { + if storeInfo.Name == storeName { + substoreCommitHash = storeInfo.Core.CommitID.Hash + height = storeInfo.Core.CommitID.Version + } + } + if len(substoreCommitHash) == 0 { + return nil, cmn.NewError("failed to get substore root commit hash by store name") + } + + ci := commitInfo{ + Version: height, + StoreInfos: storeInfos, + } + + if !bytes.Equal(appHash, ci.Hash()) { + return nil, cmn.NewError("the merkle root of multiStoreCommitInfo doesn't equal to appHash") + } + return substoreCommitHash, nil +} + +// VerifyRangeProof verify iavl RangeProof +func VerifyRangeProof(key, value []byte, substoreCommitHash []byte, rangeProof *iavl.RangeProof) error { + + // verify the proof to ensure data integrity. + err := rangeProof.Verify(substoreCommitHash) + if err != nil { + return errors.Wrap(err, "proof root hash doesn't equal to substore commit root hash") + } + + if len(value) != 0 { + // verify existence proof + err = rangeProof.VerifyItem(key, value) + if err != nil { + return errors.Wrap(err, "failed in existence verification") + } + } else { + // verify absence proof + err = rangeProof.VerifyAbsence(key) + if err != nil { + return errors.Wrap(err, "failed in absence verification") + } + } + + return nil +} + +// RequireProof return whether proof is require for the subpath +func RequireProof(subpath string) bool { + // Currently, only when query subpath is "/store" or "/key", will proof be included in response. + // If there are some changes about proof building in iavlstore.go, we must change code here to keep consistency with iavlstore.go:212 + if subpath == "/store" || subpath == "/key" { + return true + } + return false +} diff --git a/store/multistoreproof_test.go b/store/multistoreproof_test.go new file mode 100644 index 000000000..b4e8a84b1 --- /dev/null +++ b/store/multistoreproof_test.go @@ -0,0 +1,120 @@ +package store + +import ( + "encoding/hex" + "github.com/stretchr/testify/assert" + "github.com/tendermint/iavl" + cmn "github.com/tendermint/tendermint/libs/common" + "testing" +) + +func TestVerifyMultiStoreCommitInfo(t *testing.T) { + appHash, _ := hex.DecodeString("ebf3c1fb724d3458023c8fefef7b33add2fc1e84") + + substoreRootHash, _ := hex.DecodeString("ea5d468431015c2cd6295e9a0bb1fc0e49033828") + storeName := "acc" + + var storeInfos []storeInfo + + gocRootHash, _ := hex.DecodeString("62c171bb022e47d1f745608ff749e676dbd25f78") + storeInfos = append(storeInfos, storeInfo{ + Name: "gov", + Core: storeCore{ + CommitID: CommitID{ + Version: 689, + Hash: gocRootHash, + }, + }, + }) + + storeInfos = append(storeInfos, storeInfo{ + Name: "main", + Core: storeCore{ + CommitID: CommitID{ + Version: 689, + Hash: nil, + }, + }, + }) + + accRootHash, _ := hex.DecodeString("ea5d468431015c2cd6295e9a0bb1fc0e49033828") + storeInfos = append(storeInfos, storeInfo{ + Name: "acc", + Core: storeCore{ + CommitID: CommitID{ + Version: 689, + Hash: accRootHash, + }, + }, + }) + + storeInfos = append(storeInfos, storeInfo{ + Name: "ibc", + Core: storeCore{ + CommitID: CommitID{ + Version: 689, + Hash: nil, + }, + }, + }) + + stakeRootHash, _ := hex.DecodeString("987d1d27b8771d93aa3691262f661d2c85af7ca4") + storeInfos = append(storeInfos, storeInfo{ + Name: "stake", + Core: storeCore{ + CommitID: CommitID{ + Version: 689, + Hash: stakeRootHash, + }, + }, + }) + + slashingRootHash, _ := hex.DecodeString("388ee6e5b11f367069beb1eefd553491afe9d73e") + storeInfos = append(storeInfos, storeInfo{ + Name: "slashing", + Core: storeCore{ + CommitID: CommitID{ + Version: 689, + Hash: slashingRootHash, + }, + }, + }) + + commitHash, err := VerifyMultiStoreCommitInfo(storeName, storeInfos, appHash) + assert.Nil(t, err) + assert.Equal(t, commitHash, substoreRootHash) + + appHash, _ = hex.DecodeString("29de216bf5e2531c688de36caaf024cd3bb09ee3") + + _, err = VerifyMultiStoreCommitInfo(storeName, storeInfos, appHash) + assert.Error(t, err, "appHash doesn't match to the merkle root of multiStoreCommitInfo") +} + +func TestVerifyRangeProof(t *testing.T) { + tree := iavl.NewTree(nil, 0) + + rand := cmn.NewRand() + rand.Seed(0) // for determinism + for _, ikey := range []byte{0x11, 0x32, 0x50, 0x72, 0x99} { + key := []byte{ikey} + tree.Set(key, []byte(rand.Str(8))) + } + + root := tree.Hash() + + key := []byte{0x32} + val, proof, err := tree.GetWithProof(key) + assert.Nil(t, err) + assert.NotEmpty(t, val) + assert.NotEmpty(t, proof) + err = VerifyRangeProof(key, val, root, proof) + assert.Nil(t, err) + + key = []byte{0x40} + val, proof, err = tree.GetWithProof(key) + assert.Nil(t, err) + assert.Empty(t, val) + assert.NotEmpty(t, proof) + err = VerifyRangeProof(key, val, root, proof) + assert.Nil(t, err) +} diff --git a/store/rootmultistore.go b/store/rootmultistore.go index 04f8e44e6..d9cf8a29a 100644 --- a/store/rootmultistore.go +++ b/store/rootmultistore.go @@ -291,6 +291,18 @@ func (rs *rootMultiStore) Query(req abci.RequestQuery) abci.ResponseQuery { // trim the path and make the query req.Path = subpath res := queryable.Query(req) + + if !req.Prove || !RequireProof(subpath) { + return res + } + + commitInfo, errMsg := getCommitInfo(rs.db, res.Height) + if errMsg != nil { + return sdk.ErrInternal(errMsg.Error()).QueryResult() + } + + res.Proof = buildMultiStoreProof(res.Proof, storeName, commitInfo.StoreInfos) + return res } diff --git a/tests/util.go b/tests/util.go index 1138bc95e..48649eaf7 100644 --- a/tests/util.go +++ b/tests/util.go @@ -10,6 +10,7 @@ import ( tmclient "github.com/tendermint/tendermint/rpc/client" ctypes "github.com/tendermint/tendermint/rpc/core/types" rpcclient "github.com/tendermint/tendermint/rpc/lib/client" + "strings" ) // Wait for the next tendermint block from the Tendermint RPC @@ -185,6 +186,17 @@ func WaitForRPC(laddr string) { } } +// ExtractPortFromAddress extract port from listenAddress +// The listenAddress must be some strings like tcp://0.0.0.0:12345 +func ExtractPortFromAddress(listenAddress string) string { + stringList := strings.Split(listenAddress, ":") + length := len(stringList) + if length != 3 { + panic(fmt.Errorf("expected listen address: tcp://0.0.0.0:12345, got %s", listenAddress)) + } + return stringList[2] +} + var cdc = amino.NewCodec() func init() { diff --git a/types/account.go b/types/account.go index 1df600c36..429cbf285 100644 --- a/types/account.go +++ b/types/account.go @@ -105,7 +105,7 @@ func (aa *AccAddress) UnmarshalJSON(data []byte) error { var s string err := json.Unmarshal(data, &s) if err != nil { - return nil + return err } aa2, err := AccAddressFromBech32(s) diff --git a/types/decimal.go b/types/decimal.go index baf2d9573..8e7db1340 100644 --- a/types/decimal.go +++ b/types/decimal.go @@ -415,6 +415,14 @@ func MinDec(d1, d2 Dec) Dec { return d2 } +// maximum decimal between two +func MaxDec(d1, d2 Dec) Dec { + if d1.LT(d2) { + return d2 + } + return d1 +} + // intended to be used with require/assert: require.True(DecEq(...)) func DecEq(t *testing.T, exp, got Dec) (*testing.T, bool, string, Dec, Dec) { return t, exp.Equal(got), "expected:\t%v\ngot:\t\t%v", exp, got diff --git a/types/stake.go b/types/stake.go index f41125177..ba11cd5f8 100644 --- a/types/stake.go +++ b/types/stake.go @@ -75,6 +75,10 @@ type ValidatorSet interface { Slash(Context, crypto.PubKey, int64, int64, Dec) Jail(Context, crypto.PubKey) // jail a validator Unjail(Context, crypto.PubKey) // unjail a validator + + // Delegation allows for getting a particular delegation for a given validator + // and delegator outside the scope of the staking module. + Delegation(Context, AccAddress, ValAddress) Delegation } //_______________________________________________________________________________ @@ -95,3 +99,13 @@ type DelegationSet interface { IterateDelegations(ctx Context, delegator AccAddress, fn func(index int64, delegation Delegation) (stop bool)) } + +// validator event hooks +// These can be utilized to communicate between a staking keeper +// and another keeper which must take particular actions when +// validators are bonded and unbonded. The second keeper must implement +// this interface, which then the staking keeper can call. +type ValidatorHooks interface { + OnValidatorBonded(ctx Context, address ConsAddress) // Must be called when a validator is bonded + OnValidatorBeginUnbonding(ctx Context, address ConsAddress) // Must be called when a validator begins unbonding +} diff --git a/x/auth/ante.go b/x/auth/ante.go index c0a129be3..65071e69a 100644 --- a/x/auth/ante.go +++ b/x/auth/ante.go @@ -65,7 +65,7 @@ func NewAnteHandler(am AccountMapper, fck FeeCollectionKeeper) sdk.AnteHandler { return newCtx, err.Result(), true } - sigs := stdTx.GetSignatures() + sigs := stdTx.GetSignatures() // When simulating, this would just be a 0-length slice. signerAddrs := stdTx.GetSigners() msgs := tx.GetMsgs() @@ -88,10 +88,7 @@ func NewAnteHandler(am AccountMapper, fck FeeCollectionKeeper) sdk.AnteHandler { // check signature, return account with incremented nonce signBytes := StdSignBytes(newCtx.ChainID(), accNums[i], sequences[i], fee, msgs, stdTx.GetMemo()) - signerAcc, res := processSig( - newCtx, am, - signerAddr, sig, signBytes, - ) + signerAcc, res := processSig(newCtx, am, signerAddr, sig, signBytes, simulate) if !res.IsOK() { return newCtx, res, true } @@ -149,24 +146,24 @@ func validateBasic(tx StdTx) (err sdk.Error) { // if the account doesn't have a pubkey, set it. func processSig( ctx sdk.Context, am AccountMapper, - addr sdk.AccAddress, sig StdSignature, signBytes []byte) ( + addr sdk.AccAddress, sig StdSignature, signBytes []byte, simulate bool) ( acc Account, res sdk.Result) { - // Get the account. acc = am.GetAccount(ctx, addr) if acc == nil { return nil, sdk.ErrUnknownAddress(addr.String()).Result() } - // Check account number. accnum := acc.GetAccountNumber() + seq := acc.GetSequence() + + // Check account number. if accnum != sig.AccountNumber { return nil, sdk.ErrInvalidSequence( fmt.Sprintf("Invalid account number. Got %d, expected %d", sig.AccountNumber, accnum)).Result() } - // Check and increment sequence number. - seq := acc.GetSequence() + // Check sequence number. if seq != sig.Sequence { return nil, sdk.ErrInvalidSequence( fmt.Sprintf("Invalid sequence. Got %d, expected %d", sig.Sequence, seq)).Result() @@ -176,31 +173,48 @@ func processSig( // Handle w/ #870 panic(err) } + pubKey, res := processPubKey(acc, sig, simulate) + if !res.IsOK() { + return nil, res + } + err = acc.SetPubKey(pubKey) + if err != nil { + return nil, sdk.ErrInternal("setting PubKey on signer's account").Result() + } + + consumeSignatureVerificationGas(ctx.GasMeter(), pubKey) + if !simulate && !pubKey.VerifyBytes(signBytes, sig.Signature) { + return nil, sdk.ErrUnauthorized("signature verification failed").Result() + } + + return +} + +func processPubKey(acc Account, sig StdSignature, simulate bool) (crypto.PubKey, sdk.Result) { // If pubkey is not known for account, // set it from the StdSignature. pubKey := acc.GetPubKey() + if simulate { + // In simulate mode the transaction comes with no signatures, thus + // if the account's pubkey is nil, both signature verification + // and gasKVStore.Set() shall consume the largest amount, i.e. + // it takes more gas to verifiy secp256k1 keys than ed25519 ones. + if pubKey == nil { + return secp256k1.GenPrivKey().PubKey(), sdk.Result{} + } + return pubKey, sdk.Result{} + } if pubKey == nil { pubKey = sig.PubKey if pubKey == nil { return nil, sdk.ErrInvalidPubKey("PubKey not found").Result() } - if !bytes.Equal(pubKey.Address(), addr) { + if !bytes.Equal(pubKey.Address(), acc.GetAddress()) { return nil, sdk.ErrInvalidPubKey( - fmt.Sprintf("PubKey does not match Signer address %v", addr)).Result() - } - err = acc.SetPubKey(pubKey) - if err != nil { - return nil, sdk.ErrInternal("setting PubKey on signer's account").Result() + fmt.Sprintf("PubKey does not match Signer address %v", acc.GetAddress())).Result() } } - - // Check sig. - consumeSignatureVerificationGas(ctx.GasMeter(), pubKey) - if !pubKey.VerifyBytes(signBytes, sig.Signature) { - return nil, sdk.ErrUnauthorized("signature verification failed").Result() - } - - return + return pubKey, sdk.Result{} } func consumeSignatureVerificationGas(meter sdk.GasMeter, pubkey crypto.PubKey) { diff --git a/x/auth/ante_test.go b/x/auth/ante_test.go index a841bc776..e13784512 100644 --- a/x/auth/ante_test.go +++ b/x/auth/ante_test.go @@ -4,14 +4,14 @@ import ( "fmt" "testing" + sdk "github.com/cosmos/cosmos-sdk/types" + wire "github.com/cosmos/cosmos-sdk/wire" "github.com/stretchr/testify/require" abci "github.com/tendermint/tendermint/abci/types" "github.com/tendermint/tendermint/crypto" "github.com/tendermint/tendermint/crypto/ed25519" + "github.com/tendermint/tendermint/crypto/secp256k1" "github.com/tendermint/tendermint/libs/log" - - sdk "github.com/cosmos/cosmos-sdk/types" - wire "github.com/cosmos/cosmos-sdk/wire" ) func newTestMsg(addrs ...sdk.AccAddress) *sdk.TestMsg { @@ -567,3 +567,63 @@ func TestAnteHandlerSetPubKey(t *testing.T) { acc2 = mapper.GetAccount(ctx, addr2) require.Nil(t, acc2.GetPubKey()) } + +func TestProcessPubKey(t *testing.T) { + ms, capKey, _ := setupMultiStore() + cdc := wire.NewCodec() + RegisterBaseAccount(cdc) + mapper := NewAccountMapper(cdc, capKey, ProtoBaseAccount) + ctx := sdk.NewContext(ms, abci.Header{ChainID: "mychainid"}, false, log.NewNopLogger()) + // keys + _, addr1 := privAndAddr() + priv2, _ := privAndAddr() + acc1 := mapper.NewAccountWithAddress(ctx, addr1) + type args struct { + acc Account + sig StdSignature + simulate bool + } + tests := []struct { + name string + args args + wantErr bool + }{ + {"no sigs, simulate off", args{acc1, StdSignature{}, false}, true}, + {"no sigs, simulate on", args{acc1, StdSignature{}, true}, false}, + {"pubkey doesn't match addr, simulate off", args{acc1, StdSignature{PubKey: priv2.PubKey()}, false}, true}, + {"pubkey doesn't match addr, simulate on", args{acc1, StdSignature{PubKey: priv2.PubKey()}, true}, false}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + _, err := processPubKey(tt.args.acc, tt.args.sig, tt.args.simulate) + require.Equal(t, tt.wantErr, !err.IsOK()) + }) + } +} + +func TestConsumeSignatureVerificationGas(t *testing.T) { + type args struct { + meter sdk.GasMeter + pubkey crypto.PubKey + } + tests := []struct { + name string + args args + gasConsumed int64 + wantPanic bool + }{ + {"PubKeyEd25519", args{sdk.NewInfiniteGasMeter(), ed25519.GenPrivKey().PubKey()}, ed25519VerifyCost, false}, + {"PubKeySecp256k1", args{sdk.NewInfiniteGasMeter(), secp256k1.GenPrivKey().PubKey()}, secp256k1VerifyCost, false}, + {"unknown key", args{sdk.NewInfiniteGasMeter(), nil}, 0, true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.wantPanic { + require.Panics(t, func() { consumeSignatureVerificationGas(tt.args.meter, tt.args.pubkey) }) + } else { + consumeSignatureVerificationGas(tt.args.meter, tt.args.pubkey) + require.Equal(t, tt.args.meter.GasConsumed(), tt.gasConsumed) + } + }) + } +} diff --git a/x/auth/client/context/context.go b/x/auth/client/context/context.go index 8d0a94136..5e55696b8 100644 --- a/x/auth/client/context/context.go +++ b/x/auth/client/context/context.go @@ -148,3 +148,32 @@ func (ctx TxContext) BuildAndSign(name, passphrase string, msgs []sdk.Msg) ([]by return ctx.Sign(name, passphrase, msg) } + +// BuildWithPubKey builds a single message to be signed from a TxContext given a set of +// messages and attach the public key associated to the given name. +// It returns an error if a fee is supplied but cannot be parsed or the key cannot be +// retrieved. +func (ctx TxContext) BuildWithPubKey(name string, msgs []sdk.Msg) ([]byte, error) { + msg, err := ctx.Build(msgs) + if err != nil { + return nil, err + } + + keybase, err := keys.GetKeyBase() + if err != nil { + return nil, err + } + + info, err := keybase.Get(name) + if err != nil { + return nil, err + } + + sigs := []auth.StdSignature{{ + AccountNumber: msg.AccountNumber, + Sequence: msg.Sequence, + PubKey: info.GetPubKey(), + }} + + return ctx.Codec.MarshalBinary(auth.NewStdTx(msg.Msgs, msg.Fee, sigs, msg.Memo)) +} diff --git a/x/auth/client/rest/query.go b/x/auth/client/rest/query.go index 07b109d40..6ad50a14d 100644 --- a/x/auth/client/rest/query.go +++ b/x/auth/client/rest/query.go @@ -33,13 +33,13 @@ func QueryAccountRequestHandlerFn( addr, err := sdk.AccAddressFromBech32(bech32addr) if err != nil { - utils.WriteErrorResponse(&w, http.StatusInternalServerError, err.Error()) + utils.WriteErrorResponse(w, http.StatusInternalServerError, err.Error()) return } res, err := cliCtx.QueryStore(auth.AddressStoreKey(addr), storeName) if err != nil { - utils.WriteErrorResponse(&w, http.StatusInternalServerError, fmt.Sprintf("couldn't query account. Error: %s", err.Error())) + utils.WriteErrorResponse(w, http.StatusInternalServerError, fmt.Sprintf("couldn't query account. Error: %s", err.Error())) return } @@ -52,14 +52,14 @@ func QueryAccountRequestHandlerFn( // decode the value account, err := decoder(res) if err != nil { - utils.WriteErrorResponse(&w, http.StatusInternalServerError, fmt.Sprintf("couldn't parse query result. Result: %s. Error: %s", res, err.Error())) + utils.WriteErrorResponse(w, http.StatusInternalServerError, fmt.Sprintf("couldn't parse query result. Result: %s. Error: %s", res, err.Error())) return } // print out whole account output, err := cdc.MarshalJSON(account) if err != nil { - utils.WriteErrorResponse(&w, http.StatusInternalServerError, fmt.Sprintf("couldn't marshall query result. Error: %s", err.Error())) + utils.WriteErrorResponse(w, http.StatusInternalServerError, fmt.Sprintf("couldn't marshall query result. Error: %s", err.Error())) return } diff --git a/x/bank/app_test.go b/x/bank/app_test.go index c8d0a417d..77991f94b 100644 --- a/x/bank/app_test.go +++ b/x/bank/app_test.go @@ -21,6 +21,7 @@ type ( } appTestCase struct { + expSimPass bool expPass bool msgs []sdk.Msg accNums []int64 @@ -107,27 +108,29 @@ func TestMsgSendWithAccounts(t *testing.T) { testCases := []appTestCase{ { - msgs: []sdk.Msg{sendMsg1}, - accNums: []int64{0}, - accSeqs: []int64{0}, - expPass: true, - privKeys: []crypto.PrivKey{priv1}, + msgs: []sdk.Msg{sendMsg1}, + accNums: []int64{0}, + accSeqs: []int64{0}, + expSimPass: true, + expPass: true, + privKeys: []crypto.PrivKey{priv1}, expectedBalances: []expectedBalance{ {addr1, sdk.Coins{sdk.NewInt64Coin("foocoin", 57)}}, {addr2, sdk.Coins{sdk.NewInt64Coin("foocoin", 10)}}, }, }, { - msgs: []sdk.Msg{sendMsg1, sendMsg2}, - accNums: []int64{0}, - accSeqs: []int64{0}, - expPass: false, - privKeys: []crypto.PrivKey{priv1}, + msgs: []sdk.Msg{sendMsg1, sendMsg2}, + accNums: []int64{0}, + accSeqs: []int64{0}, + expSimPass: false, + expPass: false, + privKeys: []crypto.PrivKey{priv1}, }, } for _, tc := range testCases { - mock.SignCheckDeliver(t, mapp.BaseApp, tc.msgs, tc.accNums, tc.accSeqs, tc.expPass, tc.privKeys...) + mock.SignCheckDeliver(t, mapp.BaseApp, tc.msgs, tc.accNums, tc.accSeqs, tc.expSimPass, tc.expPass, tc.privKeys...) for _, eb := range tc.expectedBalances { mock.CheckBalance(t, mapp, eb.addr, eb.coins) @@ -144,7 +147,7 @@ func TestMsgSendWithAccounts(t *testing.T) { require.Equal(t, sdk.ToABCICode(sdk.CodespaceRoot, sdk.CodeUnauthorized), res.Code, res.Log) // resigning the tx with the bumped sequence should work - mock.SignCheckDeliver(t, mapp.BaseApp, []sdk.Msg{sendMsg1, sendMsg2}, []int64{0}, []int64{1}, true, priv1) + mock.SignCheckDeliver(t, mapp.BaseApp, []sdk.Msg{sendMsg1, sendMsg2}, []int64{0}, []int64{1}, true, true, priv1) } func TestMsgSendMultipleOut(t *testing.T) { @@ -163,11 +166,12 @@ func TestMsgSendMultipleOut(t *testing.T) { testCases := []appTestCase{ { - msgs: []sdk.Msg{sendMsg2}, - accNums: []int64{0}, - accSeqs: []int64{0}, - expPass: true, - privKeys: []crypto.PrivKey{priv1}, + msgs: []sdk.Msg{sendMsg2}, + accNums: []int64{0}, + accSeqs: []int64{0}, + expSimPass: true, + expPass: true, + privKeys: []crypto.PrivKey{priv1}, expectedBalances: []expectedBalance{ {addr1, sdk.Coins{sdk.NewInt64Coin("foocoin", 32)}}, {addr2, sdk.Coins{sdk.NewInt64Coin("foocoin", 47)}}, @@ -177,7 +181,7 @@ func TestMsgSendMultipleOut(t *testing.T) { } for _, tc := range testCases { - mock.SignCheckDeliver(t, mapp.BaseApp, tc.msgs, tc.accNums, tc.accSeqs, tc.expPass, tc.privKeys...) + mock.SignCheckDeliver(t, mapp.BaseApp, tc.msgs, tc.accNums, tc.accSeqs, tc.expSimPass, tc.expPass, tc.privKeys...) for _, eb := range tc.expectedBalances { mock.CheckBalance(t, mapp, eb.addr, eb.coins) @@ -205,11 +209,12 @@ func TestSengMsgMultipleInOut(t *testing.T) { testCases := []appTestCase{ { - msgs: []sdk.Msg{sendMsg3}, - accNums: []int64{0, 2}, - accSeqs: []int64{0, 0}, - expPass: true, - privKeys: []crypto.PrivKey{priv1, priv4}, + msgs: []sdk.Msg{sendMsg3}, + accNums: []int64{0, 2}, + accSeqs: []int64{0, 0}, + expSimPass: true, + expPass: true, + privKeys: []crypto.PrivKey{priv1, priv4}, expectedBalances: []expectedBalance{ {addr1, sdk.Coins{sdk.NewInt64Coin("foocoin", 32)}}, {addr4, sdk.Coins{sdk.NewInt64Coin("foocoin", 32)}}, @@ -220,7 +225,7 @@ func TestSengMsgMultipleInOut(t *testing.T) { } for _, tc := range testCases { - mock.SignCheckDeliver(t, mapp.BaseApp, tc.msgs, tc.accNums, tc.accSeqs, tc.expPass, tc.privKeys...) + mock.SignCheckDeliver(t, mapp.BaseApp, tc.msgs, tc.accNums, tc.accSeqs, tc.expSimPass, tc.expPass, tc.privKeys...) for _, eb := range tc.expectedBalances { mock.CheckBalance(t, mapp, eb.addr, eb.coins) @@ -240,22 +245,24 @@ func TestMsgSendDependent(t *testing.T) { testCases := []appTestCase{ { - msgs: []sdk.Msg{sendMsg1}, - accNums: []int64{0}, - accSeqs: []int64{0}, - expPass: true, - privKeys: []crypto.PrivKey{priv1}, + msgs: []sdk.Msg{sendMsg1}, + accNums: []int64{0}, + accSeqs: []int64{0}, + expSimPass: true, + expPass: true, + privKeys: []crypto.PrivKey{priv1}, expectedBalances: []expectedBalance{ {addr1, sdk.Coins{sdk.NewInt64Coin("foocoin", 32)}}, {addr2, sdk.Coins{sdk.NewInt64Coin("foocoin", 10)}}, }, }, { - msgs: []sdk.Msg{sendMsg4}, - accNums: []int64{1}, - accSeqs: []int64{0}, - expPass: true, - privKeys: []crypto.PrivKey{priv2}, + msgs: []sdk.Msg{sendMsg4}, + accNums: []int64{1}, + accSeqs: []int64{0}, + expSimPass: true, + expPass: true, + privKeys: []crypto.PrivKey{priv2}, expectedBalances: []expectedBalance{ {addr1, sdk.Coins{sdk.NewInt64Coin("foocoin", 42)}}, }, @@ -263,7 +270,7 @@ func TestMsgSendDependent(t *testing.T) { } for _, tc := range testCases { - mock.SignCheckDeliver(t, mapp.BaseApp, tc.msgs, tc.accNums, tc.accSeqs, tc.expPass, tc.privKeys...) + mock.SignCheckDeliver(t, mapp.BaseApp, tc.msgs, tc.accNums, tc.accSeqs, tc.expSimPass, tc.expPass, tc.privKeys...) for _, eb := range tc.expectedBalances { mock.CheckBalance(t, mapp, eb.addr, eb.coins) diff --git a/x/bank/client/rest/sendtx.go b/x/bank/client/rest/sendtx.go index c7baa9691..23506e0fe 100644 --- a/x/bank/client/rest/sendtx.go +++ b/x/bank/client/rest/sendtx.go @@ -4,6 +4,7 @@ import ( "io/ioutil" "net/http" + cliclient "github.com/cosmos/cosmos-sdk/client" "github.com/cosmos/cosmos-sdk/client/context" "github.com/cosmos/cosmos-sdk/client/utils" "github.com/cosmos/cosmos-sdk/crypto/keys" @@ -31,6 +32,7 @@ type sendBody struct { AccountNumber int64 `json:"account_number"` Sequence int64 `json:"sequence"` Gas int64 `json:"gas"` + GasAdjustment string `json:"gas_adjustment"` } var msgCdc = wire.NewCodec() @@ -40,6 +42,7 @@ func init() { } // SendRequestHandlerFn - http request handler to send coins to a address +// nolint: gocyclo func SendRequestHandlerFn(cdc *wire.Codec, kb keys.Keybase, cliCtx context.CLIContext) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { // collect data @@ -48,32 +51,32 @@ func SendRequestHandlerFn(cdc *wire.Codec, kb keys.Keybase, cliCtx context.CLICo to, err := sdk.AccAddressFromBech32(bech32addr) if err != nil { - utils.WriteErrorResponse(&w, http.StatusBadRequest, err.Error()) + utils.WriteErrorResponse(w, http.StatusBadRequest, err.Error()) return } var m sendBody body, err := ioutil.ReadAll(r.Body) if err != nil { - utils.WriteErrorResponse(&w, http.StatusBadRequest, err.Error()) + utils.WriteErrorResponse(w, http.StatusBadRequest, err.Error()) return } err = msgCdc.UnmarshalJSON(body, &m) if err != nil { - utils.WriteErrorResponse(&w, http.StatusBadRequest, err.Error()) + utils.WriteErrorResponse(w, http.StatusBadRequest, err.Error()) return } info, err := kb.Get(m.LocalAccountName) if err != nil { - utils.WriteErrorResponse(&w, http.StatusUnauthorized, err.Error()) + utils.WriteErrorResponse(w, http.StatusUnauthorized, err.Error()) return } // build message msg := client.BuildMsg(sdk.AccAddress(info.GetPubKey().Address()), to, m.Amount) if err != nil { // XXX rechecking same error ? - utils.WriteErrorResponse(&w, http.StatusInternalServerError, err.Error()) + utils.WriteErrorResponse(w, http.StatusInternalServerError, err.Error()) return } @@ -85,10 +88,20 @@ func SendRequestHandlerFn(cdc *wire.Codec, kb keys.Keybase, cliCtx context.CLICo Sequence: m.Sequence, } - if m.Gas == 0 { - newCtx, err := utils.EnrichCtxWithGas(txCtx, cliCtx, m.LocalAccountName, m.Password, []sdk.Msg{msg}) + adjustment, ok := utils.ParseFloat64OrReturnBadRequest(w, m.GasAdjustment, cliclient.DefaultGasAdjustment) + if !ok { + return + } + cliCtx = cliCtx.WithGasAdjustment(adjustment) + + if utils.HasDryRunArg(r) || m.Gas == 0 { + newCtx, err := utils.EnrichCtxWithGas(txCtx, cliCtx, m.LocalAccountName, []sdk.Msg{msg}) if err != nil { - utils.WriteErrorResponse(&w, http.StatusInternalServerError, err.Error()) + utils.WriteErrorResponse(w, http.StatusInternalServerError, err.Error()) + return + } + if utils.HasDryRunArg(r) { + utils.WriteSimulationResponse(w, txCtx.Gas) return } txCtx = newCtx @@ -96,19 +109,19 @@ func SendRequestHandlerFn(cdc *wire.Codec, kb keys.Keybase, cliCtx context.CLICo txBytes, err := txCtx.BuildAndSign(m.LocalAccountName, m.Password, []sdk.Msg{msg}) if err != nil { - utils.WriteErrorResponse(&w, http.StatusUnauthorized, err.Error()) + utils.WriteErrorResponse(w, http.StatusUnauthorized, err.Error()) return } res, err := cliCtx.BroadcastTx(txBytes) if err != nil { - utils.WriteErrorResponse(&w, http.StatusInternalServerError, err.Error()) + utils.WriteErrorResponse(w, http.StatusInternalServerError, err.Error()) return } output, err := wire.MarshalJSONIndent(cdc, res) if err != nil { - utils.WriteErrorResponse(&w, http.StatusInternalServerError, err.Error()) + utils.WriteErrorResponse(w, http.StatusInternalServerError, err.Error()) return } diff --git a/x/bank/simulation/msgs.go b/x/bank/simulation/msgs.go index e3f3ab77b..c53916a61 100644 --- a/x/bank/simulation/msgs.go +++ b/x/bank/simulation/msgs.go @@ -21,7 +21,7 @@ import ( // SimulateSingleInputMsgSend tests and runs a single msg send, with one input and one output, where both // accounts already exist. func SimulateSingleInputMsgSend(mapper auth.AccountMapper) simulation.Operation { - return func(t *testing.T, r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, keys []crypto.PrivKey, log string, event func(string)) (action string, fOps []simulation.FutureOperation, err sdk.Error) { + return func(tb testing.TB, r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, keys []crypto.PrivKey, log string, event func(string)) (action string, fOps []simulation.FutureOperation, err sdk.Error) { fromKey := simulation.RandomKey(r, keys) fromAddr := sdk.AccAddress(fromKey.PubKey().Address()) toKey := simulation.RandomKey(r, keys) @@ -58,7 +58,7 @@ func SimulateSingleInputMsgSend(mapper auth.AccountMapper) simulation.Operation Inputs: []bank.Input{bank.NewInput(fromAddr, coins)}, Outputs: []bank.Output{bank.NewOutput(toAddr, coins)}, } - sendAndVerifyMsgSend(t, app, mapper, msg, ctx, log, []crypto.PrivKey{fromKey}) + sendAndVerifyMsgSend(tb, app, mapper, msg, ctx, log, []crypto.PrivKey{fromKey}) event("bank/sendAndVerifyMsgSend/ok") return action, nil, nil @@ -66,7 +66,7 @@ func SimulateSingleInputMsgSend(mapper auth.AccountMapper) simulation.Operation } // Sends and verifies the transition of a msg send. This fails if there are repeated inputs or outputs -func sendAndVerifyMsgSend(t *testing.T, app *baseapp.BaseApp, mapper auth.AccountMapper, msg bank.MsgSend, ctx sdk.Context, log string, privkeys []crypto.PrivKey) { +func sendAndVerifyMsgSend(tb testing.TB, app *baseapp.BaseApp, mapper auth.AccountMapper, msg bank.MsgSend, ctx sdk.Context, log string, privkeys []crypto.PrivKey) { initialInputAddrCoins := make([]sdk.Coins, len(msg.Inputs)) initialOutputAddrCoins := make([]sdk.Coins, len(msg.Outputs)) AccountNumbers := make([]int64, len(msg.Inputs)) @@ -91,12 +91,12 @@ func sendAndVerifyMsgSend(t *testing.T, app *baseapp.BaseApp, mapper auth.Accoun // TODO: Do this in a more 'canonical' way fmt.Println(res) fmt.Println(log) - t.FailNow() + tb.FailNow() } for i := 0; i < len(msg.Inputs); i++ { terminalInputCoins := mapper.GetAccount(ctx, msg.Inputs[i].Address).GetCoins() - require.Equal(t, + require.Equal(tb, initialInputAddrCoins[i].Minus(msg.Inputs[i].Coins), terminalInputCoins, fmt.Sprintf("Input #%d had an incorrect amount of coins\n%s", i, log), @@ -104,11 +104,9 @@ func sendAndVerifyMsgSend(t *testing.T, app *baseapp.BaseApp, mapper auth.Accoun } for i := 0; i < len(msg.Outputs); i++ { terminalOutputCoins := mapper.GetAccount(ctx, msg.Outputs[i].Address).GetCoins() - require.Equal(t, - initialOutputAddrCoins[i].Plus(msg.Outputs[i].Coins), - terminalOutputCoins, - fmt.Sprintf("Output #%d had an incorrect amount of coins\n%s", i, log), - ) + if !terminalOutputCoins.IsEqual(initialOutputAddrCoins[i].Plus(msg.Outputs[i].Coins)) { + tb.Fatalf("Output #%d had an incorrect amount of coins\n%s", i, log) + } } } diff --git a/x/gov/client/rest/rest.go b/x/gov/client/rest/rest.go index 4e6a5c1b3..8bf675b02 100644 --- a/x/gov/client/rest/rest.go +++ b/x/gov/client/rest/rest.go @@ -77,11 +77,11 @@ func postProposalHandlerFn(cdc *wire.Codec, cliCtx context.CLIContext) http.Hand msg := gov.NewMsgSubmitProposal(req.Title, req.Description, req.ProposalType, req.Proposer, req.InitialDeposit) err = msg.ValidateBasic() if err != nil { - utils.WriteErrorResponse(&w, http.StatusBadRequest, err.Error()) + utils.WriteErrorResponse(w, http.StatusBadRequest, err.Error()) return } - signAndBuild(w, cliCtx, req.BaseReq, msg, cdc) + signAndBuild(w, r, cliCtx, req.BaseReq, msg, cdc) } } @@ -92,7 +92,7 @@ func depositHandlerFn(cdc *wire.Codec, cliCtx context.CLIContext) http.HandlerFu if len(strProposalID) == 0 { err := errors.New("proposalId required but not specified") - utils.WriteErrorResponse(&w, http.StatusBadRequest, err.Error()) + utils.WriteErrorResponse(w, http.StatusBadRequest, err.Error()) return } @@ -114,11 +114,11 @@ func depositHandlerFn(cdc *wire.Codec, cliCtx context.CLIContext) http.HandlerFu msg := gov.NewMsgDeposit(req.Depositer, proposalID, req.Amount) err = msg.ValidateBasic() if err != nil { - utils.WriteErrorResponse(&w, http.StatusBadRequest, err.Error()) + utils.WriteErrorResponse(w, http.StatusBadRequest, err.Error()) return } - signAndBuild(w, cliCtx, req.BaseReq, msg, cdc) + signAndBuild(w, r, cliCtx, req.BaseReq, msg, cdc) } } @@ -129,7 +129,7 @@ func voteHandlerFn(cdc *wire.Codec, cliCtx context.CLIContext) http.HandlerFunc if len(strProposalID) == 0 { err := errors.New("proposalId required but not specified") - utils.WriteErrorResponse(&w, http.StatusBadRequest, err.Error()) + utils.WriteErrorResponse(w, http.StatusBadRequest, err.Error()) return } @@ -151,11 +151,11 @@ func voteHandlerFn(cdc *wire.Codec, cliCtx context.CLIContext) http.HandlerFunc msg := gov.NewMsgVote(req.Voter, proposalID, req.Option) err = msg.ValidateBasic() if err != nil { - utils.WriteErrorResponse(&w, http.StatusBadRequest, err.Error()) + utils.WriteErrorResponse(w, http.StatusBadRequest, err.Error()) return } - signAndBuild(w, cliCtx, req.BaseReq, msg, cdc) + signAndBuild(w, r, cliCtx, req.BaseReq, msg, cdc) } } @@ -166,7 +166,7 @@ func queryProposalHandlerFn(cdc *wire.Codec) http.HandlerFunc { if len(strProposalID) == 0 { err := errors.New("proposalId required but not specified") - utils.WriteErrorResponse(&w, http.StatusBadRequest, err.Error()) + utils.WriteErrorResponse(w, http.StatusBadRequest, err.Error()) return } @@ -183,13 +183,13 @@ func queryProposalHandlerFn(cdc *wire.Codec) http.HandlerFunc { bz, err := cdc.MarshalJSON(params) if err != nil { - utils.WriteErrorResponse(&w, http.StatusBadRequest, err.Error()) + utils.WriteErrorResponse(w, http.StatusBadRequest, err.Error()) return } res, err := cliCtx.QueryWithData("custom/gov/proposal", bz) if err != nil { - utils.WriteErrorResponse(&w, http.StatusInternalServerError, err.Error()) + utils.WriteErrorResponse(w, http.StatusInternalServerError, err.Error()) return } @@ -205,7 +205,7 @@ func queryDepositHandlerFn(cdc *wire.Codec) http.HandlerFunc { if len(strProposalID) == 0 { err := errors.New("proposalId required but not specified") - utils.WriteErrorResponse(&w, http.StatusBadRequest, err.Error()) + utils.WriteErrorResponse(w, http.StatusBadRequest, err.Error()) return } @@ -216,14 +216,14 @@ func queryDepositHandlerFn(cdc *wire.Codec) http.HandlerFunc { if len(bechDepositerAddr) == 0 { err := errors.New("depositer address required but not specified") - utils.WriteErrorResponse(&w, http.StatusBadRequest, err.Error()) + utils.WriteErrorResponse(w, http.StatusBadRequest, err.Error()) return } depositerAddr, err := sdk.AccAddressFromBech32(bechDepositerAddr) if err != nil { err := errors.Errorf("'%s' needs to be bech32 encoded", RestDepositer) - utils.WriteErrorResponse(&w, http.StatusBadRequest, err.Error()) + utils.WriteErrorResponse(w, http.StatusBadRequest, err.Error()) return } @@ -236,13 +236,13 @@ func queryDepositHandlerFn(cdc *wire.Codec) http.HandlerFunc { bz, err := cdc.MarshalJSON(params) if err != nil { - utils.WriteErrorResponse(&w, http.StatusBadRequest, err.Error()) + utils.WriteErrorResponse(w, http.StatusBadRequest, err.Error()) return } res, err := cliCtx.QueryWithData("custom/gov/deposit", bz) if err != nil { - utils.WriteErrorResponse(&w, http.StatusInternalServerError, err.Error()) + utils.WriteErrorResponse(w, http.StatusInternalServerError, err.Error()) return } @@ -252,11 +252,11 @@ func queryDepositHandlerFn(cdc *wire.Codec) http.HandlerFunc { res, err := cliCtx.QueryWithData("custom/gov/proposal", cdc.MustMarshalBinary(gov.QueryProposalParams{params.ProposalID})) if err != nil || len(res) == 0 { err := errors.Errorf("proposalID [%d] does not exist", proposalID) - utils.WriteErrorResponse(&w, http.StatusNotFound, err.Error()) + utils.WriteErrorResponse(w, http.StatusNotFound, err.Error()) return } err = errors.Errorf("depositer [%s] did not deposit on proposalID [%d]", bechDepositerAddr, proposalID) - utils.WriteErrorResponse(&w, http.StatusNotFound, err.Error()) + utils.WriteErrorResponse(w, http.StatusNotFound, err.Error()) return } @@ -272,7 +272,7 @@ func queryVoteHandlerFn(cdc *wire.Codec) http.HandlerFunc { if len(strProposalID) == 0 { err := errors.New("proposalId required but not specified") - utils.WriteErrorResponse(&w, http.StatusBadRequest, err.Error()) + utils.WriteErrorResponse(w, http.StatusBadRequest, err.Error()) return } @@ -283,14 +283,14 @@ func queryVoteHandlerFn(cdc *wire.Codec) http.HandlerFunc { if len(bechVoterAddr) == 0 { err := errors.New("voter address required but not specified") - utils.WriteErrorResponse(&w, http.StatusBadRequest, err.Error()) + utils.WriteErrorResponse(w, http.StatusBadRequest, err.Error()) return } voterAddr, err := sdk.AccAddressFromBech32(bechVoterAddr) if err != nil { err := errors.Errorf("'%s' needs to be bech32 encoded", RestVoter) - utils.WriteErrorResponse(&w, http.StatusBadRequest, err.Error()) + utils.WriteErrorResponse(w, http.StatusBadRequest, err.Error()) return } @@ -302,13 +302,13 @@ func queryVoteHandlerFn(cdc *wire.Codec) http.HandlerFunc { } bz, err := cdc.MarshalJSON(params) if err != nil { - utils.WriteErrorResponse(&w, http.StatusBadRequest, err.Error()) + utils.WriteErrorResponse(w, http.StatusBadRequest, err.Error()) return } res, err := cliCtx.QueryWithData("custom/gov/vote", bz) if err != nil { - utils.WriteErrorResponse(&w, http.StatusInternalServerError, err.Error()) + utils.WriteErrorResponse(w, http.StatusInternalServerError, err.Error()) return } @@ -317,17 +317,17 @@ func queryVoteHandlerFn(cdc *wire.Codec) http.HandlerFunc { if vote.Empty() { bz, err := cdc.MarshalJSON(gov.QueryProposalParams{params.ProposalID}) if err != nil { - utils.WriteErrorResponse(&w, http.StatusBadRequest, err.Error()) + utils.WriteErrorResponse(w, http.StatusBadRequest, err.Error()) return } res, err := cliCtx.QueryWithData("custom/gov/proposal", bz) if err != nil || len(res) == 0 { err := errors.Errorf("proposalID [%d] does not exist", proposalID) - utils.WriteErrorResponse(&w, http.StatusNotFound, err.Error()) + utils.WriteErrorResponse(w, http.StatusNotFound, err.Error()) return } err = errors.Errorf("voter [%s] did not deposit on proposalID [%d]", bechVoterAddr, proposalID) - utils.WriteErrorResponse(&w, http.StatusNotFound, err.Error()) + utils.WriteErrorResponse(w, http.StatusNotFound, err.Error()) return } w.Write(res) @@ -343,7 +343,7 @@ func queryVotesOnProposalHandlerFn(cdc *wire.Codec) http.HandlerFunc { if len(strProposalID) == 0 { err := errors.New("proposalId required but not specified") - utils.WriteErrorResponse(&w, http.StatusBadRequest, err.Error()) + utils.WriteErrorResponse(w, http.StatusBadRequest, err.Error()) return } @@ -359,13 +359,13 @@ func queryVotesOnProposalHandlerFn(cdc *wire.Codec) http.HandlerFunc { } bz, err := cdc.MarshalJSON(params) if err != nil { - utils.WriteErrorResponse(&w, http.StatusBadRequest, err.Error()) + utils.WriteErrorResponse(w, http.StatusBadRequest, err.Error()) return } res, err := cliCtx.QueryWithData("custom/gov/votes", bz) if err != nil { - utils.WriteErrorResponse(&w, http.StatusInternalServerError, err.Error()) + utils.WriteErrorResponse(w, http.StatusInternalServerError, err.Error()) return } @@ -388,7 +388,7 @@ func queryProposalsWithParameterFn(cdc *wire.Codec) http.HandlerFunc { voterAddr, err := sdk.AccAddressFromBech32(bechVoterAddr) if err != nil { err := errors.Errorf("'%s' needs to be bech32 encoded", RestVoter) - utils.WriteErrorResponse(&w, http.StatusBadRequest, err.Error()) + utils.WriteErrorResponse(w, http.StatusBadRequest, err.Error()) return } params.Voter = voterAddr @@ -398,7 +398,7 @@ func queryProposalsWithParameterFn(cdc *wire.Codec) http.HandlerFunc { depositerAddr, err := sdk.AccAddressFromBech32(bechDepositerAddr) if err != nil { err := errors.Errorf("'%s' needs to be bech32 encoded", RestDepositer) - utils.WriteErrorResponse(&w, http.StatusBadRequest, err.Error()) + utils.WriteErrorResponse(w, http.StatusBadRequest, err.Error()) return } params.Depositer = depositerAddr @@ -408,7 +408,7 @@ func queryProposalsWithParameterFn(cdc *wire.Codec) http.HandlerFunc { proposalStatus, err := gov.ProposalStatusFromString(strProposalStatus) if err != nil { err := errors.Errorf("'%s' is not a valid Proposal Status", strProposalStatus) - utils.WriteErrorResponse(&w, http.StatusBadRequest, err.Error()) + utils.WriteErrorResponse(w, http.StatusBadRequest, err.Error()) return } params.ProposalStatus = proposalStatus @@ -423,7 +423,7 @@ func queryProposalsWithParameterFn(cdc *wire.Codec) http.HandlerFunc { bz, err := cdc.MarshalJSON(params) if err != nil { - utils.WriteErrorResponse(&w, http.StatusBadRequest, err.Error()) + utils.WriteErrorResponse(w, http.StatusBadRequest, err.Error()) return } @@ -431,7 +431,7 @@ func queryProposalsWithParameterFn(cdc *wire.Codec) http.HandlerFunc { res, err := cliCtx.QueryWithData("custom/gov/proposals", bz) if err != nil { - utils.WriteErrorResponse(&w, http.StatusInternalServerError, err.Error()) + utils.WriteErrorResponse(w, http.StatusInternalServerError, err.Error()) return } diff --git a/x/gov/client/rest/util.go b/x/gov/client/rest/util.go index f98f7bfa5..a5fae3c3f 100644 --- a/x/gov/client/rest/util.go +++ b/x/gov/client/rest/util.go @@ -6,6 +6,7 @@ import ( "net/http" "strconv" + "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" @@ -20,17 +21,18 @@ type baseReq struct { AccountNumber int64 `json:"account_number"` Sequence int64 `json:"sequence"` Gas int64 `json:"gas"` + GasAdjustment string `json:"gas_adjustment"` } func buildReq(w http.ResponseWriter, r *http.Request, cdc *wire.Codec, req interface{}) error { body, err := ioutil.ReadAll(r.Body) if err != nil { - utils.WriteErrorResponse(&w, http.StatusBadRequest, err.Error()) + utils.WriteErrorResponse(w, http.StatusBadRequest, err.Error()) return err } err = cdc.UnmarshalJSON(body, req) if err != nil { - utils.WriteErrorResponse(&w, http.StatusBadRequest, err.Error()) + utils.WriteErrorResponse(w, http.StatusBadRequest, err.Error()) return err } return nil @@ -38,27 +40,27 @@ func buildReq(w http.ResponseWriter, r *http.Request, cdc *wire.Codec, req inter func (req baseReq) baseReqValidate(w http.ResponseWriter) bool { if len(req.Name) == 0 { - utils.WriteErrorResponse(&w, http.StatusUnauthorized, "Name required but not specified") + utils.WriteErrorResponse(w, http.StatusUnauthorized, "Name required but not specified") return false } if len(req.Password) == 0 { - utils.WriteErrorResponse(&w, http.StatusUnauthorized, "Password required but not specified") + utils.WriteErrorResponse(w, http.StatusUnauthorized, "Password required but not specified") return false } if len(req.ChainID) == 0 { - utils.WriteErrorResponse(&w, http.StatusUnauthorized, "ChainID required but not specified") + utils.WriteErrorResponse(w, http.StatusUnauthorized, "ChainID required but not specified") return false } if req.AccountNumber < 0 { - utils.WriteErrorResponse(&w, http.StatusUnauthorized, "Account Number required but not specified") + utils.WriteErrorResponse(w, http.StatusUnauthorized, "Account Number required but not specified") return false } if req.Sequence < 0 { - utils.WriteErrorResponse(&w, http.StatusUnauthorized, "Sequence required but not specified") + utils.WriteErrorResponse(w, http.StatusUnauthorized, "Sequence required but not specified") return false } return true @@ -66,7 +68,7 @@ func (req baseReq) baseReqValidate(w http.ResponseWriter) bool { // TODO: Build this function out into a more generic base-request // (probably should live in client/lcd). -func signAndBuild(w http.ResponseWriter, cliCtx context.CLIContext, baseReq baseReq, msg sdk.Msg, cdc *wire.Codec) { +func signAndBuild(w http.ResponseWriter, r *http.Request, cliCtx context.CLIContext, baseReq baseReq, msg sdk.Msg, cdc *wire.Codec) { var err error txCtx := authctx.TxContext{ Codec: cdc, @@ -76,29 +78,39 @@ func signAndBuild(w http.ResponseWriter, cliCtx context.CLIContext, baseReq base Gas: baseReq.Gas, } - if baseReq.Gas == 0 { - newCtx, err := utils.EnrichCtxWithGas(txCtx, cliCtx, baseReq.Name, baseReq.Password, []sdk.Msg{msg}) + adjustment, ok := utils.ParseFloat64OrReturnBadRequest(w, baseReq.GasAdjustment, client.DefaultGasAdjustment) + if !ok { + return + } + cliCtx = cliCtx.WithGasAdjustment(adjustment) + + if utils.HasDryRunArg(r) || baseReq.Gas == 0 { + newCtx, err := utils.EnrichCtxWithGas(txCtx, cliCtx, baseReq.Name, []sdk.Msg{msg}) if err != nil { - utils.WriteErrorResponse(&w, http.StatusInternalServerError, err.Error()) + utils.WriteErrorResponse(w, http.StatusInternalServerError, err.Error()) + return + } + if utils.HasDryRunArg(r) { + utils.WriteSimulationResponse(w, txCtx.Gas) return } txCtx = newCtx } txBytes, err := txCtx.BuildAndSign(baseReq.Name, baseReq.Password, []sdk.Msg{msg}) if err != nil { - utils.WriteErrorResponse(&w, http.StatusUnauthorized, err.Error()) + utils.WriteErrorResponse(w, http.StatusUnauthorized, err.Error()) return } res, err := cliCtx.BroadcastTx(txBytes) if err != nil { - utils.WriteErrorResponse(&w, http.StatusInternalServerError, err.Error()) + utils.WriteErrorResponse(w, http.StatusInternalServerError, err.Error()) return } output, err := wire.MarshalJSONIndent(cdc, res) if err != nil { - utils.WriteErrorResponse(&w, http.StatusInternalServerError, err.Error()) + utils.WriteErrorResponse(w, http.StatusInternalServerError, err.Error()) return } diff --git a/x/gov/simulation/msgs.go b/x/gov/simulation/msgs.go index 0b1530138..eca8accae 100644 --- a/x/gov/simulation/msgs.go +++ b/x/gov/simulation/msgs.go @@ -5,8 +5,6 @@ import ( "math/rand" "testing" - "github.com/stretchr/testify/require" - "github.com/tendermint/tendermint/crypto" "github.com/cosmos/cosmos-sdk/baseapp" @@ -23,24 +21,15 @@ const ( // SimulateMsgSubmitProposal simulates a msg Submit Proposal // Note: Currently doesn't ensure that the proposal txt is in JSON form func SimulateMsgSubmitProposal(k gov.Keeper, sk stake.Keeper) simulation.Operation { - return func(t *testing.T, r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, keys []crypto.PrivKey, log string, event func(string)) (action string, fOps []simulation.FutureOperation, err sdk.Error) { - key := simulation.RandomKey(r, keys) - addr := sdk.AccAddress(key.PubKey().Address()) - deposit := randomDeposit(r) - msg := gov.NewMsgSubmitProposal( - simulation.RandStringOfLength(r, 5), - simulation.RandStringOfLength(r, 5), - gov.ProposalTypeText, - addr, - deposit, - ) - require.Nil(t, msg.ValidateBasic(), "expected msg to pass ValidateBasic: %s", msg.GetSignBytes()) + handler := gov.NewHandler(k) + return func(tb testing.TB, r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, keys []crypto.PrivKey, log string, event func(string)) (action string, fOps []simulation.FutureOperation, err sdk.Error) { + msg := simulationCreateMsgSubmitProposal(tb, r, keys, log) ctx, write := ctx.CacheContext() - result := gov.NewHandler(k)(ctx, msg) + result := handler(ctx, msg) if result.IsOK() { // Update pool to keep invariants pool := sk.GetPool(ctx) - pool.LooseTokens = pool.LooseTokens.Sub(sdk.NewDecFromInt(deposit.AmountOf(denom))) + pool.LooseTokens = pool.LooseTokens.Sub(sdk.NewDecFromInt(msg.InitialDeposit.AmountOf(denom))) sk.SetPool(ctx, pool) write() } @@ -50,9 +39,26 @@ func SimulateMsgSubmitProposal(k gov.Keeper, sk stake.Keeper) simulation.Operati } } +func simulationCreateMsgSubmitProposal(tb testing.TB, r *rand.Rand, keys []crypto.PrivKey, log string) gov.MsgSubmitProposal { + key := simulation.RandomKey(r, keys) + addr := sdk.AccAddress(key.PubKey().Address()) + deposit := randomDeposit(r) + msg := gov.NewMsgSubmitProposal( + simulation.RandStringOfLength(r, 5), + simulation.RandStringOfLength(r, 5), + gov.ProposalTypeText, + addr, + deposit, + ) + if msg.ValidateBasic() != nil { + tb.Fatalf("expected msg to pass ValidateBasic: %s, log %s", msg.GetSignBytes(), log) + } + return msg +} + // SimulateMsgDeposit func SimulateMsgDeposit(k gov.Keeper, sk stake.Keeper) simulation.Operation { - return func(t *testing.T, r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, keys []crypto.PrivKey, log string, event func(string)) (action string, fOp []simulation.FutureOperation, err sdk.Error) { + return func(tb testing.TB, r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, keys []crypto.PrivKey, log string, event func(string)) (action string, fOp []simulation.FutureOperation, err sdk.Error) { key := simulation.RandomKey(r, keys) addr := sdk.AccAddress(key.PubKey().Address()) proposalID, ok := randomProposalID(r, k, ctx) @@ -61,7 +67,9 @@ func SimulateMsgDeposit(k gov.Keeper, sk stake.Keeper) simulation.Operation { } deposit := randomDeposit(r) msg := gov.NewMsgDeposit(addr, proposalID, deposit) - require.Nil(t, msg.ValidateBasic(), "expected msg to pass ValidateBasic: %s", msg.GetSignBytes()) + if msg.ValidateBasic() != nil { + tb.Fatalf("expected msg to pass ValidateBasic: %s, log %s", msg.GetSignBytes(), log) + } ctx, write := ctx.CacheContext() result := gov.NewHandler(k)(ctx, msg) if result.IsOK() { @@ -79,7 +87,7 @@ func SimulateMsgDeposit(k gov.Keeper, sk stake.Keeper) simulation.Operation { // SimulateMsgVote func SimulateMsgVote(k gov.Keeper, sk stake.Keeper) simulation.Operation { - return func(t *testing.T, r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, keys []crypto.PrivKey, log string, event func(string)) (action string, fOp []simulation.FutureOperation, err sdk.Error) { + return func(tb testing.TB, r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, keys []crypto.PrivKey, log string, event func(string)) (action string, fOp []simulation.FutureOperation, err sdk.Error) { key := simulation.RandomKey(r, keys) addr := sdk.AccAddress(key.PubKey().Address()) proposalID, ok := randomProposalID(r, k, ctx) @@ -88,7 +96,9 @@ func SimulateMsgVote(k gov.Keeper, sk stake.Keeper) simulation.Operation { } option := randomVotingOption(r) msg := gov.NewMsgVote(addr, proposalID, option) - require.Nil(t, msg.ValidateBasic(), "expected msg to pass ValidateBasic: %s", msg.GetSignBytes()) + if msg.ValidateBasic() != nil { + tb.Fatalf("expected msg to pass ValidateBasic: %s, log %s", msg.GetSignBytes(), log) + } ctx, write := ctx.CacheContext() result := gov.NewHandler(k)(ctx, msg) if result.IsOK() { diff --git a/x/ibc/app_test.go b/x/ibc/app_test.go index 5c3a0df78..f761f861d 100644 --- a/x/ibc/app_test.go +++ b/x/ibc/app_test.go @@ -70,10 +70,10 @@ func TestIBCMsgs(t *testing.T) { Sequence: 0, } - mock.SignCheckDeliver(t, mapp.BaseApp, []sdk.Msg{transferMsg}, []int64{0}, []int64{0}, true, priv1) + mock.SignCheckDeliver(t, mapp.BaseApp, []sdk.Msg{transferMsg}, []int64{0}, []int64{0}, true, true, priv1) mock.CheckBalance(t, mapp, addr1, emptyCoins) - mock.SignCheckDeliver(t, mapp.BaseApp, []sdk.Msg{transferMsg}, []int64{0}, []int64{1}, false, priv1) - mock.SignCheckDeliver(t, mapp.BaseApp, []sdk.Msg{receiveMsg}, []int64{0}, []int64{2}, true, priv1) + mock.SignCheckDeliver(t, mapp.BaseApp, []sdk.Msg{transferMsg}, []int64{0}, []int64{1}, false, false, priv1) + mock.SignCheckDeliver(t, mapp.BaseApp, []sdk.Msg{receiveMsg}, []int64{0}, []int64{2}, true, true, priv1) mock.CheckBalance(t, mapp, addr1, coins) - mock.SignCheckDeliver(t, mapp.BaseApp, []sdk.Msg{receiveMsg}, []int64{0}, []int64{2}, false, priv1) + mock.SignCheckDeliver(t, mapp.BaseApp, []sdk.Msg{receiveMsg}, []int64{0}, []int64{2}, false, false, priv1) } diff --git a/x/ibc/client/rest/transfer.go b/x/ibc/client/rest/transfer.go index 765208b05..0580ff406 100644 --- a/x/ibc/client/rest/transfer.go +++ b/x/ibc/client/rest/transfer.go @@ -4,6 +4,7 @@ import ( "io/ioutil" "net/http" + "github.com/cosmos/cosmos-sdk/client" "github.com/cosmos/cosmos-sdk/client/context" "github.com/cosmos/cosmos-sdk/client/utils" "github.com/cosmos/cosmos-sdk/crypto/keys" @@ -29,10 +30,12 @@ type transferBody struct { AccountNumber int64 `json:"account_number"` Sequence int64 `json:"sequence"` Gas int64 `json:"gas"` + GasAdjustment string `json:"gas_adjustment"` } // TransferRequestHandler - http request handler to transfer coins to a address // on a different chain via IBC +// nolint: gocyclo func TransferRequestHandlerFn(cdc *wire.Codec, kb keys.Keybase, cliCtx context.CLIContext) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) @@ -41,26 +44,26 @@ func TransferRequestHandlerFn(cdc *wire.Codec, kb keys.Keybase, cliCtx context.C to, err := sdk.AccAddressFromBech32(bech32addr) if err != nil { - utils.WriteErrorResponse(&w, http.StatusBadRequest, err.Error()) + utils.WriteErrorResponse(w, http.StatusBadRequest, err.Error()) return } var m transferBody body, err := ioutil.ReadAll(r.Body) if err != nil { - utils.WriteErrorResponse(&w, http.StatusBadRequest, err.Error()) + utils.WriteErrorResponse(w, http.StatusBadRequest, err.Error()) return } err = cdc.UnmarshalJSON(body, &m) if err != nil { - utils.WriteErrorResponse(&w, http.StatusBadRequest, err.Error()) + utils.WriteErrorResponse(w, http.StatusBadRequest, err.Error()) return } info, err := kb.Get(m.LocalAccountName) if err != nil { - utils.WriteErrorResponse(&w, http.StatusUnauthorized, err.Error()) + utils.WriteErrorResponse(w, http.StatusUnauthorized, err.Error()) return } @@ -76,10 +79,20 @@ func TransferRequestHandlerFn(cdc *wire.Codec, kb keys.Keybase, cliCtx context.C Gas: m.Gas, } - if m.Gas == 0 { - newCtx, err := utils.EnrichCtxWithGas(txCtx, cliCtx, m.LocalAccountName, m.Password, []sdk.Msg{msg}) + adjustment, ok := utils.ParseFloat64OrReturnBadRequest(w, m.GasAdjustment, client.DefaultGasAdjustment) + if !ok { + return + } + cliCtx = cliCtx.WithGasAdjustment(adjustment) + + if utils.HasDryRunArg(r) || m.Gas == 0 { + newCtx, err := utils.EnrichCtxWithGas(txCtx, cliCtx, m.LocalAccountName, []sdk.Msg{msg}) if err != nil { - utils.WriteErrorResponse(&w, http.StatusInternalServerError, err.Error()) + utils.WriteErrorResponse(w, http.StatusInternalServerError, err.Error()) + return + } + if utils.HasDryRunArg(r) { + utils.WriteSimulationResponse(w, txCtx.Gas) return } txCtx = newCtx @@ -87,19 +100,19 @@ func TransferRequestHandlerFn(cdc *wire.Codec, kb keys.Keybase, cliCtx context.C txBytes, err := txCtx.BuildAndSign(m.LocalAccountName, m.Password, []sdk.Msg{msg}) if err != nil { - utils.WriteErrorResponse(&w, http.StatusUnauthorized, err.Error()) + utils.WriteErrorResponse(w, http.StatusUnauthorized, err.Error()) return } res, err := cliCtx.BroadcastTx(txBytes) if err != nil { - utils.WriteErrorResponse(&w, http.StatusInternalServerError, err.Error()) + utils.WriteErrorResponse(w, http.StatusInternalServerError, err.Error()) return } output, err := cdc.MarshalJSON(res) if err != nil { - utils.WriteErrorResponse(&w, http.StatusInternalServerError, err.Error()) + utils.WriteErrorResponse(w, http.StatusInternalServerError, err.Error()) return } diff --git a/x/mock/app_test.go b/x/mock/app_test.go index 7477e1204..460757a04 100644 --- a/x/mock/app_test.go +++ b/x/mock/app_test.go @@ -61,14 +61,14 @@ func TestCheckAndDeliverGenTx(t *testing.T) { SignCheckDeliver( t, mApp.BaseApp, []sdk.Msg{msg}, []int64{accs[0].GetAccountNumber()}, []int64{accs[0].GetSequence()}, - true, privKeys[0], + true, true, privKeys[0], ) // Signing a tx with the wrong privKey should result in an auth error res := SignCheckDeliver( t, mApp.BaseApp, []sdk.Msg{msg}, []int64{accs[1].GetAccountNumber()}, []int64{accs[1].GetSequence() + 1}, - false, privKeys[1], + true, false, privKeys[1], ) require.Equal(t, sdk.ToABCICode(sdk.CodespaceRoot, sdk.CodeUnauthorized), res.Code, res.Log) @@ -76,7 +76,7 @@ func TestCheckAndDeliverGenTx(t *testing.T) { SignCheckDeliver( t, mApp.BaseApp, []sdk.Msg{msg}, []int64{accs[0].GetAccountNumber()}, []int64{accs[0].GetSequence() + 1}, - true, privKeys[0], + true, true, privKeys[0], ) } diff --git a/x/mock/simulation/random_simulate_blocks.go b/x/mock/simulation/random_simulate_blocks.go index 995013ef8..fc7543280 100644 --- a/x/mock/simulation/random_simulate_blocks.go +++ b/x/mock/simulation/random_simulate_blocks.go @@ -5,6 +5,7 @@ import ( "fmt" "math" "math/rand" + "os" "sort" "testing" "time" @@ -16,7 +17,6 @@ import ( "github.com/cosmos/cosmos-sdk/baseapp" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/x/mock" - "github.com/stretchr/testify/require" ) // Simulate tests application by sending random messages. @@ -28,34 +28,10 @@ func Simulate( SimulateFromSeed(t, app, appStateFn, time, ops, setups, invariants, numBlocks, blockSize, commit) } -// SimulateFromSeed tests an application by running the provided -// operations, testing the provided invariants, but using the provided seed. -func SimulateFromSeed( - t *testing.T, app *baseapp.BaseApp, appStateFn func(r *rand.Rand, keys []crypto.PrivKey, accs []sdk.AccAddress) json.RawMessage, seed int64, ops []Operation, setups []RandSetup, - invariants []Invariant, numBlocks int, blockSize int, commit bool, -) { - log := fmt.Sprintf("Starting SimulateFromSeed with randomness created with seed %d", int(seed)) - r := rand.New(rand.NewSource(seed)) - - unixTime := r.Int63n(int64(math.Pow(2, 40))) - - // Set the timestamp for simulation - timestamp := time.Unix(unixTime, 0) - log = fmt.Sprintf("%s\nStarting the simulation from time %v, unixtime %v", log, timestamp.UTC().Format(time.UnixDate), timestamp.Unix()) - fmt.Printf("%s\n", log) - timeDiff := maxTimePerBlock - minTimePerBlock - - keys, accs := mock.GeneratePrivKeyAddressPairsFromRand(r, numKeys) - - // Setup event stats - events := make(map[string]uint) - event := func(what string) { - log += "\nevent - " + what - events[what]++ - } - +func initChain(r *rand.Rand, keys []crypto.PrivKey, accs []sdk.AccAddress, setups []RandSetup, app *baseapp.BaseApp, + appStateFn func(r *rand.Rand, keys []crypto.PrivKey, accs []sdk.AccAddress) json.RawMessage) (validators map[string]mockValidator) { res := app.InitChain(abci.RequestInitChain{AppStateBytes: appStateFn(r, keys, accs)}) - validators := make(map[string]mockValidator) + validators = make(map[string]mockValidator) for _, validator := range res.Validators { validators[string(validator.Address)] = mockValidator{validator, GetMemberOfInitialState(r, initialLivenessWeightings)} } @@ -64,83 +40,161 @@ func SimulateFromSeed( setups[i](r, keys) } + return +} + +func randTimestamp(r *rand.Rand) time.Time { + unixTime := r.Int63n(int64(math.Pow(2, 40))) + return time.Unix(unixTime, 0) +} + +// SimulateFromSeed tests an application by running the provided +// operations, testing the provided invariants, but using the provided seed. +func SimulateFromSeed( + tb testing.TB, app *baseapp.BaseApp, appStateFn func(r *rand.Rand, keys []crypto.PrivKey, accs []sdk.AccAddress) json.RawMessage, seed int64, ops []Operation, setups []RandSetup, + invariants []Invariant, numBlocks int, blockSize int, commit bool, +) { + testingMode, t, b := getTestingMode(tb) + log := fmt.Sprintf("Starting SimulateFromSeed with randomness created with seed %d", int(seed)) + r := rand.New(rand.NewSource(seed)) + timestamp := randTimestamp(r) + log = updateLog(testingMode, log, "Starting the simulation from time %v, unixtime %v", timestamp.UTC().Format(time.UnixDate), timestamp.Unix()) + fmt.Printf("%s\n", log) + timeDiff := maxTimePerBlock - minTimePerBlock + + keys, accs := mock.GeneratePrivKeyAddressPairsFromRand(r, numKeys) + + // Setup event stats + events := make(map[string]uint) + event := func(what string) { + log = updateLog(testingMode, log, "event - %s", what) + events[what]++ + } + + validators := initChain(r, keys, accs, setups, app, appStateFn) + header := abci.Header{Height: 0, Time: timestamp} opCount := 0 - request := abci.RequestBeginBlock{Header: header} - var pastTimes []time.Time + var pastSigningValidators [][]abci.SigningValidator + + request := RandomRequestBeginBlock(r, validators, livenessTransitionMatrix, evidenceFraction, pastTimes, pastSigningValidators, event, header, log) // These are operations which have been queued by previous operations operationQueue := make(map[int][]Operation) - for i := 0; i < numBlocks; i++ { + if !testingMode { + b.ResetTimer() + } + blockSimulator := createBlockSimulator(testingMode, tb, t, event, invariants, ops, operationQueue, numBlocks) + for i := 0; i < numBlocks; i++ { // Log the header time for future lookup pastTimes = append(pastTimes, header.Time) + pastSigningValidators = append(pastSigningValidators, request.LastCommitInfo.Validators) // Run the BeginBlock handler app.BeginBlock(request) + log = updateLog(testingMode, log, "BeginBlock") - log += "\nBeginBlock" - - // Make sure invariants hold at beginning of block - AssertAllInvariants(t, app, invariants, log) + if testingMode { + // Make sure invariants hold at beginning of block + AssertAllInvariants(t, app, invariants, log) + } ctx := app.NewContext(false, header) + thisBlockSize := getBlockSize(r, blockSize) - var thisBlockSize int - load := r.Float64() - switch { - case load < 0.33: - thisBlockSize = 0 - case load < 0.66: - thisBlockSize = r.Intn(blockSize * 2) - default: - thisBlockSize = r.Intn(blockSize * 4) - } // Run queued operations. Ignores blocksize if blocksize is too small - log, numQueuedOpsRan := runQueuedOperations(operationQueue, int(header.Height), t, r, app, ctx, keys, log, event) + log, numQueuedOpsRan := runQueuedOperations(operationQueue, int(header.Height), tb, r, app, ctx, keys, log, event) opCount += numQueuedOpsRan thisBlockSize -= numQueuedOpsRan - for j := 0; j < thisBlockSize; j++ { - logUpdate, futureOps, err := ops[r.Intn(len(ops))](t, r, app, ctx, keys, log, event) - log += "\n" + logUpdate - queueOperations(operationQueue, futureOps) - - require.Nil(t, err, log) - if onOperation { - AssertAllInvariants(t, app, invariants, log) - } - if opCount%200 == 0 { - fmt.Printf("\rSimulating... block %d/%d, operation %d.", header.Height, numBlocks, opCount) - } - opCount++ - } + log, operations := blockSimulator(thisBlockSize, r, app, ctx, keys, log, header) + opCount += operations res := app.EndBlock(abci.RequestEndBlock{}) header.Height++ header.Time = header.Time.Add(time.Duration(minTimePerBlock) * time.Second).Add(time.Duration(int64(r.Intn(int(timeDiff)))) * time.Second) + log = updateLog(testingMode, log, "EndBlock") - log += "\nEndBlock" - - // Make sure invariants hold at end of block - AssertAllInvariants(t, app, invariants, log) - + if testingMode { + // Make sure invariants hold at end of block + AssertAllInvariants(t, app, invariants, log) + } if commit { app.Commit() } // Generate a random RequestBeginBlock with the current validator set for the next block - request = RandomRequestBeginBlock(t, r, validators, livenessTransitionMatrix, evidenceFraction, pastTimes, event, header, log) + request = RandomRequestBeginBlock(r, validators, livenessTransitionMatrix, evidenceFraction, pastTimes, pastSigningValidators, event, header, log) // Update the validator set - validators = updateValidators(t, r, validators, res.ValidatorUpdates, event) + validators = updateValidators(tb, r, validators, res.ValidatorUpdates, event) } - fmt.Printf("\nSimulation complete. Final height (blocks): %d, final time (seconds): %v\n", header.Height, header.Time) + fmt.Printf("\nSimulation complete. Final height (blocks): %d, final time (seconds), : %v, operations ran %d\n", header.Height, header.Time, opCount) DisplayEvents(events) } +// Returns a function to simulate blocks. Written like this to avoid constant parameters being passed everytime, to minimize +// memory overhead +func createBlockSimulator(testingMode bool, tb testing.TB, t *testing.T, event func(string), invariants []Invariant, ops []Operation, operationQueue map[int][]Operation, totalNumBlocks int) func( + blocksize int, r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, privKeys []crypto.PrivKey, log string, header abci.Header) (updatedLog string, opCount int) { + return func(blocksize int, r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, + keys []crypto.PrivKey, log string, header abci.Header) (updatedLog string, opCount int) { + for j := 0; j < blocksize; j++ { + logUpdate, futureOps, err := ops[r.Intn(len(ops))](tb, r, app, ctx, keys, log, event) + log = updateLog(testingMode, log, logUpdate) + if err != nil { + tb.Fatalf("error on operation %d within block %d, %v, log %s", header.Height, opCount, err, log) + } + + queueOperations(operationQueue, futureOps) + if testingMode { + if onOperation { + AssertAllInvariants(t, app, invariants, log) + } + if opCount%50 == 0 { + fmt.Printf("\rSimulating... block %d/%d, operation %d/%d.", header.Height, totalNumBlocks, opCount, blocksize) + } + } + opCount++ + } + return log, opCount + } +} + +func getTestingMode(tb testing.TB) (testingMode bool, t *testing.T, b *testing.B) { + testingMode = false + if _t, ok := tb.(*testing.T); ok { + t = _t + testingMode = true + } else { + b = tb.(*testing.B) + } + return +} + +func updateLog(testingMode bool, log string, update string, args ...interface{}) (updatedLog string) { + if testingMode { + update = fmt.Sprintf(update, args...) + return fmt.Sprintf("%s\n%s", log, update) + } + return "" +} + +func getBlockSize(r *rand.Rand, blockSize int) int { + load := r.Float64() + switch { + case load < 0.33: + return 0 + case load < 0.66: + return r.Intn(blockSize * 2) + default: + return r.Intn(blockSize * 4) + } +} + // adds all future operations into the operation queue. func queueOperations(queuedOperations map[int][]Operation, futureOperations []FutureOperation) { if futureOperations == nil { @@ -155,7 +209,7 @@ func queueOperations(queuedOperations map[int][]Operation, futureOperations []Fu } } -func runQueuedOperations(queueOperations map[int][]Operation, height int, t *testing.T, r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, +func runQueuedOperations(queueOperations map[int][]Operation, height int, tb testing.TB, r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, privKeys []crypto.PrivKey, log string, event func(string)) (updatedLog string, numOpsRan int) { updatedLog = log if queuedOps, ok := queueOperations[height]; ok { @@ -164,9 +218,12 @@ func runQueuedOperations(queueOperations map[int][]Operation, height int, t *tes // For now, queued operations cannot queue more operations. // If a need arises for us to support queued messages to queue more messages, this can // be changed. - logUpdate, _, err := queuedOps[i](t, r, app, ctx, privKeys, updatedLog, event) - updatedLog += "\n" + logUpdate - require.Nil(t, err, updatedLog) + logUpdate, _, err := queuedOps[i](tb, r, app, ctx, privKeys, updatedLog, event) + updatedLog = fmt.Sprintf("%s\n%s", updatedLog, logUpdate) + if err != nil { + fmt.Fprint(os.Stderr, updatedLog) + tb.FailNow() + } } delete(queueOperations, height) return updatedLog, numOps @@ -186,14 +243,13 @@ func getKeys(validators map[string]mockValidator) []string { } // RandomRequestBeginBlock generates a list of signing validators according to the provided list of validators, signing fraction, and evidence fraction -func RandomRequestBeginBlock(t *testing.T, r *rand.Rand, validators map[string]mockValidator, livenessTransitions TransitionMatrix, evidenceFraction float64, - pastTimes []time.Time, event func(string), header abci.Header, log string) abci.RequestBeginBlock { +func RandomRequestBeginBlock(r *rand.Rand, validators map[string]mockValidator, livenessTransitions TransitionMatrix, evidenceFraction float64, + pastTimes []time.Time, pastSigningValidators [][]abci.SigningValidator, event func(string), header abci.Header, log string) abci.RequestBeginBlock { if len(validators) == 0 { return abci.RequestBeginBlock{Header: header} } signingValidators := make([]abci.SigningValidator, len(validators)) i := 0 - for _, key := range getKeys(validators) { mVal := validators[key] mVal.livenessState = livenessTransitions.NextState(r, mVal.livenessState) @@ -219,27 +275,33 @@ func RandomRequestBeginBlock(t *testing.T, r *rand.Rand, validators map[string]m } i++ } + // TODO: Determine capacity before allocation evidence := make([]abci.Evidence, 0) - for r.Float64() < evidenceFraction { - height := header.Height - time := header.Time - if r.Float64() < pastEvidenceFraction { - height = int64(r.Intn(int(header.Height))) - time = pastTimes[height] + // Anything but the first block + if len(pastTimes) > 0 { + for r.Float64() < evidenceFraction { + height := header.Height + time := header.Time + vals := signingValidators + if r.Float64() < pastEvidenceFraction { + height = int64(r.Intn(int(header.Height))) + time = pastTimes[height] + vals = pastSigningValidators[height] + } + validator := vals[r.Intn(len(vals))].Validator + var totalVotingPower int64 + for _, val := range vals { + totalVotingPower += val.Validator.Power + } + evidence = append(evidence, abci.Evidence{ + Type: tmtypes.ABCIEvidenceTypeDuplicateVote, + Validator: validator, + Height: height, + Time: time, + TotalVotingPower: totalVotingPower, + }) + event("beginblock/evidence") } - validator := signingValidators[r.Intn(len(signingValidators))].Validator - var currentTotalVotingPower int64 - for _, mVal := range validators { - currentTotalVotingPower += mVal.val.Power - } - evidence = append(evidence, abci.Evidence{ - Type: tmtypes.ABCIEvidenceTypeDuplicateVote, - Validator: validator, - Height: height, - Time: time, - TotalVotingPower: currentTotalVotingPower, - }) - event("beginblock/evidence") } return abci.RequestBeginBlock{ Header: header, @@ -258,11 +320,19 @@ func AssertAllInvariants(t *testing.T, app *baseapp.BaseApp, tests []Invariant, } // updateValidators mimicks Tendermint's update logic -func updateValidators(t *testing.T, r *rand.Rand, current map[string]mockValidator, updates []abci.Validator, event func(string)) map[string]mockValidator { +func updateValidators(tb testing.TB, r *rand.Rand, current map[string]mockValidator, updates []abci.Validator, event func(string)) map[string]mockValidator { for _, update := range updates { switch { case update.Power == 0: - require.NotNil(t, current[string(update.PubKey.Data)], "tried to delete a nonexistent validator") + // // TEMPORARY DEBUG CODE TO PROVE THAT THE OLD METHOD WAS BROKEN + // // (i.e. didn't catch in the event of problem) + // if val, ok := tb.(*testing.T); ok { + // require.NotNil(val, current[string(update.PubKey.Data)]) + // } + // // CORRECT CHECK + // if _, ok := current[string(update.PubKey.Data)]; !ok { + // tb.Fatalf("tried to delete a nonexistent validator") + // } event("endblock/validatorupdates/kicked") delete(current, string(update.PubKey.Data)) default: diff --git a/x/mock/simulation/types.go b/x/mock/simulation/types.go index 2516e07ae..2f91a4f26 100644 --- a/x/mock/simulation/types.go +++ b/x/mock/simulation/types.go @@ -23,7 +23,7 @@ type ( // Operations can optionally provide a list of "FutureOperations" to run later // These will be ran at the beginning of the corresponding block. Operation func( - t *testing.T, r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, + tb testing.TB, r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, privKeys []crypto.PrivKey, log string, event func(string), ) (action string, futureOperations []FutureOperation, err sdk.Error) diff --git a/x/mock/test_utils.go b/x/mock/test_utils.go index c97f1c0c8..caaca6c9a 100644 --- a/x/mock/test_utils.go +++ b/x/mock/test_utils.go @@ -71,13 +71,13 @@ func CheckGenTx( // returned. func SignCheckDeliver( t *testing.T, app *baseapp.BaseApp, msgs []sdk.Msg, accNums []int64, - seq []int64, expPass bool, priv ...crypto.PrivKey, + seq []int64, expSimPass, expPass bool, priv ...crypto.PrivKey, ) sdk.Result { tx := GenTx(msgs, accNums, seq, priv...) // Must simulate now as CheckTx doesn't run Msgs anymore res := app.Simulate(tx) - if expPass { + if expSimPass { require.Equal(t, sdk.ABCICodeOK, res.Code, res.Log) } else { require.NotEqual(t, sdk.ABCICodeOK, res.Code, res.Log) diff --git a/x/slashing/app_test.go b/x/slashing/app_test.go index f9ec0833f..96dd1ff87 100644 --- a/x/slashing/app_test.go +++ b/x/slashing/app_test.go @@ -79,7 +79,7 @@ func checkValidator(t *testing.T, mapp *mock.App, keeper stake.Keeper, } func checkValidatorSigningInfo(t *testing.T, mapp *mock.App, keeper Keeper, - addr sdk.ValAddress, expFound bool) ValidatorSigningInfo { + addr sdk.ConsAddress, expFound bool) ValidatorSigningInfo { ctxCheck := mapp.BaseApp.NewContext(true, abci.Header{}) signingInfo, found := keeper.getValidatorSigningInfo(ctxCheck, addr) require.Equal(t, expFound, found) @@ -102,7 +102,7 @@ func TestSlashingMsgs(t *testing.T) { createValidatorMsg := stake.NewMsgCreateValidator( sdk.ValAddress(addr1), priv1.PubKey(), bondCoin, description, ) - mock.SignCheckDeliver(t, mapp.BaseApp, []sdk.Msg{createValidatorMsg}, []int64{0}, []int64{0}, true, priv1) + mock.SignCheckDeliver(t, mapp.BaseApp, []sdk.Msg{createValidatorMsg}, []int64{0}, []int64{0}, true, true, priv1) mock.CheckBalance(t, mapp, addr1, sdk.Coins{genCoin.Minus(bondCoin)}) mapp.BeginBlock(abci.RequestBeginBlock{}) @@ -113,9 +113,9 @@ func TestSlashingMsgs(t *testing.T) { unjailMsg := MsgUnjail{ValidatorAddr: sdk.ValAddress(validator.PubKey.Address())} // no signing info yet - checkValidatorSigningInfo(t, mapp, keeper, sdk.ValAddress(addr1), false) + checkValidatorSigningInfo(t, mapp, keeper, sdk.ConsAddress(addr1), false) // unjail should fail with unknown validator - res := mock.SignCheckDeliver(t, mapp.BaseApp, []sdk.Msg{unjailMsg}, []int64{0}, []int64{1}, false, priv1) + res := mock.SignCheckDeliver(t, mapp.BaseApp, []sdk.Msg{unjailMsg}, []int64{0}, []int64{1}, false, false, priv1) require.Equal(t, sdk.ToABCICode(DefaultCodespace, CodeValidatorNotJailed), res.Code) } diff --git a/x/slashing/client/cli/query.go b/x/slashing/client/cli/query.go index 9f6d834dd..87d0ad41d 100644 --- a/x/slashing/client/cli/query.go +++ b/x/slashing/client/cli/query.go @@ -25,7 +25,7 @@ func GetCmdQuerySigningInfo(storeName string, cdc *wire.Codec) *cobra.Command { return err } - key := slashing.GetValidatorSigningInfoKey(sdk.ValAddress(pk.Address())) + key := slashing.GetValidatorSigningInfoKey(sdk.ConsAddress(pk.Address())) cliCtx := context.NewCLIContext().WithCodec(cdc) res, err := cliCtx.QueryStore(key, storeName) diff --git a/x/slashing/client/rest/query.go b/x/slashing/client/rest/query.go index 291679375..78c4a2d2f 100644 --- a/x/slashing/client/rest/query.go +++ b/x/slashing/client/rest/query.go @@ -30,7 +30,7 @@ func signingInfoHandlerFn(cliCtx context.CLIContext, storeName string, cdc *wire return } - key := slashing.GetValidatorSigningInfoKey(sdk.ValAddress(pk.Address())) + key := slashing.GetValidatorSigningInfoKey(sdk.ConsAddress(pk.Address())) res, err := cliCtx.QueryStore(key, storeName) if err != nil { diff --git a/x/slashing/client/rest/tx.go b/x/slashing/client/rest/tx.go index c7efdc97e..faf0341ce 100644 --- a/x/slashing/client/rest/tx.go +++ b/x/slashing/client/rest/tx.go @@ -7,6 +7,7 @@ import ( "io/ioutil" "net/http" + "github.com/cosmos/cosmos-sdk/client" "github.com/cosmos/cosmos-sdk/client/context" "github.com/cosmos/cosmos-sdk/client/utils" "github.com/cosmos/cosmos-sdk/crypto/keys" @@ -33,37 +34,39 @@ type UnjailBody struct { AccountNumber int64 `json:"account_number"` Sequence int64 `json:"sequence"` Gas int64 `json:"gas"` + GasAdjustment string `json:"gas_adjustment"` ValidatorAddr string `json:"validator_addr"` } +// nolint: gocyclo func unjailRequestHandlerFn(cdc *wire.Codec, kb keys.Keybase, cliCtx context.CLIContext) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { var m UnjailBody body, err := ioutil.ReadAll(r.Body) if err != nil { - utils.WriteErrorResponse(&w, http.StatusBadRequest, err.Error()) + utils.WriteErrorResponse(w, http.StatusBadRequest, err.Error()) return } err = json.Unmarshal(body, &m) if err != nil { - utils.WriteErrorResponse(&w, http.StatusBadRequest, err.Error()) + utils.WriteErrorResponse(w, http.StatusBadRequest, err.Error()) return } info, err := kb.Get(m.LocalAccountName) if err != nil { - utils.WriteErrorResponse(&w, http.StatusUnauthorized, err.Error()) + utils.WriteErrorResponse(w, http.StatusUnauthorized, err.Error()) return } valAddr, err := sdk.ValAddressFromBech32(m.ValidatorAddr) if err != nil { - utils.WriteErrorResponse(&w, http.StatusInternalServerError, fmt.Sprintf("Couldn't decode validator. Error: %s", err.Error())) + utils.WriteErrorResponse(w, http.StatusInternalServerError, fmt.Sprintf("Couldn't decode validator. Error: %s", err.Error())) return } if !bytes.Equal(info.GetPubKey().Address(), valAddr) { - utils.WriteErrorResponse(&w, http.StatusUnauthorized, "Must use own validator address") + utils.WriteErrorResponse(w, http.StatusUnauthorized, "Must use own validator address") return } @@ -77,10 +80,20 @@ func unjailRequestHandlerFn(cdc *wire.Codec, kb keys.Keybase, cliCtx context.CLI msg := slashing.NewMsgUnjail(valAddr) - if m.Gas == 0 { - newCtx, err := utils.EnrichCtxWithGas(txCtx, cliCtx, m.LocalAccountName, m.Password, []sdk.Msg{msg}) + adjustment, ok := utils.ParseFloat64OrReturnBadRequest(w, m.GasAdjustment, client.DefaultGasAdjustment) + if !ok { + return + } + cliCtx = cliCtx.WithGasAdjustment(adjustment) + + if utils.HasDryRunArg(r) || m.Gas == 0 { + newCtx, err := utils.EnrichCtxWithGas(txCtx, cliCtx, m.LocalAccountName, []sdk.Msg{msg}) if err != nil { - utils.WriteErrorResponse(&w, http.StatusInternalServerError, err.Error()) + utils.WriteErrorResponse(w, http.StatusInternalServerError, err.Error()) + return + } + if utils.HasDryRunArg(r) { + utils.WriteSimulationResponse(w, txCtx.Gas) return } txCtx = newCtx @@ -88,19 +101,19 @@ func unjailRequestHandlerFn(cdc *wire.Codec, kb keys.Keybase, cliCtx context.CLI txBytes, err := txCtx.BuildAndSign(m.LocalAccountName, m.Password, []sdk.Msg{msg}) if err != nil { - utils.WriteErrorResponse(&w, http.StatusUnauthorized, "Must use own validator address") + utils.WriteErrorResponse(w, http.StatusUnauthorized, "Must use own validator address") return } res, err := cliCtx.BroadcastTx(txBytes) if err != nil { - utils.WriteErrorResponse(&w, http.StatusInternalServerError, err.Error()) + utils.WriteErrorResponse(w, http.StatusInternalServerError, err.Error()) return } output, err := json.MarshalIndent(res, "", " ") if err != nil { - utils.WriteErrorResponse(&w, http.StatusInternalServerError, err.Error()) + utils.WriteErrorResponse(w, http.StatusInternalServerError, err.Error()) return } diff --git a/x/slashing/errors.go b/x/slashing/errors.go index 4573d5e14..77cb2d28e 100644 --- a/x/slashing/errors.go +++ b/x/slashing/errors.go @@ -12,20 +12,28 @@ const ( // Default slashing codespace DefaultCodespace sdk.CodespaceType = 10 - CodeInvalidValidator CodeType = 101 - CodeValidatorJailed CodeType = 102 - CodeValidatorNotJailed CodeType = 103 + CodeInvalidValidator CodeType = 101 + CodeValidatorJailed CodeType = 102 + CodeValidatorNotJailed CodeType = 103 + CodeMissingSelfDelegation CodeType = 104 ) func ErrNoValidatorForAddress(codespace sdk.CodespaceType) sdk.Error { return sdk.NewError(codespace, CodeInvalidValidator, "that address is not associated with any known validator") } + func ErrBadValidatorAddr(codespace sdk.CodespaceType) sdk.Error { return sdk.NewError(codespace, CodeInvalidValidator, "validator does not exist for that address") } + func ErrValidatorJailed(codespace sdk.CodespaceType) sdk.Error { return sdk.NewError(codespace, CodeValidatorJailed, "validator still jailed, cannot yet be unjailed") } + func ErrValidatorNotJailed(codespace sdk.CodespaceType) sdk.Error { return sdk.NewError(codespace, CodeValidatorNotJailed, "validator not jailed, cannot be unjailed") } + +func ErrMissingSelfDelegation(codespace sdk.CodespaceType) sdk.Error { + return sdk.NewError(codespace, CodeMissingSelfDelegation, "validator has no self-delegation; cannot be unjailed") +} diff --git a/x/slashing/handler.go b/x/slashing/handler.go index d79ea73c2..c43ed6be6 100644 --- a/x/slashing/handler.go +++ b/x/slashing/handler.go @@ -19,35 +19,38 @@ func NewHandler(k Keeper) sdk.Handler { // Validators must submit a transaction to unjail itself after // having been jailed (and thus unbonded) for downtime func handleMsgUnjail(ctx sdk.Context, msg MsgUnjail, k Keeper) sdk.Result { - - // Validator must exist validator := k.validatorSet.Validator(ctx, msg.ValidatorAddr) if validator == nil { return ErrNoValidatorForAddress(k.codespace).Result() } + // cannot be unjailed if no self-delegation exists + selfDel := k.validatorSet.Delegation(ctx, sdk.AccAddress(msg.ValidatorAddr), msg.ValidatorAddr) + if selfDel == nil { + return ErrMissingSelfDelegation(k.codespace).Result() + } + if !validator.GetJailed() { return ErrValidatorNotJailed(k.codespace).Result() } - addr := sdk.ValAddress(validator.GetPubKey().Address()) + addr := sdk.ConsAddress(validator.GetPubKey().Address()) - // Signing info must exist info, found := k.getValidatorSigningInfo(ctx, addr) if !found { return ErrNoValidatorForAddress(k.codespace).Result() } - // Cannot be unjailed until out of jail + // cannot be unjailed until out of jail if ctx.BlockHeader().Time.Before(info.JailedUntil) { return ErrValidatorJailed(k.codespace).Result() } - // Update the starting height (so the validator can't be immediately jailed again) + // update the starting height so the validator can't be immediately jailed + // again info.StartHeight = ctx.BlockHeight() k.setValidatorSigningInfo(ctx, addr, info) - // Unjail the validator k.validatorSet.Unjail(ctx, validator.GetPubKey()) tags := sdk.NewTags("action", []byte("unjail"), "validator", []byte(msg.ValidatorAddr.String())) diff --git a/x/slashing/handler_test.go b/x/slashing/handler_test.go index 8e3b719f4..c5afb8738 100644 --- a/x/slashing/handler_test.go +++ b/x/slashing/handler_test.go @@ -2,6 +2,7 @@ package slashing import ( "testing" + "time" "github.com/stretchr/testify/require" @@ -19,7 +20,7 @@ func TestCannotUnjailUnlessJailed(t *testing.T) { got := stake.NewHandler(sk)(ctx, msg) require.True(t, got.IsOK()) stake.EndBlocker(ctx, sk) - require.Equal(t, ck.GetCoins(ctx, addr), sdk.Coins{{sk.GetParams(ctx).BondDenom, initCoins.Sub(amt)}}) + 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, sdk.ValAddress(addr)).GetPower())) // assert non-jailed validator can't be unjailed @@ -27,3 +28,64 @@ func TestCannotUnjailUnlessJailed(t *testing.T) { require.False(t, got.IsOK(), "allowed unjail of non-jailed validator") require.Equal(t, sdk.ToABCICode(DefaultCodespace, CodeValidatorNotJailed), got.Code) } + +func TestJailedValidatorDelegations(t *testing.T) { + ctx, _, stakeKeeper, _, slashingKeeper := createTestInput(t) + + stakeParams := stakeKeeper.GetParams(ctx) + stakeParams.UnbondingTime = 0 + stakeKeeper.SetParams(ctx, stakeParams) + + // create a validator + amount := int64(10) + valPubKey, bondAmount := pks[0], sdk.NewInt(amount) + valAddr, consAddr := sdk.ValAddress(addrs[1]), sdk.ConsAddress(addrs[0]) + + msgCreateVal := newTestMsgCreateValidator(valAddr, valPubKey, bondAmount) + got := stake.NewHandler(stakeKeeper)(ctx, msgCreateVal) + require.True(t, got.IsOK(), "expected create validator msg to be ok, got: %v", got) + + // set dummy signing info + newInfo := ValidatorSigningInfo{ + StartHeight: int64(0), + IndexOffset: int64(0), + JailedUntil: time.Unix(0, 0), + SignedBlocksCounter: int64(0), + } + slashingKeeper.setValidatorSigningInfo(ctx, consAddr, newInfo) + + // delegate tokens to the validator + delAddr := sdk.AccAddress(addrs[2]) + msgDelegate := newTestMsgDelegate(delAddr, valAddr, bondAmount) + got = stake.NewHandler(stakeKeeper)(ctx, msgDelegate) + require.True(t, got.IsOK(), "expected delegation to be ok, got %v", got) + + unbondShares := sdk.NewDec(10) + + // unbond validator total self-delegations (which should jail the validator) + msgBeginUnbonding := stake.NewMsgBeginUnbonding(sdk.AccAddress(valAddr), valAddr, unbondShares) + got = stake.NewHandler(stakeKeeper)(ctx, msgBeginUnbonding) + require.True(t, got.IsOK(), "expected begin unbonding validator msg to be ok, got: %v", got) + + msgCompleteUnbonding := stake.NewMsgCompleteUnbonding(sdk.AccAddress(valAddr), valAddr) + got = stake.NewHandler(stakeKeeper)(ctx, msgCompleteUnbonding) + require.True(t, got.IsOK(), "expected complete unbonding validator msg to be ok, got: %v", got) + + // verify validator still exists and is jailed + validator, found := stakeKeeper.GetValidator(ctx, valAddr) + require.True(t, found) + require.True(t, validator.GetJailed()) + + // verify the validator cannot unjail itself + got = NewHandler(slashingKeeper)(ctx, NewMsgUnjail(valAddr)) + require.False(t, got.IsOK(), "expected jailed validator to not be able to unjail, got: %v", got) + + // self-delegate to validator + msgSelfDelegate := newTestMsgDelegate(sdk.AccAddress(valAddr), valAddr, bondAmount) + got = stake.NewHandler(stakeKeeper)(ctx, msgSelfDelegate) + require.True(t, got.IsOK(), "expected delegation to not be ok, got %v", got) + + // verify the validator can now unjail itself + got = NewHandler(slashingKeeper)(ctx, NewMsgUnjail(valAddr)) + require.True(t, got.IsOK(), "expected jailed validator to be able to unjail, got: %v", got) +} diff --git a/x/slashing/hooks.go b/x/slashing/hooks.go new file mode 100644 index 000000000..f5f3cc48c --- /dev/null +++ b/x/slashing/hooks.go @@ -0,0 +1,46 @@ +package slashing + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" +) + +// Create a new slashing period when a validator is bonded +func (k Keeper) onValidatorBonded(ctx sdk.Context, address sdk.ConsAddress) { + slashingPeriod := ValidatorSlashingPeriod{ + ValidatorAddr: address, + StartHeight: ctx.BlockHeight(), + EndHeight: 0, + SlashedSoFar: sdk.ZeroDec(), + } + k.addOrUpdateValidatorSlashingPeriod(ctx, slashingPeriod) +} + +// Mark the slashing period as having ended when a validator begins unbonding +func (k Keeper) onValidatorBeginUnbonding(ctx sdk.Context, address sdk.ConsAddress) { + slashingPeriod := k.getValidatorSlashingPeriodForHeight(ctx, address, ctx.BlockHeight()) + slashingPeriod.EndHeight = ctx.BlockHeight() + k.addOrUpdateValidatorSlashingPeriod(ctx, slashingPeriod) +} + +// Wrapper struct for sdk.ValidatorHooks +type ValidatorHooks struct { + k Keeper +} + +// Assert implementation +var _ sdk.ValidatorHooks = ValidatorHooks{} + +// Return a sdk.ValidatorHooks interface over the wrapper struct +func (k Keeper) ValidatorHooks() sdk.ValidatorHooks { + return ValidatorHooks{k} +} + +// Implements sdk.ValidatorHooks +func (v ValidatorHooks) OnValidatorBonded(ctx sdk.Context, address sdk.ConsAddress) { + v.k.onValidatorBonded(ctx, address) +} + +// Implements sdk.ValidatorHooks +func (v ValidatorHooks) OnValidatorBeginUnbonding(ctx sdk.Context, address sdk.ConsAddress) { + v.k.onValidatorBeginUnbonding(ctx, address) +} diff --git a/x/slashing/hooks_test.go b/x/slashing/hooks_test.go new file mode 100644 index 000000000..0731fd8f2 --- /dev/null +++ b/x/slashing/hooks_test.go @@ -0,0 +1,26 @@ +package slashing + +import ( + "testing" + + "github.com/stretchr/testify/require" + + sdk "github.com/cosmos/cosmos-sdk/types" +) + +func TestHookOnValidatorBonded(t *testing.T) { + ctx, _, _, _, keeper := createTestInput(t) + addr := sdk.ConsAddress(addrs[0]) + keeper.onValidatorBonded(ctx, addr) + period := keeper.getValidatorSlashingPeriodForHeight(ctx, addr, ctx.BlockHeight()) + require.Equal(t, ValidatorSlashingPeriod{addr, ctx.BlockHeight(), 0, sdk.ZeroDec()}, period) +} + +func TestHookOnValidatorBeginUnbonding(t *testing.T) { + ctx, _, _, _, keeper := createTestInput(t) + addr := sdk.ConsAddress(addrs[0]) + keeper.onValidatorBonded(ctx, addr) + keeper.onValidatorBeginUnbonding(ctx, addr) + period := keeper.getValidatorSlashingPeriodForHeight(ctx, addr, ctx.BlockHeight()) + require.Equal(t, ValidatorSlashingPeriod{addr, ctx.BlockHeight(), ctx.BlockHeight(), sdk.ZeroDec()}, period) +} diff --git a/x/slashing/keeper.go b/x/slashing/keeper.go index 6d8e47cbe..272516585 100644 --- a/x/slashing/keeper.go +++ b/x/slashing/keeper.go @@ -40,7 +40,7 @@ func (k Keeper) handleDoubleSign(ctx sdk.Context, addr crypto.Address, infractio logger := ctx.Logger().With("module", "x/slashing") time := ctx.BlockHeader().Time age := time.Sub(timestamp) - address := sdk.ValAddress(addr) + address := sdk.ConsAddress(addr) pubkey, err := k.getPubkey(ctx, addr) if err != nil { panic(fmt.Sprintf("Validator address %v not found", addr)) @@ -56,8 +56,14 @@ 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)) + // 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, address, fraction, infractionHeight) + logger.Info(fmt.Sprintf("Fraction slashed capped by slashing period from %v to %v", fraction, revisedFraction)) + // Slash validator - k.validatorSet.Slash(ctx, pubkey, infractionHeight, power, k.SlashFractionDoubleSign(ctx)) + k.validatorSet.Slash(ctx, pubkey, infractionHeight, power, revisedFraction) // Jail validator k.validatorSet.Jail(ctx, pubkey) @@ -76,7 +82,7 @@ func (k Keeper) handleDoubleSign(ctx sdk.Context, addr crypto.Address, infractio func (k Keeper) handleValidatorSignature(ctx sdk.Context, addr crypto.Address, power int64, signed bool) { logger := ctx.Logger().With("module", "x/slashing") height := ctx.BlockHeight() - address := sdk.ValAddress(addr) + address := sdk.ConsAddress(addr) pubkey, err := k.getPubkey(ctx, addr) if err != nil { panic(fmt.Sprintf("Validator address %v not found", addr)) @@ -169,7 +175,3 @@ func (k Keeper) deleteAddrPubkeyRelation(ctx sdk.Context, addr crypto.Address) { store := ctx.KVStore(k.storeKey) store.Delete(getAddrPubkeyRelationKey(addr)) } - -func getAddrPubkeyRelationKey(address []byte) []byte { - return append([]byte{0x03}, address...) -} diff --git a/x/slashing/keeper_test.go b/x/slashing/keeper_test.go index 3bdb043a8..c0932acf4 100644 --- a/x/slashing/keeper_test.go +++ b/x/slashing/keeper_test.go @@ -24,13 +24,14 @@ func TestHandleDoubleSign(t *testing.T) { // initial setup ctx, ck, sk, _, keeper := createTestInput(t) + sk = sk.WithValidatorHooks(keeper.ValidatorHooks()) amtInt := int64(100) addr, val, amt := addrs[0], pks[0], sdk.NewInt(amtInt) got := stake.NewHandler(sk)(ctx, newTestMsgCreateValidator(sdk.ValAddress(addr), val, amt)) require.True(t, got.IsOK()) validatorUpdates := stake.EndBlocker(ctx, sk) keeper.AddValidators(ctx, validatorUpdates) - require.Equal(t, ck.GetCoins(ctx, addr), sdk.Coins{{sk.GetParams(ctx).BondDenom, initCoins.Sub(amt)}}) + 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, sdk.ValAddress(addr)).GetPower())) // handle a signature to set signing info @@ -58,12 +59,68 @@ func TestHandleDoubleSign(t *testing.T) { ) } +// Test that the amount a validator is slashed for multiple double signs +// is correctly capped by the slashing period in which they were committed +func TestSlashingPeriodCap(t *testing.T) { + + // initial setup + ctx, ck, sk, _, keeper := createTestInput(t) + sk = sk.WithValidatorHooks(keeper.ValidatorHooks()) + amtInt := int64(100) + addr, val, amt := addrs[0], pks[0], sdk.NewInt(amtInt) + got := stake.NewHandler(sk)(ctx, newTestMsgCreateValidator(addr, 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())) + + // handle a signature to set signing info + keeper.handleValidatorSignature(ctx, val.Address(), amtInt, true) + + // double sign less than max age + keeper.handleDoubleSign(ctx, val.Address(), 0, time.Unix(0, 0), amtInt) + + // should be jailed + require.True(t, sk.Validator(ctx, addr).GetJailed()) + // update block height + ctx = ctx.WithBlockHeight(int64(1)) + // unjail to measure power + sk.Unjail(ctx, val) + // 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, val.Address(), 0, time.Unix(0, 0), amtInt) + // should be jailed + require.True(t, sk.Validator(ctx, addr).GetJailed()) + // update block height + ctx = ctx.WithBlockHeight(int64(2)) + // unjail to measure power + sk.Unjail(ctx, val) + // 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()) + + // double sign again, new slashing period + keeper.handleDoubleSign(ctx, val.Address(), 2, time.Unix(0, 0), amtInt) + // should be jailed + require.True(t, sk.Validator(ctx, addr).GetJailed()) + // unjail to measure power + sk.Unjail(ctx, val) + // 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()) +} + // Test a validator through uptime, downtime, revocation, // unrevocation, starting height reset, and revocation again func TestHandleAbsentValidator(t *testing.T) { // initial setup ctx, ck, sk, _, keeper := createTestInput(t) + sk = sk.WithValidatorHooks(keeper.ValidatorHooks()) amtInt := int64(100) addr, val, amt := addrs[0], pks[0], sdk.NewInt(amtInt) sh := stake.NewHandler(sk) @@ -72,9 +129,9 @@ func TestHandleAbsentValidator(t *testing.T) { require.True(t, got.IsOK()) validatorUpdates := stake.EndBlocker(ctx, sk) keeper.AddValidators(ctx, validatorUpdates) - require.Equal(t, ck.GetCoins(ctx, addr), sdk.Coins{{sk.GetParams(ctx).BondDenom, initCoins.Sub(amt)}}) + 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, sdk.ValAddress(addr)).GetPower())) - info, found := keeper.getValidatorSigningInfo(ctx, sdk.ValAddress(val.Address())) + info, found := keeper.getValidatorSigningInfo(ctx, sdk.ConsAddress(val.Address())) require.False(t, found) require.Equal(t, int64(0), info.StartHeight) require.Equal(t, int64(0), info.IndexOffset) @@ -89,7 +146,7 @@ func TestHandleAbsentValidator(t *testing.T) { ctx = ctx.WithBlockHeight(height) keeper.handleValidatorSignature(ctx, val.Address(), amtInt, true) } - info, found = keeper.getValidatorSigningInfo(ctx, sdk.ValAddress(val.Address())) + 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), info.SignedBlocksCounter) @@ -99,7 +156,7 @@ func TestHandleAbsentValidator(t *testing.T) { ctx = ctx.WithBlockHeight(height) keeper.handleValidatorSignature(ctx, val.Address(), amtInt, false) } - info, found = keeper.getValidatorSigningInfo(ctx, sdk.ValAddress(val.Address())) + 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), info.SignedBlocksCounter) @@ -113,14 +170,14 @@ func TestHandleAbsentValidator(t *testing.T) { // 501st block missed ctx = ctx.WithBlockHeight(height) keeper.handleValidatorSignature(ctx, val.Address(), amtInt, false) - info, found = keeper.getValidatorSigningInfo(ctx, sdk.ValAddress(val.Address())) + 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)-1, info.SignedBlocksCounter) // validator should have been jailed validator, _ = sk.GetValidatorByPubKey(ctx, val) - require.Equal(t, sdk.Unbonded, validator.GetStatus()) + require.Equal(t, sdk.Unbonding, validator.GetStatus()) // unrevocation should fail prior to jail expiration got = slh(ctx, NewMsgUnjail(sdk.ValAddress(addr))) @@ -141,7 +198,7 @@ func TestHandleAbsentValidator(t *testing.T) { require.Equal(t, int64(amtInt)-slashAmt, pool.BondedTokens.RoundInt64()) // validator start height should have been changed - info, found = keeper.getValidatorSigningInfo(ctx, sdk.ValAddress(val.Address())) + 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) @@ -167,7 +224,7 @@ func TestHandleAbsentValidator(t *testing.T) { keeper.handleValidatorSignature(ctx, val.Address(), amtInt, false) } validator, _ = sk.GetValidatorByPubKey(ctx, val) - require.Equal(t, sdk.Unbonded, validator.GetStatus()) + require.Equal(t, sdk.Unbonding, validator.GetStatus()) } // Test a new validator entering the validator set @@ -182,7 +239,7 @@ func TestHandleNewValidator(t *testing.T) { require.True(t, got.IsOK()) validatorUpdates := stake.EndBlocker(ctx, sk) keeper.AddValidators(ctx, validatorUpdates) - require.Equal(t, ck.GetCoins(ctx, addr), sdk.Coins{{sk.GetParams(ctx).BondDenom, initCoins.SubRaw(amt)}}) + require.Equal(t, ck.GetCoins(ctx, sdk.AccAddress(addr)), sdk.Coins{{sk.GetParams(ctx).BondDenom, initCoins.SubRaw(amt)}}) require.Equal(t, sdk.NewDec(amt), sk.Validator(ctx, sdk.ValAddress(addr)).GetPower()) // 1000 first blocks not a validator @@ -193,7 +250,7 @@ func TestHandleNewValidator(t *testing.T) { ctx = ctx.WithBlockHeight(keeper.SignedBlocksWindow(ctx) + 2) keeper.handleValidatorSignature(ctx, val.Address(), 100, false) - info, found := keeper.getValidatorSigningInfo(ctx, sdk.ValAddress(val.Address())) + info, found := keeper.getValidatorSigningInfo(ctx, sdk.ConsAddress(val.Address())) require.True(t, found) require.Equal(t, int64(keeper.SignedBlocksWindow(ctx)+1), info.StartHeight) require.Equal(t, int64(2), info.IndexOffset) @@ -236,7 +293,7 @@ func TestHandleAlreadyJailed(t *testing.T) { // validator should have been jailed and slashed validator, _ := sk.GetValidatorByPubKey(ctx, val) - require.Equal(t, sdk.Unbonded, validator.GetStatus()) + require.Equal(t, sdk.Unbonding, validator.GetStatus()) // validator should have been slashed require.Equal(t, int64(amtInt-1), validator.GetTokens().RoundInt64()) diff --git a/x/slashing/keys.go b/x/slashing/keys.go new file mode 100644 index 000000000..2af9e069a --- /dev/null +++ b/x/slashing/keys.go @@ -0,0 +1,43 @@ +package slashing + +import ( + "encoding/binary" + + sdk "github.com/cosmos/cosmos-sdk/types" +) + +// key prefix bytes +var ( + ValidatorSigningInfoKey = []byte{0x01} // Prefix for signing info + ValidatorSigningBitArrayKey = []byte{0x02} // Prefix for signature bit array + ValidatorSlashingPeriodKey = []byte{0x03} // Prefix for slashing period + AddrPubkeyRelationKey = []byte{0x04} // Prefix for address-pubkey relation +) + +// stored by *Tendermint* address (not owner address) +func GetValidatorSigningInfoKey(v sdk.ConsAddress) []byte { + return append(ValidatorSigningInfoKey, v.Bytes()...) +} + +// stored by *Tendermint* address (not owner address) +func GetValidatorSigningBitArrayKey(v sdk.ConsAddress, i int64) []byte { + b := make([]byte, 8) + binary.LittleEndian.PutUint64(b, uint64(i)) + return append(ValidatorSigningBitArrayKey, append(v.Bytes(), b...)...) +} + +// stored by *Tendermint* address (not owner address) +func GetValidatorSlashingPeriodPrefix(v sdk.ConsAddress) []byte { + return append(ValidatorSlashingPeriodKey, v.Bytes()...) +} + +// stored by *Tendermint* address (not owner address) followed by start height +func GetValidatorSlashingPeriodKey(v sdk.ConsAddress, startHeight int64) []byte { + b := make([]byte, 8) + binary.LittleEndian.PutUint64(b, uint64(startHeight)) + return append(GetValidatorSlashingPeriodPrefix(v), b...) +} + +func getAddrPubkeyRelationKey(address []byte) []byte { + return append(AddrPubkeyRelationKey, address...) +} diff --git a/x/slashing/signing_info.go b/x/slashing/signing_info.go index 25a83e833..e76fea53f 100644 --- a/x/slashing/signing_info.go +++ b/x/slashing/signing_info.go @@ -1,7 +1,6 @@ package slashing import ( - "encoding/binary" "fmt" "time" @@ -9,7 +8,7 @@ import ( ) // Stored by *validator* address (not owner address) -func (k Keeper) getValidatorSigningInfo(ctx sdk.Context, address sdk.ValAddress) (info ValidatorSigningInfo, found bool) { +func (k Keeper) getValidatorSigningInfo(ctx sdk.Context, address sdk.ConsAddress) (info ValidatorSigningInfo, found bool) { store := ctx.KVStore(k.storeKey) bz := store.Get(GetValidatorSigningInfoKey(address)) if bz == nil { @@ -22,14 +21,14 @@ func (k Keeper) getValidatorSigningInfo(ctx sdk.Context, address sdk.ValAddress) } // Stored by *validator* address (not owner address) -func (k Keeper) setValidatorSigningInfo(ctx sdk.Context, address sdk.ValAddress, info ValidatorSigningInfo) { +func (k Keeper) setValidatorSigningInfo(ctx sdk.Context, address sdk.ConsAddress, info ValidatorSigningInfo) { store := ctx.KVStore(k.storeKey) bz := k.cdc.MustMarshalBinary(info) store.Set(GetValidatorSigningInfoKey(address), bz) } // Stored by *validator* address (not owner address) -func (k Keeper) getValidatorSigningBitArray(ctx sdk.Context, address sdk.ValAddress, index int64) (signed bool) { +func (k Keeper) getValidatorSigningBitArray(ctx sdk.Context, address sdk.ConsAddress, index int64) (signed bool) { store := ctx.KVStore(k.storeKey) bz := store.Get(GetValidatorSigningBitArrayKey(address, index)) if bz == nil { @@ -42,7 +41,7 @@ func (k Keeper) getValidatorSigningBitArray(ctx sdk.Context, address sdk.ValAddr } // Stored by *validator* address (not owner address) -func (k Keeper) setValidatorSigningBitArray(ctx sdk.Context, address sdk.ValAddress, index int64, signed bool) { +func (k Keeper) setValidatorSigningBitArray(ctx sdk.Context, address sdk.ConsAddress, index int64, signed bool) { store := ctx.KVStore(k.storeKey) bz := k.cdc.MustMarshalBinary(signed) store.Set(GetValidatorSigningBitArrayKey(address, index), bz) @@ -71,15 +70,3 @@ func (i ValidatorSigningInfo) HumanReadableString() string { return fmt.Sprintf("Start height: %d, index offset: %d, jailed until: %v, signed blocks counter: %d", i.StartHeight, i.IndexOffset, i.JailedUntil, i.SignedBlocksCounter) } - -// Stored by *validator* address (not owner address) -func GetValidatorSigningInfoKey(v sdk.ValAddress) []byte { - return append([]byte{0x01}, v.Bytes()...) -} - -// Stored by *validator* address (not owner address) -func GetValidatorSigningBitArrayKey(v sdk.ValAddress, i int64) []byte { - b := make([]byte, 8) - binary.LittleEndian.PutUint64(b, uint64(i)) - return append([]byte{0x02}, append(v.Bytes(), b...)...) -} diff --git a/x/slashing/signing_info_test.go b/x/slashing/signing_info_test.go index f92c43581..7aff0da95 100644 --- a/x/slashing/signing_info_test.go +++ b/x/slashing/signing_info_test.go @@ -11,7 +11,7 @@ import ( func TestGetSetValidatorSigningInfo(t *testing.T) { ctx, _, _, _, keeper := createTestInput(t) - info, found := keeper.getValidatorSigningInfo(ctx, sdk.ValAddress(addrs[0])) + info, found := keeper.getValidatorSigningInfo(ctx, sdk.ConsAddress(addrs[0])) require.False(t, found) newInfo := ValidatorSigningInfo{ StartHeight: int64(4), @@ -19,8 +19,8 @@ func TestGetSetValidatorSigningInfo(t *testing.T) { JailedUntil: time.Unix(2, 0), SignedBlocksCounter: int64(10), } - keeper.setValidatorSigningInfo(ctx, sdk.ValAddress(addrs[0]), newInfo) - info, found = keeper.getValidatorSigningInfo(ctx, sdk.ValAddress(addrs[0])) + keeper.setValidatorSigningInfo(ctx, sdk.ConsAddress(addrs[0]), newInfo) + info, found = keeper.getValidatorSigningInfo(ctx, sdk.ConsAddress(addrs[0])) require.True(t, found) require.Equal(t, info.StartHeight, int64(4)) require.Equal(t, info.IndexOffset, int64(3)) @@ -30,9 +30,9 @@ func TestGetSetValidatorSigningInfo(t *testing.T) { func TestGetSetValidatorSigningBitArray(t *testing.T) { ctx, _, _, _, keeper := createTestInput(t) - signed := keeper.getValidatorSigningBitArray(ctx, sdk.ValAddress(addrs[0]), 0) + signed := keeper.getValidatorSigningBitArray(ctx, sdk.ConsAddress(addrs[0]), 0) require.False(t, signed) // treat empty key as unsigned - keeper.setValidatorSigningBitArray(ctx, sdk.ValAddress(addrs[0]), 0, true) - signed = keeper.getValidatorSigningBitArray(ctx, sdk.ValAddress(addrs[0]), 0) + keeper.setValidatorSigningBitArray(ctx, sdk.ConsAddress(addrs[0]), 0, true) + signed = keeper.getValidatorSigningBitArray(ctx, sdk.ConsAddress(addrs[0]), 0) require.True(t, signed) // now should be signed } diff --git a/x/slashing/simulation/msgs.go b/x/slashing/simulation/msgs.go index dc7f63ba6..9cbb2c48a 100644 --- a/x/slashing/simulation/msgs.go +++ b/x/slashing/simulation/msgs.go @@ -5,8 +5,6 @@ import ( "math/rand" "testing" - "github.com/stretchr/testify/require" - "github.com/tendermint/tendermint/crypto" "github.com/cosmos/cosmos-sdk/baseapp" @@ -17,11 +15,13 @@ import ( // SimulateMsgUnjail func SimulateMsgUnjail(k slashing.Keeper) simulation.Operation { - return func(t *testing.T, r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, keys []crypto.PrivKey, log string, event func(string)) (action string, fOp []simulation.FutureOperation, err sdk.Error) { + return func(tb testing.TB, r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, keys []crypto.PrivKey, log string, event func(string)) (action string, fOp []simulation.FutureOperation, err sdk.Error) { key := simulation.RandomKey(r, keys) address := sdk.ValAddress(key.PubKey().Address()) msg := slashing.NewMsgUnjail(address) - require.Nil(t, msg.ValidateBasic(), "expected msg to pass ValidateBasic: %s", msg.GetSignBytes()) + if msg.ValidateBasic() != nil { + tb.Fatalf("expected msg to pass ValidateBasic: %s, log %s", msg.GetSignBytes(), log) + } ctx, write := ctx.CacheContext() result := slashing.NewHandler(k)(ctx, msg) if result.IsOK() { diff --git a/x/slashing/slashing_period.go b/x/slashing/slashing_period.go new file mode 100644 index 000000000..61d25071e --- /dev/null +++ b/x/slashing/slashing_period.go @@ -0,0 +1,107 @@ +package slashing + +import ( + "encoding/binary" + "fmt" + + sdk "github.com/cosmos/cosmos-sdk/types" +) + +// Cap an infraction's slash amount by the slashing period in which it was committed +func (k Keeper) capBySlashingPeriod(ctx sdk.Context, address sdk.ConsAddress, fraction sdk.Dec, infractionHeight int64) (revisedFraction sdk.Dec) { + + // Fetch the newest slashing period starting before this infraction was committed + slashingPeriod := k.getValidatorSlashingPeriodForHeight(ctx, address, infractionHeight) + + // 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)) + } + + // Calculate the updated total slash amount + // This is capped at the slashing fraction for the worst infraction within this slashing period + totalToSlash := sdk.MaxDec(slashingPeriod.SlashedSoFar, fraction) + + // Calculate the remainder which we now must slash + revisedFraction = totalToSlash.Sub(slashingPeriod.SlashedSoFar) + + // Update the slashing period struct + slashingPeriod.SlashedSoFar = totalToSlash + k.addOrUpdateValidatorSlashingPeriod(ctx, slashingPeriod) + + return +} + +// Stored by validator Tendermint address (not owner address) +// This function retrieves the most recent slashing period starting +// before a particular height - so the slashing period that was "in effect" +// at the time of an infraction committed at that height. +func (k Keeper) getValidatorSlashingPeriodForHeight(ctx sdk.Context, address sdk.ConsAddress, height int64) (slashingPeriod ValidatorSlashingPeriod) { + store := ctx.KVStore(k.storeKey) + // Get the most recent slashing period at or before the infraction height + start := GetValidatorSlashingPeriodPrefix(address) + end := sdk.PrefixEndBytes(GetValidatorSlashingPeriodKey(address, height)) + iterator := store.ReverseIterator(start, end) + if !iterator.Valid() { + panic("expected to find slashing period, but none was found") + } + slashingPeriod = k.unmarshalSlashingPeriodKeyValue(iterator.Key(), iterator.Value()) + return +} + +// Stored by validator Tendermint address (not owner address) +// This function sets a validator slashing period for a particular validator, +// start height, end height, and current slashed-so-far total, or updates +// an existing slashing period for the same validator and start height. +func (k Keeper) addOrUpdateValidatorSlashingPeriod(ctx sdk.Context, slashingPeriod ValidatorSlashingPeriod) { + slashingPeriodValue := ValidatorSlashingPeriodValue{ + EndHeight: slashingPeriod.EndHeight, + SlashedSoFar: slashingPeriod.SlashedSoFar, + } + store := ctx.KVStore(k.storeKey) + bz := k.cdc.MustMarshalBinary(slashingPeriodValue) + store.Set(GetValidatorSlashingPeriodKey(slashingPeriod.ValidatorAddr, slashingPeriod.StartHeight), bz) +} + +// Unmarshal key/value into a ValidatorSlashingPeriod +func (k Keeper) unmarshalSlashingPeriodKeyValue(key []byte, value []byte) ValidatorSlashingPeriod { + 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])) + return ValidatorSlashingPeriod{ + ValidatorAddr: address, + StartHeight: startHeight, + EndHeight: slashingPeriodValue.EndHeight, + SlashedSoFar: slashingPeriodValue.SlashedSoFar, + } +} + +// Construct a new `ValidatorSlashingPeriod` struct +func NewValidatorSlashingPeriod(startHeight int64, endHeight int64, slashedSoFar sdk.Dec) ValidatorSlashingPeriod { + return ValidatorSlashingPeriod{ + StartHeight: startHeight, + EndHeight: endHeight, + SlashedSoFar: slashedSoFar, + } +} + +// Slashing period for a validator +type ValidatorSlashingPeriod struct { + ValidatorAddr sdk.ConsAddress `json:"validator_addr"` // validator which this slashing period is for + StartHeight int64 `json:"start_height"` // starting height of the slashing period + EndHeight int64 `json:"end_height"` // ending height of the slashing period, or sentinel value of 0 for in-progress + SlashedSoFar sdk.Dec `json:"slashed_so_far"` // fraction of validator stake slashed so far in this slashing period +} + +// Value part of slashing period (validator address & start height are stored in the key) +type ValidatorSlashingPeriodValue struct { + EndHeight int64 `json:"end_height"` + SlashedSoFar sdk.Dec `json:"slashed_so_far"` +} + +// Return human readable slashing period +func (p ValidatorSlashingPeriod) HumanReadableString() string { + return fmt.Sprintf("Start height: %d, end height: %d, slashed so far: %v", + p.StartHeight, p.EndHeight, p.SlashedSoFar) +} diff --git a/x/slashing/slashing_period_test.go b/x/slashing/slashing_period_test.go new file mode 100644 index 000000000..54157bb9c --- /dev/null +++ b/x/slashing/slashing_period_test.go @@ -0,0 +1,86 @@ +package slashing + +import ( + "testing" + + "github.com/stretchr/testify/require" + + sdk "github.com/cosmos/cosmos-sdk/types" +) + +func TestGetSetValidatorSlashingPeriod(t *testing.T) { + ctx, _, _, _, keeper := createTestInput(t) + addr := sdk.ConsAddress(addrs[0]) + height := int64(5) + require.Panics(t, func() { keeper.getValidatorSlashingPeriodForHeight(ctx, addr, height) }) + newPeriod := ValidatorSlashingPeriod{ + ValidatorAddr: addr, + StartHeight: height, + EndHeight: height + 10, + SlashedSoFar: sdk.ZeroDec(), + } + keeper.addOrUpdateValidatorSlashingPeriod(ctx, newPeriod) + + // Get at start height + retrieved := keeper.getValidatorSlashingPeriodForHeight(ctx, addr, height) + require.Equal(t, newPeriod, retrieved) + + // Get after start height (works) + retrieved = keeper.getValidatorSlashingPeriodForHeight(ctx, addr, int64(6)) + require.Equal(t, newPeriod, retrieved) + + // Get before start height (panic) + require.Panics(t, func() { keeper.getValidatorSlashingPeriodForHeight(ctx, addr, int64(0)) }) + + // Get after end height (panic) + newPeriod.EndHeight = int64(4) + keeper.addOrUpdateValidatorSlashingPeriod(ctx, newPeriod) + require.Panics(t, func() { keeper.capBySlashingPeriod(ctx, addr, sdk.ZeroDec(), height) }) + + // Back to old end height + newPeriod.EndHeight = height + 10 + keeper.addOrUpdateValidatorSlashingPeriod(ctx, newPeriod) + + // Set a new, later period + anotherPeriod := ValidatorSlashingPeriod{ + ValidatorAddr: addr, + StartHeight: height + 1, + EndHeight: height + 11, + SlashedSoFar: sdk.ZeroDec(), + } + keeper.addOrUpdateValidatorSlashingPeriod(ctx, anotherPeriod) + + // Old period retrieved for prior height + retrieved = keeper.getValidatorSlashingPeriodForHeight(ctx, addr, height) + require.Equal(t, newPeriod, retrieved) + + // New period retrieved at new height + retrieved = keeper.getValidatorSlashingPeriodForHeight(ctx, addr, height+1) + require.Equal(t, anotherPeriod, retrieved) +} + +func TestValidatorSlashingPeriodCap(t *testing.T) { + ctx, _, _, _, keeper := createTestInput(t) + addr := sdk.ConsAddress(addrs[0]) + height := int64(5) + newPeriod := ValidatorSlashingPeriod{ + ValidatorAddr: addr, + StartHeight: height, + EndHeight: height + 10, + SlashedSoFar: sdk.ZeroDec(), + } + keeper.addOrUpdateValidatorSlashingPeriod(ctx, newPeriod) + half := sdk.NewDec(1).Quo(sdk.NewDec(2)) + + // First slash should be full + fractionA := keeper.capBySlashingPeriod(ctx, addr, half, height) + require.True(t, fractionA.Equal(half)) + + // Second slash should be capped + fractionB := keeper.capBySlashingPeriod(ctx, addr, half, height) + require.True(t, fractionB.Equal(sdk.ZeroDec())) + + // Third slash should be capped to difference + fractionC := keeper.capBySlashingPeriod(ctx, addr, sdk.OneDec(), height) + require.True(t, fractionC.Equal(half)) +} diff --git a/x/slashing/test_common.go b/x/slashing/test_common.go index 105382378..afbe47d55 100644 --- a/x/slashing/test_common.go +++ b/x/slashing/test_common.go @@ -4,6 +4,7 @@ import ( "encoding/hex" "os" "testing" + "time" "github.com/stretchr/testify/require" @@ -30,10 +31,10 @@ var ( newPubKey("0B485CFC0EECC619440448436F8FC9DF40566F2369E72400281454CB552AFB51"), newPubKey("0B485CFC0EECC619440448436F8FC9DF40566F2369E72400281454CB552AFB52"), } - addrs = []sdk.AccAddress{ - sdk.AccAddress(pks[0].Address()), - sdk.AccAddress(pks[1].Address()), - sdk.AccAddress(pks[2].Address()), + addrs = []sdk.ValAddress{ + sdk.ValAddress(pks[0].Address()), + sdk.ValAddress(pks[1].Address()), + sdk.ValAddress(pks[2].Address()), } initCoins = sdk.NewInt(200) ) @@ -61,7 +62,7 @@ func createTestInput(t *testing.T) (sdk.Context, bank.Keeper, stake.Keeper, para ms.MountStoreWithDB(keyParams, sdk.StoreTypeIAVL, db) err := ms.LoadLatestVersion() require.Nil(t, err) - ctx := sdk.NewContext(ms, abci.Header{}, false, log.NewTMLogger(os.Stdout)) + ctx := sdk.NewContext(ms, abci.Header{Time: time.Unix(0, 0)}, false, log.NewTMLogger(os.Stdout)) cdc := createTestCodec() accountMapper := auth.NewAccountMapper(cdc, keyAcc, auth.ProtoBaseAccount) ck := bank.NewKeeper(accountMapper) @@ -75,7 +76,7 @@ func createTestInput(t *testing.T) (sdk.Context, bank.Keeper, stake.Keeper, para require.Nil(t, err) for _, addr := range addrs { - _, _, err = ck.AddCoins(ctx, addr, sdk.Coins{ + _, _, err = ck.AddCoins(ctx, sdk.AccAddress(addr), sdk.Coins{ {sk.GetParams(ctx).BondDenom, initCoins}, }) } @@ -108,3 +109,11 @@ func newTestMsgCreateValidator(address sdk.ValAddress, pubKey crypto.PubKey, amt Delegation: sdk.Coin{"steak", amt}, } } + +func newTestMsgDelegate(delAddr sdk.AccAddress, valAddr sdk.ValAddress, delAmount sdk.Int) stake.MsgDelegate { + return stake.MsgDelegate{ + DelegatorAddr: delAddr, + ValidatorAddr: valAddr, + Delegation: sdk.Coin{"steak", delAmount}, + } +} diff --git a/x/slashing/tick_test.go b/x/slashing/tick_test.go index 9eb956e67..25167578d 100644 --- a/x/slashing/tick_test.go +++ b/x/slashing/tick_test.go @@ -21,7 +21,7 @@ func TestBeginBlocker(t *testing.T) { require.True(t, got.IsOK()) validatorUpdates := stake.EndBlocker(ctx, sk) keeper.AddValidators(ctx, validatorUpdates) - require.Equal(t, ck.GetCoins(ctx, addr), sdk.Coins{{sk.GetParams(ctx).BondDenom, initCoins.Sub(amt)}}) + 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, sdk.ValAddress(addr)).GetPower())) val := abci.Validator{ @@ -40,7 +40,7 @@ func TestBeginBlocker(t *testing.T) { } BeginBlocker(ctx, req, keeper) - info, found := keeper.getValidatorSigningInfo(ctx, sdk.ValAddress(pk.Address())) + info, found := keeper.getValidatorSigningInfo(ctx, sdk.ConsAddress(pk.Address())) require.True(t, found) require.Equal(t, ctx.BlockHeight(), info.StartHeight) require.Equal(t, int64(1), info.IndexOffset) @@ -80,5 +80,5 @@ func TestBeginBlocker(t *testing.T) { // validator should be jailed validator, found := sk.GetValidatorByPubKey(ctx, pk) require.True(t, found) - require.Equal(t, sdk.Unbonded, validator.GetStatus()) + require.Equal(t, sdk.Unbonding, validator.GetStatus()) } diff --git a/x/stake/app_test.go b/x/stake/app_test.go index 587e90b46..bfeff5df7 100644 --- a/x/stake/app_test.go +++ b/x/stake/app_test.go @@ -131,7 +131,7 @@ func TestStakeMsgs(t *testing.T) { sdk.ValAddress(addr1), priv1.PubKey(), bondCoin, description, ) - mock.SignCheckDeliver(t, mApp.BaseApp, []sdk.Msg{createValidatorMsg}, []int64{0}, []int64{0}, true, priv1) + mock.SignCheckDeliver(t, mApp.BaseApp, []sdk.Msg{createValidatorMsg}, []int64{0}, []int64{0}, true, true, priv1) mock.CheckBalance(t, mApp, addr1, sdk.Coins{genCoin.Minus(bondCoin)}) mApp.BeginBlock(abci.RequestBeginBlock{}) @@ -145,7 +145,7 @@ func TestStakeMsgs(t *testing.T) { addr1, sdk.ValAddress(addr2), priv2.PubKey(), bondCoin, description, ) - mock.SignCheckDeliver(t, mApp.BaseApp, []sdk.Msg{createValidatorMsgOnBehalfOf}, []int64{0, 1}, []int64{1, 0}, true, priv1, priv2) + mock.SignCheckDeliver(t, mApp.BaseApp, []sdk.Msg{createValidatorMsgOnBehalfOf}, []int64{0, 1}, []int64{1, 0}, true, true, priv1, priv2) mock.CheckBalance(t, mApp, addr1, sdk.Coins{genCoin.Minus(bondCoin).Minus(bondCoin)}) mApp.BeginBlock(abci.RequestBeginBlock{}) @@ -161,7 +161,7 @@ func TestStakeMsgs(t *testing.T) { description = NewDescription("bar_moniker", "", "", "") editValidatorMsg := NewMsgEditValidator(sdk.ValAddress(addr1), description) - mock.SignCheckDeliver(t, mApp.BaseApp, []sdk.Msg{editValidatorMsg}, []int64{0}, []int64{2}, true, priv1) + mock.SignCheckDeliver(t, mApp.BaseApp, []sdk.Msg{editValidatorMsg}, []int64{0}, []int64{2}, true, true, priv1) validator = checkValidator(t, mApp, keeper, sdk.ValAddress(addr1), true) require.Equal(t, description, validator.Description) @@ -169,13 +169,13 @@ func TestStakeMsgs(t *testing.T) { mock.CheckBalance(t, mApp, addr2, sdk.Coins{genCoin}) delegateMsg := NewMsgDelegate(addr2, sdk.ValAddress(addr1), bondCoin) - mock.SignCheckDeliver(t, mApp.BaseApp, []sdk.Msg{delegateMsg}, []int64{1}, []int64{1}, true, priv2) + mock.SignCheckDeliver(t, mApp.BaseApp, []sdk.Msg{delegateMsg}, []int64{1}, []int64{1}, true, true, priv2) mock.CheckBalance(t, mApp, addr2, sdk.Coins{genCoin.Minus(bondCoin)}) checkDelegation(t, mApp, keeper, addr2, sdk.ValAddress(addr1), true, sdk.NewDec(10)) // begin unbonding beginUnbondingMsg := NewMsgBeginUnbonding(addr2, sdk.ValAddress(addr1), sdk.NewDec(10)) - mock.SignCheckDeliver(t, mApp.BaseApp, []sdk.Msg{beginUnbondingMsg}, []int64{1}, []int64{2}, true, priv2) + mock.SignCheckDeliver(t, mApp.BaseApp, []sdk.Msg{beginUnbondingMsg}, []int64{1}, []int64{2}, true, true, priv2) // delegation should exist anymore checkDelegation(t, mApp, keeper, addr2, sdk.ValAddress(addr1), false, sdk.Dec{}) diff --git a/x/stake/client/rest/tx.go b/x/stake/client/rest/tx.go index 24a8c3875..9925b6b1c 100644 --- a/x/stake/client/rest/tx.go +++ b/x/stake/client/rest/tx.go @@ -6,6 +6,7 @@ import ( "io/ioutil" "net/http" + "github.com/cosmos/cosmos-sdk/client" "github.com/cosmos/cosmos-sdk/client/context" "github.com/cosmos/cosmos-sdk/client/utils" "github.com/cosmos/cosmos-sdk/crypto/keys" @@ -60,6 +61,7 @@ type EditDelegationsBody struct { AccountNumber int64 `json:"account_number"` Sequence int64 `json:"sequence"` Gas int64 `json:"gas"` + GasAdjustment string `json:"gas_adjustment"` Delegations []msgDelegationsInput `json:"delegations"` BeginUnbondings []msgBeginUnbondingInput `json:"begin_unbondings"` CompleteUnbondings []msgCompleteUnbondingInput `json:"complete_unbondings"` @@ -106,18 +108,18 @@ func delegationsRequestHandlerFn(cdc *wire.Codec, kb keys.Keybase, cliCtx contex for _, msg := range m.Delegations { delAddr, err := sdk.AccAddressFromBech32(msg.DelegatorAddr) if err != nil { - utils.WriteErrorResponse(&w, http.StatusInternalServerError, fmt.Sprintf("Couldn't decode delegator. Error: %s", err.Error())) + utils.WriteErrorResponse(w, http.StatusInternalServerError, fmt.Sprintf("Couldn't decode delegator. Error: %s", err.Error())) return } valAddr, err := sdk.ValAddressFromBech32(msg.ValidatorAddr) if err != nil { - utils.WriteErrorResponse(&w, http.StatusInternalServerError, fmt.Sprintf("Couldn't decode validator. Error: %s", err.Error())) + utils.WriteErrorResponse(w, http.StatusInternalServerError, fmt.Sprintf("Couldn't decode validator. Error: %s", err.Error())) return } if !bytes.Equal(info.GetPubKey().Address(), delAddr) { - utils.WriteErrorResponse(&w, http.StatusUnauthorized, "Must use own delegator address") + utils.WriteErrorResponse(w, http.StatusUnauthorized, "Must use own delegator address") return } @@ -133,29 +135,29 @@ func delegationsRequestHandlerFn(cdc *wire.Codec, kb keys.Keybase, cliCtx contex for _, msg := range m.BeginRedelegates { delAddr, err := sdk.AccAddressFromBech32(msg.DelegatorAddr) if err != nil { - utils.WriteErrorResponse(&w, http.StatusInternalServerError, fmt.Sprintf("Couldn't decode validator. Error: %s", err.Error())) + utils.WriteErrorResponse(w, http.StatusInternalServerError, fmt.Sprintf("Couldn't decode validator. Error: %s", err.Error())) return } if !bytes.Equal(info.GetPubKey().Address(), delAddr) { - utils.WriteErrorResponse(&w, http.StatusUnauthorized, "Must use own delegator address") + utils.WriteErrorResponse(w, http.StatusUnauthorized, "Must use own delegator address") return } valSrcAddr, err := sdk.ValAddressFromBech32(msg.ValidatorSrcAddr) if err != nil { - utils.WriteErrorResponse(&w, http.StatusInternalServerError, fmt.Sprintf("Couldn't decode validator. Error: %s", err.Error())) + utils.WriteErrorResponse(w, http.StatusInternalServerError, fmt.Sprintf("Couldn't decode validator. Error: %s", err.Error())) return } valDstAddr, err := sdk.ValAddressFromBech32(msg.ValidatorDstAddr) if err != nil { - utils.WriteErrorResponse(&w, http.StatusInternalServerError, fmt.Sprintf("Couldn't decode validator. Error: %s", err.Error())) + utils.WriteErrorResponse(w, http.StatusInternalServerError, fmt.Sprintf("Couldn't decode validator. Error: %s", err.Error())) return } shares, err := sdk.NewDecFromStr(msg.SharesAmount) if err != nil { - utils.WriteErrorResponse(&w, http.StatusInternalServerError, fmt.Sprintf("Couldn't decode shares amount. Error: %s", err.Error())) + utils.WriteErrorResponse(w, http.StatusInternalServerError, fmt.Sprintf("Couldn't decode shares amount. Error: %s", err.Error())) return } @@ -172,24 +174,24 @@ func delegationsRequestHandlerFn(cdc *wire.Codec, kb keys.Keybase, cliCtx contex for _, msg := range m.CompleteRedelegates { delAddr, err := sdk.AccAddressFromBech32(msg.DelegatorAddr) if err != nil { - utils.WriteErrorResponse(&w, http.StatusInternalServerError, fmt.Sprintf("Couldn't decode delegator. Error: %s", err.Error())) + utils.WriteErrorResponse(w, http.StatusInternalServerError, fmt.Sprintf("Couldn't decode delegator. Error: %s", err.Error())) return } valSrcAddr, err := sdk.ValAddressFromBech32(msg.ValidatorSrcAddr) if err != nil { - utils.WriteErrorResponse(&w, http.StatusInternalServerError, fmt.Sprintf("Couldn't decode validator. Error: %s", err.Error())) + utils.WriteErrorResponse(w, http.StatusInternalServerError, fmt.Sprintf("Couldn't decode validator. Error: %s", err.Error())) return } valDstAddr, err := sdk.ValAddressFromBech32(msg.ValidatorDstAddr) if err != nil { - utils.WriteErrorResponse(&w, http.StatusInternalServerError, fmt.Sprintf("Couldn't decode validator. Error: %s", err.Error())) + utils.WriteErrorResponse(w, http.StatusInternalServerError, fmt.Sprintf("Couldn't decode validator. Error: %s", err.Error())) return } if !bytes.Equal(info.GetPubKey().Address(), delAddr) { - utils.WriteErrorResponse(&w, http.StatusUnauthorized, "Must use own delegator address") + utils.WriteErrorResponse(w, http.StatusUnauthorized, "Must use own delegator address") return } @@ -205,24 +207,24 @@ func delegationsRequestHandlerFn(cdc *wire.Codec, kb keys.Keybase, cliCtx contex for _, msg := range m.BeginUnbondings { delAddr, err := sdk.AccAddressFromBech32(msg.DelegatorAddr) if err != nil { - utils.WriteErrorResponse(&w, http.StatusInternalServerError, fmt.Sprintf("Couldn't decode delegator. Error: %s", err.Error())) + utils.WriteErrorResponse(w, http.StatusInternalServerError, fmt.Sprintf("Couldn't decode delegator. Error: %s", err.Error())) return } if !bytes.Equal(info.GetPubKey().Address(), delAddr) { - utils.WriteErrorResponse(&w, http.StatusUnauthorized, "Must use own delegator address") + utils.WriteErrorResponse(w, http.StatusUnauthorized, "Must use own delegator address") return } valAddr, err := sdk.ValAddressFromBech32(msg.ValidatorAddr) if err != nil { - utils.WriteErrorResponse(&w, http.StatusInternalServerError, fmt.Sprintf("Couldn't decode validator. Error: %s", err.Error())) + utils.WriteErrorResponse(w, http.StatusInternalServerError, fmt.Sprintf("Couldn't decode validator. Error: %s", err.Error())) return } shares, err := sdk.NewDecFromStr(msg.SharesAmount) if err != nil { - utils.WriteErrorResponse(&w, http.StatusInternalServerError, fmt.Sprintf("Couldn't decode shares amount. Error: %s", err.Error())) + utils.WriteErrorResponse(w, http.StatusInternalServerError, fmt.Sprintf("Couldn't decode shares amount. Error: %s", err.Error())) return } @@ -238,18 +240,18 @@ func delegationsRequestHandlerFn(cdc *wire.Codec, kb keys.Keybase, cliCtx contex for _, msg := range m.CompleteUnbondings { delAddr, err := sdk.AccAddressFromBech32(msg.DelegatorAddr) if err != nil { - utils.WriteErrorResponse(&w, http.StatusInternalServerError, fmt.Sprintf("Couldn't decode delegator. Error: %s", err.Error())) + utils.WriteErrorResponse(w, http.StatusInternalServerError, fmt.Sprintf("Couldn't decode delegator. Error: %s", err.Error())) return } valAddr, err := sdk.ValAddressFromBech32(msg.ValidatorAddr) if err != nil { - utils.WriteErrorResponse(&w, http.StatusInternalServerError, fmt.Sprintf("Couldn't decode validator. Error: %s", err.Error())) + utils.WriteErrorResponse(w, http.StatusInternalServerError, fmt.Sprintf("Couldn't decode validator. Error: %s", err.Error())) return } if !bytes.Equal(info.GetPubKey().Address(), delAddr) { - utils.WriteErrorResponse(&w, http.StatusUnauthorized, "Must use own delegator address") + utils.WriteErrorResponse(w, http.StatusUnauthorized, "Must use own delegator address") return } @@ -276,10 +278,20 @@ func delegationsRequestHandlerFn(cdc *wire.Codec, kb keys.Keybase, cliCtx contex m.Sequence++ - if m.Gas == 0 { - newCtx, err := utils.EnrichCtxWithGas(txCtx, cliCtx, m.LocalAccountName, m.Password, []sdk.Msg{msg}) + adjustment, ok := utils.ParseFloat64OrReturnBadRequest(w, m.GasAdjustment, client.DefaultGasAdjustment) + if !ok { + return + } + cliCtx = cliCtx.WithGasAdjustment(adjustment) + + if utils.HasDryRunArg(r) || m.Gas == 0 { + newCtx, err := utils.EnrichCtxWithGas(txCtx, cliCtx, m.LocalAccountName, []sdk.Msg{msg}) if err != nil { - utils.WriteErrorResponse(&w, http.StatusInternalServerError, err.Error()) + utils.WriteErrorResponse(w, http.StatusInternalServerError, err.Error()) + return + } + if utils.HasDryRunArg(r) { + utils.WriteSimulationResponse(w, txCtx.Gas) return } txCtx = newCtx @@ -287,7 +299,7 @@ func delegationsRequestHandlerFn(cdc *wire.Codec, kb keys.Keybase, cliCtx contex txBytes, err := txCtx.BuildAndSign(m.LocalAccountName, m.Password, []sdk.Msg{msg}) if err != nil { - utils.WriteErrorResponse(&w, http.StatusUnauthorized, err.Error()) + utils.WriteErrorResponse(w, http.StatusUnauthorized, err.Error()) return } @@ -301,7 +313,7 @@ func delegationsRequestHandlerFn(cdc *wire.Codec, kb keys.Keybase, cliCtx contex for i, txBytes := range signedTxs { res, err := cliCtx.BroadcastTx(txBytes) if err != nil { - utils.WriteErrorResponse(&w, http.StatusInternalServerError, err.Error()) + utils.WriteErrorResponse(w, http.StatusInternalServerError, err.Error()) return } @@ -310,7 +322,7 @@ func delegationsRequestHandlerFn(cdc *wire.Codec, kb keys.Keybase, cliCtx contex output, err := wire.MarshalJSONIndent(cdc, results[:]) if err != nil { - utils.WriteErrorResponse(&w, http.StatusInternalServerError, err.Error()) + utils.WriteErrorResponse(w, http.StatusInternalServerError, err.Error()) return } diff --git a/x/stake/handler.go b/x/stake/handler.go index c8be6a835..4b478fffd 100644 --- a/x/stake/handler.go +++ b/x/stake/handler.go @@ -1,6 +1,7 @@ package stake import ( + "bytes" "time" sdk "github.com/cosmos/cosmos-sdk/types" @@ -128,17 +129,19 @@ func handleMsgEditValidator(ctx sdk.Context, msg types.MsgEditValidator, k keepe } func handleMsgDelegate(ctx sdk.Context, msg types.MsgDelegate, k keeper.Keeper) sdk.Result { - validator, found := k.GetValidator(ctx, msg.ValidatorAddr) if !found { return ErrNoValidatorFound(k.Codespace()).Result() } + if msg.Delegation.Denom != k.GetParams(ctx).BondDenom { return ErrBadDenom(k.Codespace()).Result() } - if validator.Jailed { + + if validator.Jailed && !bytes.Equal(validator.Operator, msg.DelegatorAddr) { return ErrValidatorJailed(k.Codespace()).Result() } + _, err := k.Delegate(ctx, msg.DelegatorAddr, msg.Delegation, validator, true) if err != nil { return err.Result() @@ -149,6 +152,7 @@ func handleMsgDelegate(ctx sdk.Context, msg types.MsgDelegate, k keeper.Keeper) tags.Delegator, []byte(msg.DelegatorAddr.String()), tags.DstValidator, []byte(msg.ValidatorAddr.String()), ) + return sdk.Result{ Tags: tags, } diff --git a/x/stake/handler_test.go b/x/stake/handler_test.go index f7d01bcfc..68c342fd8 100644 --- a/x/stake/handler_test.go +++ b/x/stake/handler_test.go @@ -85,8 +85,8 @@ func TestValidatorByPowerIndex(t *testing.T) { keeper.Jail(ctx, keep.PKs[0]) validator, found = keeper.GetValidator(ctx, validatorAddr) require.True(t, found) - require.Equal(t, sdk.Unbonded, validator.Status) // ensure is unbonded - require.Equal(t, int64(500000), validator.Tokens.RoundInt64()) // ensure is unbonded + require.Equal(t, sdk.Unbonding, validator.Status) // ensure is unbonding + require.Equal(t, int64(500000), validator.Tokens.RoundInt64()) // ensure tokens slashed // the old power record should have been deleted as the power changed require.False(t, keep.ValidatorByPowerIndexExists(ctx, keeper, power)) @@ -193,6 +193,97 @@ func TestDuplicatesMsgCreateValidatorOnBehalfOf(t *testing.T) { require.False(t, got.IsOK(), "%v", got) } +func TestLegacyValidatorDelegations(t *testing.T) { + ctx, _, keeper := keep.CreateTestInput(t, false, int64(1000)) + setInstantUnbondPeriod(keeper, ctx) + + bondAmount := int64(10) + valAddr, valPubKey := sdk.ValAddress(keep.Addrs[0]), keep.PKs[0] + delAddr := keep.Addrs[1] + + // create validator + msgCreateVal := newTestMsgCreateValidator(valAddr, valPubKey, bondAmount) + got := handleMsgCreateValidator(ctx, msgCreateVal, keeper) + require.True(t, got.IsOK(), "expected create validator msg to be ok, got %v", got) + + // verify the validator exists and has the correct attributes + validator, found := keeper.GetValidator(ctx, valAddr) + require.True(t, found) + require.Equal(t, sdk.Bonded, validator.Status) + require.Equal(t, bondAmount, validator.DelegatorShares.RoundInt64()) + require.Equal(t, bondAmount, validator.BondedTokens().RoundInt64()) + + // delegate tokens to the validator + msgDelegate := newTestMsgDelegate(delAddr, valAddr, bondAmount) + got = handleMsgDelegate(ctx, msgDelegate, keeper) + require.True(t, got.IsOK(), "expected delegation to be ok, got %v", got) + + // verify validator bonded shares + validator, found = keeper.GetValidator(ctx, valAddr) + require.True(t, found) + require.Equal(t, bondAmount*2, validator.DelegatorShares.RoundInt64()) + require.Equal(t, bondAmount*2, validator.BondedTokens().RoundInt64()) + + // unbond validator total self-delegations (which should jail the validator) + unbondShares := sdk.NewDec(10) + msgBeginUnbonding := NewMsgBeginUnbonding(sdk.AccAddress(valAddr), valAddr, unbondShares) + msgCompleteUnbonding := NewMsgCompleteUnbonding(sdk.AccAddress(valAddr), valAddr) + + got = handleMsgBeginUnbonding(ctx, msgBeginUnbonding, keeper) + require.True(t, got.IsOK(), "expected begin unbonding validator msg to be ok, got %v", got) + + got = handleMsgCompleteUnbonding(ctx, msgCompleteUnbonding, keeper) + require.True(t, got.IsOK(), "expected complete unbonding validator msg to be ok, got %v", got) + + // verify the validator record still exists, is jailed, and has correct tokens + validator, found = keeper.GetValidator(ctx, valAddr) + require.True(t, found) + require.True(t, validator.Jailed) + require.Equal(t, sdk.NewDec(10), validator.Tokens) + + // verify delegation still exists + bond, found := keeper.GetDelegation(ctx, delAddr, valAddr) + require.True(t, found) + require.Equal(t, bondAmount, bond.Shares.RoundInt64()) + require.Equal(t, bondAmount, validator.DelegatorShares.RoundInt64()) + + // verify a delegator cannot create a new delegation to the now jailed validator + msgDelegate = newTestMsgDelegate(delAddr, valAddr, bondAmount) + got = handleMsgDelegate(ctx, msgDelegate, keeper) + require.False(t, got.IsOK(), "expected delegation to not be ok, got %v", got) + + // verify the validator can still self-delegate + msgSelfDelegate := newTestMsgDelegate(sdk.AccAddress(valAddr), valAddr, bondAmount) + got = handleMsgDelegate(ctx, msgSelfDelegate, keeper) + require.True(t, got.IsOK(), "expected delegation to not be ok, got %v", got) + + // verify validator bonded shares + validator, found = keeper.GetValidator(ctx, valAddr) + require.True(t, found) + require.Equal(t, bondAmount*2, validator.DelegatorShares.RoundInt64()) + require.Equal(t, bondAmount*2, validator.Tokens.RoundInt64()) + + // unjail the validator now that is has non-zero self-delegated shares + keeper.Unjail(ctx, valPubKey) + + // verify the validator can now accept delegations + msgDelegate = newTestMsgDelegate(delAddr, valAddr, bondAmount) + got = handleMsgDelegate(ctx, msgDelegate, keeper) + require.True(t, got.IsOK(), "expected delegation to be ok, got %v", got) + + // verify validator bonded shares + validator, found = keeper.GetValidator(ctx, valAddr) + require.True(t, found) + require.Equal(t, bondAmount*3, validator.DelegatorShares.RoundInt64()) + require.Equal(t, bondAmount*3, validator.Tokens.RoundInt64()) + + // verify new delegation + bond, found = keeper.GetDelegation(ctx, delAddr, valAddr) + require.True(t, found) + require.Equal(t, bondAmount*2, bond.Shares.RoundInt64()) + require.Equal(t, bondAmount*3, validator.DelegatorShares.RoundInt64()) +} + func TestIncrementsMsgDelegate(t *testing.T) { initBond := int64(1000) ctx, accMapper, keeper := keep.CreateTestInput(t, false, initBond) diff --git a/x/stake/keeper/delegation.go b/x/stake/keeper/delegation.go index 9f7a402fc..9a596ffbc 100644 --- a/x/stake/keeper/delegation.go +++ b/x/stake/keeper/delegation.go @@ -2,6 +2,7 @@ package keeper import ( "bytes" + "time" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/x/stake/types" @@ -288,6 +289,7 @@ func (k Keeper) unbond(ctx sdk.Context, delAddr sdk.AccAddress, valAddr sdk.ValA if bytes.Equal(delegation.DelegatorAddr, validator.Operator) && validator.Jailed == false { validator.Jailed = true } + k.RemoveDelegation(ctx, delegation) } else { // Update height @@ -307,11 +309,37 @@ func (k Keeper) unbond(ctx sdk.Context, delAddr sdk.AccAddress, valAddr sdk.ValA k.RemoveValidator(ctx, validator.Operator) } - return + return amount, nil } //______________________________________________________________________________________________________ +// get info for begin functions: MinTime and CreationHeight +func (k Keeper) getBeginInfo(ctx sdk.Context, params types.Params, valSrcAddr sdk.ValAddress) ( + minTime time.Time, height int64, completeNow bool) { + + validator, found := k.GetValidator(ctx, valSrcAddr) + switch { + case !found || validator.Status == sdk.Bonded: + + // the longest wait - just unbonding period from now + minTime = ctx.BlockHeader().Time.Add(params.UnbondingTime) + height = ctx.BlockHeader().Height + return minTime, height, false + + case validator.IsUnbonded(ctx): + return minTime, height, true + + case validator.Status == sdk.Unbonding: + minTime = validator.UnbondingMinTime + height = validator.UnbondingHeight + return minTime, height, false + + default: + panic("unknown validator status") + } +} + // complete unbonding an unbonding record func (k Keeper) BeginUnbonding(ctx sdk.Context, delAddr sdk.AccAddress, valAddr sdk.ValAddress, sharesAmount sdk.Dec) sdk.Error { @@ -329,12 +357,22 @@ func (k Keeper) BeginUnbonding(ctx sdk.Context, // create the unbonding delegation params := k.GetParams(ctx) - minTime := ctx.BlockHeader().Time.Add(params.UnbondingTime) + minTime, height, completeNow := k.getBeginInfo(ctx, params, valAddr) balance := sdk.Coin{params.BondDenom, returnAmount.RoundInt()} + // no need to create the ubd object just complete now + if completeNow { + _, _, err := k.coinKeeper.AddCoins(ctx, delAddr, sdk.Coins{balance}) + if err != nil { + return err + } + return nil + } + ubd := types.UnbondingDelegation{ DelegatorAddr: delAddr, ValidatorAddr: valAddr, + CreationHeight: height, MinTime: minTime, Balance: balance, InitialBalance: balance, @@ -391,12 +429,17 @@ func (k Keeper) BeginRedelegation(ctx sdk.Context, delAddr sdk.AccAddress, } // create the unbonding delegation - minTime := ctx.BlockHeader().Time.Add(params.UnbondingTime) + minTime, height, completeNow := k.getBeginInfo(ctx, params, valSrcAddr) + + if completeNow { // no need to create the redelegation object + return nil + } red := types.Redelegation{ DelegatorAddr: delAddr, ValidatorSrcAddr: valSrcAddr, ValidatorDstAddr: valDstAddr, + CreationHeight: height, MinTime: minTime, SharesDst: sharesCreated, SharesSrc: sharesAmount, diff --git a/x/stake/keeper/delegation_test.go b/x/stake/keeper/delegation_test.go index cd86f456e..023642bb2 100644 --- a/x/stake/keeper/delegation_test.go +++ b/x/stake/keeper/delegation_test.go @@ -7,6 +7,7 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/x/stake/types" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -162,9 +163,7 @@ func TestUnbondDelegation(t *testing.T) { } keeper.SetDelegation(ctx, delegation) - var err error - var amount sdk.Dec - amount, err = keeper.unbond(ctx, addrDels[0], addrVals[0], sdk.NewDec(6)) + amount, err := keeper.unbond(ctx, addrDels[0], addrVals[0], sdk.NewDec(6)) require.NoError(t, err) require.Equal(t, int64(6), amount.RoundInt64()) // shares to be added to an unbonding delegation / redelegation @@ -180,6 +179,190 @@ func TestUnbondDelegation(t *testing.T) { require.Equal(t, int64(4), pool.BondedTokens.RoundInt64()) } +// test removing all self delegation from a validator which should +// shift it from the bonded to unbonded state +func TestUndelegateSelfDelegation(t *testing.T) { + + ctx, _, keeper := CreateTestInput(t, false, 0) + pool := keeper.GetPool(ctx) + pool.LooseTokens = sdk.NewDec(20) + + //create a validator with a self-delegation + validator := types.NewValidator(addrVals[0], PKs[0], types.Description{}) + validator, pool, issuedShares := validator.AddTokensFromDel(pool, sdk.NewInt(10)) + require.Equal(t, int64(10), issuedShares.RoundInt64()) + keeper.SetPool(ctx, pool) + validator = keeper.UpdateValidator(ctx, validator) + pool = keeper.GetPool(ctx) + selfDelegation := types.Delegation{ + DelegatorAddr: sdk.AccAddress(addrVals[0].Bytes()), + ValidatorAddr: addrVals[0], + Shares: issuedShares, + } + keeper.SetDelegation(ctx, selfDelegation) + + // create a second delegation to this validator + validator, pool, issuedShares = validator.AddTokensFromDel(pool, sdk.NewInt(10)) + require.Equal(t, int64(10), issuedShares.RoundInt64()) + keeper.SetPool(ctx, pool) + validator = keeper.UpdateValidator(ctx, validator) + pool = keeper.GetPool(ctx) + delegation := types.Delegation{ + DelegatorAddr: addrDels[0], + ValidatorAddr: addrVals[0], + Shares: issuedShares, + } + keeper.SetDelegation(ctx, delegation) + + val0AccAddr := sdk.AccAddress(addrVals[0].Bytes()) + err := keeper.BeginUnbonding(ctx, val0AccAddr, addrVals[0], sdk.NewDec(10)) + require.NoError(t, err) + + validator, found := keeper.GetValidator(ctx, addrVals[0]) + require.True(t, found) + require.Equal(t, int64(10), validator.Tokens.RoundInt64()) + require.Equal(t, sdk.Unbonding, validator.Status) +} + +func TestUndelegateFromUnbondingValidator(t *testing.T) { + ctx, _, keeper := CreateTestInput(t, false, 0) + pool := keeper.GetPool(ctx) + pool.LooseTokens = sdk.NewDec(20) + + //create a validator with a self-delegation + validator := types.NewValidator(addrVals[0], PKs[0], types.Description{}) + + validator, pool, issuedShares := validator.AddTokensFromDel(pool, sdk.NewInt(10)) + require.Equal(t, int64(10), issuedShares.RoundInt64()) + keeper.SetPool(ctx, pool) + validator = keeper.UpdateValidator(ctx, validator) + pool = keeper.GetPool(ctx) + selfDelegation := types.Delegation{ + DelegatorAddr: sdk.AccAddress(addrVals[0].Bytes()), + ValidatorAddr: addrVals[0], + Shares: issuedShares, + } + keeper.SetDelegation(ctx, selfDelegation) + + // create a second delegation to this validator + validator, pool, issuedShares = validator.AddTokensFromDel(pool, sdk.NewInt(10)) + require.Equal(t, int64(10), issuedShares.RoundInt64()) + keeper.SetPool(ctx, pool) + validator = keeper.UpdateValidator(ctx, validator) + pool = keeper.GetPool(ctx) + delegation := types.Delegation{ + DelegatorAddr: addrDels[0], + ValidatorAddr: addrVals[0], + Shares: issuedShares, + } + keeper.SetDelegation(ctx, delegation) + + header := ctx.BlockHeader() + blockHeight := int64(10) + header.Height = blockHeight + blockTime := time.Unix(333, 0) + header.Time = blockTime + ctx = ctx.WithBlockHeader(header) + + // unbond the all self-delegation to put validator in unbonding state + val0AccAddr := sdk.AccAddress(addrVals[0].Bytes()) + err := keeper.BeginUnbonding(ctx, val0AccAddr, addrVals[0], sdk.NewDec(10)) + require.NoError(t, err) + + validator, found := keeper.GetValidator(ctx, addrVals[0]) + require.True(t, found) + require.Equal(t, blockHeight, validator.UnbondingHeight) + params := keeper.GetParams(ctx) + require.True(t, blockTime.Add(params.UnbondingTime).Equal(validator.UnbondingMinTime)) + + //change the context + header = ctx.BlockHeader() + blockHeight2 := int64(20) + header.Height = blockHeight2 + blockTime2 := time.Unix(444, 0) + header.Time = blockTime2 + ctx = ctx.WithBlockHeader(header) + + // unbond some of the other delegation's shares + err = keeper.BeginUnbonding(ctx, addrDels[0], addrVals[0], sdk.NewDec(6)) + require.NoError(t, err) + + // retrieve the unbonding delegation + ubd, found := keeper.GetUnbondingDelegation(ctx, addrDels[0], addrVals[0]) + require.True(t, found) + require.True(t, ubd.Balance.IsEqual(sdk.NewInt64Coin(params.BondDenom, 6))) + assert.Equal(t, blockHeight, ubd.CreationHeight) + assert.True(t, blockTime.Add(params.UnbondingTime).Equal(ubd.MinTime)) +} + +func TestUndelegateFromUnbondedValidator(t *testing.T) { + ctx, _, keeper := CreateTestInput(t, false, 0) + pool := keeper.GetPool(ctx) + pool.LooseTokens = sdk.NewDec(20) + + //create a validator with a self-delegation + validator := types.NewValidator(addrVals[0], PKs[0], types.Description{}) + + validator, pool, issuedShares := validator.AddTokensFromDel(pool, sdk.NewInt(10)) + require.Equal(t, int64(10), issuedShares.RoundInt64()) + keeper.SetPool(ctx, pool) + validator = keeper.UpdateValidator(ctx, validator) + pool = keeper.GetPool(ctx) + val0AccAddr := sdk.AccAddress(addrVals[0].Bytes()) + selfDelegation := types.Delegation{ + DelegatorAddr: val0AccAddr, + ValidatorAddr: addrVals[0], + Shares: issuedShares, + } + keeper.SetDelegation(ctx, selfDelegation) + + // create a second delegation to this validator + validator, pool, issuedShares = validator.AddTokensFromDel(pool, sdk.NewInt(10)) + require.Equal(t, int64(10), issuedShares.RoundInt64()) + keeper.SetPool(ctx, pool) + validator = keeper.UpdateValidator(ctx, validator) + pool = keeper.GetPool(ctx) + delegation := types.Delegation{ + DelegatorAddr: addrDels[0], + ValidatorAddr: addrVals[0], + Shares: issuedShares, + } + keeper.SetDelegation(ctx, delegation) + + header := ctx.BlockHeader() + blockHeight := int64(10) + header.Height = blockHeight + blockTime := time.Unix(333, 0) + header.Time = blockTime + ctx = ctx.WithBlockHeader(header) + + // unbond the all self-delegation to put validator in unbonding state + err := keeper.BeginUnbonding(ctx, val0AccAddr, addrVals[0], sdk.NewDec(10)) + require.NoError(t, err) + + validator, found := keeper.GetValidator(ctx, addrVals[0]) + require.True(t, found) + require.Equal(t, blockHeight, validator.UnbondingHeight) + params := keeper.GetParams(ctx) + require.True(t, blockTime.Add(params.UnbondingTime).Equal(validator.UnbondingMinTime)) + + // change the context to one which makes the validator considered unbonded + header = ctx.BlockHeader() + blockHeight2 := int64(20) + header.Height = blockHeight2 + blockTime2 := time.Unix(444, 0).Add(params.UnbondingTime) + header.Time = blockTime2 + ctx = ctx.WithBlockHeader(header) + + // unbond some of the other delegation's shares + err = keeper.BeginUnbonding(ctx, addrDels[0], addrVals[0], sdk.NewDec(6)) + require.NoError(t, err) + + // no ubd should have been found, coins should have been returned direcly to account + ubd, found := keeper.GetUnbondingDelegation(ctx, addrDels[0], addrVals[0]) + require.False(t, found, "%v", ubd) +} + // Make sure that that the retrieving the delegations doesn't affect the state func TestGetRedelegationsFromValidator(t *testing.T) { ctx, _, keeper := CreateTestInput(t, false, 0) @@ -259,3 +442,206 @@ func TestRedelegation(t *testing.T) { _, found = keeper.GetRedelegation(ctx, addrDels[0], addrVals[0], addrVals[1]) require.False(t, found) } + +func TestRedelegateSelfDelegation(t *testing.T) { + + ctx, _, keeper := CreateTestInput(t, false, 0) + pool := keeper.GetPool(ctx) + pool.LooseTokens = sdk.NewDec(30) + + //create a validator with a self-delegation + validator := types.NewValidator(addrVals[0], PKs[0], types.Description{}) + validator, pool, issuedShares := validator.AddTokensFromDel(pool, sdk.NewInt(10)) + require.Equal(t, int64(10), issuedShares.RoundInt64()) + keeper.SetPool(ctx, pool) + validator = keeper.UpdateValidator(ctx, validator) + pool = keeper.GetPool(ctx) + val0AccAddr := sdk.AccAddress(addrVals[0].Bytes()) + selfDelegation := types.Delegation{ + DelegatorAddr: val0AccAddr, + ValidatorAddr: addrVals[0], + Shares: issuedShares, + } + keeper.SetDelegation(ctx, selfDelegation) + + // create a second validator + validator2 := types.NewValidator(addrVals[1], PKs[1], types.Description{}) + validator2, pool, issuedShares = validator2.AddTokensFromDel(pool, sdk.NewInt(10)) + require.Equal(t, int64(10), issuedShares.RoundInt64()) + keeper.SetPool(ctx, pool) + validator2 = keeper.UpdateValidator(ctx, validator2) + + // create a second delegation to this validator + validator, pool, issuedShares = validator.AddTokensFromDel(pool, sdk.NewInt(10)) + require.Equal(t, int64(10), issuedShares.RoundInt64()) + keeper.SetPool(ctx, pool) + validator = keeper.UpdateValidator(ctx, validator) + pool = keeper.GetPool(ctx) + delegation := types.Delegation{ + DelegatorAddr: addrDels[0], + ValidatorAddr: addrVals[0], + Shares: issuedShares, + } + keeper.SetDelegation(ctx, delegation) + + err := keeper.BeginRedelegation(ctx, val0AccAddr, addrVals[0], addrVals[1], sdk.NewDec(10)) + require.NoError(t, err) + + validator, found := keeper.GetValidator(ctx, addrVals[0]) + require.True(t, found) + require.Equal(t, int64(10), validator.Tokens.RoundInt64()) + require.Equal(t, sdk.Unbonding, validator.Status) +} + +func TestRedelegateFromUnbondingValidator(t *testing.T) { + ctx, _, keeper := CreateTestInput(t, false, 0) + pool := keeper.GetPool(ctx) + pool.LooseTokens = sdk.NewDec(30) + + //create a validator with a self-delegation + validator := types.NewValidator(addrVals[0], PKs[0], types.Description{}) + + validator, pool, issuedShares := validator.AddTokensFromDel(pool, sdk.NewInt(10)) + require.Equal(t, int64(10), issuedShares.RoundInt64()) + keeper.SetPool(ctx, pool) + validator = keeper.UpdateValidator(ctx, validator) + pool = keeper.GetPool(ctx) + val0AccAddr := sdk.AccAddress(addrVals[0].Bytes()) + selfDelegation := types.Delegation{ + DelegatorAddr: val0AccAddr, + ValidatorAddr: addrVals[0], + Shares: issuedShares, + } + keeper.SetDelegation(ctx, selfDelegation) + + // create a second delegation to this validator + validator, pool, issuedShares = validator.AddTokensFromDel(pool, sdk.NewInt(10)) + require.Equal(t, int64(10), issuedShares.RoundInt64()) + keeper.SetPool(ctx, pool) + validator = keeper.UpdateValidator(ctx, validator) + pool = keeper.GetPool(ctx) + delegation := types.Delegation{ + DelegatorAddr: addrDels[0], + ValidatorAddr: addrVals[0], + Shares: issuedShares, + } + keeper.SetDelegation(ctx, delegation) + + // create a second validator + validator2 := types.NewValidator(addrVals[1], PKs[1], types.Description{}) + validator2, pool, issuedShares = validator2.AddTokensFromDel(pool, sdk.NewInt(10)) + require.Equal(t, int64(10), issuedShares.RoundInt64()) + keeper.SetPool(ctx, pool) + validator2 = keeper.UpdateValidator(ctx, validator2) + + header := ctx.BlockHeader() + blockHeight := int64(10) + header.Height = blockHeight + blockTime := time.Unix(333, 0) + header.Time = blockTime + ctx = ctx.WithBlockHeader(header) + + // unbond the all self-delegation to put validator in unbonding state + err := keeper.BeginUnbonding(ctx, val0AccAddr, addrVals[0], sdk.NewDec(10)) + require.NoError(t, err) + + validator, found := keeper.GetValidator(ctx, addrVals[0]) + require.True(t, found) + require.Equal(t, blockHeight, validator.UnbondingHeight) + params := keeper.GetParams(ctx) + require.True(t, blockTime.Add(params.UnbondingTime).Equal(validator.UnbondingMinTime)) + + //change the context + header = ctx.BlockHeader() + blockHeight2 := int64(20) + header.Height = blockHeight2 + blockTime2 := time.Unix(444, 0) + header.Time = blockTime2 + ctx = ctx.WithBlockHeader(header) + + // unbond some of the other delegation's shares + err = keeper.BeginRedelegation(ctx, addrDels[0], addrVals[0], addrVals[1], sdk.NewDec(6)) + require.NoError(t, err) + + // retrieve the unbonding delegation + ubd, found := keeper.GetRedelegation(ctx, addrDels[0], addrVals[0], addrVals[1]) + require.True(t, found) + require.True(t, ubd.Balance.IsEqual(sdk.NewInt64Coin(params.BondDenom, 6))) + assert.Equal(t, blockHeight, ubd.CreationHeight) + assert.True(t, blockTime.Add(params.UnbondingTime).Equal(ubd.MinTime)) +} + +func TestRedelegateFromUnbondedValidator(t *testing.T) { + ctx, _, keeper := CreateTestInput(t, false, 0) + pool := keeper.GetPool(ctx) + pool.LooseTokens = sdk.NewDec(30) + + //create a validator with a self-delegation + validator := types.NewValidator(addrVals[0], PKs[0], types.Description{}) + + validator, pool, issuedShares := validator.AddTokensFromDel(pool, sdk.NewInt(10)) + require.Equal(t, int64(10), issuedShares.RoundInt64()) + keeper.SetPool(ctx, pool) + validator = keeper.UpdateValidator(ctx, validator) + pool = keeper.GetPool(ctx) + val0AccAddr := sdk.AccAddress(addrVals[0].Bytes()) + selfDelegation := types.Delegation{ + DelegatorAddr: val0AccAddr, + ValidatorAddr: addrVals[0], + Shares: issuedShares, + } + keeper.SetDelegation(ctx, selfDelegation) + + // create a second delegation to this validator + validator, pool, issuedShares = validator.AddTokensFromDel(pool, sdk.NewInt(10)) + require.Equal(t, int64(10), issuedShares.RoundInt64()) + keeper.SetPool(ctx, pool) + validator = keeper.UpdateValidator(ctx, validator) + pool = keeper.GetPool(ctx) + delegation := types.Delegation{ + DelegatorAddr: addrDels[0], + ValidatorAddr: addrVals[0], + Shares: issuedShares, + } + keeper.SetDelegation(ctx, delegation) + + // create a second validator + validator2 := types.NewValidator(addrVals[1], PKs[1], types.Description{}) + validator2, pool, issuedShares = validator2.AddTokensFromDel(pool, sdk.NewInt(10)) + require.Equal(t, int64(10), issuedShares.RoundInt64()) + keeper.SetPool(ctx, pool) + validator2 = keeper.UpdateValidator(ctx, validator2) + + header := ctx.BlockHeader() + blockHeight := int64(10) + header.Height = blockHeight + blockTime := time.Unix(333, 0) + header.Time = blockTime + ctx = ctx.WithBlockHeader(header) + + // unbond the all self-delegation to put validator in unbonding state + err := keeper.BeginUnbonding(ctx, val0AccAddr, addrVals[0], sdk.NewDec(10)) + require.NoError(t, err) + + validator, found := keeper.GetValidator(ctx, addrVals[0]) + require.True(t, found) + require.Equal(t, blockHeight, validator.UnbondingHeight) + params := keeper.GetParams(ctx) + require.True(t, blockTime.Add(params.UnbondingTime).Equal(validator.UnbondingMinTime)) + + // change the context to one which makes the validator considered unbonded + header = ctx.BlockHeader() + blockHeight2 := int64(20) + header.Height = blockHeight2 + blockTime2 := time.Unix(444, 0).Add(params.UnbondingTime) + header.Time = blockTime2 + ctx = ctx.WithBlockHeader(header) + + // unbond some of the other delegation's shares + err = keeper.BeginRedelegation(ctx, addrDels[0], addrVals[0], addrVals[1], sdk.NewDec(6)) + require.NoError(t, err) + + // no ubd should have been found, coins should have been returned direcly to account + ubd, found := keeper.GetRedelegation(ctx, addrDels[0], addrVals[0], addrVals[1]) + require.False(t, found, "%v", ubd) +} diff --git a/x/stake/keeper/keeper.go b/x/stake/keeper/keeper.go index 187649c5f..14c834387 100644 --- a/x/stake/keeper/keeper.go +++ b/x/stake/keeper/keeper.go @@ -10,9 +10,10 @@ import ( // keeper of the stake store type Keeper struct { - storeKey sdk.StoreKey - cdc *wire.Codec - coinKeeper bank.Keeper + storeKey sdk.StoreKey + cdc *wire.Codec + coinKeeper bank.Keeper + validatorHooks sdk.ValidatorHooks // codespace codespace sdk.CodespaceType @@ -20,14 +21,24 @@ type Keeper struct { func NewKeeper(cdc *wire.Codec, key sdk.StoreKey, ck bank.Keeper, codespace sdk.CodespaceType) Keeper { keeper := Keeper{ - storeKey: key, - cdc: cdc, - coinKeeper: ck, - codespace: codespace, + storeKey: key, + cdc: cdc, + coinKeeper: ck, + validatorHooks: nil, + codespace: codespace, } return keeper } +// Set the validator hooks +func (k Keeper) WithValidatorHooks(v sdk.ValidatorHooks) Keeper { + if k.validatorHooks != nil { + panic("cannot set validator hooks twice") + } + k.validatorHooks = v + return k +} + //_________________________________________________________________________ // return the codespace diff --git a/x/stake/keeper/sdk_types.go b/x/stake/keeper/sdk_types.go index 480df701b..e4c7a1c19 100644 --- a/x/stake/keeper/sdk_types.go +++ b/x/stake/keeper/sdk_types.go @@ -91,6 +91,7 @@ func (k Keeper) Delegation(ctx sdk.Context, addrDel sdk.AccAddress, addrVal sdk. if !ok { return nil } + return bond } diff --git a/x/stake/keeper/slash.go b/x/stake/keeper/slash.go index aa6fe974d..be26d1a23 100644 --- a/x/stake/keeper/slash.go +++ b/x/stake/keeper/slash.go @@ -18,8 +18,12 @@ import ( // Infraction committed equal to or less than an unbonding period in the past, // so all unbonding delegations and redelegations from that height are stored // CONTRACT: +// Slash will not slash unbonded validators (for the above reason) +// CONTRACT: // Infraction committed at the current height or at a past height, // not at a height in the future +// +// nolint: gocyclo func (k Keeper) Slash(ctx sdk.Context, pubkey crypto.PubKey, infractionHeight int64, power int64, slashFactor sdk.Dec) { logger := ctx.Logger().With("module", "x/stake") @@ -43,6 +47,12 @@ func (k Keeper) Slash(ctx sdk.Context, pubkey crypto.PubKey, infractionHeight in pubkey.Address())) return } + + // should not be slashing unbonded + if validator.IsUnbonded(ctx) { + panic(fmt.Sprintf("should not be slashing unbonded validator: %v", validator)) + } + operatorAddress := validator.GetOperator() // Track remaining slash amount for the validator @@ -91,25 +101,24 @@ func (k Keeper) Slash(ctx sdk.Context, pubkey crypto.PubKey, infractionHeight in // Cannot decrease balance below zero tokensToBurn := sdk.MinDec(remainingSlashAmount, validator.Tokens) - // Get the current pool + // burn validator's tokens pool := k.GetPool(ctx) - // remove tokens from the validator validator, pool = validator.RemoveTokens(pool, tokensToBurn) - // burn tokens pool.LooseTokens = pool.LooseTokens.Sub(tokensToBurn) - // update the pool k.SetPool(ctx, pool) + // update the validator, possibly kicking it out validator = k.UpdateValidator(ctx, validator) - // remove validator if it has been reduced to zero shares + + // remove validator if it has no more tokens if validator.Tokens.IsZero() { k.RemoveValidator(ctx, validator.Operator) } // Log that a slash occurred! logger.Info(fmt.Sprintf( - "Validator %s slashed by slashFactor %v, burned %v tokens", - pubkey.Address(), slashFactor, tokensToBurn)) + "Validator %s slashed by slashFactor %s, burned %v tokens", + pubkey.Address(), slashFactor.String(), tokensToBurn)) // TODO Return event(s), blocked on https://github.com/tendermint/tendermint/pull/1803 return @@ -134,12 +143,12 @@ func (k Keeper) Unjail(ctx sdk.Context, pubkey crypto.PubKey) { } // set the jailed flag on a validator -func (k Keeper) setJailed(ctx sdk.Context, pubkey crypto.PubKey, jailed bool) { +func (k Keeper) setJailed(ctx sdk.Context, pubkey crypto.PubKey, isJailed bool) { validator, found := k.GetValidatorByPubKey(ctx, pubkey) if !found { - panic(fmt.Errorf("Validator with pubkey %s not found, cannot set jailed to %v", pubkey, jailed)) + panic(fmt.Errorf("Validator with pubkey %s not found, cannot set jailed to %v", pubkey, isJailed)) } - validator.Jailed = jailed + validator.Jailed = isJailed k.UpdateValidator(ctx, validator) // update validator, possibly unbonding or bonding it return } @@ -179,6 +188,7 @@ func (k Keeper) slashUnbondingDelegation(ctx sdk.Context, unbondingDelegation ty unbondingDelegation.Balance.Amount = unbondingDelegation.Balance.Amount.Sub(unbondingSlashAmount) k.SetUnbondingDelegation(ctx, unbondingDelegation) pool := k.GetPool(ctx) + // Burn loose tokens // Ref https://github.com/cosmos/cosmos-sdk/pull/1278#discussion_r198657760 pool.LooseTokens = pool.LooseTokens.Sub(slashAmount) @@ -239,6 +249,7 @@ func (k Keeper) slashRedelegation(ctx sdk.Context, validator types.Validator, re if err != nil { panic(fmt.Errorf("error unbonding delegator: %v", err)) } + // Burn loose tokens pool := k.GetPool(ctx) pool.LooseTokens = pool.LooseTokens.Sub(tokensToBurn) diff --git a/x/stake/keeper/slash_test.go b/x/stake/keeper/slash_test.go index 30236bb8d..65bac2d80 100644 --- a/x/stake/keeper/slash_test.go +++ b/x/stake/keeper/slash_test.go @@ -11,8 +11,8 @@ import ( abci "github.com/tendermint/tendermint/abci/types" ) -// setup helper function -// creates two validators +// TODO integrate with test_common.go helper (CreateTestInput) +// setup helper function - creates two validators func setupHelper(t *testing.T, amt int64) (sdk.Context, Keeper, types.Params) { // setup ctx, _, keeper := CreateTestInput(t, false, amt) @@ -34,8 +34,11 @@ func setupHelper(t *testing.T, amt int64) (sdk.Context, Keeper, types.Params) { return ctx, keeper, params } +//_________________________________________________________________________________ + // tests Jail, Unjail func TestRevocation(t *testing.T) { + // setup ctx, keeper, _ := setupHelper(t, 10) addr := addrVals[0] @@ -57,7 +60,6 @@ func TestRevocation(t *testing.T) { val, found = keeper.GetValidator(ctx, addr) require.True(t, found) require.False(t, val.GetJailed()) - } // tests slashUnbondingDelegation @@ -95,8 +97,10 @@ func TestSlashUnbondingDelegation(t *testing.T) { require.Equal(t, int64(5), slashAmount.RoundInt64()) ubd, found := keeper.GetUnbondingDelegation(ctx, addrDels[0], addrVals[0]) require.True(t, found) + // initialbalance unchanged require.Equal(t, sdk.NewInt64Coin(params.BondDenom, 10), ubd.InitialBalance) + // balance decreased require.Equal(t, sdk.NewInt64Coin(params.BondDenom, 5), ubd.Balance) newPool := keeper.GetPool(ctx) @@ -155,14 +159,18 @@ func TestSlashRedelegation(t *testing.T) { require.Equal(t, int64(5), slashAmount.RoundInt64()) rd, found = keeper.GetRedelegation(ctx, addrDels[0], addrVals[0], addrVals[1]) require.True(t, found) + // initialbalance unchanged require.Equal(t, sdk.NewInt64Coin(params.BondDenom, 10), rd.InitialBalance) + // balance decreased require.Equal(t, sdk.NewInt64Coin(params.BondDenom, 5), rd.Balance) + // shares decreased del, found = keeper.GetDelegation(ctx, addrDels[0], addrVals[1]) require.True(t, found) require.Equal(t, int64(5), del.Shares.RoundInt64()) + // pool bonded tokens decreased newPool := keeper.GetPool(ctx) require.Equal(t, int64(5), oldPool.BondedTokens.Sub(newPool.BondedTokens).RoundInt64()) @@ -177,7 +185,7 @@ func TestSlashAtFutureHeight(t *testing.T) { } // tests Slash at the current height -func TestSlashAtCurrentHeight(t *testing.T) { +func TestSlashValidatorAtCurrentHeight(t *testing.T) { ctx, keeper, _ := setupHelper(t, 10) pk := PKs[0] fraction := sdk.NewDecWithPrec(5, 1) diff --git a/x/stake/keeper/validator.go b/x/stake/keeper/validator.go index cb225df6c..47a5d5f66 100644 --- a/x/stake/keeper/validator.go +++ b/x/stake/keeper/validator.go @@ -2,6 +2,7 @@ package keeper import ( "bytes" + "container/list" "fmt" abci "github.com/tendermint/tendermint/abci/types" @@ -11,6 +12,19 @@ import ( "github.com/cosmos/cosmos-sdk/x/stake/types" ) +// Cache the amino decoding of validators, as it can be the case that repeated slashing calls +// cause many calls to GetValidator, which were shown to throttle the state machine in our +// simulation. Note this is quite biased though, as the simulator does more slashes than a +// live chain should, however we require the slashing to be fast as noone pays gas for it. +type cachedValidator struct { + val types.Validator + marshalled string // marshalled amino bytes for the validator object (not operator address) +} + +// validatorCache-key: validator amino bytes +var validatorCache = make(map[string]cachedValidator, 500) +var validatorCacheList = list.New() + // get a single validator func (k Keeper) GetValidator(ctx sdk.Context, addr sdk.ValAddress) (validator types.Validator, found bool) { store := ctx.KVStore(k.storeKey) @@ -18,6 +32,28 @@ func (k Keeper) GetValidator(ctx sdk.Context, addr sdk.ValAddress) (validator ty if value == nil { return validator, false } + + // If these amino encoded bytes are in the cache, return the cached validator + strValue := string(value) + if val, ok := validatorCache[strValue]; ok { + valToReturn := val.val + // Doesn't mutate the cache's value + valToReturn.Operator = addr + return valToReturn, true + } + + // amino bytes weren't found in cache, so amino unmarshal and add it to the cache + validator = types.MustUnmarshalValidator(k.cdc, addr, value) + cachedVal := cachedValidator{validator, strValue} + validatorCache[strValue] = cachedValidator{validator, strValue} + validatorCacheList.PushBack(cachedVal) + + // if the cache is too big, pop off the last element from it + if validatorCacheList.Len() > 500 { + valToRemove := validatorCacheList.Remove(validatorCacheList.Front()).(cachedValidator) + delete(validatorCache, valToRemove.marshalled) + } + validator = types.MustUnmarshalValidator(k.cdc, addr, value) return validator, true } @@ -212,6 +248,7 @@ func (k Keeper) UpdateValidator(ctx sdk.Context, validator types.Validator) type cliffPower := k.GetCliffValidatorPower(ctx) switch { + // if the validator is already bonded and the power is increasing, we need // perform the following: // a) update Tendermint @@ -240,10 +277,11 @@ func (k Keeper) UpdateValidator(ctx sdk.Context, validator types.Validator) type bytes.Compare(valPower, cliffPower) == -1: //(valPower < cliffPower // skip to completion - // default case - validator was either: + default: + // default case - validator was either: // a) not-bonded and now has power-rank greater than cliff validator // b) bonded and now has decreased in power - default: + // update the validator set for this validator updatedVal, updated := k.UpdateBondedValidators(ctx, validator) if updated { @@ -307,10 +345,13 @@ func (k Keeper) updateCliffValidator(ctx sdk.Context, affectedVal types.Validato newCliffValRank := GetValidatorsByPowerIndexKey(newCliffVal, pool) if bytes.Equal(affectedVal.Operator, newCliffVal.Operator) { + // The affected validator remains the cliff validator, however, since // the store does not contain the new power, update the new power rank. store.Set(ValidatorPowerCliffKey, affectedValRank) + } else if bytes.Compare(affectedValRank, newCliffValRank) > 0 { + // The affected validator no longer remains the cliff validator as it's // power is greater than the new cliff validator. k.setCliffValidator(ctx, newCliffVal, pool) @@ -321,7 +362,7 @@ func (k Keeper) updateCliffValidator(ctx sdk.Context, affectedVal types.Validato func (k Keeper) updateForJailing(ctx sdk.Context, oldFound bool, oldValidator, newValidator types.Validator) types.Validator { if newValidator.Jailed && oldFound && oldValidator.Status == sdk.Bonded { - newValidator = k.unbondValidator(ctx, newValidator) + newValidator = k.beginUnbondingValidator(ctx, newValidator) // need to also clear the cliff validator spot because the jail has // opened up a new spot which will be filled when @@ -416,20 +457,20 @@ func (k Keeper) UpdateBondedValidators( } } - // increment bondedValidatorsCount / get the validator to bond - if !validator.Jailed { - if validator.Status != sdk.Bonded { - validatorToBond = validator - if newValidatorBonded { - panic("already decided to bond a validator, can't bond another!") - } - newValidatorBonded = true - } - } else { - // TODO: document why we must break here. + // if we've reached jailed validators no further bonded validators exist + if validator.Jailed { break } + // increment bondedValidatorsCount / get the validator to bond + if validator.Status != sdk.Bonded { + validatorToBond = validator + if newValidatorBonded { + panic("already decided to bond a validator, can't bond another!") + } + newValidatorBonded = true + } + // increment the total number of bonded validators and potentially mark // the validator to bond if validator.Status != sdk.Bonded { @@ -464,13 +505,15 @@ func (k Keeper) UpdateBondedValidators( } if bytes.Equal(validatorToBond.Operator, affectedValidator.Operator) { - // unbond the old cliff validator iff the affected validator was - // newly bonded and has greater power - k.unbondValidator(ctx, oldCliffVal) + + // begin unbonding the old cliff validator iff the affected + // validator was newly bonded and has greater power + k.beginUnbondingValidator(ctx, oldCliffVal) } else { - // otherwise unbond the affected validator, which must have been - // kicked out - affectedValidator = k.unbondValidator(ctx, affectedValidator) + + // otherwise begin unbonding the affected validator, which must + // have been kicked out + affectedValidator = k.beginUnbondingValidator(ctx, affectedValidator) } } @@ -563,25 +606,30 @@ func kickOutValidators(k Keeper, ctx sdk.Context, toKickOut map[string]byte) { if !found { panic(fmt.Sprintf("validator record not found for address: %v\n", ownerAddr)) } - k.unbondValidator(ctx, validator) + k.beginUnbondingValidator(ctx, validator) } } // perform all the store operations for when a validator status becomes unbonded -func (k Keeper) unbondValidator(ctx sdk.Context, validator types.Validator) types.Validator { +func (k Keeper) beginUnbondingValidator(ctx sdk.Context, validator types.Validator) types.Validator { store := ctx.KVStore(k.storeKey) pool := k.GetPool(ctx) + params := k.GetParams(ctx) // sanity check - if validator.Status == sdk.Unbonded { - panic(fmt.Sprintf("should not already be unbonded, validator: %v\n", validator)) + if validator.Status == sdk.Unbonded || + validator.Status == sdk.Unbonding { + panic(fmt.Sprintf("should not already be unbonded or unbonding, validator: %v\n", validator)) } // set the status - validator, pool = validator.UpdateStatus(pool, sdk.Unbonded) + validator, pool = validator.UpdateStatus(pool, sdk.Unbonding) k.SetPool(ctx, pool) + validator.UnbondingMinTime = ctx.BlockHeader().Time.Add(params.UnbondingTime) + validator.UnbondingHeight = ctx.BlockHeader().Height + // save the now unbonded validator record k.SetValidator(ctx, validator) @@ -591,6 +639,13 @@ func (k Keeper) unbondValidator(ctx sdk.Context, validator types.Validator) type // also remove from the Bonded types.Validators Store store.Delete(GetValidatorsBondedIndexKey(validator.Operator)) + + // call the unbond hook if present + if k.validatorHooks != nil { + k.validatorHooks.OnValidatorBeginUnbonding(ctx, validator.ConsAddress()) + } + + // return updated validator return validator } @@ -617,6 +672,12 @@ func (k Keeper) bondValidator(ctx sdk.Context, validator types.Validator) types. bzABCI := k.cdc.MustMarshalBinary(validator.ABCIValidator()) store.Set(GetTendermintUpdatesKey(validator.Operator), bzABCI) + // call the bond hook if present + if k.validatorHooks != nil { + k.validatorHooks.OnValidatorBonded(ctx, validator.ConsAddress()) + } + + // return updated validator return validator } diff --git a/x/stake/keeper/validator_test.go b/x/stake/keeper/validator_test.go index e4a2663ae..2f471a2a5 100644 --- a/x/stake/keeper/validator_test.go +++ b/x/stake/keeper/validator_test.go @@ -137,7 +137,7 @@ func TestUpdateBondedValidatorsDecreaseCliff(t *testing.T) { expectedValStatus := map[int]sdk.BondStatus{ 9: sdk.Bonded, 8: sdk.Bonded, 7: sdk.Bonded, 5: sdk.Bonded, 4: sdk.Bonded, - 0: sdk.Unbonded, 1: sdk.Unbonded, 2: sdk.Unbonded, 3: sdk.Unbonded, 6: sdk.Unbonded, + 0: sdk.Unbonding, 1: sdk.Unbonding, 2: sdk.Unbonding, 3: sdk.Unbonding, 6: sdk.Unbonding, } // require all the validators have their respective statuses @@ -145,9 +145,11 @@ func TestUpdateBondedValidatorsDecreaseCliff(t *testing.T) { valAddr := validators[valIdx].Operator val, _ := keeper.GetValidator(ctx, valAddr) - require.Equal( - t, val.GetStatus(), status, - fmt.Sprintf("expected validator to have status: %s", sdk.BondStatusToString(status))) + assert.Equal( + t, status, val.GetStatus(), + fmt.Sprintf("expected validator at index %v to have status: %s", + valIdx, + sdk.BondStatusToString(status))) } } @@ -610,8 +612,8 @@ func TestFullValidatorSetPowerChange(t *testing.T) { validators[i], found = keeper.GetValidator(ctx, validators[i].Operator) require.True(t, found) } - assert.Equal(t, sdk.Unbonded, validators[0].Status) - assert.Equal(t, sdk.Unbonded, validators[1].Status) + assert.Equal(t, sdk.Unbonding, validators[0].Status) + assert.Equal(t, sdk.Unbonding, validators[1].Status) assert.Equal(t, sdk.Bonded, validators[2].Status) assert.Equal(t, sdk.Bonded, validators[3].Status) assert.Equal(t, sdk.Unbonded, validators[4].Status) diff --git a/x/stake/simulation/invariants.go b/x/stake/simulation/invariants.go index 1e961d05d..859bb0591 100644 --- a/x/stake/simulation/invariants.go +++ b/x/stake/simulation/invariants.go @@ -45,6 +45,7 @@ func SupplyInvariants(ck bank.Keeper, k stake.Keeper, am auth.AccountMapper) sim case sdk.Bonded: bonded = bonded.Add(validator.GetPower()) case sdk.Unbonding: + loose = loose.Add(validator.GetTokens().RoundInt()) case sdk.Unbonded: loose = loose.Add(validator.GetTokens().RoundInt()) } diff --git a/x/stake/simulation/msgs.go b/x/stake/simulation/msgs.go index 97ed112c0..af42dba49 100644 --- a/x/stake/simulation/msgs.go +++ b/x/stake/simulation/msgs.go @@ -5,8 +5,6 @@ import ( "math/rand" "testing" - "github.com/stretchr/testify/require" - "github.com/cosmos/cosmos-sdk/baseapp" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/x/auth" @@ -19,7 +17,9 @@ import ( // SimulateMsgCreateValidator func SimulateMsgCreateValidator(m auth.AccountMapper, k stake.Keeper) simulation.Operation { - return func(t *testing.T, r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, keys []crypto.PrivKey, log string, event func(string)) (action string, fOp []simulation.FutureOperation, err sdk.Error) { + handler := stake.NewHandler(k) + return func(tb testing.TB, r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, keys []crypto.PrivKey, log string, event func(string)) (action string, fOp []simulation.FutureOperation, err sdk.Error) { + denom := k.GetParams(ctx).BondDenom description := stake.Description{ Moniker: simulation.RandStringOfLength(r, 10), @@ -41,9 +41,11 @@ func SimulateMsgCreateValidator(m auth.AccountMapper, k stake.Keeper) simulation PubKey: pubkey, Delegation: sdk.NewCoin(denom, amount), } - require.Nil(t, msg.ValidateBasic(), "expected msg to pass ValidateBasic: %s", msg.GetSignBytes()) + if msg.ValidateBasic() != nil { + tb.Fatalf("expected msg to pass ValidateBasic: %s, log %s", msg.GetSignBytes(), log) + } ctx, write := ctx.CacheContext() - result := stake.NewHandler(k)(ctx, msg) + result := handler(ctx, msg) if result.IsOK() { write() } @@ -56,7 +58,9 @@ func SimulateMsgCreateValidator(m auth.AccountMapper, k stake.Keeper) simulation // SimulateMsgEditValidator func SimulateMsgEditValidator(k stake.Keeper) simulation.Operation { - return func(t *testing.T, r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, keys []crypto.PrivKey, log string, event func(string)) (action string, fOp []simulation.FutureOperation, err sdk.Error) { + handler := stake.NewHandler(k) + return func(tb testing.TB, r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, keys []crypto.PrivKey, log string, event func(string)) (action string, fOp []simulation.FutureOperation, err sdk.Error) { + description := stake.Description{ Moniker: simulation.RandStringOfLength(r, 10), Identity: simulation.RandStringOfLength(r, 10), @@ -70,9 +74,11 @@ func SimulateMsgEditValidator(k stake.Keeper) simulation.Operation { Description: description, ValidatorAddr: address, } - require.Nil(t, msg.ValidateBasic(), "expected msg to pass ValidateBasic: %s", msg.GetSignBytes()) + if msg.ValidateBasic() != nil { + tb.Fatalf("expected msg to pass ValidateBasic: %s, log %s", msg.GetSignBytes(), log) + } ctx, write := ctx.CacheContext() - result := stake.NewHandler(k)(ctx, msg) + result := handler(ctx, msg) if result.IsOK() { write() } @@ -84,7 +90,9 @@ func SimulateMsgEditValidator(k stake.Keeper) simulation.Operation { // SimulateMsgDelegate func SimulateMsgDelegate(m auth.AccountMapper, k stake.Keeper) simulation.Operation { - return func(t *testing.T, r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, keys []crypto.PrivKey, log string, event func(string)) (action string, fOp []simulation.FutureOperation, err sdk.Error) { + handler := stake.NewHandler(k) + return func(tb testing.TB, r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, keys []crypto.PrivKey, log string, event func(string)) (action string, fOp []simulation.FutureOperation, err sdk.Error) { + denom := k.GetParams(ctx).BondDenom validatorKey := simulation.RandomKey(r, keys) validatorAddress := sdk.ValAddress(validatorKey.PubKey().Address()) @@ -102,9 +110,11 @@ func SimulateMsgDelegate(m auth.AccountMapper, k stake.Keeper) simulation.Operat ValidatorAddr: validatorAddress, Delegation: sdk.NewCoin(denom, amount), } - require.Nil(t, msg.ValidateBasic(), "expected msg to pass ValidateBasic: %s", msg.GetSignBytes()) + if msg.ValidateBasic() != nil { + tb.Fatalf("expected msg to pass ValidateBasic: %s, log %s", msg.GetSignBytes(), log) + } ctx, write := ctx.CacheContext() - result := stake.NewHandler(k)(ctx, msg) + result := handler(ctx, msg) if result.IsOK() { write() } @@ -116,7 +126,9 @@ func SimulateMsgDelegate(m auth.AccountMapper, k stake.Keeper) simulation.Operat // SimulateMsgBeginUnbonding func SimulateMsgBeginUnbonding(m auth.AccountMapper, k stake.Keeper) simulation.Operation { - return func(t *testing.T, r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, keys []crypto.PrivKey, log string, event func(string)) (action string, fOp []simulation.FutureOperation, err sdk.Error) { + handler := stake.NewHandler(k) + return func(tb testing.TB, r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, keys []crypto.PrivKey, log string, event func(string)) (action string, fOp []simulation.FutureOperation, err sdk.Error) { + denom := k.GetParams(ctx).BondDenom validatorKey := simulation.RandomKey(r, keys) validatorAddress := sdk.ValAddress(validatorKey.PubKey().Address()) @@ -134,9 +146,11 @@ func SimulateMsgBeginUnbonding(m auth.AccountMapper, k stake.Keeper) simulation. ValidatorAddr: validatorAddress, SharesAmount: sdk.NewDecFromInt(amount), } - require.Nil(t, msg.ValidateBasic(), "expected msg to pass ValidateBasic: %s", msg.GetSignBytes()) + if msg.ValidateBasic() != nil { + tb.Fatalf("expected msg to pass ValidateBasic: %s, log %s", msg.GetSignBytes(), log) + } ctx, write := ctx.CacheContext() - result := stake.NewHandler(k)(ctx, msg) + result := handler(ctx, msg) if result.IsOK() { write() } @@ -148,7 +162,9 @@ func SimulateMsgBeginUnbonding(m auth.AccountMapper, k stake.Keeper) simulation. // SimulateMsgCompleteUnbonding func SimulateMsgCompleteUnbonding(k stake.Keeper) simulation.Operation { - return func(t *testing.T, r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, keys []crypto.PrivKey, log string, event func(string)) (action string, fOp []simulation.FutureOperation, err sdk.Error) { + handler := stake.NewHandler(k) + return func(tb testing.TB, r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, keys []crypto.PrivKey, log string, event func(string)) (action string, fOp []simulation.FutureOperation, err sdk.Error) { + validatorKey := simulation.RandomKey(r, keys) validatorAddress := sdk.ValAddress(validatorKey.PubKey().Address()) delegatorKey := simulation.RandomKey(r, keys) @@ -157,9 +173,11 @@ func SimulateMsgCompleteUnbonding(k stake.Keeper) simulation.Operation { DelegatorAddr: delegatorAddress, ValidatorAddr: validatorAddress, } - require.Nil(t, msg.ValidateBasic(), "expected msg to pass ValidateBasic: %s", msg.GetSignBytes()) + if msg.ValidateBasic() != nil { + tb.Fatalf("expected msg to pass ValidateBasic: %s, log %s", msg.GetSignBytes(), log) + } ctx, write := ctx.CacheContext() - result := stake.NewHandler(k)(ctx, msg) + result := handler(ctx, msg) if result.IsOK() { write() } @@ -171,7 +189,9 @@ func SimulateMsgCompleteUnbonding(k stake.Keeper) simulation.Operation { // SimulateMsgBeginRedelegate func SimulateMsgBeginRedelegate(m auth.AccountMapper, k stake.Keeper) simulation.Operation { - return func(t *testing.T, r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, keys []crypto.PrivKey, log string, event func(string)) (action string, fOp []simulation.FutureOperation, err sdk.Error) { + handler := stake.NewHandler(k) + return func(tb testing.TB, r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, keys []crypto.PrivKey, log string, event func(string)) (action string, fOp []simulation.FutureOperation, err sdk.Error) { + denom := k.GetParams(ctx).BondDenom sourceValidatorKey := simulation.RandomKey(r, keys) sourceValidatorAddress := sdk.ValAddress(sourceValidatorKey.PubKey().Address()) @@ -193,9 +213,11 @@ func SimulateMsgBeginRedelegate(m auth.AccountMapper, k stake.Keeper) simulation ValidatorDstAddr: destValidatorAddress, SharesAmount: sdk.NewDecFromInt(amount), } - require.Nil(t, msg.ValidateBasic(), "expected msg to pass ValidateBasic: %s", msg.GetSignBytes()) + if msg.ValidateBasic() != nil { + tb.Fatalf("expected msg to pass ValidateBasic: %s, log %s", msg.GetSignBytes(), log) + } ctx, write := ctx.CacheContext() - result := stake.NewHandler(k)(ctx, msg) + result := handler(ctx, msg) if result.IsOK() { write() } @@ -207,7 +229,9 @@ func SimulateMsgBeginRedelegate(m auth.AccountMapper, k stake.Keeper) simulation // SimulateMsgCompleteRedelegate func SimulateMsgCompleteRedelegate(k stake.Keeper) simulation.Operation { - return func(t *testing.T, r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, keys []crypto.PrivKey, log string, event func(string)) (action string, fOp []simulation.FutureOperation, err sdk.Error) { + handler := stake.NewHandler(k) + return func(tb testing.TB, r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, keys []crypto.PrivKey, log string, event func(string)) (action string, fOp []simulation.FutureOperation, err sdk.Error) { + validatorSrcKey := simulation.RandomKey(r, keys) validatorSrcAddress := sdk.ValAddress(validatorSrcKey.PubKey().Address()) validatorDstKey := simulation.RandomKey(r, keys) @@ -219,9 +243,11 @@ func SimulateMsgCompleteRedelegate(k stake.Keeper) simulation.Operation { ValidatorSrcAddr: validatorSrcAddress, ValidatorDstAddr: validatorDstAddress, } - require.Nil(t, msg.ValidateBasic(), "expected msg to pass ValidateBasic: %s", msg.GetSignBytes()) + if msg.ValidateBasic() != nil { + tb.Fatalf("expected msg to pass ValidateBasic: %s, log %s", msg.GetSignBytes(), log) + } ctx, write := ctx.CacheContext() - result := stake.NewHandler(k)(ctx, msg) + result := handler(ctx, msg) if result.IsOK() { write() } diff --git a/x/stake/types/validator.go b/x/stake/types/validator.go index 2cb952db2..0d9309311 100644 --- a/x/stake/types/validator.go +++ b/x/stake/types/validator.go @@ -3,6 +3,7 @@ package types import ( "bytes" "fmt" + "time" abci "github.com/tendermint/tendermint/abci/types" "github.com/tendermint/tendermint/crypto" @@ -31,15 +32,14 @@ type Validator struct { Description Description `json:"description"` // description terms for the validator BondHeight int64 `json:"bond_height"` // earliest height as a bonded validator BondIntraTxCounter int16 `json:"bond_intra_tx_counter"` // block-local tx index of validator change - ProposerRewardPool sdk.Coins `json:"proposer_reward_pool"` // XXX reward pool collected from being the proposer + + UnbondingHeight int64 `json:"unbonding_height"` // if unbonding, height at which this validator has begun unbonding + UnbondingMinTime time.Time `json:"unbonding_time"` // if unbonding, min time for the validator to complete unbonding Commission sdk.Dec `json:"commission"` // XXX the commission rate of fees charged to any delegators CommissionMax sdk.Dec `json:"commission_max"` // XXX maximum commission rate which this validator can ever charge CommissionChangeRate sdk.Dec `json:"commission_change_rate"` // XXX maximum daily increase of the validator commission CommissionChangeToday sdk.Dec `json:"commission_change_today"` // XXX commission rate change today, reset each day (UTC time) - - // fee related - LastBondedTokens sdk.Dec `json:"prev_bonded_tokens"` // Previous bonded tokens held } // NewValidator - initialize a new validator @@ -54,12 +54,12 @@ func NewValidator(operator sdk.ValAddress, pubKey crypto.PubKey, description Des Description: description, BondHeight: int64(0), BondIntraTxCounter: int16(0), - ProposerRewardPool: sdk.Coins{}, + UnbondingHeight: int64(0), + UnbondingMinTime: time.Unix(0, 0), Commission: sdk.ZeroDec(), CommissionMax: sdk.ZeroDec(), CommissionChangeRate: sdk.ZeroDec(), CommissionChangeToday: sdk.ZeroDec(), - LastBondedTokens: sdk.ZeroDec(), } } @@ -73,12 +73,12 @@ type validatorValue struct { Description Description BondHeight int64 BondIntraTxCounter int16 - ProposerRewardPool sdk.Coins + UnbondingHeight int64 + UnbondingMinTime time.Time Commission sdk.Dec CommissionMax sdk.Dec CommissionChangeRate sdk.Dec CommissionChangeToday sdk.Dec - LastBondedTokens sdk.Dec } // return the redelegation without fields contained within the key for the store @@ -92,12 +92,12 @@ func MustMarshalValidator(cdc *wire.Codec, validator Validator) []byte { Description: validator.Description, BondHeight: validator.BondHeight, BondIntraTxCounter: validator.BondIntraTxCounter, - ProposerRewardPool: validator.ProposerRewardPool, + UnbondingHeight: validator.UnbondingHeight, + UnbondingMinTime: validator.UnbondingMinTime, Commission: validator.Commission, CommissionMax: validator.CommissionMax, CommissionChangeRate: validator.CommissionChangeRate, CommissionChangeToday: validator.CommissionChangeToday, - LastBondedTokens: validator.LastBondedTokens, } return cdc.MustMarshalBinary(val) } @@ -108,7 +108,6 @@ func MustUnmarshalValidator(cdc *wire.Codec, operatorAddr, value []byte) Validat if err != nil { panic(err) } - return validator } @@ -134,12 +133,12 @@ func UnmarshalValidator(cdc *wire.Codec, operatorAddr, value []byte) (validator Description: storeValue.Description, BondHeight: storeValue.BondHeight, BondIntraTxCounter: storeValue.BondIntraTxCounter, - ProposerRewardPool: storeValue.ProposerRewardPool, + UnbondingHeight: storeValue.UnbondingHeight, + UnbondingMinTime: storeValue.UnbondingMinTime, Commission: storeValue.Commission, CommissionMax: storeValue.CommissionMax, CommissionChangeRate: storeValue.CommissionChangeRate, CommissionChangeToday: storeValue.CommissionChangeToday, - LastBondedTokens: storeValue.LastBondedTokens, }, nil } @@ -161,12 +160,12 @@ func (v Validator) HumanReadableString() (string, error) { resp += fmt.Sprintf("Delegator Shares: %s\n", v.DelegatorShares.String()) resp += fmt.Sprintf("Description: %s\n", v.Description) resp += fmt.Sprintf("Bond Height: %d\n", v.BondHeight) - resp += fmt.Sprintf("Proposer Reward Pool: %s\n", v.ProposerRewardPool.String()) + resp += fmt.Sprintf("Unbonding Height: %d\n", v.UnbondingHeight) + resp += fmt.Sprintf("Minimum Unbonding Time: %v\n", v.UnbondingMinTime) resp += fmt.Sprintf("Commission: %s\n", v.Commission.String()) resp += fmt.Sprintf("Max Commission Rate: %s\n", v.CommissionMax.String()) resp += fmt.Sprintf("Commission Change Rate: %s\n", v.CommissionChangeRate.String()) resp += fmt.Sprintf("Commission Change Today: %s\n", v.CommissionChangeToday.String()) - resp += fmt.Sprintf("Previous Bonded Tokens: %s\n", v.LastBondedTokens.String()) return resp, nil } @@ -186,15 +185,14 @@ type BechValidator struct { Description Description `json:"description"` // description terms for the validator BondHeight int64 `json:"bond_height"` // earliest height as a bonded validator BondIntraTxCounter int16 `json:"bond_intra_tx_counter"` // block-local tx index of validator change - ProposerRewardPool sdk.Coins `json:"proposer_reward_pool"` // XXX reward pool collected from being the proposer + + UnbondingHeight int64 `json:"unbonding_height"` // if unbonding, height at which this validator has begun unbonding + UnbondingMinTime time.Time `json:"unbonding_time"` // if unbonding, min time for the validator to complete unbonding Commission sdk.Dec `json:"commission"` // XXX the commission rate of fees charged to any delegators CommissionMax sdk.Dec `json:"commission_max"` // XXX maximum commission rate which this validator can ever charge CommissionChangeRate sdk.Dec `json:"commission_change_rate"` // XXX maximum daily increase of the validator commission CommissionChangeToday sdk.Dec `json:"commission_change_today"` // XXX commission rate change today, reset each day (UTC time) - - // fee related - LastBondedTokens sdk.Dec `json:"prev_bonded_shares"` // last bonded token amount } // get the bech validator from the the regular validator @@ -216,14 +214,13 @@ func (v Validator) Bech32Validator() (BechValidator, error) { Description: v.Description, BondHeight: v.BondHeight, BondIntraTxCounter: v.BondIntraTxCounter, - ProposerRewardPool: v.ProposerRewardPool, + UnbondingHeight: v.UnbondingHeight, + UnbondingMinTime: v.UnbondingMinTime, Commission: v.Commission, CommissionMax: v.CommissionMax, CommissionChangeRate: v.CommissionChangeRate, CommissionChangeToday: v.CommissionChangeToday, - - LastBondedTokens: v.LastBondedTokens, }, nil } @@ -238,12 +235,15 @@ func (v Validator) Equal(c2 Validator) bool { v.Tokens.Equal(c2.Tokens) && v.DelegatorShares.Equal(c2.DelegatorShares) && v.Description == c2.Description && - v.ProposerRewardPool.IsEqual(c2.ProposerRewardPool) && v.Commission.Equal(c2.Commission) && v.CommissionMax.Equal(c2.CommissionMax) && v.CommissionChangeRate.Equal(c2.CommissionChangeRate) && - v.CommissionChangeToday.Equal(c2.CommissionChangeToday) && - v.LastBondedTokens.Equal(c2.LastBondedTokens) + v.CommissionChangeToday.Equal(c2.CommissionChangeToday) +} + +// return the TM validator address +func (v Validator) ConsAddress() sdk.ConsAddress { + return sdk.ConsAddress(v.PubKey.Address()) } // constant used in flags to indicate that description field should not be updated @@ -423,6 +423,20 @@ func (v Validator) BondedTokens() sdk.Dec { return sdk.ZeroDec() } +// Returns if the validator should be considered unbonded +func (v Validator) IsUnbonded(ctx sdk.Context) bool { + switch v.Status { + case sdk.Unbonded: + return true + case sdk.Unbonding: + ctxTime := ctx.BlockHeader().Time + if ctxTime.After(v.UnbondingMinTime) { + return true + } + } + return false +} + //______________________________________________________________________ // ensure fulfills the sdk validator types