diff --git a/.circleci/config.yml b/.circleci/config.yml index 7a77d8d21..bb4e957b1 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -85,6 +85,23 @@ jobs: export PATH="$GOBIN:$PATH" make test_cli + test_sim: + <<: *defaults + parallelism: 1 + steps: + - attach_workspace: + at: /tmp/workspace + - restore_cache: + key: v1-pkg-cache + - restore_cache: + key: v1-tree-{{ .Environment.CIRCLE_SHA1 }} + - run: + name: Test simulation + command: | + export PATH="$GOBIN:$PATH" + export GAIA_SIMULATION_SEED=1531897442166404087 + make test_sim + test_cover: <<: *defaults parallelism: 4 @@ -144,6 +161,9 @@ workflows: - test_cli: requires: - setup_dependencies + - test_sim: + requires: + - setup_dependencies - test_cover: requires: - setup_dependencies diff --git a/Makefile b/Makefile index 4c01f68e4..6459f08e8 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,6 @@ PACKAGES=$(shell go list ./... | grep -v '/vendor/') -PACKAGES_NOCLITEST=$(shell go list ./... | grep -v '/vendor/' | grep -v github.com/cosmos/cosmos-sdk/cmd/gaia/cli_test) +PACKAGES_NOCLITEST=$(shell go list ./... | grep -v '/vendor/' | grep -v '/simulation' | grep -v github.com/cosmos/cosmos-sdk/cmd/gaia/cli_test) +PACKAGES_SIMTEST=$(shell go list ./... | grep -v '/vendor/' | grep '/simulation') COMMIT_HASH := $(shell git rev-parse --short HEAD) BUILD_TAGS = netgo ledger BUILD_FLAGS = -tags "${BUILD_TAGS}" -ldflags "-X github.com/cosmos/cosmos-sdk/version.GitCommit=${COMMIT_HASH}" @@ -127,6 +128,16 @@ test_unit: test_race: @go test -race $(PACKAGES_NOCLITEST) +test_sim: + @echo "Running individual module simulations." + @go test $(PACKAGES_SIMTEST) -v + @echo "Running full Gaia simulation. This may take several minutes." + @echo "Pass the flag 'SimulationSeed' to run with a constant seed." + @echo "Pass the flag 'SimulationNumKeys' to run with the specified number of keys." + @echo "Pass the flag 'SimulationNumBlocks' to run with the specified number of blocks." + @echo "Pass the flag 'SimulationBlockSize' to run with the specified block size (operations per block)." + @go test ./cmd/gaia/app -run TestFullGaiaSimulation -SimulationEnabled=true -SimulationBlockSize=200 -v + test_cover: @bash tests/test_cover.sh @@ -212,4 +223,4 @@ remotenet-status: check_tools get_tools get_vendor_deps draw_deps test test_cli test_unit \ test_cover test_lint benchmark devdoc_init devdoc devdoc_save devdoc_update \ build-linux build-docker-gaiadnode localnet-start localnet-stop remotenet-start \ -remotenet-stop remotenet-status format check-ledger +remotenet-stop remotenet-status format check-ledger test_sim diff --git a/PENDING.md b/PENDING.md index 40a2b34c5..31ba32350 100644 --- a/PENDING.md +++ b/PENDING.md @@ -4,9 +4,26 @@ BREAKING CHANGES * [baseapp] Msgs are no longer run on CheckTx, removed `ctx.IsCheckTx()` * [x/stake] Fixed the period check for the inflation calculation * [x/stake] Inflation doesn't use rationals in calculation (performance boost) +* \#1606 The following CLI commands have been switched to use `--from` + * `gaiacli stake create-validator --address-validator` + * `gaiacli stake edit-validator --address-validator` + * `gaiacli stake delegate --address-delegator` + * `gaiacli stake unbond begin --address-delegator` + * `gaiacli stake unbond complete --address-delegator` + * `gaiacli stake redelegate begin --address-delegator` + * `gaiacli stake redelegate complete --address-delegator` + * `gaiacli stake unrevoke [validator-address]` + * `gaiacli gov submit-proposal --proposer` + * `gaiacli gov deposit --depositer` + * `gaiacli gov vote --voter` FEATURES * [lcd] Can now query governance proposals by ProposalStatus +* [x/mock/simulation] Randomized simulation framework + * Modules specify invariants and operations, preferably in an x/[module]/simulation package + * Modules can test random combinations of their own operations + * Applications can integrate operations and invariants from modules together for an integrated simulation +* [baseapp] Initialize validator set on ResponseInitChain * Added support for cosmos-sdk-cli tool under cosmos-sdk/cmd * This allows SDK users to init a new project repository with a single command. diff --git a/baseapp/baseapp.go b/baseapp/baseapp.go index da75591a2..ca5e8fd97 100644 --- a/baseapp/baseapp.go +++ b/baseapp/baseapp.go @@ -290,7 +290,7 @@ func (app *BaseApp) InitChain(req abci.RequestInitChain) (res abci.ResponseInitC if app.initChainer == nil { return } - app.initChainer(app.deliverState.ctx, req) // no error + res = app.initChainer(app.deliverState.ctx, req) // NOTE: we don't commit, but BeginBlock for block 1 // starts from this deliverState diff --git a/client/lcd/lcd_test.go b/client/lcd/lcd_test.go index f5b2d263e..2de92575a 100644 --- a/client/lcd/lcd_test.go +++ b/client/lcd/lcd_test.go @@ -358,25 +358,20 @@ func TestTxs(t *testing.T) { } func TestValidatorsQuery(t *testing.T) { - cleanup, pks, port := InitializeTestLCD(t, 2, []sdk.AccAddress{}) + cleanup, pks, port := InitializeTestLCD(t, 1, []sdk.AccAddress{}) defer cleanup() - require.Equal(t, 2, len(pks)) + require.Equal(t, 1, len(pks)) validators := getValidators(t, port) - require.Equal(t, len(validators), 2) + require.Equal(t, len(validators), 1) // make sure all the validators were found (order unknown because sorted by owner addr) - foundVal1, foundVal2 := false, false - pk1Bech := sdk.MustBech32ifyValPub(pks[0]) - pk2Bech := sdk.MustBech32ifyValPub(pks[1]) - if validators[0].PubKey == pk1Bech || validators[1].PubKey == pk1Bech { - foundVal1 = true + foundVal := false + pkBech := sdk.MustBech32ifyValPub(pks[0]) + if validators[0].PubKey == pkBech { + foundVal = true } - if validators[0].PubKey == pk2Bech || validators[1].PubKey == pk2Bech { - foundVal2 = true - } - require.True(t, foundVal1, "pk1Bech %v, owner1 %v, owner2 %v", pk1Bech, validators[0].Owner, validators[1].Owner) - require.True(t, foundVal2, "pk2Bech %v, owner1 %v, owner2 %v", pk2Bech, validators[0].Owner, validators[1].Owner) + require.True(t, foundVal, "pkBech %v, owner %v", pkBech, validators[0].Owner) } func TestBonding(t *testing.T) { diff --git a/cmd/gaia/app/app.go b/cmd/gaia/app/app.go index 7b0f85e30..ab8a27e6c 100644 --- a/cmd/gaia/app/app.go +++ b/cmd/gaia/app/app.go @@ -173,7 +173,7 @@ func (app *GaiaApp) initChainer(ctx sdk.Context, req abci.RequestInitChain) abci } // load the initial stake information - err = stake.InitGenesis(ctx, app.stakeKeeper, genesisState.StakeData) + validators, err := stake.InitGenesis(ctx, app.stakeKeeper, genesisState.StakeData) if err != nil { panic(err) // TODO https://github.com/cosmos/cosmos-sdk/issues/468 // return sdk.ErrGenesisParse("").TraceCause(err, "") @@ -181,7 +181,9 @@ func (app *GaiaApp) initChainer(ctx sdk.Context, req abci.RequestInitChain) abci gov.InitGenesis(ctx, app.govKeeper, gov.DefaultGenesisState()) - return abci.ResponseInitChain{} + return abci.ResponseInitChain{ + Validators: validators, + } } // export the state of gaia for a genesis file diff --git a/cmd/gaia/app/sim_test.go b/cmd/gaia/app/sim_test.go new file mode 100644 index 000000000..f0bea1e17 --- /dev/null +++ b/cmd/gaia/app/sim_test.go @@ -0,0 +1,100 @@ +package app + +import ( + "encoding/json" + "flag" + "math/rand" + "testing" + + "github.com/stretchr/testify/require" + + dbm "github.com/tendermint/tendermint/libs/db" + "github.com/tendermint/tendermint/libs/log" + + sdk "github.com/cosmos/cosmos-sdk/types" + banksim "github.com/cosmos/cosmos-sdk/x/bank/simulation" + "github.com/cosmos/cosmos-sdk/x/mock/simulation" + stake "github.com/cosmos/cosmos-sdk/x/stake" + stakesim "github.com/cosmos/cosmos-sdk/x/stake/simulation" +) + +var ( + seed int64 + numKeys int + numBlocks int + blockSize int + enabled bool +) + +func init() { + flag.Int64Var(&seed, "SimulationSeed", 42, "Simulation random seed") + flag.IntVar(&numKeys, "SimulationNumKeys", 10, "Number of keys (accounts)") + flag.IntVar(&numBlocks, "SimulationNumBlocks", 100, "Number of blocks") + flag.IntVar(&blockSize, "SimulationBlockSize", 100, "Operations per block") + flag.BoolVar(&enabled, "SimulationEnabled", false, "Enable the simulation") +} + +func appStateFn(r *rand.Rand, accs []sdk.AccAddress) json.RawMessage { + var genesisAccounts []GenesisAccount + + // Randomly generate some genesis accounts + for _, addr := range accs { + coins := sdk.Coins{sdk.Coin{"steak", sdk.NewInt(100)}} + genesisAccounts = append(genesisAccounts, GenesisAccount{ + Address: addr, + Coins: coins, + }) + } + + // Default genesis state + stakeGenesis := stake.DefaultGenesisState() + stakeGenesis.Pool.LooseTokens = sdk.NewRat(1000) + genesis := GenesisState{ + Accounts: genesisAccounts, + StakeData: stakeGenesis, + } + + // Marshal genesis + appState, err := MakeCodec().MarshalJSON(genesis) + if err != nil { + panic(err) + } + + return appState +} + +func TestFullGaiaSimulation(t *testing.T) { + if !enabled { + t.Skip("Skipping Gaia simulation") + } + + // Setup Gaia application + logger := log.NewNopLogger() + db := dbm.NewMemDB() + app := NewGaiaApp(logger, db, nil) + require.Equal(t, "GaiaApp", app.Name()) + + // Run randomized simulation + simulation.SimulateFromSeed( + t, app.BaseApp, appStateFn, seed, + []simulation.TestAndRunTx{ + banksim.TestAndRunSingleInputMsgSend(app.accountMapper), + stakesim.SimulateMsgCreateValidator(app.accountMapper, app.stakeKeeper), + stakesim.SimulateMsgEditValidator(app.stakeKeeper), + stakesim.SimulateMsgDelegate(app.accountMapper, app.stakeKeeper), + stakesim.SimulateMsgBeginUnbonding(app.accountMapper, app.stakeKeeper), + stakesim.SimulateMsgCompleteUnbonding(app.stakeKeeper), + stakesim.SimulateMsgBeginRedelegate(app.accountMapper, app.stakeKeeper), + stakesim.SimulateMsgCompleteRedelegate(app.stakeKeeper), + }, + []simulation.RandSetup{}, + []simulation.Invariant{ + banksim.NonnegativeBalanceInvariant(app.accountMapper), + stakesim.AllInvariants(app.coinKeeper, app.stakeKeeper, app.accountMapper), + }, + numKeys, + numBlocks, + blockSize, + ) + +} diff --git a/cmd/gaia/cli_test/cli_test.go b/cmd/gaia/cli_test/cli_test.go index 650021d31..beac34097 100644 --- a/cmd/gaia/cli_test/cli_test.go +++ b/cmd/gaia/cli_test/cli_test.go @@ -118,7 +118,6 @@ func TestGaiaCLICreateValidator(t *testing.T) { // create validator cvStr := fmt.Sprintf("gaiacli stake create-validator %v", flags) cvStr += fmt.Sprintf(" --from=%s", "bar") - cvStr += fmt.Sprintf(" --address-validator=%s", barAddr) cvStr += fmt.Sprintf(" --pubkey=%s", barCeshPubKey) cvStr += fmt.Sprintf(" --amount=%v", "2steak") cvStr += fmt.Sprintf(" --moniker=%v", "bar-vally") @@ -137,7 +136,6 @@ func TestGaiaCLICreateValidator(t *testing.T) { unbondStr := fmt.Sprintf("gaiacli stake unbond begin %v", flags) unbondStr += fmt.Sprintf(" --from=%s", "bar") unbondStr += fmt.Sprintf(" --address-validator=%s", barAddr) - unbondStr += fmt.Sprintf(" --address-delegator=%s", barAddr) unbondStr += fmt.Sprintf(" --shares-amount=%v", "1") success := executeWrite(t, unbondStr, pass) @@ -176,7 +174,15 @@ func TestGaiaCLISubmitProposal(t *testing.T) { fooAcc := executeGetAccount(t, fmt.Sprintf("gaiacli account %s %v", fooAddr, flags)) require.Equal(t, int64(50), fooAcc.GetCoins().AmountOf("steak").Int64()) - executeWrite(t, fmt.Sprintf("gaiacli gov submit-proposal %v --proposer=%s --deposit=5steak --type=Text --title=Test --description=test --from=foo", flags, fooAddr), pass) + // unbond a single share + spStr := fmt.Sprintf("gaiacli gov submit-proposal %v", flags) + spStr += fmt.Sprintf(" --from=%s", "foo") + spStr += fmt.Sprintf(" --deposit=%s", "5steak") + spStr += fmt.Sprintf(" --type=%s", "Text") + spStr += fmt.Sprintf(" --title=%s", "Test") + spStr += fmt.Sprintf(" --description=%s", "test") + + executeWrite(t, spStr, pass) tests.WaitForNextNBlocksTM(2, port) fooAcc = executeGetAccount(t, fmt.Sprintf("gaiacli account %s %v", fooAddr, flags)) @@ -186,7 +192,12 @@ func TestGaiaCLISubmitProposal(t *testing.T) { require.Equal(t, int64(1), proposal1.GetProposalID()) require.Equal(t, gov.StatusDepositPeriod, proposal1.GetStatus()) - executeWrite(t, fmt.Sprintf("gaiacli gov deposit %v --depositer=%s --deposit=10steak --proposalID=1 --from=foo", flags, fooAddr), pass) + depositStr := fmt.Sprintf("gaiacli gov deposit %v", flags) + depositStr += fmt.Sprintf(" --from=%s", "foo") + depositStr += fmt.Sprintf(" --deposit=%s", "10steak") + depositStr += fmt.Sprintf(" --proposalID=%s", "1") + + executeWrite(t, depositStr, pass) tests.WaitForNextNBlocksTM(2, port) fooAcc = executeGetAccount(t, fmt.Sprintf("gaiacli account %s %v", fooAddr, flags)) @@ -195,10 +206,15 @@ func TestGaiaCLISubmitProposal(t *testing.T) { require.Equal(t, int64(1), proposal1.GetProposalID()) require.Equal(t, gov.StatusVotingPeriod, proposal1.GetStatus()) - executeWrite(t, fmt.Sprintf("gaiacli gov vote %v --proposalID=1 --voter=%s --option=Yes --from=foo", flags, fooAddr), pass) + voteStr := fmt.Sprintf("gaiacli gov vote %v", flags) + voteStr += fmt.Sprintf(" --from=%s", "foo") + voteStr += fmt.Sprintf(" --proposalID=%s", "1") + voteStr += fmt.Sprintf(" --option=%s", "Yes") + + executeWrite(t, voteStr, pass) tests.WaitForNextNBlocksTM(2, port) - vote := executeGetVote(t, fmt.Sprintf("gaiacli gov query-vote --proposalID=1 --voter=%s --output=json %v", fooAddr, flags)) + vote := executeGetVote(t, fmt.Sprintf("gaiacli gov query-vote --proposalID=1 --voter=%s --output=json %v", fooAddr, flags)) require.Equal(t, int64(1), vote.ProposalID) require.Equal(t, gov.OptionYes, vote.Option) } diff --git a/cmd/gaia/cmd/gaiadebug/hack.go b/cmd/gaia/cmd/gaiadebug/hack.go index 11d2dfa73..aa3bc939e 100644 --- a/cmd/gaia/cmd/gaiadebug/hack.go +++ b/cmd/gaia/cmd/gaiadebug/hack.go @@ -249,10 +249,12 @@ func (app *GaiaApp) initChainer(ctx sdk.Context, req abci.RequestInitChain) abci } // load the initial stake information - err = stake.InitGenesis(ctx, app.stakeKeeper, genesisState.StakeData) + validators, err := stake.InitGenesis(ctx, app.stakeKeeper, genesisState.StakeData) if err != nil { panic(err) // TODO https://github.com/cosmos/cosmos-sdk/issues/468 // return sdk.ErrGenesisParse("").TraceCause(err, "") } - return abci.ResponseInitChain{} + return abci.ResponseInitChain{ + Validators: validators, + } } diff --git a/docs/light/api.md b/docs/light/api.md new file mode 100644 index 000000000..26fbe77c2 --- /dev/null +++ b/docs/light/api.md @@ -0,0 +1,444 @@ +# Cosmos Hub (Gaia) LCD API + +This document describes the API that is exposed by the specific LCD implementation of the Cosmos +Hub (Gaia). Those APIs are exposed by a REST server and can easily be accessed over HTTP/WS(websocket) +connections. + +The complete API is comprised of the sub-APIs of different modules. The modules in the Cosmos Hub +(Gaia) API are: + +* ICS0 (TendermintAPI) +* ICS1 (KeyAPI) +* ICS20 (TokenAPI) +* ICS21 (StakingAPI) - not yet implemented +* ICS22 (GovernanceAPI) - not yet implemented + +Error messages my change and should be only used for display purposes. Error messages should not be +used for determining the error type. + +## ICS0 - TendermintAPI - not yet implemented + +Exposes the same functionality as the Tendermint RPC from a full node. It aims to have a very +similar API. + +### /broadcast_tx_sync - POST + +url: /broadcast_tx_sync + +Functionality: Submit a signed transaction synchronously. This returns a response from CheckTx. + +Parameters: + +| Parameter | Type | Default | Required | Description | +| ----------- | ------ | ------- | -------- | --------------- | +| transaction | string | null | true | signed tx bytes | + +Returns on success: + +```json +{ + "rest api":"2.0", + "code":200, + "error":"", + "result":{ + "code":0, + "hash":"0D33F2F03A5234F38706E43004489E061AC40A2E", + "data":"", + "log":"" + } +} +``` + +Returns on failure: + +```json +{ + "rest api":"2.0", + "code":500, + "error":"Could not submit the transaction synchronously.", + "result":{} +} +``` + +### /broadcast_tx_async - POST + +url: /broadcast_tx_async + +Functionality: Submit a signed transaction asynchronously. This does not return a response from CheckTx. + +Parameters: + +| Parameter | Type | Default | Required | Description | +| ----------- | ------ | ------- | -------- | --------------- | +| transaction | string | null | true | signed tx bytes | + +Returns on success: + +```json +{ + "rest api":"2.0", + "code":200, + "error":"", + "result": { + "code":0, + "hash":"E39AAB7A537ABAA237831742DCE1117F187C3C52", + "data":"", + "log":"" + } +} +``` + +Returns on failure: + +```json +{ + "rest api":"2.0", + "code":500, + "error":"Could not submit the transaction asynchronously.", + "result":{} +} +``` + +### /broadcast_tx_commit - POST + +url: /broadcast_tx_commit + +Functionality: Submit a signed transaction and waits for it to be committed in a block. + +Parameters: + +| Parameter | Type | Default | Required | Description | +| ----------- | ------ | ------- | -------- | --------------- | +| transaction | string | null | true | signed tx bytes | + +Returns on success: + +```json +{ + "rest api":"2.0", + "code":200, + "error":"", + "result":{ + "height":26682, + "hash":"75CA0F856A4DA078FC4911580360E70CEFB2EBEE", + "deliver_tx":{ + "log":"", + "data":"", + "code":0 + }, + "check_tx":{ + "log":"", + "data":"", + "code":0 + } +} +``` + +Returns on failure: + +```json +{ + "rest api":"2.0", + "code":500, + "error":"Could not commit the transaction.", + "result":{} +} +``` + +## ICS1 - KeyAPI + +This API exposes all functionality needed for key creation, signing and management. + +### /keys - GET + +url: /keys + +Functionality: Gets a list of all the keys. + +Returns on success: + +```json +{ + "rest api":"2.0", + "code":200, + "error":"", + "result":{ + "keys":[ + { + "name":"monkey", + "address":"cosmosaccaddr1fedh326uxqlxs8ph9ej7cf854gz7fd5zlym5pd", + "pub_key":"cosmosaccpub1zcjduc3q8s8ha96ry4xc5xvjp9tr9w9p0e5lk5y0rpjs5epsfxs4wmf72x3shvus0t" + }, + { + "name":"test", + "address":"cosmosaccaddr1thlqhjqw78zvcy0ua4ldj9gnazqzavyw4eske2", + "pub_key":"cosmosaccpub1zcjduc3qyx6hlf825jcnj39adpkaxjer95q7yvy25yhfj3dmqy2ctev0rxmse9cuak" + } + ], + "block_height":5241 + } +} +``` + +Returns on failure: + +```json +{ + "rest api":"2.0", + "code":500, + "error":"Could not retrieve the keys.", + "result":{} +} +``` + +### /keys/recover - POST + +url: /keys/recover + +Functionality: Recover your key from seed and persist it encrypted with the password. + +Parameter: + +| Parameter | Type | Default | Required | Description | +| --------- | ------ | ------- | -------- | ---------------- | +| name | string | null | true | name of key | +| password | string | null | true | password of key | +| seed | string | null | true | seed of key | + +Returns on success: + +```json +{ + "rest api":"2.0", + "code":200, + "error":"", + "result":{ + "address":"BD607C37147656A507A5A521AA9446EB72B2C907" + } +} +``` + +Returns on failure: + +```json +{ + "rest api":"2.0", + "code":500, + "error":"Could not recover the key.", + "result":{} +} +``` + +### /keys/create - POST + +url: /keys/create + +Functionality: Create a new key. + +Parameter: + +| Parameter | Type | Default | Required | Description | +| --------- | ------ | ------- | -------- | ---------------- | +| name | string | null | true | name of key | +| password | string | null | true | password of key | + +Returns on success: + +```json +{ + "rest api":"2.0", + "code":200, + "error":"", + "result":{ + "seed":"crime carpet recycle erase simple prepare moral dentist fee cause pitch trigger when velvet animal abandon" + } +} +``` + +Returns on failure: + +```json +{ + "rest api":"2.0", + "code":500, + "error":"Could not create new key.", + "result":{} +} +``` + +### /keys/{name} - GET + +url: /keys/{name} + +Functionality: Get the information for the specified key. + +Returns on success: + +```json +{ + "rest api":"2.0", + "code":200, + "error":"", + "result":{ + "name":"test", + "address":"cosmosaccaddr1thlqhjqw78zvcy0ua4ldj9gnazqzavyw4eske2", + "pub_key":"cosmosaccpub1zcjduc3qyx6hlf825jcnj39adpkaxjer95q7yvy25yhfj3dmqy2ctev0rxmse9cuak" + } +} +``` + +Returns on failure: + +```json +{ + "rest api":"2.0", + "code":500, + "error":"Could not find information on the specified key.", + "result":{} +} +``` + +### /keys/{name} - PUT + +url: /keys/{name} + +Functionality: Change the encryption password for the specified key. + +Parameters: + +| Parameter | Type | Default | Required | Description | +| --------------- | ------ | ------- | -------- | --------------- | +| old_password | string | null | true | old password | +| new_password | string | null | true | new password | + +Returns on success: + +```json +{ + "rest api":"2.0", + "code":200, + "error":"", + "result":{} +} +``` + +Returns on failure: + +```json +{ + "rest api":"2.0", + "code":500, + "error":"Could not update the specified key.", + "result":{} +} +``` + +### /keys/{name} - DELETE + +url: /keys/{name} + +Functionality: Delete the specified key. + +Parameters: + +| Parameter | Type | Default | Required | Description | +| --------- | ------ | ------- | -------- | ---------------- | +| password | string | null | true | password of key | + +Returns on success: + +```json +{ + "rest api":"2.0", + "code":200, + "error":"", + "result":{} +} +``` + +Returns on failure: + +```json +{ + "rest api":"2.0", + "code":500, + "error":"Could not delete the specified key.", + "result":{} +} +``` + +## ICS20 - TokenAPI + +The TokenAPI exposes all functionality needed to query account balances and send transactions. + +### /bank/balance/{account} - GET + +url: /bank/balance/{account} + +Functionality: Query the specified account. + +Returns on success: + +```json +{ + "rest api":"2.0", + "code":200, + "error":"", + "result": { + "atom":1000, + "photon":500, + "ether":20 + } +} +``` + +Returns on error: + +```json +{ + "rest api":"2.0", + "code":500, + "error":"Could not find any balance for the specified account.", + "result":{} +} +``` + +### /bank/create_transfer - POST + +url: /bank/create_transfer + +Functionality: Create a transfer in the bank module. + +Parameters: + +| Parameter | Type | Default | Required | Description | +| ------------ | ------ | ------- | -------- | ------------------------- | +| sender | string | null | true | Address of sender | +| receiver | string | null | true | address of receiver | +| chain_id | string | null | true | chain id | +| amount | int | null | true | amount of the token | +| denomonation | string | null | true | denomonation of the token | + +Returns on success: + +```json +{ + "rest api":"2.0", + "code":200, + "error":"", + "result":{ + "transaction":"TODO:" + } +} +``` + +Returns on failure: + +```json +{ + "rest api":"2.0", + "code":500, + "error":"Could not create the transaction.", + "result":{} +} +``` diff --git a/docs/light/getting_started.md b/docs/light/getting_started.md new file mode 100644 index 000000000..5f11956c0 --- /dev/null +++ b/docs/light/getting_started.md @@ -0,0 +1,40 @@ +# Getting Started + +To start a rest server, we need to specify the following parameters: +| Parameter | Type | Default | Required | Description | +| ----------- | --------- | ----------------------- | -------- | ---------------------------------------------------- | +| chain-id | string | null | true | chain id of the full node to connect | +| node | URL | "tcp://localhost:46657" | true | address of the full node to connect | +| laddr | URL | "tcp://localhost:1317" | true | address to run the rest server on | +| trust-node | bool | "false" | true | Whether this LCD is connected to a trusted full node | +| trust-store | DIRECTORY | "$HOME/.lcd" | false | directory for save checkpoints and validator sets | + +Sample command: + +```bash +gaiacli light-client --chain-id=test --laddr=tcp://localhost:1317 --node tcp://localhost:46657 --trust-node=false +``` + +## Gaia Light Use Cases + +LCD could be very helpful for related service providers. For a wallet service provider, LCD could +make transaction faster and more reliable in the following cases. + +### Create an account + +![deposit](pics/create-account.png) + +First you need to get a new seed phrase :[get-seed](api.md#keysseed---get) + +After having new seed, you could generate a new account with it : [keys](api.md#keys---post) + +### Transfer a token + +![transfer](pics/transfer-tokens.png) + +The first step is to build an asset transfer transaction. Here we can post all necessary parameters +to /create_transfer to get the unsigned transaction byte array. Refer to this link for detailed +operation: [build transaction](api.md#create_transfer---post) + +Then sign the returned transaction byte array with users' private key. Finally broadcast the signed +transaction. Refer to this link for how to broadcast the signed transaction: [broadcast transaction](api.md#create_transfer---post) diff --git a/docs/light/load_balancer.md b/docs/light/load_balancer.md new file mode 100644 index 000000000..0cc280827 --- /dev/null +++ b/docs/light/load_balancer.md @@ -0,0 +1,203 @@ +# Load Balancing Module - WIP + +The LCD will be an important bridge between service providers and cosmos blockchain network. Suppose +a service provider wants to monitor token information for millions of accounts. Then it has to keep +sending a large mount of requests to LCD to query token information. As a result, LCD will send huge +requests to full node to get token information and necessary proof which will cost full node much +computing and bandwidth resource. Too many requests to a single full node may result in some bad +situations: + +```text +1. The full node crash possibility increases. +2. The reply delay increases. +3. The system reliability will decrease. +4. As the full node may belong to other people or associates, they may deny too frequent access from a single client. +``` + +It is very urgent to solve this problems. Here we consider to import load balancing into LCD. By the +help of load balancing, LCD can distribute millions of requests to a set of full nodes. Thus the +load of each full node won't be too heavy and the unavailable full nodes will be wiped out of query +list. In addition, the system reliability will increase. + +## Design + +This module need combine with client to realize the real load balancing. It can embed the +[HTTP Client](https://github.com/tendermint/tendermint/rpc/lib/client/httpclient.go). In other +words,we realise the new httpclient based on `HTTP`. + +```go +type HTTPLoadBalancer struct { + rpcs map[string]*rpcclient.JSONRPCClient + *WSEvents +} +``` + +## The Diagram of LCD RPC WorkFlow with LoadBalance + +![The Diagram of LCD RPC WorkFlow](pics/loadbalanceDiagram.png) + +In the above sequence diagram, application calls the `Request()`, and LCD finally call the +`HTTP.Request()` through the SecureClient `Wrapper`. In every `HTTP.Request()`, `Getclient()` +selects the current working rpcclient by the load balancing algorithm,then run the +`JSONRPCClient.Call()` to request from the Full Node, finally `UpdateClient()` updates the weight of + the current rpcclient according to the status that is returned by the full node. The `GetAddr()` + and `UpdateAddrWeight()` are realized in the load balancing module. + +There are some abilities to do: + +* Add the Remote Address +* Delete the Remote Address +* Update the weights of the addresses + +## Load balancing Strategies + +We can design some strategies like nginx to combine the different load balancing algorithms to get +the final remote. We can also get the status of the remote server to add or delete the addresses and + update weights of the addresses. + +In a word,it can make the entire LCD work more effective in actual conditions. +We are working this module independently in this [Github Repository](https://github.com/MrXJC/GoLoadBalance). + +## Interface And Type + +### Balancer + +This interface `Balancer`is the core of the package. Every load balancing algorithm should realize +it,and it defined two interfaces. + +* `init` initialize the balancer, assigns the variables which `DoBalance` needs. +* `DoBalance` load balance the full node addresses according to the current situation. + +```go +package balance + +type Balancer interface { + init(NodeAddrs) + DoBalance(NodeAddrs) (*NodeAddr,int,error) +} +``` + +### NodeAddr + +* host: ip address +* port: the number of port +* weight: the weight of this full node address,default:1 + +This NodeAddr is the base struct of the address. + +```go +type NodeAddr struct{ + host string + port int + weight int +} + +func (p *NodeAddr) GetHost() string + +func (p *NodeAddr) GetPort() int + +func (p *NodeAddr) GetWeight() int + +func (p *NodeAddr) updateWeight(weight int) +``` + +The `weight` is the important factor that schedules which full node the LCD calls. The weight can be +changed by the information from the full node. So we have the function `updateWegiht`. + +### NodeAddrs + +>in `balance/types.go` + +`NodeAddrs` is the list of the full node address. This is the member variable in the +BalanceManager(`BalancerMgr`). + +```go +type NodeAddrs []*NodeAddr +``` + +## Load Balancing Algorithm + +### Random + +>in `balance/random.go` + +Random algorithm selects a remote address randomly to process the request. The probability of them +being selected is the same. + +### RandomWeight + +>in `balance/random.go` + +RandomWeight Algorithm also selects a remote address randomly to process the request. But the higher +the weight, the greater the probability. + +### RoundRobin + +>in `balance/roundrobin.go` + +RoundRobin Algorithm selects a remote address orderly. Every remote address have the same +probability to be selected. + +### RoundRobinWeight + +>in `balance/roundrobin.go` + +RoundRobinWeight Algorthm selects a remote address orderly. But every remote address have different +probability to be selected which are determined by their weight. + +### Hash + +//TODO + +## Load Balancing Manager + +### BalanceMgr + +>in `balance/manager.go` + +* addrs: the set of the remote full node addresses +* balancers: map the string of balancer name to the specific balancer +* change: record whether the machine reinitialize after the `addrs` changes + +`BalanceMgr` is the manager of many balancer. It is the access of load balancing. Its main function +is to maintain the `NodeAddrs` and to call the specific load balancing algorithm above. + +```go +type BalanceMgr struct{ + addrs NodeAddrs + balancers map[string]Balancer + change map[string]bool +} + +func (p *BalanceMgr) RegisterBalancer(name string,balancer Balancer) + +func (p *BalanceMgr) updateBalancer(name string) + +func (p *BalanceMgr) AddNodeAddr(addr *NodeAddr) + +func (p *BalanceMgr) DeleteNodeAddr(i int) + +func (p *BalanceMgr) UpdateWeightNodeAddr(i int,weight int) + +func (p *BalanceMgr) GetAddr(name string)(*NodeAddr,int,error) { + // if addrs change,update the balancer which we use. + if p.change[name]{ + p.updateBalancer(name) + } + + // get the balancer by name + balancer := p.balancers[name] + + // use the load balancing algorithm + addr,index,err := balancer.DoBalance(p.addrs) + + return addr,index,err +} +``` + +* `RegisterBalancer`: register the basic balancer implementing the `Balancer` interface and initialize them. +* `updateBalancer`: update the specific balancer after the `addrs` change. +* `AddNodeAddr`: add the remote address and set all the values of the `change` to true. +* `DeleteNodeAddr`: delete the remote address and set all the values of the `change` to true. +* `UpdateWeightNodeAddr`: update the weight of the remote address and set all the values of the `change` to true. +* `GetAddr`:select the address by the balancer the `name` decides. diff --git a/docs/light/pics/C2H.png b/docs/light/pics/C2H.png new file mode 100644 index 000000000..49f7e07f3 Binary files /dev/null and b/docs/light/pics/C2H.png differ diff --git a/docs/light/pics/H2C.png b/docs/light/pics/H2C.png new file mode 100644 index 000000000..027eafcde Binary files /dev/null and b/docs/light/pics/H2C.png differ diff --git a/docs/light/pics/MA.png b/docs/light/pics/MA.png new file mode 100644 index 000000000..ae3823962 Binary files /dev/null and b/docs/light/pics/MA.png differ diff --git a/docs/light/pics/absence1.png b/docs/light/pics/absence1.png new file mode 100755 index 000000000..70b4c9e8d Binary files /dev/null and b/docs/light/pics/absence1.png differ diff --git a/docs/light/pics/absence2.png b/docs/light/pics/absence2.png new file mode 100755 index 000000000..6ce5bcde3 Binary files /dev/null and b/docs/light/pics/absence2.png differ diff --git a/docs/light/pics/absence3.png b/docs/light/pics/absence3.png new file mode 100755 index 000000000..d3afdc2c5 Binary files /dev/null and b/docs/light/pics/absence3.png differ diff --git a/docs/light/pics/architecture.png b/docs/light/pics/architecture.png new file mode 100644 index 000000000..741a90c26 Binary files /dev/null and b/docs/light/pics/architecture.png differ diff --git a/docs/light/pics/changeProcess.png b/docs/light/pics/changeProcess.png new file mode 100755 index 000000000..1771c239b Binary files /dev/null and b/docs/light/pics/changeProcess.png differ diff --git a/docs/light/pics/commitValidation.png b/docs/light/pics/commitValidation.png new file mode 100755 index 000000000..89985bcc1 Binary files /dev/null and b/docs/light/pics/commitValidation.png differ diff --git a/docs/light/pics/create-account.png b/docs/light/pics/create-account.png new file mode 100644 index 000000000..35a249b97 Binary files /dev/null and b/docs/light/pics/create-account.png differ diff --git a/docs/light/pics/deposit.png b/docs/light/pics/deposit.png new file mode 100644 index 000000000..1fb3acdc5 Binary files /dev/null and b/docs/light/pics/deposit.png differ diff --git a/docs/light/pics/existProof.png b/docs/light/pics/existProof.png new file mode 100755 index 000000000..ee5de0851 Binary files /dev/null and b/docs/light/pics/existProof.png differ diff --git a/docs/light/pics/high-level.png b/docs/light/pics/high-level.png new file mode 100644 index 000000000..73a14e0d7 Binary files /dev/null and b/docs/light/pics/high-level.png differ diff --git a/docs/light/pics/light-client-architecture.png b/docs/light/pics/light-client-architecture.png new file mode 100644 index 000000000..df44aeef5 Binary files /dev/null and b/docs/light/pics/light-client-architecture.png differ diff --git a/docs/light/pics/loadbalanceDiagram.png b/docs/light/pics/loadbalanceDiagram.png new file mode 100644 index 000000000..56956ee9d Binary files /dev/null and b/docs/light/pics/loadbalanceDiagram.png differ diff --git a/docs/light/pics/simpleMerkleTree.png b/docs/light/pics/simpleMerkleTree.png new file mode 100755 index 000000000..5c4e2b76e Binary files /dev/null and b/docs/light/pics/simpleMerkleTree.png differ diff --git a/docs/light/pics/substoreProof.png b/docs/light/pics/substoreProof.png new file mode 100755 index 000000000..90dadaef3 Binary files /dev/null and b/docs/light/pics/substoreProof.png differ diff --git a/docs/light/pics/transfer-tokens.png b/docs/light/pics/transfer-tokens.png new file mode 100644 index 000000000..c6635a30c Binary files /dev/null and b/docs/light/pics/transfer-tokens.png differ diff --git a/docs/light/pics/transfer.png b/docs/light/pics/transfer.png new file mode 100644 index 000000000..7642e996c Binary files /dev/null and b/docs/light/pics/transfer.png differ diff --git a/docs/light/pics/trustPropagate.png b/docs/light/pics/trustPropagate.png new file mode 100755 index 000000000..a743cfc72 Binary files /dev/null and b/docs/light/pics/trustPropagate.png differ diff --git a/docs/light/pics/updateValidatorToHeight.png b/docs/light/pics/updateValidatorToHeight.png new file mode 100755 index 000000000..b2c9349b6 Binary files /dev/null and b/docs/light/pics/updateValidatorToHeight.png differ diff --git a/docs/light/pics/validatorSetChange.png b/docs/light/pics/validatorSetChange.png new file mode 100755 index 000000000..16dbf39be Binary files /dev/null and b/docs/light/pics/validatorSetChange.png differ diff --git a/docs/light/pics/withdraw.png b/docs/light/pics/withdraw.png new file mode 100644 index 000000000..2249b5e34 Binary files /dev/null and b/docs/light/pics/withdraw.png differ diff --git a/docs/light/readme.md b/docs/light/readme.md new file mode 100644 index 000000000..53e3b59bd --- /dev/null +++ b/docs/light/readme.md @@ -0,0 +1,100 @@ +# Cosmos-Sdk Light Client + +## Introduction + +A light client allows clients, such as mobile phones, to receive proofs of the state of the +blockchain from any full node. Light clients do not have to trust any full node, since they are able +to verify any proof they receive and hence full nodes cannot lie about the state of the network. + +A light client can provide the same security as a full node with the minimal requirements on +bandwidth, computing and storage resource. Besides, it can also provide modular functionality +according to users' configuration. These fantastic features allow developers to build fully secure, +efficient and usable mobile apps, websites or any other applications without deploying or +maintaining any full blockchain nodes. + +LCD will be used in the Cosmos Hub, the first Hub in the Cosmos network. + +## Contents + +1. [**Overview**](##Overview) +2. [**Get Started**](getting_started.md) +3. [**API**](api.md) +4. [**Specifications**](hspecification.md) + +## Overview + +### What is a Light Client + +The LCD is split into two separate components. The first component is generic for any Tendermint +based application. It handles the security and connectivity aspects of following the header chain +and verify proofs from full nodes against locally trusted validator set. Furthermore it exposes +exactly the same API as any Tendermint Core node. The second component is specific for the Cosmos +Hub (Gaiad). It works as a query endpoint and exposes the application specific functionality, which +can be arbitrary. All queries against the application state have to go through the query endpoint. +The advantage of the query endpoint is that it can verify the proofs that the application returns. + +### High-Level Architecture + +An application developer that would like to build a third party integration can ship his application +with the LCD for the Cosmos Hub (or any other zone) and only needs to initialise it. Afterwards his +application can interact with the zone as if it was running against a full node. + +![high-level](pics/high-level.png) + +An application developer that wants to build an third party application for the Cosmos Hub (or any +other zone) should build it against it's canonical API. That API is a combination of multiple parts. +All zones have to expose ICS0 (TendermintAPI). Beyond that any zone is free to choose any +combination of module APIs, depending on which modules the state machine uses. The Cosmos Hub will +initially support ICS0 (TendermintAPI), ICS1 (KeyAPI), ICS20 (TokenAPI), ICS21 (StakingAPI) and +ICS22 (GovernanceAPI). + +All applications are expected to only run against the LCD. The LCD is the only piece of software +that offers stability guarantees around the zone API. + +### Comparision + +A full node of ABCI is different from its light client in the following ways: + +|| Full Node | LCD | Description| +|-| ------------- | ----- | -------------- | +| Execute and verify transactions|Yes|No|Full node will execute and verify all transactions while LCD won't| +| Verify and save blocks|Yes|No|Full node will verify and save all blocks while LCD won't| +| Participate consensus| Yes|No|Only when the full node is a validtor, it will participate consensus. LCD nodes never participate consensus| +| Bandwidth cost|Huge|Little|Full node will receive all blocks. if the bandwidth is limited, it will fall behind the main network. What's more, if it happens to be a validator,it will slow down the consensus process. LCD requires little bandwidth. Only when serving local request, it will cost bandwidth| +| Computing resource|Huge|Little|Full node will execute all transactions and verify all blocks which require much computing resource| +| Storage resource|Huge|Little|Full node will save all blocks and ABCI states. LCD just saves validator sets and some checkpoints| +| Power consume|Huge|Little|Full nodes have to be deployed on machines which have high performance and will be running all the time. So power consume will be huge. LCD can be deployed on the same machines as users' applications, or on independent machines but with poor performance. Besides, LCD can be shutdown anytime when necessary. So LCD only consume very little power, even mobile devices can meet the power requirement| +| Provide APIs|All cosmos APIs|Modular APIs|Full node supports all cosmos APIs. LCD provides modular APIs according to users' configuration| +| Secuity level| High|High|Full node will verify all transactions and blocks by itself. LCD can't do this, but it can query any data from other full nodes and verify the data independently. So both full node and LCD don't need to trust any third nodes, they all can achieve high security| + +According to the above table, LCD can meet all users' functionality and security requirements, but +only requires little resource on bandwidth, computing, storage and power. + +## How does LCD achieve high security? + +### Trusted validator set + +The base design philosophy of lcd follows the two rules: + +1. **Doesn't trust any blockchain nodes, including validator nodes and other full nodes** +2. **Only trusts the whole validator set** + +The original trusted validator set should be prepositioned into its trust store, usually this +validator set comes from genesis file. During running time, if LCD detects different validator set, +it will verify it and save new validated validator set to trust store. + +![validator-set-change](pics/validatorSetChange.png) + +### Trust propagation + +From the above section, we come to know how to get trusted validator set and how lcd keeps track of +validator set evolution. Validator set is the foundation of trust, and the trust can propagate to +other blockchain data, such as block and transaction. The propagate architecture is shown as +follows: + +![change-process](pics/trustPropagate.png) + +In general, by trusted validator set, LCD can verify each block commit which contains all pre-commit +data and block header data. Then the block hash, data hash and appHash are trusted. Based on this +and merkle proof, all transactions data and ABCI states can be verified too. Detailed implementation +will be posted on technical specification. diff --git a/docs/light/specification.md b/docs/light/specification.md new file mode 100644 index 000000000..48c87d047 --- /dev/null +++ b/docs/light/specification.md @@ -0,0 +1,318 @@ +# Specifications + +This specification describes how to implement the LCD. LCD supports modular APIs. Currently, only +ICS0 (TendermintAPI), ICS1 (Key API) and ICS20 (Token API) are supported. Later, if necessary, more +APIs can be included. + +## Build and Verify Proof of ABCI States + +As we all know, storage of cosmos-sdk based application contains multi-substores. Each substore is +implemented by a IAVL store. These substores are organized by simple Merkle tree. To build the tree, +we need to extract name, height and store root hash from these substores to build a set of simple +Merkle leaf nodes, then calculate hash from leaf nodes to root. The root hash of the simple Merkle +tree is the AppHash which will be included in block header. + +![Simple Merkle Tree](pics/simpleMerkleTree.png) + +As we have discussed in [LCD trust-propagation](https://github.com/irisnet/cosmos-sdk/tree/bianjie/lcd_spec/docs/spec/lcd#trust-propagation), +the AppHash can be verified by checking voting power against a trusted validator set. Here we just +need to build proof from ABCI state to AppHash. The proof contains two parts: + +* IAVL proof +* Substore to AppHash proof + +### IAVL Proof + +The proof has two types: existance proof and absence proof. If the query key exists in the IAVL +store, then it returns key-value and its existance proof. On the other hand, if the key doesn't +exist, then it only returns absence proof which can demostrate the key definitely doesn't exist. + +### IAVL Existance Proof + +```go +type CommitID struct { + Version int64 + Hash []byte +} + +type storeCore struct { + CommitID CommitID +} + +type MultiStoreCommitID struct { + Name string + Core storeCore +} + +type proofInnerNode struct { + Height int8 + Size int64 + Version int64 + Left []byte + Right []byte +} + +type KeyExistsProof struct { + MultiStoreCommitInfo []MultiStoreCommitID //All substore commitIDs + StoreName string //Current substore name + Height int64 //The commit height of current substore + RootHash cmn.HexBytes //The root hash of this IAVL tree + Version int64 //The version of the key-value in this IAVL tree + InnerNodes []proofInnerNode //The path from to root node to key-value leaf node +} +``` + +The data structure of exist proof is shown as above. The process to build and verify existance proof +is shown as follows: + +![Exist Proof](pics/existProof.png) + +Steps to build proof: + +* Access the IAVL tree from the root node. +* Record the visited nodes in InnerNodes, +* Once the target leaf node is found, assign leaf node version to proof version +* Assign the current IAVL tree height to proof height +* Assign the current IAVL tree rootHash to proof rootHash +* Assign the current substore name to proof StoreName +* Read multistore commitInfo from db by height and assign it to proof StoreCommitInfo + +Steps to verify proof: + +* Build leaf node with key, value and proof version. +* Calculate leaf node hash +* Assign the hash to the first innerNode's rightHash, then calculate first innerNode hash +* Propagate the hash calculation process. If prior innerNode is the left child of next innerNode, then assign the prior innerNode hash to the left hash of next innerNode. Otherwise, assign the prior innerNode hash to the right hash of next innerNode. +* The hash of last innerNode should be equal to the rootHash of this proof. Otherwise, the proof is invalid. + +### IAVL Absence Proof + +As we all know, all IAVL leaf nodes are sorted by the key of each leaf nodes. So we can calculate +the postition of the target key in the whole key set of this IAVL tree. As shown below, we can find +out the left key and the right key. If we can demonstrate that both left key and right key +definitely exist, and they are adjacent nodes. Thus the target key definitely doesn't exist. + +![Absence Proof1](pics/absence1.png) + +If the target key is larger than the right most leaf node or less than the left most key, then the +target key definitely doesn't exist. + +![Absence Proof2](pics/absence2.png)![Absence Proof3](pics/absence3.png) + +```go +type proofLeafNode struct { + KeyBytes cmn.HexBytes + ValueBytes cmn.HexBytes + Version int64 +} + +type pathWithNode struct { + InnerNodes []proofInnerNode + Node proofLeafNode +} + +type KeyAbsentProof struct { + MultiStoreCommitInfo []MultiStoreCommitID + StoreName string + Height int64 + RootHash cmn.HexBytes + Left *pathWithNode // Proof the left key exist + Right *pathWithNode //Proof the right key exist +} +``` + +The above is the data structure of absence proof. Steps to build proof: + +* Access the IAVL tree from the root node. +* Get the deserved index(Marked as INDEX) of the key in whole key set. +* If the returned index equals to 0, the right index should be 0 and left node doesn't exist +* If the returned index equals to the size of the whole key set, the left node index should be INDEX-1 and the right node doesn't exist. +* Otherwise, the right node index should be INDEX and the left node index should be INDEX-1 +* Assign the current IAVL tree height to proof height +* Assign the current IAVL tree rootHash to proof rootHash +* Assign the current substore name to proof StoreName +* Read multistore commitInfo from db by height and assign it to proof StoreCommitInfo + +Steps to verify proof: + +* If only right node exist, verify its exist proof and verify if it is the left most node +* If only left node exist, verify its exist proof and verify if it is the right most node. +* If both right node and left node exist, verify if they are adjacent. + +### Substores to AppHash Proof + +After verify the IAVL proof, then we can start to verify substore proof against AppHash. Firstly, +iterate MultiStoreCommitInfo and find the substore commitID by proof StoreName. Verify if yhe Hash +in commitID equals to proof RootHash. If not, the proof is invalid. Then sort the substore +commitInfo array by the hash of substore name. Finally, build the simple Merkle tree with all +substore commitInfo array and verify if the Merkle root hash equal to appHash. + +![substore proof](pics/substoreProof.png) + +```go +func SimpleHashFromTwoHashes(left []byte, right []byte) []byte { + var hasher = ripemd160.New() + + err := encodeByteSlice(hasher, left) + if err != nil { + panic(err) + } + + err = encodeByteSlice(hasher, right) + if err != nil { + panic(err) + } + + return hasher.Sum(nil) +} + +func SimpleHashFromHashes(hashes [][]byte) []byte { + // Recursive impl. + switch len(hashes) { + case 0: + return nil + case 1: + return hashes[0] + default: + left := SimpleHashFromHashes(hashes[:(len(hashes)+1)/2]) + right := SimpleHashFromHashes(hashes[(len(hashes)+1)/2:]) + return SimpleHashFromTwoHashes(left, right) + } +} +``` + +## Verify block header against validator set + +Above sections refer appHash frequently. But where does the trusted appHash come from? Actually, +appHash exist in block header, so next we need to verify blocks header at specific height against +LCD trusted validator set. The validation flow is shown as follows: + +![commit verification](pics/commitValidation.png) + +When the trusted validator set doesn't match the block header, we need to try to update our +validator set to the height of this block. LCD have a rule that each validator set change should not +affact more than 1/3 voting power. Compare with the trusted validator set, if the voting power of +target validator set changes more than 1/3. We have to verify if there are hidden validator set +change before the target validator set. Only when all validator set changes obey this rule, can our +validator set update be accomplished. + +For instance: + +![Update validator set to height](pics/updateValidatorToHeight.png) + +* Update to 10000, tooMuchChangeErr +* Update to 5050, tooMuchChangeErr +* Update to 2575, Success +* Update to 5050, Success +* Update to 10000,tooMuchChangeErr +* Update to 7525, Success +* Update to 10000, Success + +## Load Balancing + +To improve LCD reliability and TPS, we recommend to connect LCD to more than one fullnode. But the +complexity will increase a lot. So load balancing module will be imported as the adapter. Please +refer to this link for detailed description: [load balancing](https://github.com/irisnet/cosmos-sdk/blob/bianjie/lcd_spec/docs/spec/lcd/loadbalance.md) + +## ICS1 (KeyAPI) + +### [/keys - GET](api.md#keys---get) + +Load the key store: + +```go +db, err := dbm.NewGoLevelDB(KeyDBName, filepath.Join(rootDir, "keys")) +if err != nil { + return nil, err +} + +keybase = client.GetKeyBase(db) +``` + +Iterate through the key store. + +```go +var res []Info +iter := kb.db.Iterator(nil, nil) +defer iter.Close() + +for ; iter.Valid(); iter.Next() { + // key := iter.Key() + info, err := readInfo(iter.Value()) + if err != nil { + return nil, err + } + res = append(res, info) +} + +return res, nil +``` + +Encode the addresses and public keys in bech32. + +```go +bechAccount, err := sdk.Bech32ifyAcc(sdk.Address(info.PubKey.Address().Bytes())) +if err != nil { + return KeyOutput{}, err +} + +bechPubKey, err := sdk.Bech32ifyAccPub(info.PubKey) +if err != nil { + return KeyOutput{}, err +} + +return KeyOutput{ + Name: info.Name, + Address: bechAccount, + PubKey: bechPubKey, +}, nil +``` + +### [/keys/recover - POST](api.md#keys/recover---get) + +1. Load the key store. +2. Parameter checking. Name, password and seed should not be empty. +3. Check for keys with the same name. +4. Build the key from the name, password and seed. +5. Persist the key to key store. + +### [/keys/create - GET](api.md#keys/create---get)** + +1. Load the key store. +2. Create a new key in the key store. +3. Save the key to disk. +4. Return the seed. + +### [/keys/{name} - GET](api.md#keysname---get) + +1. Load the key store. +2. Iterate the whole key store to find the key by name. +3. Encode address and public key in bech32. + +### [/keys/{name} - PUT](api.md#keysname---put) + +1. Load the key store. +2. Iterate the whole key store to find the key by name. +3. Verify if that the old-password matches the current key password. +4. Re-persist the key with the new password. + +### [/keys/{name} - DELETE](api.md#keysname---delete) + +1. Load the key store. +2. Iterate the whole key store to find the key by name. +3. Verify that the specified password matches the current key password. +4. Delete the key from the key store. + +## ICS20 (TokenAPI) + +### [/bank/balance/{account}](api.md#balanceaccount---get) + +1. Decode the address from bech32 to hex. +2. Send a query request to a full node. Ask for proof if required by Gaia Light. +3. Verify the proof against the root of trust. + +### [/bank/create_transfer](api.md#create_transfer---post) + +1. Check the parameters. +2. Build the transaction with the specified parameters. +3. Serialise the transaction and return the JSON encoded sign bytes. diff --git a/docs/light/todo.md b/docs/light/todo.md new file mode 100644 index 000000000..ce1f8508a --- /dev/null +++ b/docs/light/todo.md @@ -0,0 +1,16 @@ +# TODO + +This document is a place to gather all points for future development. + +## API + +* finalise ICS0 - TendermintAPI + * make sure that the explorer and voyager can use it +* add ICS21 - StakingAPI +* add ICS22 - GovernanceAPI +* split Gaia Light into reusable components that other zones can leverage + * it should be possible to register extra standards on the light client + * the setup should be similar to how the app is currently started +* implement Gaia light and the general light client in Rust + * export the API as a C interface + * write thin wrappers around the C interface in JS, Swift and Kotlin/Java diff --git a/docs/sdk/clients.md b/docs/sdk/clients.md index 9fa015908..0767745e5 100644 --- a/docs/sdk/clients.md +++ b/docs/sdk/clients.md @@ -119,9 +119,8 @@ On the testnet, we delegate `steak` instead of `atom`. Here's how you can bond t ```bash gaiacli stake delegate \ --amount=10steak \ - --address-delegator= \ --address-validator=$(gaiad tendermint show_validator) \ - --name= \ + --from= \ --chain-id=gaia-6002 ``` @@ -136,15 +135,16 @@ While tokens are bonded, they are pooled with all the other bonded tokens in the If for any reason the validator misbehaves, or you want to unbond a certain amount of tokens, use this following command. You can unbond a specific amount of`shares`\(eg:`12.1`\) or all of them \(`MAX`\). ```bash -gaiacli stake unbond \ - --address-delegator= \ +gaiacli stake unbond begin \ --address-validator=$(gaiad tendermint show_validator) \ - --shares=MAX \ - --name= \ + --shares-percent=1 \ + --from= \ --chain-id=gaia-6002 ``` -You can check your balance and your stake delegation to see that the unbonding went through successfully. +Later you must use the `gaiacli stake unbond complete` command to finish +unbonding at which point you can can check your balance and your stake +delegation to see that the unbonding went through successfully. ```bash gaiacli account diff --git a/docs/sdk/gaiacli.md b/docs/sdk/gaiacli.md index f42bfd145..cd4271e42 100644 --- a/docs/sdk/gaiacli.md +++ b/docs/sdk/gaiacli.md @@ -108,9 +108,8 @@ On the testnet, we delegate `steak` instead of `atom`. Here's how you can bond t ```bash gaiacli stake delegate \ --amount=10steak \ - --address-delegator= \ --address-validator=$(gaiad tendermint show_validator) \ - --name= \ + --from= \ --chain-id=gaia-6002 ``` @@ -125,15 +124,16 @@ While tokens are bonded, they are pooled with all the other bonded tokens in the If for any reason the validator misbehaves, or you want to unbond a certain amount of tokens, use this following command. You can unbond a specific amount of`shares`\(eg:`12.1`\) or all of them \(`MAX`\). ```bash -gaiacli stake unbond \ - --address-delegator= \ +gaiacli stake unbond begin \ --address-validator=$(gaiad tendermint show_validator) \ - --shares=MAX \ - --name= \ + --shares-percent=1 \ + --from= \ --chain-id=gaia-6002 ``` -You can check your balance and your stake delegation to see that the unbonding went through successfully. +Later you must use the `gaiacli stake unbond complete` command to finish +unbonding at which point you can can check your balance and your stake +delegation to see that the unbonding went through successfully. ```bash gaiacli account diff --git a/examples/democoin/mock/validator.go b/examples/democoin/mock/validator.go index 84d41d488..c3d01b170 100644 --- a/examples/democoin/mock/validator.go +++ b/examples/democoin/mock/validator.go @@ -28,6 +28,11 @@ func (v Validator) GetPubKey() crypto.PubKey { return nil } +// Implements sdk.Validator +func (v Validator) GetTokens() sdk.Rat { + return sdk.ZeroRat() +} + // Implements sdk.Validator func (v Validator) GetPower() sdk.Rat { return v.Power diff --git a/server/init.go b/server/init.go index 39f3d0b7f..a644c0c1a 100644 --- a/server/init.go +++ b/server/init.go @@ -48,10 +48,9 @@ var ( // genesis piece structure for creating combined genesis type GenesisTx struct { - NodeID string `json:"node_id"` - IP string `json:"ip"` - Validator tmtypes.GenesisValidator `json:"validator"` - AppGenTx json.RawMessage `json:"app_gen_tx"` + NodeID string `json:"node_id"` + IP string `json:"ip"` + AppGenTx json.RawMessage `json:"app_gen_tx"` } // Storage for init command input parameters @@ -121,16 +120,15 @@ func gentxWithConfig(cdc *wire.Codec, appInit AppInit, config *cfg.Config, genTx nodeID := string(nodeKey.ID()) pubKey := readOrCreatePrivValidator(config) - appGenTx, cliPrint, validator, err := appInit.AppGenTx(cdc, pubKey, genTxConfig) + appGenTx, cliPrint, _, err := appInit.AppGenTx(cdc, pubKey, genTxConfig) if err != nil { return } tx := GenesisTx{ - NodeID: nodeID, - IP: genTxConfig.IP, - Validator: validator, - AppGenTx: appGenTx, + NodeID: nodeID, + IP: genTxConfig.IP, + AppGenTx: appGenTx, } bz, err := wire.MarshalJSONIndent(cdc, tx) if err != nil { @@ -312,7 +310,6 @@ func processGenTxs(genTxsDir string, cdc *wire.Codec) ( genTx := genTxs[nodeID] // combine some stuff - validators = append(validators, genTx.Validator) appGenTxs = append(appGenTxs, genTx.AppGenTx) // Add a persistent peer diff --git a/tools/Makefile b/tools/Makefile index 66ad10f6e..a11f2ec70 100644 --- a/tools/Makefile +++ b/tools/Makefile @@ -126,10 +126,10 @@ else @echo "Installing unparam" go get -v $(UNPARAM) endif -ifdef GOYCLO_CHECK - @echo "goyclo is already installed. Run 'make update_tools' to update." +ifdef GOCYCLO_CHECK + @echo "gocyclo is already installed. Run 'make update_tools' to update." else - @echo "Installing goyclo" + @echo "Installing gocyclo" go get -v $(GOCYCLO) endif diff --git a/types/coin.go b/types/coin.go index eba645932..862614ca0 100644 --- a/types/coin.go +++ b/types/coin.go @@ -15,9 +15,13 @@ type Coin struct { } func NewCoin(denom string, amount int64) Coin { + return NewIntCoin(denom, NewInt(amount)) +} + +func NewIntCoin(denom string, amount Int) Coin { return Coin{ Denom: denom, - Amount: NewInt(amount), + Amount: amount, } } diff --git a/types/stake.go b/types/stake.go index eb3f66082..e48577c0b 100644 --- a/types/stake.go +++ b/types/stake.go @@ -43,6 +43,7 @@ type Validator interface { GetOwner() AccAddress // owner AccAddress to receive/return validators coins GetPubKey() crypto.PubKey // validation pubkey GetPower() Rat // validation power + GetTokens() Rat // validation tokens GetDelegatorShares() Rat // Total out standing delegator shares GetBondHeight() int64 // height in which the validator became active } diff --git a/x/bank/app_test.go b/x/bank/app_test.go index 74a421bd7..2deb5de38 100644 --- a/x/bank/app_test.go +++ b/x/bank/app_test.go @@ -5,8 +5,6 @@ import ( "github.com/stretchr/testify/require" - "math/rand" - sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/x/auth" "github.com/cosmos/cosmos-sdk/x/mock" @@ -83,21 +81,6 @@ func getMockApp(t *testing.T) *mock.App { return mapp } -func TestBankWithRandomMessages(t *testing.T) { - mapp := getMockApp(t) - setup := func(r *rand.Rand, keys []crypto.PrivKey) { - return - } - - mapp.RandomizedTesting( - t, - []mock.TestAndRunTx{TestAndRunSingleInputMsgSend}, - []mock.RandSetup{setup}, - []mock.Invariant{ModuleInvariants}, - 100, 30, 30, - ) -} - func TestMsgSendWithAccounts(t *testing.T) { mapp := getMockApp(t) diff --git a/x/bank/simulation/invariants.go b/x/bank/simulation/invariants.go new file mode 100644 index 000000000..847288e1f --- /dev/null +++ b/x/bank/simulation/invariants.go @@ -0,0 +1,50 @@ +package simulation + +import ( + "fmt" + "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" + "github.com/cosmos/cosmos-sdk/x/mock" + "github.com/cosmos/cosmos-sdk/x/mock/simulation" + abci "github.com/tendermint/tendermint/abci/types" +) + +// NonnegativeBalanceInvariant checks that all accounts in the application have non-negative balances +func NonnegativeBalanceInvariant(mapper auth.AccountMapper) simulation.Invariant { + return func(t *testing.T, app *baseapp.BaseApp, log string) { + ctx := app.NewContext(false, abci.Header{}) + accts := mock.GetAllAccounts(mapper, ctx) + for _, acc := range accts { + coins := acc.GetCoins() + require.True(t, coins.IsNotNegative(), + fmt.Sprintf("%s has a negative denomination of %s\n%s", + acc.GetAddress().String(), + coins.String(), + log), + ) + } + } +} + +// TotalCoinsInvariant checks that the sum of the coins across all accounts +// is what is expected +func TotalCoinsInvariant(mapper auth.AccountMapper, totalSupplyFn func() sdk.Coins) simulation.Invariant { + return func(t *testing.T, app *baseapp.BaseApp, log string) { + ctx := app.NewContext(false, abci.Header{}) + totalCoins := sdk.Coins{} + + chkAccount := func(acc auth.Account) bool { + coins := acc.GetCoins() + totalCoins = totalCoins.Plus(coins) + return false + } + + mapper.IterateAccounts(ctx, chkAccount) + require.Equal(t, totalSupplyFn(), totalCoins, log) + } +} diff --git a/x/bank/simulation/msgs.go b/x/bank/simulation/msgs.go new file mode 100644 index 000000000..3a7248875 --- /dev/null +++ b/x/bank/simulation/msgs.go @@ -0,0 +1,117 @@ +package simulation + +import ( + "errors" + "fmt" + "math/big" + "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" + "github.com/cosmos/cosmos-sdk/x/bank" + "github.com/cosmos/cosmos-sdk/x/mock" + "github.com/cosmos/cosmos-sdk/x/mock/simulation" + "github.com/tendermint/tendermint/crypto" +) + +// TestAndRunSingleInputMsgSend tests and runs a single msg send, with one input and one output, where both +// accounts already exist. +func TestAndRunSingleInputMsgSend(mapper auth.AccountMapper) simulation.TestAndRunTx { + return func(t *testing.T, r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, keys []crypto.PrivKey, log string, event func(string)) (action string, err sdk.Error) { + fromKey := simulation.RandomKey(r, keys) + fromAddr := sdk.AccAddress(fromKey.PubKey().Address()) + toKey := simulation.RandomKey(r, keys) + // Disallow sending money to yourself + for { + if !fromKey.Equals(toKey) { + break + } + toKey = simulation.RandomKey(r, keys) + } + toAddr := sdk.AccAddress(toKey.PubKey().Address()) + initFromCoins := mapper.GetAccount(ctx, fromAddr).GetCoins() + + denomIndex := r.Intn(len(initFromCoins)) + amt, goErr := randPositiveInt(r, initFromCoins[denomIndex].Amount) + if goErr != nil { + return "skipping bank send due to account having no coins of denomination " + initFromCoins[denomIndex].Denom, nil + } + + action = fmt.Sprintf("%s is sending %s %s to %s", + fromAddr.String(), + amt.String(), + initFromCoins[denomIndex].Denom, + toAddr.String(), + ) + log = fmt.Sprintf("%s\n%s", log, action) + + coins := sdk.Coins{{initFromCoins[denomIndex].Denom, amt}} + var msg = bank.MsgSend{ + Inputs: []bank.Input{bank.NewInput(fromAddr, coins)}, + Outputs: []bank.Output{bank.NewOutput(toAddr, coins)}, + } + sendAndVerifyMsgSend(t, app, mapper, msg, ctx, log, []crypto.PrivKey{fromKey}) + event("bank/sendAndVerifyMsgSend/ok") + + return action, nil + } +} + +// 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) { + initialInputAddrCoins := make([]sdk.Coins, len(msg.Inputs)) + initialOutputAddrCoins := make([]sdk.Coins, len(msg.Outputs)) + AccountNumbers := make([]int64, len(msg.Inputs)) + SequenceNumbers := make([]int64, len(msg.Inputs)) + + for i := 0; i < len(msg.Inputs); i++ { + acc := mapper.GetAccount(ctx, msg.Inputs[i].Address) + AccountNumbers[i] = acc.GetAccountNumber() + SequenceNumbers[i] = acc.GetSequence() + initialInputAddrCoins[i] = acc.GetCoins() + } + for i := 0; i < len(msg.Outputs); i++ { + acc := mapper.GetAccount(ctx, msg.Outputs[i].Address) + initialOutputAddrCoins[i] = acc.GetCoins() + } + tx := mock.GenTx([]sdk.Msg{msg}, + AccountNumbers, + SequenceNumbers, + privkeys...) + res := app.Deliver(tx) + if !res.IsOK() { + // TODO: Do this in a more 'canonical' way + fmt.Println(res) + fmt.Println(log) + t.FailNow() + } + + for i := 0; i < len(msg.Inputs); i++ { + terminalInputCoins := mapper.GetAccount(ctx, msg.Inputs[i].Address).GetCoins() + require.Equal(t, + initialInputAddrCoins[i].Minus(msg.Inputs[i].Coins), + terminalInputCoins, + fmt.Sprintf("Input #%d had an incorrect amount of coins\n%s", i, log), + ) + } + 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), + ) + } +} + +func randPositiveInt(r *rand.Rand, max sdk.Int) (sdk.Int, error) { + if !max.GT(sdk.OneInt()) { + return sdk.Int{}, errors.New("max too small") + } + max = max.Sub(sdk.OneInt()) + return sdk.NewIntFromBigInt(new(big.Int).Rand(r, max.BigInt())).Add(sdk.OneInt()), nil +} diff --git a/x/bank/simulation/sim_test.go b/x/bank/simulation/sim_test.go new file mode 100644 index 000000000..5d76dd058 --- /dev/null +++ b/x/bank/simulation/sim_test.go @@ -0,0 +1,44 @@ +package simulation + +import ( + "encoding/json" + "math/rand" + "testing" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/x/bank" + "github.com/cosmos/cosmos-sdk/x/mock" + "github.com/cosmos/cosmos-sdk/x/mock/simulation" +) + +func TestBankWithRandomMessages(t *testing.T) { + mapp := mock.NewApp() + + bank.RegisterWire(mapp.Cdc) + mapper := mapp.AccountMapper + coinKeeper := bank.NewKeeper(mapper) + mapp.Router().AddRoute("bank", bank.NewHandler(coinKeeper)) + + err := mapp.CompleteSetup([]*sdk.KVStoreKey{}) + if err != nil { + panic(err) + } + + appStateFn := func(r *rand.Rand, accs []sdk.AccAddress) json.RawMessage { + mock.RandomSetGenesis(r, mapp, accs, []string{"stake"}) + return json.RawMessage("{}") + } + + simulation.Simulate( + t, mapp.BaseApp, appStateFn, + []simulation.TestAndRunTx{ + TestAndRunSingleInputMsgSend(mapper), + }, + []simulation.RandSetup{}, + []simulation.Invariant{ + NonnegativeBalanceInvariant(mapper), + TotalCoinsInvariant(mapper, func() sdk.Coins { return mapp.TotalCoinsSupply }), + }, + 100, 30, 30, + ) +} diff --git a/x/bank/test_helpers.go b/x/bank/test_helpers.go deleted file mode 100644 index 1dad0ba26..000000000 --- a/x/bank/test_helpers.go +++ /dev/null @@ -1,150 +0,0 @@ -package bank - -import ( - "errors" - "fmt" - "math/big" - "math/rand" - "testing" - - sdk "github.com/cosmos/cosmos-sdk/types" - "github.com/cosmos/cosmos-sdk/x/auth" - "github.com/cosmos/cosmos-sdk/x/mock" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - abci "github.com/tendermint/tendermint/abci/types" - "github.com/tendermint/tendermint/crypto" -) - -// ModuleInvariants runs all invariants of the bank module. -// Currently runs non-negative balance invariant and TotalCoinsInvariant -func ModuleInvariants(t *testing.T, app *mock.App, log string) { - NonnegativeBalanceInvariant(t, app, log) - TotalCoinsInvariant(t, app, log) -} - -// NonnegativeBalanceInvariant checks that all accounts in the application have non-negative balances -func NonnegativeBalanceInvariant(t *testing.T, app *mock.App, log string) { - ctx := app.NewContext(false, abci.Header{}) - accts := mock.GetAllAccounts(app.AccountMapper, ctx) - for _, acc := range accts { - coins := acc.GetCoins() - assert.True(t, coins.IsNotNegative(), - fmt.Sprintf("%s has a negative denomination of %s\n%s", - acc.GetAddress().String(), - coins.String(), - log), - ) - } -} - -// TotalCoinsInvariant checks that the sum of the coins across all accounts -// is what is expected -func TotalCoinsInvariant(t *testing.T, app *mock.App, log string) { - ctx := app.BaseApp.NewContext(false, abci.Header{}) - totalCoins := sdk.Coins{} - - chkAccount := func(acc auth.Account) bool { - coins := acc.GetCoins() - totalCoins = totalCoins.Plus(coins) - return false - } - - app.AccountMapper.IterateAccounts(ctx, chkAccount) - require.Equal(t, app.TotalCoinsSupply, totalCoins, log) -} - -// TestAndRunSingleInputMsgSend tests and runs a single msg send, with one input and one output, where both -// accounts already exist. -func TestAndRunSingleInputMsgSend(t *testing.T, r *rand.Rand, app *mock.App, ctx sdk.Context, keys []crypto.PrivKey, log string) (action string, err sdk.Error) { - fromKey := keys[r.Intn(len(keys))] - fromAddr := sdk.AccAddress(fromKey.PubKey().Address()) - toKey := keys[r.Intn(len(keys))] - // Disallow sending money to yourself - for { - if !fromKey.Equals(toKey) { - break - } - toKey = keys[r.Intn(len(keys))] - } - toAddr := sdk.AccAddress(toKey.PubKey().Address()) - initFromCoins := app.AccountMapper.GetAccount(ctx, fromAddr).GetCoins() - - denomIndex := r.Intn(len(initFromCoins)) - amt, goErr := randPositiveInt(r, initFromCoins[denomIndex].Amount) - if goErr != nil { - return "skipping bank send due to account having no coins of denomination " + initFromCoins[denomIndex].Denom, nil - } - - action = fmt.Sprintf("%s is sending %s %s to %s", - fromAddr.String(), - amt.String(), - initFromCoins[denomIndex].Denom, - toAddr.String(), - ) - log = fmt.Sprintf("%s\n%s", log, action) - - coins := sdk.Coins{{initFromCoins[denomIndex].Denom, amt}} - var msg = MsgSend{ - Inputs: []Input{NewInput(fromAddr, coins)}, - Outputs: []Output{NewOutput(toAddr, coins)}, - } - sendAndVerifyMsgSend(t, app, msg, ctx, log, []crypto.PrivKey{fromKey}) - - return action, nil -} - -// Sends and verifies the transition of a msg send. This fails if there are repeated inputs or outputs -func sendAndVerifyMsgSend(t *testing.T, app *mock.App, msg 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)) - SequenceNumbers := make([]int64, len(msg.Inputs)) - - for i := 0; i < len(msg.Inputs); i++ { - acc := app.AccountMapper.GetAccount(ctx, msg.Inputs[i].Address) - AccountNumbers[i] = acc.GetAccountNumber() - SequenceNumbers[i] = acc.GetSequence() - initialInputAddrCoins[i] = acc.GetCoins() - } - for i := 0; i < len(msg.Outputs); i++ { - acc := app.AccountMapper.GetAccount(ctx, msg.Outputs[i].Address) - initialOutputAddrCoins[i] = acc.GetCoins() - } - tx := mock.GenTx([]sdk.Msg{msg}, - AccountNumbers, - SequenceNumbers, - privkeys...) - res := app.Deliver(tx) - if !res.IsOK() { - // TODO: Do this in a more 'canonical' way - fmt.Println(res) - fmt.Println(log) - t.FailNow() - } - - for i := 0; i < len(msg.Inputs); i++ { - terminalInputCoins := app.AccountMapper.GetAccount(ctx, msg.Inputs[i].Address).GetCoins() - require.Equal(t, - initialInputAddrCoins[i].Minus(msg.Inputs[i].Coins), - terminalInputCoins, - fmt.Sprintf("Input #%d had an incorrect amount of coins\n%s", i, log), - ) - } - for i := 0; i < len(msg.Outputs); i++ { - terminalOutputCoins := app.AccountMapper.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), - ) - } -} - -func randPositiveInt(r *rand.Rand, max sdk.Int) (sdk.Int, error) { - if !max.GT(sdk.OneInt()) { - return sdk.Int{}, errors.New("max too small") - } - max = max.Sub(sdk.OneInt()) - return sdk.NewIntFromBigInt(new(big.Int).Rand(r, max.BigInt())).Add(sdk.OneInt()), nil -} diff --git a/x/gov/client/cli/tx.go b/x/gov/client/cli/tx.go index 88c4b7d17..c1bb62bc7 100644 --- a/x/gov/client/cli/tx.go +++ b/x/gov/client/cli/tx.go @@ -20,8 +20,6 @@ const ( flagDescription = "description" flagProposalType = "type" flagDeposit = "deposit" - flagProposer = "proposer" - flagDepositer = "depositer" flagVoter = "voter" flagOption = "option" ) @@ -32,13 +30,15 @@ func GetCmdSubmitProposal(cdc *wire.Codec) *cobra.Command { Use: "submit-proposal", Short: "Submit a proposal along with an initial deposit", RunE: func(cmd *cobra.Command, args []string) error { + ctx := context.NewCoreContextFromViper().WithDecoder(authcmd.GetAccountDecoder(cdc)) + title := viper.GetString(flagTitle) description := viper.GetString(flagDescription) strProposalType := viper.GetString(flagProposalType) initialDeposit := viper.GetString(flagDeposit) // get the from address from the name flag - from, err := sdk.AccAddressFromBech32(viper.GetString(flagProposer)) + fromAddr, err := ctx.GetFromAddress() if err != nil { return err } @@ -54,7 +54,7 @@ func GetCmdSubmitProposal(cdc *wire.Codec) *cobra.Command { } // create the message - msg := gov.NewMsgSubmitProposal(title, description, proposalType, from, amount) + msg := gov.NewMsgSubmitProposal(title, description, proposalType, fromAddr, amount) err = msg.ValidateBasic() if err != nil { @@ -62,10 +62,8 @@ func GetCmdSubmitProposal(cdc *wire.Codec) *cobra.Command { } // build and sign the transaction, then broadcast to Tendermint - ctx := context.NewCoreContextFromViper().WithDecoder(authcmd.GetAccountDecoder(cdc)) // proposalID must be returned, and it is a part of response ctx.PrintResponse = true - err = ctx.EnsureSignBuildBroadcast(ctx.FromAddressName, []sdk.Msg{msg}, cdc) if err != nil { return err @@ -78,7 +76,6 @@ func GetCmdSubmitProposal(cdc *wire.Codec) *cobra.Command { cmd.Flags().String(flagDescription, "", "description of proposal") cmd.Flags().String(flagProposalType, "", "proposalType of proposal") cmd.Flags().String(flagDeposit, "", "deposit of proposal") - cmd.Flags().String(flagProposer, "", "proposer of proposal") return cmd } @@ -89,8 +86,10 @@ func GetCmdDeposit(cdc *wire.Codec) *cobra.Command { Use: "deposit", Short: "deposit tokens for activing proposal", RunE: func(cmd *cobra.Command, args []string) error { + ctx := context.NewCoreContextFromViper().WithDecoder(authcmd.GetAccountDecoder(cdc)) + // get the from address from the name flag - depositer, err := sdk.AccAddressFromBech32(viper.GetString(flagDepositer)) + depositerAddr, err := ctx.GetFromAddress() if err != nil { return err } @@ -103,7 +102,7 @@ func GetCmdDeposit(cdc *wire.Codec) *cobra.Command { } // create the message - msg := gov.NewMsgDeposit(depositer, proposalID, amount) + msg := gov.NewMsgDeposit(depositerAddr, proposalID, amount) err = msg.ValidateBasic() if err != nil { @@ -111,8 +110,6 @@ func GetCmdDeposit(cdc *wire.Codec) *cobra.Command { } // build and sign the transaction, then broadcast to Tendermint - ctx := context.NewCoreContextFromViper().WithDecoder(authcmd.GetAccountDecoder(cdc)) - err = ctx.EnsureSignBuildBroadcast(ctx.FromAddressName, []sdk.Msg{msg}, cdc) if err != nil { return err @@ -122,7 +119,6 @@ func GetCmdDeposit(cdc *wire.Codec) *cobra.Command { } cmd.Flags().String(flagProposalID, "", "proposalID of proposal depositing on") - cmd.Flags().String(flagDepositer, "", "depositer of deposit") cmd.Flags().String(flagDeposit, "", "amount of deposit") return cmd @@ -134,9 +130,9 @@ func GetCmdVote(cdc *wire.Codec) *cobra.Command { Use: "vote", Short: "vote for an active proposal, options: Yes/No/NoWithVeto/Abstain", RunE: func(cmd *cobra.Command, args []string) error { + ctx := context.NewCoreContextFromViper().WithDecoder(authcmd.GetAccountDecoder(cdc)) - bechVoter := viper.GetString(flagVoter) - voter, err := sdk.AccAddressFromBech32(bechVoter) + voterAddr, err := ctx.GetFromAddress() if err != nil { return err } @@ -151,18 +147,17 @@ func GetCmdVote(cdc *wire.Codec) *cobra.Command { } // create the message - msg := gov.NewMsgVote(voter, proposalID, byteVoteOption) + msg := gov.NewMsgVote(voterAddr, proposalID, byteVoteOption) err = msg.ValidateBasic() if err != nil { return err } - fmt.Printf("Vote[Voter:%s,ProposalID:%d,Option:%s]", bechVoter, msg.ProposalID, msg.Option.String()) + fmt.Printf("Vote[Voter:%s,ProposalID:%d,Option:%s]", + voterAddr.String(), msg.ProposalID, msg.Option.String()) // build and sign the transaction, then broadcast to Tendermint - ctx := context.NewCoreContextFromViper().WithDecoder(authcmd.GetAccountDecoder(cdc)) - err = ctx.EnsureSignBuildBroadcast(ctx.FromAddressName, []sdk.Msg{msg}, cdc) if err != nil { return err @@ -172,7 +167,6 @@ func GetCmdVote(cdc *wire.Codec) *cobra.Command { } cmd.Flags().String(flagProposalID, "", "proposalID of proposal voting on") - cmd.Flags().String(flagVoter, "", "bech32 voter address") cmd.Flags().String(flagOption, "", "vote option {Yes, No, NoWithVeto, Abstain}") return cmd diff --git a/x/gov/proposals.go b/x/gov/proposals.go index bb6b0aed4..4a07342cb 100644 --- a/x/gov/proposals.go +++ b/x/gov/proposals.go @@ -167,11 +167,11 @@ func (pt *ProposalKind) UnmarshalJSON(data []byte) error { // Turns VoteOption byte to String func (pt ProposalKind) String() string { switch pt { - case 0x00: - return "Text" case 0x01: - return "ParameterChange" + return "Text" case 0x02: + return "ParameterChange" + case 0x03: return "SoftwareUpgrade" default: return "" diff --git a/x/gov/test_common.go b/x/gov/test_common.go index 5567e4697..7ea85fc72 100644 --- a/x/gov/test_common.go +++ b/x/gov/test_common.go @@ -61,12 +61,14 @@ func getInitChainer(mapp *mock.App, keeper Keeper, stakeKeeper stake.Keeper) sdk stakeGenesis := stake.DefaultGenesisState() stakeGenesis.Pool.LooseTokens = sdk.NewRat(100000) - err := stake.InitGenesis(ctx, stakeKeeper, stakeGenesis) + validators, err := stake.InitGenesis(ctx, stakeKeeper, stakeGenesis) if err != nil { panic(err) } InitGenesis(ctx, keeper, DefaultGenesisState()) - return abci.ResponseInitChain{} + return abci.ResponseInitChain{ + Validators: validators, + } } } diff --git a/x/mock/app.go b/x/mock/app.go index 27f3b9d46..175222fa8 100644 --- a/x/mock/app.go +++ b/x/mock/app.go @@ -202,8 +202,7 @@ func RandomSetGenesis(r *rand.Rand, app *App, addrs []sdk.AccAddress, denoms []s (&baseAcc).SetCoins(coins) accts[i] = &baseAcc } - - SetGenesis(app, accts) + app.GenesisAccounts = accts } // GetAllAccounts returns all accounts in the accountMapper. diff --git a/x/mock/random_simulate_blocks.go b/x/mock/random_simulate_blocks.go deleted file mode 100644 index a37913065..000000000 --- a/x/mock/random_simulate_blocks.go +++ /dev/null @@ -1,95 +0,0 @@ -package mock - -import ( - "fmt" - "math/big" - "math/rand" - "testing" - "time" - - sdk "github.com/cosmos/cosmos-sdk/types" - "github.com/stretchr/testify/require" - abci "github.com/tendermint/tendermint/abci/types" -) - -// RandomizedTesting tests application by sending random messages. -func (app *App) RandomizedTesting( - t *testing.T, ops []TestAndRunTx, setups []RandSetup, - invariants []Invariant, numKeys int, numBlocks int, blockSize int, -) { - time := time.Now().UnixNano() - app.RandomizedTestingFromSeed(t, time, ops, setups, invariants, numKeys, numBlocks, blockSize) -} - -// RandomizedTestingFromSeed tests an application by running the provided -// operations, testing the provided invariants, but using the provided seed. -func (app *App) RandomizedTestingFromSeed( - t *testing.T, seed int64, ops []TestAndRunTx, setups []RandSetup, - invariants []Invariant, numKeys int, numBlocks int, blockSize int, -) { - log := fmt.Sprintf("Starting SingleModuleTest with randomness created with seed %d", int(seed)) - keys, addrs := GeneratePrivKeyAddressPairs(numKeys) - r := rand.New(rand.NewSource(seed)) - - for i := 0; i < len(setups); i++ { - setups[i](r, keys) - } - - RandomSetGenesis(r, app, addrs, []string{"foocoin"}) - header := abci.Header{Height: 0} - - for i := 0; i < numBlocks; i++ { - app.BeginBlock(abci.RequestBeginBlock{}) - - // Make sure invariants hold at beginning of block and when nothing was - // done. - app.assertAllInvariants(t, invariants, log) - - ctx := app.NewContext(false, header) - - // TODO: Add modes to simulate "no load", "medium load", and - // "high load" blocks. - for j := 0; j < blockSize; j++ { - logUpdate, err := ops[r.Intn(len(ops))](t, r, app, ctx, keys, log) - log += "\n" + logUpdate - - require.Nil(t, err, log) - app.assertAllInvariants(t, invariants, log) - } - - app.EndBlock(abci.RequestEndBlock{}) - header.Height++ - } -} - -func (app *App) assertAllInvariants(t *testing.T, tests []Invariant, log string) { - for i := 0; i < len(tests); i++ { - tests[i](t, app, log) - } -} - -// BigInterval is a representation of the interval [lo, hi), where -// lo and hi are both of type sdk.Int -type BigInterval struct { - lo sdk.Int - hi sdk.Int -} - -// RandFromBigInterval chooses an interval uniformly from the provided list of -// BigIntervals, and then chooses an element from an interval uniformly at random. -func RandFromBigInterval(r *rand.Rand, intervals []BigInterval) sdk.Int { - if len(intervals) == 0 { - return sdk.ZeroInt() - } - - interval := intervals[r.Intn(len(intervals))] - - lo := interval.lo - hi := interval.hi - - diff := hi.Sub(lo) - result := sdk.NewIntFromBigInt(new(big.Int).Rand(r, diff.BigInt())) - result = result.Add(lo) - - return result -} diff --git a/x/mock/simulation/random_simulate_blocks.go b/x/mock/simulation/random_simulate_blocks.go new file mode 100644 index 000000000..5f507b89c --- /dev/null +++ b/x/mock/simulation/random_simulate_blocks.go @@ -0,0 +1,81 @@ +package simulation + +import ( + "encoding/json" + "fmt" + "math/rand" + "testing" + "time" + + "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" + abci "github.com/tendermint/tendermint/abci/types" +) + +// Simulate tests application by sending random messages. +func Simulate( + t *testing.T, app *baseapp.BaseApp, appStateFn func(r *rand.Rand, accs []sdk.AccAddress) json.RawMessage, ops []TestAndRunTx, setups []RandSetup, + invariants []Invariant, numKeys int, numBlocks int, blockSize int, +) { + time := time.Now().UnixNano() + SimulateFromSeed(t, app, appStateFn, time, ops, setups, invariants, numKeys, numBlocks, blockSize) +} + +// 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, accs []sdk.AccAddress) json.RawMessage, seed int64, ops []TestAndRunTx, setups []RandSetup, + invariants []Invariant, numKeys int, numBlocks int, blockSize int, +) { + log := fmt.Sprintf("Starting SimulateFromSeed with randomness created with seed %d", int(seed)) + keys, addrs := mock.GeneratePrivKeyAddressPairs(numKeys) + r := rand.New(rand.NewSource(seed)) + + // Setup event stats + events := make(map[string]uint) + event := func(what string) { + events[what]++ + } + + app.InitChain(abci.RequestInitChain{AppStateBytes: appStateFn(r, addrs)}) + for i := 0; i < len(setups); i++ { + setups[i](r, keys) + } + app.Commit() + + header := abci.Header{Height: 0} + + for i := 0; i < numBlocks; i++ { + app.BeginBlock(abci.RequestBeginBlock{}) + + // Make sure invariants hold at beginning of block and when nothing was + // done. + AssertAllInvariants(t, app, invariants, log) + + ctx := app.NewContext(false, header) + + // TODO: Add modes to simulate "no load", "medium load", and + // "high load" blocks. + for j := 0; j < blockSize; j++ { + logUpdate, err := ops[r.Intn(len(ops))](t, r, app, ctx, keys, log, event) + log += "\n" + logUpdate + + require.Nil(t, err, log) + AssertAllInvariants(t, app, invariants, log) + } + + app.EndBlock(abci.RequestEndBlock{}) + header.Height++ + } + + DisplayEvents(events) +} + +// AssertAllInvariants asserts a list of provided invariants against application state +func AssertAllInvariants(t *testing.T, app *baseapp.BaseApp, tests []Invariant, log string) { + for i := 0; i < len(tests); i++ { + tests[i](t, app, log) + } +} diff --git a/x/mock/types.go b/x/mock/simulation/types.go similarity index 76% rename from x/mock/types.go rename to x/mock/simulation/types.go index 50957e1c4..6e1d9f198 100644 --- a/x/mock/types.go +++ b/x/mock/simulation/types.go @@ -1,9 +1,10 @@ -package mock +package simulation import ( "math/rand" "testing" + "github.com/cosmos/cosmos-sdk/baseapp" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/tendermint/tendermint/crypto" ) @@ -13,8 +14,8 @@ type ( // transition was as expected. It returns a descriptive message "action" // about what this fuzzed tx actually did, for ease of debugging. TestAndRunTx func( - t *testing.T, r *rand.Rand, app *App, ctx sdk.Context, - privKeys []crypto.PrivKey, log string, + t *testing.T, r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, + privKeys []crypto.PrivKey, log string, event func(string), ) (action string, err sdk.Error) // RandSetup performs the random setup the mock module needs. @@ -23,14 +24,14 @@ type ( // An Invariant is a function which tests a particular invariant. // If the invariant has been broken, the function should halt the // test and output the log. - Invariant func(t *testing.T, app *App, log string) + Invariant func(t *testing.T, app *baseapp.BaseApp, log string) ) // PeriodicInvariant returns an Invariant function closure that asserts // a given invariant if the mock application's last block modulo the given // period is congruent to the given offset. func PeriodicInvariant(invariant Invariant, period int, offset int) Invariant { - return func(t *testing.T, app *App, log string) { + return func(t *testing.T, app *baseapp.BaseApp, log string) { if int(app.LastBlockHeight())%period == offset { invariant(t, app, log) } diff --git a/x/mock/simulation/util.go b/x/mock/simulation/util.go new file mode 100644 index 000000000..14227a1ae --- /dev/null +++ b/x/mock/simulation/util.go @@ -0,0 +1,56 @@ +package simulation + +import ( + "fmt" + "math/rand" + + crypto "github.com/tendermint/tendermint/crypto" + + sdk "github.com/cosmos/cosmos-sdk/types" +) + +// shamelessly copied from https://stackoverflow.com/questions/22892120/how-to-generate-a-random-string-of-a-fixed-length-in-golang#31832326 +// TODO we should probably move this to tendermint/libs/common/random.go + +const letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" +const ( + letterIdxBits = 6 // 6 bits to represent a letter index + letterIdxMask = 1<= 0; { + if remain == 0 { + cache, remain = r.Int63(), letterIdxMax + } + if idx := int(cache & letterIdxMask); idx < len(letterBytes) { + b[i] = letterBytes[idx] + i-- + } + cache >>= letterIdxBits + remain-- + } + return string(b) +} + +// Pretty-print events as a table +func DisplayEvents(events map[string]uint) { + // TODO + fmt.Printf("Events: %v\n", events) +} + +// Pick a random key from an array +func RandomKey(r *rand.Rand, keys []crypto.PrivKey) crypto.PrivKey { + return keys[r.Intn( + len(keys), + )] +} + +// Generate a random amount +func RandomAmount(r *rand.Rand, max sdk.Int) sdk.Int { + return sdk.NewInt(int64(r.Intn(int(max.Int64())))) +} diff --git a/x/mock/test_utils.go b/x/mock/test_utils.go index c3179849d..c97f1c0c8 100644 --- a/x/mock/test_utils.go +++ b/x/mock/test_utils.go @@ -1,6 +1,8 @@ package mock import ( + "math/big" + "math/rand" "testing" "github.com/cosmos/cosmos-sdk/baseapp" @@ -10,6 +12,32 @@ import ( "github.com/tendermint/tendermint/crypto" ) +// BigInterval is a representation of the interval [lo, hi), where +// lo and hi are both of type sdk.Int +type BigInterval struct { + lo sdk.Int + hi sdk.Int +} + +// RandFromBigInterval chooses an interval uniformly from the provided list of +// BigIntervals, and then chooses an element from an interval uniformly at random. +func RandFromBigInterval(r *rand.Rand, intervals []BigInterval) sdk.Int { + if len(intervals) == 0 { + return sdk.ZeroInt() + } + + interval := intervals[r.Intn(len(intervals))] + + lo := interval.lo + hi := interval.hi + + diff := hi.Sub(lo) + result := sdk.NewIntFromBigInt(new(big.Int).Rand(r, diff.BigInt())) + result = result.Add(lo) + + return result +} + // CheckBalance checks the balance of an account. func CheckBalance(t *testing.T, app *App, addr sdk.AccAddress, exp sdk.Coins) { ctxCheck := app.BaseApp.NewContext(true, abci.Header{}) diff --git a/x/slashing/app_test.go b/x/slashing/app_test.go index f5f8c98f8..8151c69da 100644 --- a/x/slashing/app_test.go +++ b/x/slashing/app_test.go @@ -59,11 +59,14 @@ func getInitChainer(mapp *mock.App, keeper stake.Keeper) sdk.InitChainer { mapp.InitChainer(ctx, req) stakeGenesis := stake.DefaultGenesisState() stakeGenesis.Pool.LooseTokens = sdk.NewRat(100000) - err := stake.InitGenesis(ctx, keeper, stakeGenesis) + validators, err := stake.InitGenesis(ctx, keeper, stakeGenesis) if err != nil { panic(err) } - return abci.ResponseInitChain{} + + return abci.ResponseInitChain{ + Validators: validators, + } } } diff --git a/x/slashing/client/cli/tx.go b/x/slashing/client/cli/tx.go index 15458ad1a..ab8911c11 100644 --- a/x/slashing/client/cli/tx.go +++ b/x/slashing/client/cli/tx.go @@ -14,12 +14,12 @@ import ( func GetCmdUnrevoke(cdc *wire.Codec) *cobra.Command { cmd := &cobra.Command{ Use: "unrevoke", - Args: cobra.ExactArgs(1), + Args: cobra.ExactArgs(0), Short: "unrevoke validator previously revoked for downtime", RunE: func(cmd *cobra.Command, args []string) error { ctx := context.NewCoreContextFromViper().WithDecoder(authcmd.GetAccountDecoder(cdc)) - validatorAddr, err := sdk.AccAddressFromBech32(args[0]) + validatorAddr, err := ctx.GetFromAddress() if err != nil { return err } diff --git a/x/slashing/test_common.go b/x/slashing/test_common.go index 190932995..142093c76 100644 --- a/x/slashing/test_common.go +++ b/x/slashing/test_common.go @@ -70,7 +70,7 @@ func createTestInput(t *testing.T) (sdk.Context, bank.Keeper, stake.Keeper, para genesis.Pool.LooseTokens = sdk.NewRat(initCoins.MulRaw(int64(len(addrs))).Int64()) - err = stake.InitGenesis(ctx, sk, genesis) + _, err = stake.InitGenesis(ctx, sk, genesis) require.Nil(t, err) for _, addr := range addrs { diff --git a/x/stake/app_test.go b/x/stake/app_test.go index 606369cd6..b1490edef 100644 --- a/x/stake/app_test.go +++ b/x/stake/app_test.go @@ -65,12 +65,14 @@ func getInitChainer(mapp *mock.App, keeper Keeper) sdk.InitChainer { stakeGenesis := DefaultGenesisState() stakeGenesis.Pool.LooseTokens = sdk.NewRat(100000) - err := InitGenesis(ctx, keeper, stakeGenesis) + validators, err := InitGenesis(ctx, keeper, stakeGenesis) if err != nil { panic(err) } - return abci.ResponseInitChain{} + return abci.ResponseInitChain{ + Validators: validators, + } } } diff --git a/x/stake/client/cli/tx.go b/x/stake/client/cli/tx.go index 72317c82c..e391bad8c 100644 --- a/x/stake/client/cli/tx.go +++ b/x/stake/client/cli/tx.go @@ -28,7 +28,7 @@ func GetCmdCreateValidator(cdc *wire.Codec) *cobra.Command { if err != nil { return err } - validatorAddr, err := sdk.AccAddressFromBech32(viper.GetString(FlagAddressValidator)) + validatorAddr, err := ctx.GetFromAddress() if err != nil { return err } @@ -74,7 +74,6 @@ func GetCmdCreateValidator(cdc *wire.Codec) *cobra.Command { cmd.Flags().AddFlagSet(fsPk) cmd.Flags().AddFlagSet(fsAmount) cmd.Flags().AddFlagSet(fsDescription) - cmd.Flags().AddFlagSet(fsValidator) cmd.Flags().AddFlagSet(fsDelegator) return cmd } @@ -85,8 +84,9 @@ func GetCmdEditValidator(cdc *wire.Codec) *cobra.Command { Use: "edit-validator", Short: "edit and existing validator account", RunE: func(cmd *cobra.Command, args []string) error { + ctx := context.NewCoreContextFromViper().WithDecoder(authcmd.GetAccountDecoder(cdc)) - validatorAddr, err := sdk.AccAddressFromBech32(viper.GetString(FlagAddressValidator)) + validatorAddr, err := ctx.GetFromAddress() if err != nil { return err } @@ -99,8 +99,6 @@ func GetCmdEditValidator(cdc *wire.Codec) *cobra.Command { msg := stake.NewMsgEditValidator(validatorAddr, description) // build and sign the transaction, then broadcast to Tendermint - ctx := context.NewCoreContextFromViper().WithDecoder(authcmd.GetAccountDecoder(cdc)) - err = ctx.EnsureSignBuildBroadcast(ctx.FromAddressName, []sdk.Msg{msg}, cdc) if err != nil { return err @@ -111,7 +109,6 @@ func GetCmdEditValidator(cdc *wire.Codec) *cobra.Command { } cmd.Flags().AddFlagSet(fsDescription) - cmd.Flags().AddFlagSet(fsValidator) return cmd } @@ -121,12 +118,14 @@ func GetCmdDelegate(cdc *wire.Codec) *cobra.Command { Use: "delegate", Short: "delegate liquid tokens to an validator", RunE: func(cmd *cobra.Command, args []string) error { + ctx := context.NewCoreContextFromViper().WithDecoder(authcmd.GetAccountDecoder(cdc)) + amount, err := sdk.ParseCoin(viper.GetString(FlagAmount)) if err != nil { return err } - delegatorAddr, err := sdk.AccAddressFromBech32(viper.GetString(FlagAddressDelegator)) + delegatorAddr, err := ctx.GetFromAddress() if err != nil { return err } @@ -138,8 +137,6 @@ func GetCmdDelegate(cdc *wire.Codec) *cobra.Command { msg := stake.NewMsgDelegate(delegatorAddr, validatorAddr, amount) // build and sign the transaction, then broadcast to Tendermint - ctx := context.NewCoreContextFromViper().WithDecoder(authcmd.GetAccountDecoder(cdc)) - err = ctx.EnsureSignBuildBroadcast(ctx.FromAddressName, []sdk.Msg{msg}, cdc) if err != nil { return err @@ -150,7 +147,6 @@ func GetCmdDelegate(cdc *wire.Codec) *cobra.Command { } cmd.Flags().AddFlagSet(fsAmount) - cmd.Flags().AddFlagSet(fsDelegator) cmd.Flags().AddFlagSet(fsValidator) return cmd } @@ -175,9 +171,10 @@ func GetCmdBeginRedelegate(storeName string, cdc *wire.Codec) *cobra.Command { Use: "begin", Short: "begin redelegation", RunE: func(cmd *cobra.Command, args []string) error { + ctx := context.NewCoreContextFromViper().WithDecoder(authcmd.GetAccountDecoder(cdc)) var err error - delegatorAddr, err := sdk.AccAddressFromBech32(viper.GetString(FlagAddressDelegator)) + delegatorAddr, err := ctx.GetFromAddress() if err != nil { return err } @@ -202,8 +199,6 @@ func GetCmdBeginRedelegate(storeName string, cdc *wire.Codec) *cobra.Command { msg := stake.NewMsgBeginRedelegate(delegatorAddr, validatorSrcAddr, validatorDstAddr, sharesAmount) // build and sign the transaction, then broadcast to Tendermint - ctx := context.NewCoreContextFromViper().WithDecoder(authcmd.GetAccountDecoder(cdc)) - err = ctx.EnsureSignBuildBroadcast(ctx.FromAddressName, []sdk.Msg{msg}, cdc) if err != nil { return err @@ -214,7 +209,6 @@ func GetCmdBeginRedelegate(storeName string, cdc *wire.Codec) *cobra.Command { } cmd.Flags().AddFlagSet(fsShares) - cmd.Flags().AddFlagSet(fsDelegator) cmd.Flags().AddFlagSet(fsRedelegation) return cmd } @@ -266,8 +260,9 @@ func GetCmdCompleteRedelegate(cdc *wire.Codec) *cobra.Command { Use: "complete", Short: "complete redelegation", RunE: func(cmd *cobra.Command, args []string) error { + ctx := context.NewCoreContextFromViper().WithDecoder(authcmd.GetAccountDecoder(cdc)) - delegatorAddr, err := sdk.AccAddressFromBech32(viper.GetString(FlagAddressDelegator)) + delegatorAddr, err := ctx.GetFromAddress() if err != nil { return err } @@ -283,8 +278,6 @@ func GetCmdCompleteRedelegate(cdc *wire.Codec) *cobra.Command { msg := stake.NewMsgCompleteRedelegate(delegatorAddr, validatorSrcAddr, validatorDstAddr) // build and sign the transaction, then broadcast to Tendermint - ctx := context.NewCoreContextFromViper().WithDecoder(authcmd.GetAccountDecoder(cdc)) - err = ctx.EnsureSignBuildBroadcast(ctx.FromAddressName, []sdk.Msg{msg}, cdc) if err != nil { return err @@ -293,7 +286,6 @@ func GetCmdCompleteRedelegate(cdc *wire.Codec) *cobra.Command { return nil }, } - cmd.Flags().AddFlagSet(fsDelegator) cmd.Flags().AddFlagSet(fsRedelegation) return cmd } @@ -318,8 +310,9 @@ func GetCmdBeginUnbonding(storeName string, cdc *wire.Codec) *cobra.Command { Use: "begin", Short: "begin unbonding", RunE: func(cmd *cobra.Command, args []string) error { + ctx := context.NewCoreContextFromViper().WithDecoder(authcmd.GetAccountDecoder(cdc)) - delegatorAddr, err := sdk.AccAddressFromBech32(viper.GetString(FlagAddressDelegator)) + delegatorAddr, err := ctx.GetFromAddress() if err != nil { return err } @@ -340,8 +333,6 @@ func GetCmdBeginUnbonding(storeName string, cdc *wire.Codec) *cobra.Command { msg := stake.NewMsgBeginUnbonding(delegatorAddr, validatorAddr, sharesAmount) // build and sign the transaction, then broadcast to Tendermint - ctx := context.NewCoreContextFromViper().WithDecoder(authcmd.GetAccountDecoder(cdc)) - err = ctx.EnsureSignBuildBroadcast(ctx.FromAddressName, []sdk.Msg{msg}, cdc) if err != nil { return err @@ -352,7 +343,6 @@ func GetCmdBeginUnbonding(storeName string, cdc *wire.Codec) *cobra.Command { } cmd.Flags().AddFlagSet(fsShares) - cmd.Flags().AddFlagSet(fsDelegator) cmd.Flags().AddFlagSet(fsValidator) return cmd } @@ -363,8 +353,9 @@ func GetCmdCompleteUnbonding(cdc *wire.Codec) *cobra.Command { Use: "complete", Short: "complete unbonding", RunE: func(cmd *cobra.Command, args []string) error { + ctx := context.NewCoreContextFromViper().WithDecoder(authcmd.GetAccountDecoder(cdc)) - delegatorAddr, err := sdk.AccAddressFromBech32(viper.GetString(FlagAddressDelegator)) + delegatorAddr, err := ctx.GetFromAddress() if err != nil { return err } @@ -376,8 +367,6 @@ func GetCmdCompleteUnbonding(cdc *wire.Codec) *cobra.Command { msg := stake.NewMsgCompleteUnbonding(delegatorAddr, validatorAddr) // build and sign the transaction, then broadcast to Tendermint - ctx := context.NewCoreContextFromViper().WithDecoder(authcmd.GetAccountDecoder(cdc)) - err = ctx.EnsureSignBuildBroadcast(ctx.FromAddressName, []sdk.Msg{msg}, cdc) if err != nil { return err @@ -386,7 +375,6 @@ func GetCmdCompleteUnbonding(cdc *wire.Codec) *cobra.Command { return nil }, } - cmd.Flags().AddFlagSet(fsDelegator) cmd.Flags().AddFlagSet(fsValidator) return cmd } diff --git a/x/stake/genesis.go b/x/stake/genesis.go index b4ed80e51..ad3ac0af5 100644 --- a/x/stake/genesis.go +++ b/x/stake/genesis.go @@ -1,10 +1,12 @@ package stake import ( + abci "github.com/tendermint/tendermint/abci/types" + tmtypes "github.com/tendermint/tendermint/types" + sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/x/stake/types" "github.com/pkg/errors" - tmtypes "github.com/tendermint/tendermint/types" ) // InitGenesis sets the pool and parameters for the provided keeper and @@ -12,7 +14,8 @@ import ( // validator in the keeper along with manually setting the indexes. In // addition, it also sets any delegations found in data. Finally, it updates // the bonded validators. -func InitGenesis(ctx sdk.Context, keeper Keeper, data types.GenesisState) error { +// Returns final validator set after applying all declaration and delegations +func InitGenesis(ctx sdk.Context, keeper Keeper, data types.GenesisState) (res []abci.Validator, err error) { keeper.SetPool(ctx, data.Pool) keeper.SetNewParams(ctx, data.Params) keeper.InitIntraTxCounter(ctx) @@ -21,10 +24,10 @@ func InitGenesis(ctx sdk.Context, keeper Keeper, data types.GenesisState) error keeper.SetValidator(ctx, validator) if validator.Tokens.IsZero() { - return errors.Errorf("genesis validator cannot have zero pool shares, validator: %v", validator) + return res, errors.Errorf("genesis validator cannot have zero pool shares, validator: %v", validator) } if validator.DelegatorShares.IsZero() { - return errors.Errorf("genesis validator cannot have zero delegator shares, validator: %v", validator) + return res, errors.Errorf("genesis validator cannot have zero delegator shares, validator: %v", validator) } // Manually set indexes for the first time @@ -43,7 +46,13 @@ func InitGenesis(ctx sdk.Context, keeper Keeper, data types.GenesisState) error } keeper.UpdateBondedValidatorsFull(ctx) - return nil + + vals := keeper.GetValidatorsBonded(ctx) + res = make([]abci.Validator, len(vals)) + for i, val := range vals { + res[i] = sdk.ABCIValidator(val) + } + return } // WriteGenesis returns a GenesisState for a given context and keeper. The diff --git a/x/stake/genesis_test.go b/x/stake/genesis_test.go index e27c7fed2..ec061ea3c 100644 --- a/x/stake/genesis_test.go +++ b/x/stake/genesis_test.go @@ -1,10 +1,13 @@ package stake import ( + "fmt" "testing" "github.com/stretchr/testify/require" + abci "github.com/tendermint/tendermint/abci/types" + sdk "github.com/cosmos/cosmos-sdk/types" keep "github.com/cosmos/cosmos-sdk/x/stake/keeper" "github.com/cosmos/cosmos-sdk/x/stake/types" @@ -14,7 +17,7 @@ func TestInitGenesis(t *testing.T) { ctx, _, keeper := keep.CreateTestInput(t, false, 1000) pool := keeper.GetPool(ctx) - pool.LooseTokens = sdk.NewRat(2) + pool.BondedTokens = sdk.NewRat(2) params := keeper.GetParams(ctx) var delegations []Delegation @@ -24,17 +27,19 @@ func TestInitGenesis(t *testing.T) { NewValidator(keep.Addrs[1], keep.PKs[1], Description{Moniker: "bloop"}), } genesisState := types.NewGenesisState(pool, params, validators, delegations) - err := InitGenesis(ctx, keeper, genesisState) + _, err := InitGenesis(ctx, keeper, genesisState) require.Error(t, err) // initialize the validators + validators[0].Status = sdk.Bonded validators[0].Tokens = sdk.OneRat() validators[0].DelegatorShares = sdk.OneRat() + validators[1].Status = sdk.Bonded validators[1].Tokens = sdk.OneRat() validators[1].DelegatorShares = sdk.OneRat() genesisState = types.NewGenesisState(pool, params, validators, delegations) - err = InitGenesis(ctx, keeper, genesisState) + vals, err := InitGenesis(ctx, keeper, genesisState) require.NoError(t, err) // now make sure the validators are bonded @@ -45,4 +50,50 @@ func TestInitGenesis(t *testing.T) { resVal, found = keeper.GetValidator(ctx, keep.Addrs[1]) require.True(t, found) require.Equal(t, sdk.Bonded, resVal.Status) + + abcivals := make([]abci.Validator, len(vals)) + for i, val := range validators { + abcivals[i] = sdk.ABCIValidator(val) + } + + require.Equal(t, abcivals, vals) +} + +func TestInitGenesisLargeValidatorSet(t *testing.T) { + size := 200 + require.True(t, size > 100) + + ctx, _, keeper := keep.CreateTestInput(t, false, 1000) + + // Assigning 2 to the first 100 vals, 1 to the rest + pool := keeper.GetPool(ctx) + pool.BondedTokens = sdk.NewRat(int64(200 + (size - 100))) + + params := keeper.GetParams(ctx) + delegations := []Delegation{} + validators := make([]Validator, size) + + for i := range validators { + validators[i] = NewValidator(keep.Addrs[i], keep.PKs[i], Description{Moniker: fmt.Sprintf("#%d", i)}) + + validators[i].Status = sdk.Bonded + if i < 100 { + validators[i].Tokens = sdk.NewRat(2) + validators[i].DelegatorShares = sdk.NewRat(2) + } else { + validators[i].Tokens = sdk.OneRat() + validators[i].DelegatorShares = sdk.OneRat() + } + } + + genesisState := types.NewGenesisState(pool, params, validators, delegations) + vals, err := InitGenesis(ctx, keeper, genesisState) + require.NoError(t, err) + + abcivals := make([]abci.Validator, 100) + for i, val := range validators[:100] { + abcivals[i] = sdk.ABCIValidator(val) + } + + require.Equal(t, abcivals, vals) } diff --git a/x/stake/keeper/delegation.go b/x/stake/keeper/delegation.go index e7168109a..23b58108f 100644 --- a/x/stake/keeper/delegation.go +++ b/x/stake/keeper/delegation.go @@ -110,6 +110,22 @@ func (k Keeper) GetUnbondingDelegationsFromValidator(ctx sdk.Context, valAddr sd return ubds } +// iterate through all of the unbonding delegations +func (k Keeper) IterateUnbondingDelegations(ctx sdk.Context, fn func(index int64, ubd types.UnbondingDelegation) (stop bool)) { + store := ctx.KVStore(k.storeKey) + iterator := sdk.KVStorePrefixIterator(store, UnbondingDelegationKey) + i := int64(0) + for ; iterator.Valid(); iterator.Next() { + ubd := types.MustUnmarshalUBD(k.cdc, iterator.Key(), iterator.Value()) + stop := fn(i, ubd) + if stop { + break + } + i++ + } + iterator.Close() +} + // set the unbonding delegation and associated index func (k Keeper) SetUnbondingDelegation(ctx sdk.Context, ubd types.UnbondingDelegation) { store := ctx.KVStore(k.storeKey) @@ -298,6 +314,12 @@ func (k Keeper) unbond(ctx sdk.Context, delegatorAddr, validatorAddr sdk.AccAddr // complete unbonding an unbonding record func (k Keeper) BeginUnbonding(ctx sdk.Context, delegatorAddr, validatorAddr sdk.AccAddress, sharesAmount sdk.Rat) sdk.Error { + // TODO quick fix, instead we should use an index, see https://github.com/cosmos/cosmos-sdk/issues/1402 + _, found := k.GetUnbondingDelegation(ctx, delegatorAddr, validatorAddr) + if found { + return types.ErrExistingUnbondingDelegation(k.Codespace()) + } + returnAmount, err := k.unbond(ctx, delegatorAddr, validatorAddr, sharesAmount) if err != nil { return err diff --git a/x/stake/keeper/test_common.go b/x/stake/keeper/test_common.go index db7e382d5..19cf4d27c 100644 --- a/x/stake/keeper/test_common.go +++ b/x/stake/keeper/test_common.go @@ -23,8 +23,8 @@ import ( // dummy addresses used for testing var ( - Addrs = createTestAddrs(100) - PKs = createTestPubKeys(100) + Addrs = createTestAddrs(500) + PKs = createTestPubKeys(500) emptyAddr sdk.AccAddress emptyPubkey crypto.PubKey diff --git a/x/stake/simulation/invariants.go b/x/stake/simulation/invariants.go new file mode 100644 index 000000000..e4869693c --- /dev/null +++ b/x/stake/simulation/invariants.go @@ -0,0 +1,81 @@ +package simulation + +import ( + "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" + "github.com/cosmos/cosmos-sdk/x/bank" + "github.com/cosmos/cosmos-sdk/x/mock/simulation" + "github.com/cosmos/cosmos-sdk/x/stake" + abci "github.com/tendermint/tendermint/abci/types" +) + +// AllInvariants runs all invariants of the stake module. +// Currently: total supply, positive power +func AllInvariants(ck bank.Keeper, k stake.Keeper, am auth.AccountMapper) simulation.Invariant { + return func(t *testing.T, app *baseapp.BaseApp, log string) { + SupplyInvariants(ck, k, am)(t, app, log) + PositivePowerInvariant(k)(t, app, log) + ValidatorSetInvariant(k)(t, app, log) + } +} + +// SupplyInvariants checks that the total supply reflects all held loose tokens, bonded tokens, and unbonding delegations +func SupplyInvariants(ck bank.Keeper, k stake.Keeper, am auth.AccountMapper) simulation.Invariant { + return func(t *testing.T, app *baseapp.BaseApp, log string) { + ctx := app.NewContext(false, abci.Header{}) + pool := k.GetPool(ctx) + + loose := sdk.ZeroInt() + bonded := sdk.ZeroRat() + am.IterateAccounts(ctx, func(acc auth.Account) bool { + loose = loose.Add(acc.GetCoins().AmountOf("steak")) + return false + }) + k.IterateUnbondingDelegations(ctx, func(_ int64, ubd stake.UnbondingDelegation) bool { + loose = loose.Add(ubd.Balance.Amount) + return false + }) + k.IterateValidators(ctx, func(_ int64, validator sdk.Validator) bool { + switch validator.GetStatus() { + case sdk.Bonded: + bonded = bonded.Add(validator.GetPower()) + case sdk.Unbonding: + case sdk.Unbonded: + loose = loose.Add(validator.GetTokens().RoundInt()) + } + return false + }) + + // Loose tokens should equal coin supply plus unbonding delegations plus tokens on unbonded validators + require.True(t, pool.LooseTokens.RoundInt64() == loose.Int64(), "expected loose tokens to equal total steak held by accounts - pool.LooseTokens: %v, sum of account tokens: %v\nlog: %s", + pool.LooseTokens.RoundInt64(), loose.Int64(), log) + + // Bonded tokens should equal sum of tokens with bonded validators + require.True(t, pool.BondedTokens.Equal(bonded), "expected bonded tokens to equal total steak held by bonded validators\nlog: %s", log) + + // TODO Inflation check on total supply + } +} + +// PositivePowerInvariant checks that all stored validators have > 0 power +func PositivePowerInvariant(k stake.Keeper) simulation.Invariant { + return func(t *testing.T, app *baseapp.BaseApp, log string) { + ctx := app.NewContext(false, abci.Header{}) + k.IterateValidatorsBonded(ctx, func(_ int64, validator sdk.Validator) bool { + require.True(t, validator.GetPower().GT(sdk.ZeroRat()), "validator with non-positive power stored") + return false + }) + } +} + +// ValidatorSetInvariant checks equivalence of Tendermint validator set and SDK validator set +func ValidatorSetInvariant(k stake.Keeper) simulation.Invariant { + return func(t *testing.T, app *baseapp.BaseApp, log string) { + // TODO + } +} diff --git a/x/stake/simulation/msgs.go b/x/stake/simulation/msgs.go new file mode 100644 index 000000000..87324eed7 --- /dev/null +++ b/x/stake/simulation/msgs.go @@ -0,0 +1,253 @@ +package simulation + +import ( + "fmt" + "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" + "github.com/cosmos/cosmos-sdk/x/mock" + "github.com/cosmos/cosmos-sdk/x/mock/simulation" + "github.com/cosmos/cosmos-sdk/x/stake" + abci "github.com/tendermint/tendermint/abci/types" + "github.com/tendermint/tendermint/crypto" +) + +// SimulateMsgCreateValidator +func SimulateMsgCreateValidator(m auth.AccountMapper, k stake.Keeper) simulation.TestAndRunTx { + return func(t *testing.T, r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, keys []crypto.PrivKey, log string, event func(string)) (action string, err sdk.Error) { + denom := k.GetParams(ctx).BondDenom + description := stake.Description{ + Moniker: simulation.RandStringOfLength(r, 10), + } + key := simulation.RandomKey(r, keys) + pubkey := key.PubKey() + address := sdk.AccAddress(pubkey.Address()) + amount := m.GetAccount(ctx, address).GetCoins().AmountOf(denom) + if amount.GT(sdk.ZeroInt()) { + amount = simulation.RandomAmount(r, amount) + } + if amount.Equal(sdk.ZeroInt()) { + return "no-operation", nil + } + msg := stake.MsgCreateValidator{ + Description: description, + ValidatorAddr: address, + DelegatorAddr: address, + PubKey: pubkey, + Delegation: sdk.NewIntCoin(denom, amount), + } + require.Nil(t, msg.ValidateBasic(), "expected msg to pass ValidateBasic: %s", msg.GetSignBytes()) + ctx, write := ctx.CacheContext() + result := stake.NewHandler(k)(ctx, msg) + if result.IsOK() { + write() + } + event(fmt.Sprintf("stake/MsgCreateValidator/%v", result.IsOK())) + // require.True(t, result.IsOK(), "expected OK result but instead got %v", result) + action = fmt.Sprintf("TestMsgCreateValidator: ok %v, msg %s", result.IsOK(), msg.GetSignBytes()) + return action, nil + } +} + +// SimulateMsgEditValidator +func SimulateMsgEditValidator(k stake.Keeper) simulation.TestAndRunTx { + return func(t *testing.T, r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, keys []crypto.PrivKey, log string, event func(string)) (action string, err sdk.Error) { + description := stake.Description{ + Moniker: simulation.RandStringOfLength(r, 10), + Identity: simulation.RandStringOfLength(r, 10), + Website: simulation.RandStringOfLength(r, 10), + Details: simulation.RandStringOfLength(r, 10), + } + key := simulation.RandomKey(r, keys) + pubkey := key.PubKey() + address := sdk.AccAddress(pubkey.Address()) + msg := stake.MsgEditValidator{ + Description: description, + ValidatorAddr: address, + } + require.Nil(t, msg.ValidateBasic(), "expected msg to pass ValidateBasic: %s", msg.GetSignBytes()) + ctx, write := ctx.CacheContext() + result := stake.NewHandler(k)(ctx, msg) + if result.IsOK() { + write() + } + event(fmt.Sprintf("stake/MsgEditValidator/%v", result.IsOK())) + action = fmt.Sprintf("TestMsgEditValidator: ok %v, msg %s", result.IsOK(), msg.GetSignBytes()) + return action, nil + } +} + +// SimulateMsgDelegate +func SimulateMsgDelegate(m auth.AccountMapper, k stake.Keeper) simulation.TestAndRunTx { + return func(t *testing.T, r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, keys []crypto.PrivKey, log string, event func(string)) (action string, err sdk.Error) { + denom := k.GetParams(ctx).BondDenom + validatorKey := simulation.RandomKey(r, keys) + validatorAddress := sdk.AccAddress(validatorKey.PubKey().Address()) + delegatorKey := simulation.RandomKey(r, keys) + delegatorAddress := sdk.AccAddress(delegatorKey.PubKey().Address()) + amount := m.GetAccount(ctx, delegatorAddress).GetCoins().AmountOf(denom) + if amount.GT(sdk.ZeroInt()) { + amount = simulation.RandomAmount(r, amount) + } + if amount.Equal(sdk.ZeroInt()) { + return "no-operation", nil + } + msg := stake.MsgDelegate{ + DelegatorAddr: delegatorAddress, + ValidatorAddr: validatorAddress, + Delegation: sdk.NewIntCoin(denom, amount), + } + require.Nil(t, msg.ValidateBasic(), "expected msg to pass ValidateBasic: %s", msg.GetSignBytes()) + ctx, write := ctx.CacheContext() + result := stake.NewHandler(k)(ctx, msg) + if result.IsOK() { + write() + } + event(fmt.Sprintf("stake/MsgDelegate/%v", result.IsOK())) + action = fmt.Sprintf("TestMsgDelegate: ok %v, msg %s", result.IsOK(), msg.GetSignBytes()) + return action, nil + } +} + +// SimulateMsgBeginUnbonding +func SimulateMsgBeginUnbonding(m auth.AccountMapper, k stake.Keeper) simulation.TestAndRunTx { + return func(t *testing.T, r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, keys []crypto.PrivKey, log string, event func(string)) (action string, err sdk.Error) { + denom := k.GetParams(ctx).BondDenom + validatorKey := simulation.RandomKey(r, keys) + validatorAddress := sdk.AccAddress(validatorKey.PubKey().Address()) + delegatorKey := simulation.RandomKey(r, keys) + delegatorAddress := sdk.AccAddress(delegatorKey.PubKey().Address()) + amount := m.GetAccount(ctx, delegatorAddress).GetCoins().AmountOf(denom) + if amount.GT(sdk.ZeroInt()) { + amount = simulation.RandomAmount(r, amount) + } + if amount.Equal(sdk.ZeroInt()) { + return "no-operation", nil + } + msg := stake.MsgBeginUnbonding{ + DelegatorAddr: delegatorAddress, + ValidatorAddr: validatorAddress, + SharesAmount: sdk.NewRatFromInt(amount), + } + require.Nil(t, msg.ValidateBasic(), "expected msg to pass ValidateBasic: %s", msg.GetSignBytes()) + ctx, write := ctx.CacheContext() + result := stake.NewHandler(k)(ctx, msg) + if result.IsOK() { + write() + } + event(fmt.Sprintf("stake/MsgBeginUnbonding/%v", result.IsOK())) + action = fmt.Sprintf("TestMsgBeginUnbonding: ok %v, msg %s", result.IsOK(), msg.GetSignBytes()) + return action, nil + } +} + +// SimulateMsgCompleteUnbonding +func SimulateMsgCompleteUnbonding(k stake.Keeper) simulation.TestAndRunTx { + return func(t *testing.T, r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, keys []crypto.PrivKey, log string, event func(string)) (action string, err sdk.Error) { + validatorKey := simulation.RandomKey(r, keys) + validatorAddress := sdk.AccAddress(validatorKey.PubKey().Address()) + delegatorKey := simulation.RandomKey(r, keys) + delegatorAddress := sdk.AccAddress(delegatorKey.PubKey().Address()) + msg := stake.MsgCompleteUnbonding{ + DelegatorAddr: delegatorAddress, + ValidatorAddr: validatorAddress, + } + require.Nil(t, msg.ValidateBasic(), "expected msg to pass ValidateBasic: %s", msg.GetSignBytes()) + ctx, write := ctx.CacheContext() + result := stake.NewHandler(k)(ctx, msg) + if result.IsOK() { + write() + } + event(fmt.Sprintf("stake/MsgCompleteUnbonding/%v", result.IsOK())) + action = fmt.Sprintf("TestMsgCompleteUnbonding: ok %v, msg %s", result.IsOK(), msg.GetSignBytes()) + return action, nil + } +} + +// SimulateMsgBeginRedelegate +func SimulateMsgBeginRedelegate(m auth.AccountMapper, k stake.Keeper) simulation.TestAndRunTx { + return func(t *testing.T, r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, keys []crypto.PrivKey, log string, event func(string)) (action string, err sdk.Error) { + denom := k.GetParams(ctx).BondDenom + sourceValidatorKey := simulation.RandomKey(r, keys) + sourceValidatorAddress := sdk.AccAddress(sourceValidatorKey.PubKey().Address()) + destValidatorKey := simulation.RandomKey(r, keys) + destValidatorAddress := sdk.AccAddress(destValidatorKey.PubKey().Address()) + delegatorKey := simulation.RandomKey(r, keys) + delegatorAddress := sdk.AccAddress(delegatorKey.PubKey().Address()) + // TODO + amount := m.GetAccount(ctx, delegatorAddress).GetCoins().AmountOf(denom) + if amount.GT(sdk.ZeroInt()) { + amount = simulation.RandomAmount(r, amount) + } + if amount.Equal(sdk.ZeroInt()) { + return "no-operation", nil + } + msg := stake.MsgBeginRedelegate{ + DelegatorAddr: delegatorAddress, + ValidatorSrcAddr: sourceValidatorAddress, + ValidatorDstAddr: destValidatorAddress, + SharesAmount: sdk.NewRatFromInt(amount), + } + require.Nil(t, msg.ValidateBasic(), "expected msg to pass ValidateBasic: %s", msg.GetSignBytes()) + ctx, write := ctx.CacheContext() + result := stake.NewHandler(k)(ctx, msg) + if result.IsOK() { + write() + } + event(fmt.Sprintf("stake/MsgBeginRedelegate/%v", result.IsOK())) + action = fmt.Sprintf("TestMsgBeginRedelegate: %s", msg.GetSignBytes()) + return action, nil + } +} + +// SimulateMsgCompleteRedelegate +func SimulateMsgCompleteRedelegate(k stake.Keeper) simulation.TestAndRunTx { + return func(t *testing.T, r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, keys []crypto.PrivKey, log string, event func(string)) (action string, err sdk.Error) { + validatorSrcKey := simulation.RandomKey(r, keys) + validatorSrcAddress := sdk.AccAddress(validatorSrcKey.PubKey().Address()) + validatorDstKey := simulation.RandomKey(r, keys) + validatorDstAddress := sdk.AccAddress(validatorDstKey.PubKey().Address()) + delegatorKey := simulation.RandomKey(r, keys) + delegatorAddress := sdk.AccAddress(delegatorKey.PubKey().Address()) + msg := stake.MsgCompleteRedelegate{ + DelegatorAddr: delegatorAddress, + ValidatorSrcAddr: validatorSrcAddress, + ValidatorDstAddr: validatorDstAddress, + } + require.Nil(t, msg.ValidateBasic(), "expected msg to pass ValidateBasic: %s", msg.GetSignBytes()) + ctx, write := ctx.CacheContext() + result := stake.NewHandler(k)(ctx, msg) + if result.IsOK() { + write() + } + event(fmt.Sprintf("stake/MsgCompleteRedelegate/%v", result.IsOK())) + action = fmt.Sprintf("TestMsgCompleteRedelegate: ok %v, msg %s", result.IsOK(), msg.GetSignBytes()) + return action, nil + } +} + +// Setup +func Setup(mapp *mock.App, k stake.Keeper) simulation.RandSetup { + return func(r *rand.Rand, privKeys []crypto.PrivKey) { + ctx := mapp.NewContext(false, abci.Header{}) + stake.InitGenesis(ctx, k, stake.DefaultGenesisState()) + params := k.GetParams(ctx) + denom := params.BondDenom + loose := sdk.ZeroInt() + mapp.AccountMapper.IterateAccounts(ctx, func(acc auth.Account) bool { + balance := simulation.RandomAmount(r, sdk.NewInt(1000000)) + acc.SetCoins(acc.GetCoins().Plus(sdk.Coins{sdk.NewIntCoin(denom, balance)})) + mapp.AccountMapper.SetAccount(ctx, acc) + loose = loose.Add(balance) + return false + }) + pool := k.GetPool(ctx) + pool.LooseTokens = pool.LooseTokens.Add(sdk.NewRat(loose.Int64(), 1)) + k.SetPool(ctx, pool) + } +} diff --git a/x/stake/simulation/sim_test.go b/x/stake/simulation/sim_test.go new file mode 100644 index 000000000..391ca1996 --- /dev/null +++ b/x/stake/simulation/sim_test.go @@ -0,0 +1,59 @@ +package simulation + +import ( + "encoding/json" + "math/rand" + "testing" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/x/bank" + "github.com/cosmos/cosmos-sdk/x/mock" + "github.com/cosmos/cosmos-sdk/x/mock/simulation" + "github.com/cosmos/cosmos-sdk/x/stake" + abci "github.com/tendermint/tendermint/abci/types" +) + +// TestStakeWithRandomMessages +func TestStakeWithRandomMessages(t *testing.T) { + mapp := mock.NewApp() + + bank.RegisterWire(mapp.Cdc) + mapper := mapp.AccountMapper + coinKeeper := bank.NewKeeper(mapper) + stakeKey := sdk.NewKVStoreKey("stake") + stakeKeeper := stake.NewKeeper(mapp.Cdc, stakeKey, coinKeeper, stake.DefaultCodespace) + mapp.Router().AddRoute("stake", stake.NewHandler(stakeKeeper)) + mapp.SetEndBlocker(func(ctx sdk.Context, req abci.RequestEndBlock) abci.ResponseEndBlock { + validatorUpdates := stake.EndBlocker(ctx, stakeKeeper) + return abci.ResponseEndBlock{ + ValidatorUpdates: validatorUpdates, + } + }) + + err := mapp.CompleteSetup([]*sdk.KVStoreKey{stakeKey}) + if err != nil { + panic(err) + } + + appStateFn := func(r *rand.Rand, accs []sdk.AccAddress) json.RawMessage { + mock.RandomSetGenesis(r, mapp, accs, []string{"stake"}) + return json.RawMessage("{}") + } + + simulation.Simulate( + t, mapp.BaseApp, appStateFn, + []simulation.TestAndRunTx{ + SimulateMsgCreateValidator(mapper, stakeKeeper), + SimulateMsgEditValidator(stakeKeeper), + SimulateMsgDelegate(mapper, stakeKeeper), + SimulateMsgBeginUnbonding(mapper, stakeKeeper), + SimulateMsgCompleteUnbonding(stakeKeeper), + SimulateMsgBeginRedelegate(mapper, stakeKeeper), + SimulateMsgCompleteRedelegate(stakeKeeper), + }, []simulation.RandSetup{ + Setup(mapp, stakeKeeper), + }, []simulation.Invariant{ + AllInvariants(coinKeeper, stakeKeeper, mapp.AccountMapper), + }, 10, 100, 100, + ) +} diff --git a/x/stake/types/errors.go b/x/stake/types/errors.go index 237340b89..2ef747bae 100644 --- a/x/stake/types/errors.go +++ b/x/stake/types/errors.go @@ -120,6 +120,10 @@ func ErrNoUnbondingDelegation(codespace sdk.CodespaceType) sdk.Error { return sdk.NewError(codespace, CodeInvalidDelegation, "no unbonding delegation found") } +func ErrExistingUnbondingDelegation(codespace sdk.CodespaceType) sdk.Error { + return sdk.NewError(codespace, CodeInvalidDelegation, "existing unbonding delegation found") +} + func ErrNoRedelegation(codespace sdk.CodespaceType) sdk.Error { return sdk.NewError(codespace, CodeInvalidDelegation, "no redelegation found") } diff --git a/x/stake/types/test_utils.go b/x/stake/types/test_utils.go index 104eae3d3..1ab72119e 100644 --- a/x/stake/types/test_utils.go +++ b/x/stake/types/test_utils.go @@ -1,12 +1,7 @@ package types import ( - "fmt" - "math/rand" - "testing" - sdk "github.com/cosmos/cosmos-sdk/types" - "github.com/stretchr/testify/require" "github.com/tendermint/tendermint/crypto" ) @@ -21,171 +16,3 @@ var ( emptyAddr sdk.AccAddress emptyPubkey crypto.PubKey ) - -// Operation reflects any operation that transforms staking state. It takes in -// a RNG instance, pool, validator and returns an updated pool, updated -// validator, delta tokens, and descriptive message. -type Operation func(r *rand.Rand, pool Pool, c Validator) (Pool, Validator, sdk.Rat, string) - -// OpBondOrUnbond implements an operation that bonds or unbonds a validator -// depending on current status. -// nolint: unparam -// TODO split up into multiple operations -func OpBondOrUnbond(r *rand.Rand, pool Pool, validator Validator) (Pool, Validator, sdk.Rat, string) { - var ( - msg string - newStatus sdk.BondStatus - ) - - if validator.Status == sdk.Bonded { - msg = fmt.Sprintf("sdk.Unbonded previously bonded validator %#v", validator) - newStatus = sdk.Unbonded - - } else if validator.Status == sdk.Unbonded { - msg = fmt.Sprintf("sdk.Bonded previously bonded validator %#v", validator) - newStatus = sdk.Bonded - } - - validator, pool = validator.UpdateStatus(pool, newStatus) - return pool, validator, sdk.ZeroRat(), msg -} - -// OpAddTokens implements an operation that adds a random number of tokens to a -// validator. -func OpAddTokens(r *rand.Rand, pool Pool, validator Validator) (Pool, Validator, sdk.Rat, string) { - msg := fmt.Sprintf("validator %#v", validator) - - tokens := int64(r.Int31n(1000)) - validator, pool, _ = validator.AddTokensFromDel(pool, tokens) - msg = fmt.Sprintf("Added %d tokens to %s", tokens, msg) - - // Tokens are removed so for accounting must be negative - return pool, validator, sdk.NewRat(-1 * tokens), msg -} - -// OpRemoveShares implements an operation that removes a random number of -// delegatorshares from a validator. -func OpRemoveShares(r *rand.Rand, pool Pool, validator Validator) (Pool, Validator, sdk.Rat, string) { - var shares sdk.Rat - for { - shares = sdk.NewRat(int64(r.Int31n(1000))) - if shares.LT(validator.DelegatorShares) { - break - } - } - - msg := fmt.Sprintf("Removed %v shares from validator %#v", shares, validator) - - validator, pool, tokens := validator.RemoveDelShares(pool, shares) - return pool, validator, tokens, msg -} - -// RandomOperation returns a random staking operation. -func RandomOperation(r *rand.Rand) Operation { - operations := []Operation{ - OpBondOrUnbond, - OpAddTokens, - OpRemoveShares, - } - r.Shuffle(len(operations), func(i, j int) { - operations[i], operations[j] = operations[j], operations[i] - }) - - return operations[0] -} - -// AssertInvariants ensures invariants that should always be true are true. -// nolint: unparam -func AssertInvariants(t *testing.T, msg string, - pOrig Pool, cOrig []Validator, pMod Pool, vMods []Validator) { - - // total tokens conserved - require.True(t, - pOrig.LooseTokens.Add(pOrig.BondedTokens).Equal( - pMod.LooseTokens.Add(pMod.BondedTokens)), - "Tokens not conserved - msg: %v\n, pOrig.BondedTokens: %v, pOrig.LooseTokens: %v, pMod.BondedTokens: %v, pMod.LooseTokens: %v", - msg, - pOrig.BondedTokens, pOrig.LooseTokens, - pMod.BondedTokens, pMod.LooseTokens) - - // Nonnegative bonded tokens - require.False(t, pMod.BondedTokens.LT(sdk.ZeroRat()), - "Negative bonded shares - msg: %v\npOrig: %v\npMod: %v\n", - msg, pOrig, pMod) - - // Nonnegative loose tokens - require.False(t, pMod.LooseTokens.LT(sdk.ZeroRat()), - "Negative unbonded shares - msg: %v\npOrig: %v\npMod: %v\n", - msg, pOrig, pMod) - - for _, vMod := range vMods { - // Nonnegative ex rate - require.False(t, vMod.DelegatorShareExRate().LT(sdk.ZeroRat()), - "Applying operation \"%s\" resulted in negative validator.DelegatorShareExRate(): %v (validator.Owner: %s)", - msg, - vMod.DelegatorShareExRate(), - vMod.Owner, - ) - - // Nonnegative poolShares - require.False(t, vMod.BondedTokens().LT(sdk.ZeroRat()), - "Applying operation \"%s\" resulted in negative validator.BondedTokens(): %#v", - msg, - vMod, - ) - - // Nonnegative delShares - require.False(t, vMod.DelegatorShares.LT(sdk.ZeroRat()), - "Applying operation \"%s\" resulted in negative validator.DelegatorShares: %#v", - msg, - vMod, - ) - } -} - -// TODO: refactor this random setup - -// randomValidator generates a random validator. -// nolint: unparam -func randomValidator(r *rand.Rand, i int) Validator { - - tokens := sdk.NewRat(int64(r.Int31n(10000))) - delShares := sdk.NewRat(int64(r.Int31n(10000))) - - // TODO add more options here - status := sdk.Bonded - if r.Float64() > float64(0.5) { - status = sdk.Unbonded - } - - validator := NewValidator(addr1, pk1, Description{}) - validator.Status = status - validator.Tokens = tokens - validator.DelegatorShares = delShares - - return validator -} - -// RandomSetup generates a random staking state. -func RandomSetup(r *rand.Rand, numValidators int) (Pool, []Validator) { - pool := InitialPool() - pool.LooseTokens = sdk.NewRat(100000) - - validators := make([]Validator, numValidators) - for i := 0; i < numValidators; i++ { - validator := randomValidator(r, i) - - switch validator.Status { - case sdk.Bonded: - pool.BondedTokens = pool.BondedTokens.Add(validator.Tokens) - case sdk.Unbonded, sdk.Unbonding: - pool.LooseTokens = pool.LooseTokens.Add(validator.Tokens) - default: - panic("improper use of RandomSetup") - } - - validators[i] = validator - } - - return pool, validators -} diff --git a/x/stake/types/validator.go b/x/stake/types/validator.go index f177c123d..9c70d69da 100644 --- a/x/stake/types/validator.go +++ b/x/stake/types/validator.go @@ -434,5 +434,6 @@ func (v Validator) GetStatus() sdk.BondStatus { return v.Status } func (v Validator) GetOwner() sdk.AccAddress { return v.Owner } func (v Validator) GetPubKey() crypto.PubKey { return v.PubKey } func (v Validator) GetPower() sdk.Rat { return v.BondedTokens() } +func (v Validator) GetTokens() sdk.Rat { return v.Tokens } func (v Validator) GetDelegatorShares() sdk.Rat { return v.DelegatorShares } func (v Validator) GetBondHeight() int64 { return v.BondHeight } diff --git a/x/stake/types/validator_test.go b/x/stake/types/validator_test.go index 8d97cbce7..5e0252015 100644 --- a/x/stake/types/validator_test.go +++ b/x/stake/types/validator_test.go @@ -2,7 +2,6 @@ package types import ( "fmt" - "math/rand" "testing" sdk "github.com/cosmos/cosmos-sdk/types" @@ -234,67 +233,6 @@ func TestPossibleOverflow(t *testing.T) { msg, newValidator.DelegatorShareExRate()) } -// run random operations in a random order on a random single-validator state, assert invariants hold -func TestSingleValidatorIntegrationInvariants(t *testing.T) { - r := rand.New(rand.NewSource(41)) - - for i := 0; i < 10; i++ { - poolOrig, validatorsOrig := RandomSetup(r, 1) - require.Equal(t, 1, len(validatorsOrig)) - - // sanity check - AssertInvariants(t, "no operation", - poolOrig, validatorsOrig, - poolOrig, validatorsOrig) - - for j := 0; j < 5; j++ { - poolMod, validatorMod, _, msg := RandomOperation(r)(r, poolOrig, validatorsOrig[0]) - - validatorsMod := make([]Validator, len(validatorsOrig)) - copy(validatorsMod[:], validatorsOrig[:]) - require.Equal(t, 1, len(validatorsOrig), "j %v", j) - require.Equal(t, 1, len(validatorsMod), "j %v", j) - validatorsMod[0] = validatorMod - - AssertInvariants(t, msg, - poolOrig, validatorsOrig, - poolMod, validatorsMod) - - poolOrig = poolMod - validatorsOrig = validatorsMod - } - } -} - -// run random operations in a random order on a random multi-validator state, assert invariants hold -func TestMultiValidatorIntegrationInvariants(t *testing.T) { - r := rand.New(rand.NewSource(42)) - - for i := 0; i < 10; i++ { - poolOrig, validatorsOrig := RandomSetup(r, 100) - - AssertInvariants(t, "no operation", - poolOrig, validatorsOrig, - poolOrig, validatorsOrig) - - for j := 0; j < 5; j++ { - index := int(r.Int31n(int32(len(validatorsOrig)))) - poolMod, validatorMod, _, msg := RandomOperation(r)(r, poolOrig, validatorsOrig[index]) - validatorsMod := make([]Validator, len(validatorsOrig)) - copy(validatorsMod[:], validatorsOrig[:]) - validatorsMod[index] = validatorMod - - AssertInvariants(t, msg, - poolOrig, validatorsOrig, - poolMod, validatorsMod) - - poolOrig = poolMod - validatorsOrig = validatorsMod - - } - } -} - func TestHumanReadableString(t *testing.T) { validator := NewValidator(addr1, pk1, Description{})