From 5c06e56c24fa42034551d129ef348eb9b2bf745d Mon Sep 17 00:00:00 2001 From: Jae Kwon Date: Wed, 20 Dec 2017 17:34:51 -0800 Subject: [PATCH] WIP: Implementing App w/ tests --- app/{base.go => app.go} | 41 +++++++----- app/app_test.go | 140 ++++++++++++++++++++++++++++++++++++++++ app/val_test.go | 88 ------------------------- types/handler.go | 2 +- types/result.go | 8 +-- 5 files changed, 167 insertions(+), 112 deletions(-) rename app/{base.go => app.go} (90%) create mode 100644 app/app_test.go delete mode 100644 app/val_test.go diff --git a/app/base.go b/app/app.go similarity index 90% rename from app/base.go rename to app/app.go index df46c53be8..75867f7915 100644 --- a/app/base.go +++ b/app/app.go @@ -36,7 +36,7 @@ type App struct { handler sdk.Handler // Cached validator changes from DeliverTx - valSetDiff []abci.Validator + valUpdates []abci.Validator } var _ abci.Application = &App{} @@ -57,14 +57,21 @@ func (app *App) SetHandler(handler Handler) { } func (app *App) LoadLatestVersion() error { - curVersion := app.store.NextVersion() - app. + store := app.store + store.LoadLastVersion() + return app.initFromStore() } func (app *App) LoadVersion(version int64) error { + store := app.store + store.LoadVersion(version) + return app.initFromStore() +} + +// Initializes the remaining logic from app.store. +func (app *App) initFromStore() error { store := app.store lastCommitID := store.LastCommitID() - curVersion := store.NextVersion() main := store.GetKVStore("main") header := (*abci.Header)(nil) storeCheck := store.CacheMultiStore() @@ -74,12 +81,7 @@ func (app *App) LoadVersion(version int64) error { return errors.New("App expects MultiStore with 'main' KVStore") } - // Basic sanity check. - if curVersion != lastCommitID.Version+1 { - return errors.New("NextVersion != LastCommitID.Version+1") - } - - // If we've committed before, we expect store(main)/. + // If we've committed before, we expect main://. if !lastCommitID.IsZero() { headerBytes, ok := main.Get(mainKeyHeader) if !ok { @@ -90,11 +92,18 @@ func (app *App) LoadVersion(version int64) error { if err != nil { return errors.Wrap(err, "Failed to parse Header") } - if header.Height != curVersion-1 { - errStr := fmt.Sprintf("Expected header.Height %v but got %v", version, headerHeight) + if header.Height != lastCommitID.Version { + errStr := fmt.Sprintf("Expected main://%s.Height %v but got %v", mainKeyHeader, version, headerHeight) return errors.New(errStr) } } + + // Set App state. + app.header = header + app.storeCheck = app.store.CacheMultiStore() + app.valUpdates = nil + + return nil } //---------------------------------------- @@ -112,10 +121,8 @@ func (app *App) DeliverTx(txBytes []byte) abci.ResponseDeliverTx { var result = app.handler(ctx, app.store, tx) // After-handler hooks. - // TODO move to app.afterHandler(...). if result.Code == abci.CodeType_OK { - // XXX No longer "diff", we need to replace old entries. - app.ValSetDiff = append(app.ValSetDiff, result.ValSetDiff) + app.valUpdates = append(app.valUpdates, result.ValUpdate) } else { // Even though the Code is not OK, there will be some side effects, // like those caused by fee deductions or sequence incrementations. @@ -247,8 +254,8 @@ func (app *App) BeginBlock(req abci.RequestBeginBlock) { // Returns a list of all validator changes made in this block func (app *App) EndBlock(height uint64) (res abci.ResponseEndBlock) { // XXX Update to res.Updates. - res.Diffs = app.valSetDiff - app.valSetDiff = nil + res.Diffs = app.valUpdates + app.valUpdates = nil return } diff --git a/app/app_test.go b/app/app_test.go new file mode 100644 index 0000000000..3ac5503578 --- /dev/null +++ b/app/app_test.go @@ -0,0 +1,140 @@ +package app + +import ( + "bytes" + "encoding/json" + "fmt" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + sdk "github.com/cosmos/cosmos-sdk" + abci "github.com/tendermint/abci/types" + "github.com/tendermint/go-crypto" + cmn "github.com/tendermint/tmlibs/common" +) + +func TestBasic(t *testing.T) { + + // A mock transaction to update a validator's voting power. + type testTx struct { + Addr []byte + NewPower int64 + } + + // Create app. + app := sdk.NewApp(t.Name()) + app.SetStore(mockMultiStore()) + app.SetHandler(func(ctx Context, store MultiStore, tx Tx) Result { + + // This could be a decorator. + fromJSON(ctx.TxBytes(), &tx) + + fmt.Println(">>", tx) + }) + + // Load latest state, which should be empty. + err := app.LoadLatestVersion() + assert.Nil(t, err) + assert.Equal(t, app.NextVersion(), 1) + + // Create the validators + var numVals = 3 + var valSet = make([]*abci.Validator, numVals) + for i := 0; i < numVals; i++ { + valSet[i] = makeVal(secret(i)) + } + + // Initialize the chain + app.InitChain(abci.RequestInitChain{ + Validators: valset, + }) + + // Simulate the start of a block. + app.BeginBlock(abci.RequestBeginBlock{}) + + // Add 1 to each validator's voting power. + for i, val := range valSet { + tx := testTx{ + Addr: makePubKey(secret(i)).Address(), + NewPower: val.Power + 1, + } + txBytes := toJSON(tx) + res := app.DeliverTx(txBytes) + require.True(res.IsOK(), "%#v", res) + } + + // Simulate the end of a block. + // Get the summary of validator updates. + res := app.EndBlock(app.height) + valUpdates := res.ValidatorUpdates + + // Assert that validator updates are correct. + for _, val := range valSet { + + // Find matching update and splice it out. + for j := 0; j < len(valUpdates); { + assert.NotEqual(len(valUpdates.PubKey), 0) + + // Matched. + if bytes.Equal(valUpdate.PubKey, val.PubKey) { + assert.Equal(valUpdate.NewPower, val.Power+1) + if j < len(valUpdates)-1 { + // Splice it out. + valUpdates = append(valUpdates[:j], valUpdates[j+1:]...) + } + break + } + + // Not matched. + j += 1 + } + } + assert.Equal(t, len(valUpdates), 0, "Some validator updates were unexpected") +} + +//---------------------------------------- + +func randPower() int64 { + return cmn.RandInt64() +} + +func makeVal(secret string) *abci.Validator { + return &abci.Validator{ + PubKey: makePubKey(string).Bytes(), + Power: randPower(), + } +} + +func makePubKey(secret string) crypto.PubKey { + return makePrivKey(secret).PubKey() +} + +func makePrivKey(secret string) crypto.PrivKey { + return crypto.GenPrivKeyEd25519FromSecret([]byte(id)) +} + +func secret(index int) []byte { + return []byte(fmt.Sprintf("secret%d", index)) +} + +func copyVal(val *abci.Validator) *abci.Validator { + val2 := *val + return &val2 +} + +func toJSON(o interface{}) []byte { + bytes, err := json.Marshal(o) + if err != nil { + panic(err) + } + return bytes +} + +func fromJSON(bytes []byte, ptr interface{}) { + err := json.Unmarshal(bytes, ptr) + if err != nil { + panic(err) + } +} diff --git a/app/val_test.go b/app/val_test.go deleted file mode 100644 index c333273f25..0000000000 --- a/app/val_test.go +++ /dev/null @@ -1,88 +0,0 @@ -package app - -import ( - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - abci "github.com/tendermint/abci/types" - wire "github.com/tendermint/go-wire" - cmn "github.com/tendermint/tmlibs/common" - "github.com/tendermint/tmlibs/log" - - "github.com/cosmos/cosmos-sdk/modules/base" -) - -//----------------------------------- -// Test cases start here - -func randPower() uint64 { - return uint64(cmn.RandInt()%50 + 60) -} - -func makeVal() *abci.Validator { - return &abci.Validator{ - PubKey: cmn.RandBytes(10), - Power: randPower(), - } -} - -// withNewPower returns a copy of the validator with a different power -func withNewPower(val *abci.Validator) *abci.Validator { - res := *val - res.Power = randPower() - return &res -} - -func TestEndBlock(t *testing.T) { - assert, require := assert.New(t), require.New(t) - - logger := log.NewNopLogger() - handler := base.ValSetHandler{} - store, err := MockStoreApp("vals", logger) - require.Nil(err, "%+v", err) - app := NewBaseApp(store, handler, nil) - - val1 := makeVal() - val2 := makeVal() - val3 := makeVal() - val1a := withNewPower(val1) - val2a := withNewPower(val2) - - cases := [...]struct { - changes [][]*abci.Validator - expected []*abci.Validator - }{ - // Nothing in, nothing out, no crash - 0: {}, - // One in, one out, no problem - 1: { - changes: [][]*abci.Validator{{val1}}, - expected: []*abci.Validator{val1}, - }, - // Combine a few ones - 2: { - changes: [][]*abci.Validator{{val1}, {val2, val3}}, - expected: []*abci.Validator{val1, val2, val3}, - }, - // Make sure changes all to one validators are squished into one diff - 3: { - changes: [][]*abci.Validator{{val1}, {val2, val1a}, {val2a, val3}}, - expected: []*abci.Validator{val1a, val2a, val3}, - }, - } - - for i, tc := range cases { - app.BeginBlock(abci.RequestBeginBlock{}) - for _, c := range tc.changes { - tx := base.ValChangeTx{c}.Wrap() - txBytes := wire.BinaryBytes(tx) - res := app.DeliverTx(txBytes) - require.True(res.IsOK(), "%#v", res) - } - diff := app.EndBlock(app.height) - // TODO: don't care about order here... - assert.Equal(tc.expected, diff.Diffs, "%d", i) - } -} diff --git a/types/handler.go b/types/handler.go index 58a2e6f081..1a4ea32a13 100644 --- a/types/handler.go +++ b/types/handler.go @@ -6,4 +6,4 @@ import ( // Handler handles both ABCI DeliverTx and CheckTx requests. // Iff ABCI.CheckTx, ctx.IsCheckTx() returns true. -type Handler func(ctx Context, ms store.MultiStore, tx Tx) Result +type Handler func(ctx Context, store store.MultiStore, tx Tx) Result diff --git a/types/result.go b/types/result.go index 2ce066183c..b961879790 100644 --- a/types/result.go +++ b/types/result.go @@ -1,14 +1,10 @@ package types import ( + cmn "github.com/tendemrint/tmlibs/common" abci "github.com/tendermint/abci/types" ) -type KVPair struct { - Key []byte - Value []byte -} - // Result is the union of ResponseDeliverTx and ResponseCheckTx. type Result struct { @@ -35,5 +31,5 @@ type Result struct { ValSetDiff []abci.Validator // Tags are used for transaction indexing and pubsub. - Tags []KVPair + Tags []cmn.KVPair }