diff --git a/app/app.go b/app/app.go deleted file mode 100644 index e20f97f2c7..0000000000 --- a/app/app.go +++ /dev/null @@ -1,240 +0,0 @@ -package app - -import ( - "bytes" - "fmt" - "strings" - - abci "github.com/tendermint/abci/types" - cmn "github.com/tendermint/tmlibs/common" - "github.com/tendermint/tmlibs/log" - - sdk "github.com/cosmos/cosmos-sdk" - "github.com/cosmos/cosmos-sdk/errors" - "github.com/cosmos/cosmos-sdk/stack" - sm "github.com/cosmos/cosmos-sdk/state" - "github.com/cosmos/cosmos-sdk/version" -) - -//nolint -const ( - ModuleNameBase = "base" - ChainKey = "chain_id" -) - -// Basecoin - The ABCI application -type Basecoin struct { - info *sm.ChainState - state *Store - - handler sdk.Handler - tick Ticker - - pending []*abci.Validator - height uint64 - logger log.Logger -} - -// Ticker - tick function -type Ticker func(sm.SimpleDB) ([]*abci.Validator, error) - -var _ abci.Application = &Basecoin{} - -// NewBasecoin - create a new instance of the basecoin application -func NewBasecoin(handler sdk.Handler, store *Store, logger log.Logger) *Basecoin { - return &Basecoin{ - handler: handler, - info: sm.NewChainState(), - state: store, - logger: logger, - } -} - -// NewBasecoinTick - create a new instance of the basecoin application with tick functionality -func NewBasecoinTick(handler sdk.Handler, store *Store, logger log.Logger, tick Ticker) *Basecoin { - return &Basecoin{ - handler: handler, - info: sm.NewChainState(), - state: store, - logger: logger, - tick: tick, - } -} - -// GetChainID returns the currently stored chain -func (app *Basecoin) GetChainID() string { - return app.info.GetChainID(app.state.Committed()) -} - -// GetState is back... please kill me -func (app *Basecoin) GetState() sm.SimpleDB { - return app.state.Append() -} - -// Info - ABCI -func (app *Basecoin) Info(req abci.RequestInfo) abci.ResponseInfo { - resp := app.state.Info() - app.logger.Debug("Info", - "height", resp.LastBlockHeight, - "hash", fmt.Sprintf("%X", resp.LastBlockAppHash)) - app.height = resp.LastBlockHeight - return abci.ResponseInfo{ - Data: fmt.Sprintf("Basecoin v%v", version.Version), - LastBlockHeight: resp.LastBlockHeight, - LastBlockAppHash: resp.LastBlockAppHash, - } -} - -// InitState - used to setup state (was SetOption) -// to be used by InitChain later -func (app *Basecoin) InitState(key string, value string) string { - module, key := splitKey(key) - state := app.state.Append() - - if module == ModuleNameBase { - if key == ChainKey { - app.info.SetChainID(state, value) - return "Success" - } - return fmt.Sprintf("Error: unknown base option: %s", key) - } - - log, err := app.handler.InitState(app.logger, state, module, key, value) - if err == nil { - return log - } - return "Error: " + err.Error() -} - -// SetOption - ABCI -func (app *Basecoin) SetOption(key string, value string) string { - return "Not Implemented" -} - -// DeliverTx - ABCI -func (app *Basecoin) DeliverTx(txBytes []byte) abci.Result { - tx, err := sdk.LoadTx(txBytes) - if err != nil { - return errors.Result(err) - } - - ctx := stack.NewContext( - app.GetChainID(), - app.height, - app.logger.With("call", "delivertx"), - ) - res, err := app.handler.DeliverTx(ctx, app.state.Append(), tx) - - if err != nil { - return errors.Result(err) - } - app.addValChange(res.Diff) - return sdk.ToABCI(res) -} - -// CheckTx - ABCI -func (app *Basecoin) CheckTx(txBytes []byte) abci.Result { - tx, err := sdk.LoadTx(txBytes) - if err != nil { - return errors.Result(err) - } - - ctx := stack.NewContext( - app.GetChainID(), - app.height, - app.logger.With("call", "checktx"), - ) - res, err := app.handler.CheckTx(ctx, app.state.Check(), tx) - - if err != nil { - return errors.Result(err) - } - return sdk.ToABCI(res) -} - -// Query - ABCI -func (app *Basecoin) Query(reqQuery abci.RequestQuery) (resQuery abci.ResponseQuery) { - if len(reqQuery.Data) == 0 { - resQuery.Log = "Query cannot be zero length" - resQuery.Code = abci.CodeType_EncodingError - return - } - - return app.state.Query(reqQuery) -} - -// Commit - ABCI -func (app *Basecoin) Commit() (res abci.Result) { - // Commit state - res = app.state.Commit() - if res.IsErr() { - cmn.PanicSanity("Error getting hash: " + res.Error()) - } - return res -} - -// InitChain - ABCI -func (app *Basecoin) InitChain(req abci.RequestInitChain) { - // for _, plugin := range app.plugins.GetList() { - // plugin.InitChain(app.state, validators) - // } -} - -// BeginBlock - ABCI -func (app *Basecoin) BeginBlock(req abci.RequestBeginBlock) { - app.height++ - - // for _, plugin := range app.plugins.GetList() { - // plugin.BeginBlock(app.state, hash, header) - // } - - if app.tick != nil { - diff, err := app.tick(app.state.Append()) - if err != nil { - panic(err) - } - app.addValChange(diff) - } -} - -// EndBlock - ABCI -// Returns a list of all validator changes made in this block -func (app *Basecoin) EndBlock(height uint64) (res abci.ResponseEndBlock) { - // TODO: cleanup in case a validator exists multiple times in the list - res.Diffs = app.pending - app.pending = nil - return -} - -func (app *Basecoin) addValChange(diffs []*abci.Validator) { - for _, d := range diffs { - idx := pubKeyIndex(d, app.pending) - if idx >= 0 { - app.pending[idx] = d - } else { - app.pending = append(app.pending, d) - } - } -} - -// return index of list with validator of same PubKey, or -1 if no match -func pubKeyIndex(val *abci.Validator, list []*abci.Validator) int { - for i, v := range list { - if bytes.Equal(val.PubKey, v.PubKey) { - return i - } - } - return -1 -} - -//TODO move split key to tmlibs? - -// Splits the string at the first '/'. -// if there are none, assign default module ("base"). -func splitKey(key string) (string, string) { - if strings.Contains(key, "/") { - keyParts := strings.SplitN(key, "/", 2) - return keyParts[0], keyParts[1] - } - return ModuleNameBase, key -} diff --git a/app/app_test.go b/app/app_test.go index 1cf9834595..c5e4880229 100644 --- a/app/app_test.go +++ b/app/app_test.go @@ -55,7 +55,7 @@ func DefaultHandler(feeDenom string) sdk.Handler { type appTest struct { t *testing.T chainID string - app *Basecoin + app *BaseApp acctIn *coin.AccountWithKey acctOut *coin.AccountWithKey } @@ -100,8 +100,8 @@ func (at *appTest) feeTx(coins coin.Coins, toll coin.Coin, sequence uint32) sdk. // set the account on the app through InitState func (at *appTest) initAccount(acct *coin.AccountWithKey) { - res := at.app.InitState("coin/account", acct.MakeOption()) - require.EqualValues(at.t, res, "Success") + err := at.app.InitState("coin", "account", acct.MakeOption()) + require.Nil(at.t, err, "%+v", err) } // reset the in and out accs to be one account each with 7mycoin @@ -112,17 +112,13 @@ func (at *appTest) reset() { // Note: switch logger if you want to get more info logger := log.TestingLogger() // logger := log.NewTracingLogger(log.NewTMLogger(os.Stdout)) - store, err := NewStore("", 0, logger.With("module", "store")) + + store, err := NewStoreApp("app-test", "", 0, logger) require.Nil(at.t, err, "%+v", err) + at.app = NewBaseApp(store, DefaultHandler("mycoin"), nil) - at.app = NewBasecoin( - DefaultHandler("mycoin"), - store, - logger.With("module", "app"), - ) - - res := at.app.InitState("base/chain_id", at.chainID) - require.EqualValues(at.t, res, "Success") + err = at.app.InitState("base", "chain_id", at.chainID) + require.Nil(at.t, err, "%+v", err) at.initAccount(at.acctIn) at.initAccount(at.acctOut) @@ -146,9 +142,9 @@ func getAddr(addr []byte, state state.SimpleDB) (coin.Coins, error) { func (at *appTest) exec(t *testing.T, tx sdk.Tx, checkTx bool) (res abci.Result, diffIn, diffOut coin.Coins) { require := require.New(t) - initBalIn, err := getBalance(at.acctIn.Actor(), at.app.GetState()) + initBalIn, err := getBalance(at.acctIn.Actor(), at.app.Append()) require.Nil(err, "%+v", err) - initBalOut, err := getBalance(at.acctOut.Actor(), at.app.GetState()) + initBalOut, err := getBalance(at.acctOut.Actor(), at.app.Append()) require.Nil(err, "%+v", err) txBytes := wire.BinaryBytes(tx) @@ -158,9 +154,9 @@ func (at *appTest) exec(t *testing.T, tx sdk.Tx, checkTx bool) (res abci.Result, res = at.app.DeliverTx(txBytes) } - endBalIn, err := getBalance(at.acctIn.Actor(), at.app.GetState()) + endBalIn, err := getBalance(at.acctIn.Actor(), at.app.Append()) require.Nil(err, "%+v", err) - endBalOut, err := getBalance(at.acctOut.Actor(), at.app.GetState()) + endBalOut, err := getBalance(at.acctOut.Actor(), at.app.Append()) require.Nil(err, "%+v", err) return res, endBalIn.Minus(initBalIn), endBalOut.Minus(initBalOut) } @@ -172,29 +168,24 @@ func TestInitState(t *testing.T) { require := require.New(t) logger := log.TestingLogger() - store, err := NewStore("", 0, logger.With("module", "store")) + store, err := NewStoreApp("app-test", "", 0, logger) require.Nil(err, "%+v", err) - - app := NewBasecoin( - DefaultHandler("atom"), - store, - logger.With("module", "app"), - ) + app := NewBaseApp(store, DefaultHandler("atom"), nil) //testing ChainID chainID := "testChain" - res := app.InitState("base/chain_id", chainID) + err = app.InitState("base", "chain_id", chainID) + require.Nil(err, "%+v", err) assert.EqualValues(app.GetChainID(), chainID) - assert.EqualValues(res, "Success") // make a nice account... bal := coin.Coins{{"atom", 77}, {"eth", 12}} acct := coin.NewAccountWithKey(bal) - res = app.InitState("coin/account", acct.MakeOption()) - require.EqualValues(res, "Success") + err = app.InitState("coin", "account", acct.MakeOption()) + require.Nil(err, "%+v", err) // make sure it is set correctly, with some balance - coins, err := getBalance(acct.Actor(), app.GetState()) + coins, err := getBalance(acct.Actor(), app.Append()) require.Nil(err) assert.Equal(bal, coins) @@ -218,23 +209,22 @@ func TestInitState(t *testing.T) { } ] }` - res = app.InitState("coin/account", unsortAcc) - require.EqualValues(res, "Success") + err = app.InitState("coin", "account", unsortAcc) + require.Nil(err, "%+v", err) - coins, err = getAddr(unsortAddr, app.GetState()) + coins, err = getAddr(unsortAddr, app.Append()) require.Nil(err) assert.True(coins.IsValid()) assert.Equal(unsortCoins, coins) - res = app.InitState("base/dslfkgjdas", "") - assert.NotEqual(res, "Success") + err = app.InitState("base", "dslfkgjdas", "") + require.NotNil(err) - res = app.InitState("dslfkgjdas", "") - assert.NotEqual(res, "Success") - - res = app.InitState("dslfkgjdas/szfdjzs", "") - assert.NotEqual(res, "Success") + err = app.InitState("", "dslfkgjdas", "") + require.NotNil(err) + err = app.InitState("dslfkgjdas", "szfdjzs", "") + require.NotNil(err) } // Test CheckTx and DeliverTx with insufficient and sufficient balance @@ -301,19 +291,3 @@ func TestQuery(t *testing.T) { }) assert.NotEqual(resQueryPreCommit, resQueryPostCommit, "Query should change before/after commit") } - -func TestSplitKey(t *testing.T) { - assert := assert.New(t) - prefix, suffix := splitKey("foo/bar") - assert.EqualValues("foo", prefix) - assert.EqualValues("bar", suffix) - - prefix, suffix = splitKey("foobar") - assert.EqualValues("base", prefix) - assert.EqualValues("foobar", suffix) - - prefix, suffix = splitKey("some/complex/issue") - assert.EqualValues("some", prefix) - assert.EqualValues("complex/issue", suffix) - -} diff --git a/app/base.go b/app/base.go new file mode 100644 index 0000000000..2ba14fd646 --- /dev/null +++ b/app/base.go @@ -0,0 +1,115 @@ +package app + +import ( + "fmt" + + abci "github.com/tendermint/abci/types" + + sdk "github.com/cosmos/cosmos-sdk" + "github.com/cosmos/cosmos-sdk/errors" + "github.com/cosmos/cosmos-sdk/stack" +) + +// BaseApp - The ABCI application +type BaseApp struct { + *StoreApp + handler sdk.Handler + clock sdk.Ticker +} + +var _ abci.Application = &BaseApp{} + +// NewBaseApp extends a StoreApp with a handler and a ticker, +// which it binds to the proper abci calls +func NewBaseApp(store *StoreApp, handler sdk.Handler, clock sdk.Ticker) *BaseApp { + return &BaseApp{ + StoreApp: store, + handler: handler, + clock: clock, + } +} + +// DeliverTx - ABCI - dispatches to the handler +func (app *BaseApp) DeliverTx(txBytes []byte) abci.Result { + tx, err := sdk.LoadTx(txBytes) + if err != nil { + return errors.Result(err) + } + + ctx := stack.NewContext( + app.GetChainID(), + app.WorkingHeight(), + app.Logger().With("call", "delivertx"), + ) + res, err := app.handler.DeliverTx(ctx, app.Append(), tx) + + if err != nil { + return errors.Result(err) + } + app.AddValChange(res.Diff) + return sdk.ToABCI(res) +} + +// CheckTx - ABCI - dispatches to the handler +func (app *BaseApp) CheckTx(txBytes []byte) abci.Result { + tx, err := sdk.LoadTx(txBytes) + if err != nil { + return errors.Result(err) + } + + ctx := stack.NewContext( + app.GetChainID(), + app.WorkingHeight(), + app.Logger().With("call", "checktx"), + ) + res, err := app.handler.CheckTx(ctx, app.Check(), tx) + + if err != nil { + return errors.Result(err) + } + return sdk.ToABCI(res) +} + +// BeginBlock - ABCI - triggers Tick actions +func (app *BaseApp) BeginBlock(req abci.RequestBeginBlock) { + // execute tick if present + if app.clock != nil { + ctx := stack.NewContext( + app.GetChainID(), + app.WorkingHeight(), + app.Logger().With("call", "tick"), + ) + + diff, err := app.clock.Tick(ctx, app.Append()) + if err != nil { + panic(err) + } + app.AddValChange(diff) + } +} + +// InitState - used to setup state (was SetOption) +// to be used by InitChain later +// +// TODO: rethink this a bit more.... +func (app *BaseApp) InitState(module, key, value string) error { + state := app.Append() + logger := app.Logger().With("module", module, "key", key) + + if module == sdk.ModuleNameBase { + if key == sdk.ChainKey { + app.info.SetChainID(state, value) + return nil + } + logger.Error("Invalid genesis option") + return fmt.Errorf("Unknown base option: %s", key) + } + + log, err := app.handler.InitState(logger, state, module, key, value) + if err != nil { + logger.Error("Invalid genesis option", "err", err) + } else { + logger.Info(log) + } + return err +} diff --git a/app/doc.go b/app/doc.go new file mode 100644 index 0000000000..d880368658 --- /dev/null +++ b/app/doc.go @@ -0,0 +1,19 @@ +/* +Package app contains data structures that provide basic +data storage functionality and act as a bridge between the abci +interface and the internal sdk representations. + +StoreApp handles creating a datastore or loading an existing one +from disk, provides helpers to use in the transaction workflow +(check/deliver/commit), and provides bindings to the ABCI interface +for functionality such as handshaking with tendermint on restart, +querying the data store, and handling begin/end block and commit messages. +It does not handle CheckTx or DeliverTx, or have any logic for modifying +the state, and is quite generic if you don't wish to use the standard Handlers. + +BaseApp embeds StoreApp and extends it for the standard sdk usecase, where +we dispatch all CheckTx/DeliverTx messages to a handler (which may contain +decorators and a router to multiple modules), and supports a Ticker which +is called every BeginBlock. +*/ +package app diff --git a/app/genesis.go b/app/genesis.go deleted file mode 100644 index 81d99fead9..0000000000 --- a/app/genesis.go +++ /dev/null @@ -1,99 +0,0 @@ -package app - -import ( - "encoding/json" - - "github.com/pkg/errors" - - cmn "github.com/tendermint/tmlibs/common" -) - -// LoadGenesis - Load the genesis file into memory -func (app *Basecoin) LoadGenesis(path string) error { - genDoc, err := loadGenesis(path) - if err != nil { - return err - } - - // set chain_id - app.InitState("base/chain_id", genDoc.ChainID) - - // set accounts - for _, acct := range genDoc.AppOptions.Accounts { - _ = app.InitState("coin/account", string(acct)) - } - - // set plugin options - for _, kv := range genDoc.AppOptions.pluginOptions { - _ = app.InitState(kv.Key, kv.Value) - } - - return nil -} - -type keyValue struct { - Key string `json:"key"` - Value string `json:"value"` -} - -// FullGenesisDoc - includes tendermint (in the json, we ignore here) -type FullGenesisDoc struct { - ChainID string `json:"chain_id"` - AppOptions *GenesisDoc `json:"app_options"` -} - -// GenesisDoc - All genesis values -type GenesisDoc struct { - Accounts []json.RawMessage `json:"accounts"` - PluginOptions []json.RawMessage `json:"plugin_options"` - - pluginOptions []keyValue // unmarshaled rawmessages -} - -func loadGenesis(filePath string) (*FullGenesisDoc, error) { - bytes, err := cmn.ReadFile(filePath) - if err != nil { - return nil, errors.Wrap(err, "loading genesis file") - } - - // the basecoin genesis go-wire/data :) - genDoc := new(FullGenesisDoc) - err = json.Unmarshal(bytes, genDoc) - if err != nil { - return nil, errors.Wrap(err, "unmarshaling genesis file") - } - - if genDoc.AppOptions == nil { - genDoc.AppOptions = new(GenesisDoc) - } - - pluginOpts, err := parseGenesisList(genDoc.AppOptions.PluginOptions) - if err != nil { - return nil, err - } - genDoc.AppOptions.pluginOptions = pluginOpts - return genDoc, nil -} - -func parseGenesisList(kvzIn []json.RawMessage) (kvz []keyValue, err error) { - if len(kvzIn)%2 != 0 { - return nil, errors.New("genesis cannot have an odd number of items. Format = [key1, value1, key2, value2, ...]") - } - - for i := 0; i < len(kvzIn); i += 2 { - kv := keyValue{} - rawK := []byte(kvzIn[i]) - err := json.Unmarshal(rawK, &(kv.Key)) - if err != nil { - return nil, errors.Errorf("Non-string key: %s", string(rawK)) - } - // convert value to string if possible (otherwise raw json) - rawV := kvzIn[i+1] - err = json.Unmarshal(rawV, &(kv.Value)) - if err != nil { - kv.Value = string(rawV) - } - kvz = append(kvz, kv) - } - return kvz, nil -} diff --git a/app/genesis_test.go b/app/genesis_test.go index 2b76cd7f95..a519bc9982 100644 --- a/app/genesis_test.go +++ b/app/genesis_test.go @@ -2,59 +2,44 @@ package app import ( "encoding/hex" - "encoding/json" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - cmn "github.com/tendermint/tmlibs/common" "github.com/tendermint/tmlibs/log" + "github.com/cosmos/cosmos-sdk/genesis" "github.com/cosmos/cosmos-sdk/modules/coin" ) const genesisFilepath = "./testdata/genesis.json" const genesisAcctFilepath = "./testdata/genesis2.json" +// 2b is just like 2, but add carl who has inconsistent +// pubkey and address +const genesisBadAcctFilepath = "./testdata/genesis2b.json" + func TestLoadGenesisDoNotFailIfAppOptionsAreMissing(t *testing.T) { logger := log.TestingLogger() - store, err := NewStore("", 0, logger) + store, err := MockStoreApp("genesis", logger) require.Nil(t, err, "%+v", err) - app := NewBasecoin(DefaultHandler("mycoin"), store, logger) - err = app.LoadGenesis("./testdata/genesis3.json") + app := NewBaseApp(store, DefaultHandler("mycoin"), nil) + + err = genesis.Load(app, "./testdata/genesis3.json") require.Nil(t, err, "%+v", err) } -func TestLoadGenesis(t *testing.T) { - assert, require := assert.New(t), require.New(t) +func TestLoadGenesisFailsWithUnknownOptions(t *testing.T) { + require := require.New(t) logger := log.TestingLogger() - store, err := NewStore("", 0, logger) + store, err := MockStoreApp("genesis", logger) require.Nil(err, "%+v", err) - app := NewBasecoin(DefaultHandler("mycoin"), store, logger) - err = app.LoadGenesis(genesisFilepath) - require.Nil(err, "%+v", err) - - // check the chain id - assert.Equal("foo_bar_chain", app.GetChainID()) - - // and check the account info - previously calculated values - addr, _ := hex.DecodeString("eb98e0688217cfdeb70eddf4b33cdcc37fc53197") - - coins, err := getAddr(addr, app.GetState()) - require.Nil(err) - assert.True(coins.IsPositive()) - - // make sure balance is proper - assert.Equal(2, len(coins)) - assert.True(coins.IsValid()) - // note, that we now sort them to be valid - assert.EqualValues(654321, coins[0].Amount) - assert.EqualValues("ETH", coins[0].Denom) - assert.EqualValues(12345, coins[1].Amount) - assert.EqualValues("blank", coins[1].Denom) + app := NewBaseApp(store, DefaultHandler("mycoin"), nil) + err = genesis.Load(app, genesisFilepath) + require.NotNil(err, "%+v", err) } // Fix for issue #89, change the parse format for accounts in genesis.json @@ -62,11 +47,11 @@ func TestLoadGenesisAccountAddress(t *testing.T) { assert, require := assert.New(t), require.New(t) logger := log.TestingLogger() - store, err := NewStore("", 0, logger) + store, err := MockStoreApp("genesis", logger) require.Nil(err, "%+v", err) + app := NewBaseApp(store, DefaultHandler("mycoin"), nil) - app := NewBasecoin(DefaultHandler("mycoin"), store, logger) - err = app.LoadGenesis(genesisAcctFilepath) + err = genesis.Load(app, genesisAcctFilepath) require.Nil(err, "%+v", err) // check the chain id @@ -83,9 +68,6 @@ func TestLoadGenesisAccountAddress(t *testing.T) { {"62035D628DE7543332544AA60D90D3693B6AD51B", true, true, coin.Coins{{"one", 111}}}, // this comes from an address, should be stored proper (bob) {"C471FB670E44D219EE6DF2FC284BE38793ACBCE1", true, false, coin.Coins{{"two", 222}}}, - // this one had a mismatched address and pubkey, should not store under either (carl) - {"1234ABCDD18E8EFE3FFC4B0506BF9BF8E5B0D9E9", false, false, nil}, // this is given addr - {"700BEC5ED18E8EFE3FFC4B0506BF9BF8E5B0D9E9", false, false, nil}, // this is addr of the given pubkey // this comes from a secp256k1 public key, should be stored proper (sam) {"979F080B1DD046C452C2A8A250D18646C6B669D4", true, true, coin.Coins{{"four", 444}}}, } @@ -93,7 +75,7 @@ func TestLoadGenesisAccountAddress(t *testing.T) { for i, tc := range cases { addr, err := hex.DecodeString(tc.addr) require.Nil(err, tc.addr) - coins, err := getAddr(addr, app.GetState()) + coins, err := getAddr(addr, app.Append()) require.Nil(err, "%+v", err) if !tc.exists { assert.True(coins.IsZero(), "%d", i) @@ -105,23 +87,15 @@ func TestLoadGenesisAccountAddress(t *testing.T) { } } -func TestParseGenesisList(t *testing.T) { - assert, require := assert.New(t), require.New(t) +// When you define an account in genesis with address +// and pubkey that don't match +func TestLoadGenesisAccountInconsistentAddress(t *testing.T) { + require := require.New(t) - bytes, err := cmn.ReadFile(genesisFilepath) - require.Nil(err, "loading genesis file %+v", err) - - // the basecoin genesis go-wire/data :) - genDoc := new(FullGenesisDoc) - err = json.Unmarshal(bytes, genDoc) - require.Nil(err, "unmarshaling genesis file %+v", err) - - pluginOpts, err := parseGenesisList(genDoc.AppOptions.PluginOptions) + logger := log.TestingLogger() + store, err := MockStoreApp("genesis", logger) require.Nil(err, "%+v", err) - genDoc.AppOptions.pluginOptions = pluginOpts - - assert.Equal(genDoc.AppOptions.pluginOptions[0].Key, "plugin1/key1") - assert.Equal(genDoc.AppOptions.pluginOptions[1].Key, "plugin1/key2") - assert.Equal(genDoc.AppOptions.pluginOptions[0].Value, "value1") - assert.Equal(genDoc.AppOptions.pluginOptions[1].Value, "value2") + app := NewBaseApp(store, DefaultHandler("mycoin"), nil) + err = genesis.Load(app, genesisBadAcctFilepath) + require.NotNil(err) } diff --git a/app/store.go b/app/store.go index 05843f64ff..2ad14915ab 100644 --- a/app/store.go +++ b/app/store.go @@ -1,132 +1,140 @@ package app import ( + "bytes" "fmt" "path" "path/filepath" "strings" - "github.com/pkg/errors" abci "github.com/tendermint/abci/types" "github.com/tendermint/iavl" cmn "github.com/tendermint/tmlibs/common" dbm "github.com/tendermint/tmlibs/db" "github.com/tendermint/tmlibs/log" - "github.com/cosmos/cosmos-sdk/state" + "github.com/cosmos/cosmos-sdk/errors" + sm "github.com/cosmos/cosmos-sdk/state" ) -// Store contains the merkle tree, and all info to handle abci requests -type Store struct { - state.State +// StoreApp contains a data store and all info needed +// to perform queries and handshakes. +// +// It should be embeded in another struct for CheckTx, +// DeliverTx and initializing state from the genesis. +type StoreApp struct { + // Name is what is returned from info + Name string + + // this is the database state + info *sm.ChainState + state *sm.State + + // cached validator changes from DeliverTx + pending []*abci.Validator + + // height is last committed block, DeliverTx is the next one height uint64 + logger log.Logger } -// MockStore returns an in-memory store only intended for testing -func MockStore() *Store { - res, err := NewStore("", 0, log.NewNopLogger()) +// NewStoreApp creates a data store to handle queries +func NewStoreApp(appName, dbName string, cacheSize int, logger log.Logger) (*StoreApp, error) { + state, err := loadState(dbName, cacheSize) if err != nil { - // should never happen, abort test if it does - panic(err) + return nil, err } - return res + app := &StoreApp{ + Name: appName, + state: state, + height: state.LatestHeight(), + info: sm.NewChainState(), + logger: logger.With("module", "app"), + } + return app, nil } -// NewStore initializes an in-memory iavl.VersionedTree, or attempts to load a -// persistant tree from disk -func NewStore(dbName string, cacheSize int, logger log.Logger) (*Store, error) { - // memory backed case, just for testing - if dbName == "" { - tree := iavl.NewVersionedTree( - 0, - dbm.NewMemDB(), - ) - store := &Store{ - State: state.NewState(tree), - logger: logger, - } - return store, nil - } +// MockStoreApp returns a Store app with no persistence +func MockStoreApp(appName string, logger log.Logger) (*StoreApp, error) { + return NewStoreApp(appName, "", 0, logger) +} - // Expand the path fully - dbPath, err := filepath.Abs(dbName) - if err != nil { - return nil, errors.Wrap(err, "Invalid Database Name") - } +// GetChainID returns the currently stored chain +func (app *StoreApp) GetChainID() string { + return app.info.GetChainID(app.state.Committed()) +} - // Some external calls accidently add a ".db", which is now removed - dbPath = strings.TrimSuffix(dbPath, path.Ext(dbPath)) - - // Split the database name into it's components (dir, name) - dir := path.Dir(dbPath) - name := path.Base(dbPath) - - // Make sure the path exists - empty, _ := cmn.IsDirEmpty(dbPath + ".db") - - // Open database called "dir/name.db", if it doesn't exist it will be created - db := dbm.NewDB(name, dbm.LevelDBBackendStr, dir) - tree := iavl.NewVersionedTree(cacheSize, db) - - if empty { - logger.Info("no existing db, creating new db") - } else { - logger.Info("loading existing db") - if err = tree.Load(); err != nil { - return nil, errors.Wrap(err, "Loading tree") - } - } - - res := &Store{ - State: state.NewState(tree), - logger: logger, - } - res.height = res.State.LatestHeight() - return res, nil +// Logger returns the application base logger +func (app *StoreApp) Logger() log.Logger { + return app.logger } // Hash gets the last hash stored in the database -func (s *Store) Hash() []byte { - return s.State.LatestHash() +func (app *StoreApp) Hash() []byte { + return app.state.LatestHash() } -// Info implements abci.Application. It returns the height, hash and size (in the data). +// Committed returns the committed state, +// also exposing historical queries +// func (app *StoreApp) Committed() *Bonsai { +// return app.state.committed +// } + +// Append returns the working state for DeliverTx +func (app *StoreApp) Append() sm.SimpleDB { + return app.state.Append() +} + +// Check returns the working state for CheckTx +func (app *StoreApp) Check() sm.SimpleDB { + return app.state.Check() +} + +// CommittedHeight gets the last block height committed +// to the db +func (app *StoreApp) CommittedHeight() uint64 { + return app.height +} + +// WorkingHeight gets the current block we are writing +func (app *StoreApp) WorkingHeight() uint64 { + return app.height + 1 +} + +// Info implements abci.Application. It returns the height and hash, +// as well as the abci name and version. +// // The height is the block that holds the transactions, not the apphash itself. -func (s *Store) Info() abci.ResponseInfo { - s.logger.Info("Info synced", - "height", s.height, - "hash", fmt.Sprintf("%X", s.Hash())) +func (app *StoreApp) Info(req abci.RequestInfo) abci.ResponseInfo { + hash := app.Hash() + + app.logger.Info("Info synced", + "height", app.CommittedHeight(), + "hash", fmt.Sprintf("%X", hash)) + return abci.ResponseInfo{ - Data: cmn.Fmt("size:%v", s.State.Size()), - LastBlockHeight: s.height, - LastBlockAppHash: s.Hash(), + Data: app.Name, + LastBlockHeight: app.CommittedHeight(), + LastBlockAppHash: hash, } } -// Commit implements abci.Application -func (s *Store) Commit() abci.Result { - s.height++ - - hash, err := s.State.Commit(s.height) - if err != nil { - return abci.NewError(abci.CodeType_InternalError, err.Error()) - } - s.logger.Debug("Commit synced", - "height", s.height, - "hash", fmt.Sprintf("%X", hash), - ) - - if s.State.Size() == 0 { - return abci.NewResultOK(nil, "Empty hash for empty tree") - } - return abci.NewResultOK(hash, "") +// SetOption - ABCI +func (app *StoreApp) SetOption(key string, value string) string { + return "Not Implemented" } -// Query implements abci.Application -func (s *Store) Query(reqQuery abci.RequestQuery) (resQuery abci.ResponseQuery) { +// Query - ABCI +func (app *StoreApp) Query(reqQuery abci.RequestQuery) (resQuery abci.ResponseQuery) { + if len(reqQuery.Data) == 0 { + resQuery.Log = "Query cannot be zero length" + resQuery.Code = abci.CodeType_EncodingError + return + } + // set the query response height to current - tree := s.State.Committed() + tree := app.state.Committed() height := reqQuery.Height if height == 0 { @@ -135,10 +143,10 @@ func (s *Store) Query(reqQuery abci.RequestQuery) (resQuery abci.ResponseQuery) // we must retrun most recent, even if apphash // is not yet in the blockchain - // if tree.Tree.VersionExists(s.height - 1) { - // height = s.height - 1 + // if tree.Tree.VersionExists(app.height - 1) { + // height = app.height - 1 // } else { - height = s.height + height = app.CommittedHeight() // } } resQuery.Height = height @@ -166,3 +174,98 @@ func (s *Store) Query(reqQuery abci.RequestQuery) (resQuery abci.ResponseQuery) } return } + +// Commit implements abci.Application +func (app *StoreApp) Commit() (res abci.Result) { + app.height++ + + hash, err := app.state.Commit(app.height) + if err != nil { + // die if we can't commit, not to recover + panic(err) + } + app.logger.Debug("Commit synced", + "height", app.height, + "hash", fmt.Sprintf("%X", hash), + ) + + if app.state.Size() == 0 { + return abci.NewResultOK(nil, "Empty hash for empty tree") + } + return abci.NewResultOK(hash, "") +} + +// InitChain - ABCI +func (app *StoreApp) InitChain(req abci.RequestInitChain) {} + +// BeginBlock - ABCI +func (app *StoreApp) BeginBlock(req abci.RequestBeginBlock) {} + +// EndBlock - ABCI +// Returns a list of all validator changes made in this block +func (app *StoreApp) EndBlock(height uint64) (res abci.ResponseEndBlock) { + // TODO: cleanup in case a validator exists multiple times in the list + res.Diffs = app.pending + app.pending = nil + return +} + +// AddValChange is meant to be called by apps on DeliverTx +// results, this is added to the cache for the endblock +// changeset +func (app *StoreApp) AddValChange(diffs []*abci.Validator) { + for _, d := range diffs { + idx := pubKeyIndex(d, app.pending) + if idx >= 0 { + app.pending[idx] = d + } else { + app.pending = append(app.pending, d) + } + } +} + +// return index of list with validator of same PubKey, or -1 if no match +func pubKeyIndex(val *abci.Validator, list []*abci.Validator) int { + for i, v := range list { + if bytes.Equal(val.PubKey, v.PubKey) { + return i + } + } + return -1 +} + +func loadState(dbName string, cacheSize int) (*sm.State, error) { + // memory backed case, just for testing + if dbName == "" { + tree := iavl.NewVersionedTree(0, dbm.NewMemDB()) + return sm.NewState(tree), nil + } + + // Expand the path fully + dbPath, err := filepath.Abs(dbName) + if err != nil { + return nil, errors.ErrInternal("Invalid Database Name") + } + + // Some external calls accidently add a ".db", which is now removed + dbPath = strings.TrimSuffix(dbPath, path.Ext(dbPath)) + + // Split the database name into it's components (dir, name) + dir := path.Dir(dbPath) + name := path.Base(dbPath) + + // Make sure the path exists + empty, _ := cmn.IsDirEmpty(dbPath + ".db") + + // Open database called "dir/name.db", if it doesn't exist it will be created + db := dbm.NewDB(name, dbm.LevelDBBackendStr, dir) + tree := iavl.NewVersionedTree(cacheSize, db) + + if !empty { + if err = tree.Load(); err != nil { + return nil, errors.ErrInternal("Loading tree: " + err.Error()) + } + } + + return sm.NewState(tree), nil +} diff --git a/app/testdata/genesis2.json b/app/testdata/genesis2.json index a880b3c64c..18ec87164f 100644 --- a/app/testdata/genesis2.json +++ b/app/testdata/genesis2.json @@ -22,19 +22,6 @@ "amount": 222 } ] - }, { - "name": "carl", - "address": "1234ABCDD18E8EFE3FFC4B0506BF9BF8E5B0D9E9", - "pub_key": { - "type": "ed25519", - "data": "177C0AC45E86257F0708DC085D592AB22AAEECD1D26381B757F7C96135921858" - }, - "coins": [ - { - "denom": "three", - "amount": 333 - } - ] }, { "name": "sam", "pub_key": { diff --git a/app/testdata/genesis2b.json b/app/testdata/genesis2b.json new file mode 100644 index 0000000000..a880b3c64c --- /dev/null +++ b/app/testdata/genesis2b.json @@ -0,0 +1,52 @@ +{ + "chain_id": "addr_accounts_chain", + "app_options": { + "accounts": [{ + "name": "alice", + "pub_key": { + "type": "ed25519", + "data": "DBD9A46C45868F0A37C92B53113C09B048FBD87B5FBC2F8B199052973B8FAA36" + }, + "coins": [ + { + "denom": "one", + "amount": 111 + } + ] + }, { + "name": "bob", + "address": "C471FB670E44D219EE6DF2FC284BE38793ACBCE1", + "coins": [ + { + "denom": "two", + "amount": 222 + } + ] + }, { + "name": "carl", + "address": "1234ABCDD18E8EFE3FFC4B0506BF9BF8E5B0D9E9", + "pub_key": { + "type": "ed25519", + "data": "177C0AC45E86257F0708DC085D592AB22AAEECD1D26381B757F7C96135921858" + }, + "coins": [ + { + "denom": "three", + "amount": 333 + } + ] + }, { + "name": "sam", + "pub_key": { + "type": "secp256k1", + "data": "02AA8342F63CCCCE6DDB128525BA048CE0B2993DA3B4308746E1F216361A87651E" + }, + "coins": [ + { + "denom": "four", + "amount": 444 + } + ] + }] + } +} diff --git a/app/app_val_test.go b/app/val_test.go similarity index 94% rename from app/app_val_test.go rename to app/val_test.go index 95ea77674a..c333273f25 100644 --- a/app/app_val_test.go +++ b/app/val_test.go @@ -39,9 +39,10 @@ func TestEndBlock(t *testing.T) { assert, require := assert.New(t), require.New(t) logger := log.NewNopLogger() - store := MockStore() handler := base.ValSetHandler{} - app := NewBasecoin(handler, store, logger) + store, err := MockStoreApp("vals", logger) + require.Nil(err, "%+v", err) + app := NewBaseApp(store, handler, nil) val1 := makeVal() val2 := makeVal() diff --git a/benchmarks/app_test.go b/benchmarks/app_test.go index acbf182f78..dc9e544743 100644 --- a/benchmarks/app_test.go +++ b/benchmarks/app_test.go @@ -10,7 +10,7 @@ import ( "github.com/tendermint/tmlibs/log" sdk "github.com/cosmos/cosmos-sdk" - "github.com/cosmos/cosmos-sdk/app" + sdkapp "github.com/cosmos/cosmos-sdk/app" "github.com/cosmos/cosmos-sdk/modules/auth" "github.com/cosmos/cosmos-sdk/modules/base" "github.com/cosmos/cosmos-sdk/modules/coin" @@ -21,7 +21,7 @@ import ( ) type BenchApp struct { - App *app.Basecoin + App *sdkapp.BaseApp Accounts []*coin.AccountWithKey ChainID string } @@ -53,27 +53,20 @@ func NewBenchApp(h sdk.Handler, chainID string, n int, // logger := log.NewFilter(log.NewTMLogger(os.Stdout), log.AllowError()) // logger = log.NewTracingLogger(logger) - // TODO: disk writing - var store *app.Store - var err error - + dbDir, cache := "", 0 if persist { - tmpDir, _ := ioutil.TempDir("", "bc-app-benchmark") - store, err = app.NewStore(tmpDir, 500, logger) - } else { - store, err = app.NewStore("", 0, logger) + dbDir, _ = ioutil.TempDir("", "bc-app-benchmark") + cache = 500 } + + store, err := sdkapp.NewStoreApp("bench", dbDir, cache, logger) if err != nil { panic(err) } + app := sdkapp.NewBaseApp(store, h, nil) - app := app.NewBasecoin( - h, - store, - logger.With("module", "app"), - ) - res := app.InitState("base/chain_id", chainID) - if res != "Success" { + err = app.InitState("base", "chain_id", chainID) + if err != nil { panic("cannot set chain") } @@ -82,8 +75,8 @@ func NewBenchApp(h sdk.Handler, chainID string, n int, accts := make([]*coin.AccountWithKey, n) for i := 0; i < n; i++ { accts[i] = coin.NewAccountWithKey(money) - res := app.InitState("coin/account", accts[i].MakeOption()) - if res != "Success" { + err = app.InitState("coin", "account", accts[i].MakeOption()) + if err != nil { panic("can't set account") } } diff --git a/client/query_test.go b/client/query_test.go index 5d6a5bf5c4..f8031ee838 100644 --- a/client/query_test.go +++ b/client/query_test.go @@ -18,7 +18,7 @@ import ( "github.com/tendermint/tendermint/types" "github.com/tendermint/tmlibs/log" - "github.com/cosmos/cosmos-sdk/app" + sdkapp "github.com/cosmos/cosmos-sdk/app" "github.com/cosmos/cosmos-sdk/modules/eyes" ) @@ -26,11 +26,12 @@ var node *nm.Node func TestMain(m *testing.M) { logger := log.TestingLogger() - store, err := app.NewStore("", 0, logger) + store, err := sdkapp.MockStoreApp("query", logger) if err != nil { panic(err) } - app := app.NewBasecoin(eyes.NewHandler(), store, logger) + app := sdkapp.NewBaseApp(store, eyes.NewHandler(), nil) + node = rpctest.StartTendermint(app) code := m.Run() diff --git a/examples/basecoin/cmd/basecoin/main.go b/examples/basecoin/cmd/basecoin/main.go index 2a96f76bd1..af31e2fe80 100644 --- a/examples/basecoin/cmd/basecoin/main.go +++ b/examples/basecoin/cmd/basecoin/main.go @@ -11,6 +11,7 @@ import ( "github.com/cosmos/cosmos-sdk/modules/auth" "github.com/cosmos/cosmos-sdk/modules/base" "github.com/cosmos/cosmos-sdk/modules/coin" + "github.com/cosmos/cosmos-sdk/modules/eyes" "github.com/cosmos/cosmos-sdk/modules/fee" "github.com/cosmos/cosmos-sdk/modules/ibc" "github.com/cosmos/cosmos-sdk/modules/nonce" @@ -45,6 +46,8 @@ func BuildApp(feeDenom string) sdk.Handler { coin.NewHandler(), stack.WrapHandler(roles.NewHandler()), stack.WrapHandler(ibc.NewHandler()), + // and just for run, add eyes as well + stack.WrapHandler(eyes.NewHandler()), ) } diff --git a/examples/basecoin/tests/cli/init-server.sh b/examples/basecoin/tests/cli/init-server.sh index dfd503df7d..9923540819 100755 --- a/examples/basecoin/tests/cli/init-server.sh +++ b/examples/basecoin/tests/cli/init-server.sh @@ -12,7 +12,7 @@ test01initOption() { GENESIS_FILE=${SERVE_DIR}/genesis.json HEX="deadbeef1234deadbeef1234deadbeef1234aaaa" - ${SERVER_EXE} init ${HEX} --home="$SERVE_DIR" -p=app1/key1/val1 -p='"app2/key2/{""name"": ""joe"", ""age"": ""100""}"' >/dev/null + ${SERVER_EXE} init ${HEX} --home="$SERVE_DIR" -p=eyes/key1/val1 -p='"eyes/key2/{""name"": ""joe"", ""age"": ""100""}"' >/dev/null if ! assertTrue "line=${LINENO}" $?; then return 1; fi OPTION1KEY=$(cat ${GENESIS_FILE} | jq '.app_options.plugin_options[2]') @@ -21,9 +21,9 @@ test01initOption() { OPTION2VAL=$(cat ${GENESIS_FILE} | jq '.app_options.plugin_options[5]') OPTION2VALEXPECTED=$(echo '{"name": "joe", "age": "100"}' | jq '.') - assertEquals "line=${LINENO}" '"app1/key1"' $OPTION1KEY + assertEquals "line=${LINENO}" '"eyes/key1"' $OPTION1KEY assertEquals "line=${LINENO}" '"val1"' $OPTION1VAL - assertEquals "line=${LINENO}" '"app2/key2"' $OPTION2KEY + assertEquals "line=${LINENO}" '"eyes/key2"' $OPTION2KEY assertEquals "line=${LINENO}" "$OPTION2VALEXPECTED" "$OPTION2VAL" } diff --git a/examples/basecoin/tests/cli/rpc.sh b/examples/basecoin/tests/cli/rpc.sh index 3f5a557134..5ab6022d9b 100755 --- a/examples/basecoin/tests/cli/rpc.sh +++ b/examples/basecoin/tests/cli/rpc.sh @@ -66,7 +66,7 @@ test01GetInsecure() { INFO=$(${CLIENT_EXE} rpc info) assertTrue "line=${LINENO}, get info" "$?" DATA=$(echo $INFO | jq .response.data) - assertEquals "line=${LINENO}, basecoin info" '"Basecoin v0.7.1"' "$DATA" + assertEquals "line=${LINENO}, basecoin info" '"basecoin v0.7.1"' "$DATA" } test02GetSecure() { diff --git a/examples/counter/plugins/counter/counter_test.go b/examples/counter/plugins/counter/counter_test.go index 50af3b3d9b..5c1ef254a2 100644 --- a/examples/counter/plugins/counter/counter_test.go +++ b/examples/counter/plugins/counter/counter_test.go @@ -27,22 +27,18 @@ func TestCounterPlugin(t *testing.T) { logger := log.TestingLogger() // logger := log.NewTracingLogger(log.NewTMLogger(os.Stdout)) - store, err := app.NewStore("", 0, logger.With("module", "store")) - require.Nil(err, "%+v", err) - h := NewHandler("gold") - bcApp := app.NewBasecoin( - h, - store, - logger.With("module", "app"), - ) - bcApp.InitState("base/chain_id", chainID) + store, err := app.MockStoreApp("counter", logger) + require.Nil(err, "%+v", err) + bcApp := app.NewBaseApp(store, h, nil) + err = bcApp.InitState("base", "chain_id", chainID) + require.Nil(err, "%+v", err) // Account initialization bal := coin.Coins{{"", 1000}, {"gold", 1000}} acct := coin.NewAccountWithKey(bal) - log := bcApp.InitState("coin/account", acct.MakeOption()) - require.Equal("Success", log) + err = bcApp.InitState("coin", "account", acct.MakeOption()) + require.Nil(err, "%+v", err) // Deliver a CounterTx DeliverCounterTx := func(valid bool, counterFee coin.Coins, sequence uint32) abci.Result { diff --git a/genesis/doc.go b/genesis/doc.go new file mode 100644 index 0000000000..40636fc078 --- /dev/null +++ b/genesis/doc.go @@ -0,0 +1,61 @@ +/* +Package genesis provides some utility functions for parsing +a standard genesis file to initialize your abci application. + +We wish to support using one genesis file to initialize both +tendermint and the application, so this file format is designed +to be embedable in the tendermint genesis.json file. We reuse +the same chain_id field for tendermint, ignore the other fields, +and add a special app_options field that contains information just +for the abci app (and ignored by tendermint). + +The use of this file format for your application is not required by +the sdk and is only used by default in the start command, if you wish +to write your own start command, you can use any other method to +store and parse options for your abci application. The important part is +that the same data is available on every node. + +Example file format: + + { + "chain_id": "foo_bar_chain", + "app_options": { + "accounts": [{ + "address": "C471FB670E44D219EE6DF2FC284BE38793ACBCE1", + "pub_key": { + "type": "ed25519", + "data": "6880DB93598E283A67C4D88FC67A8858AA2DE70F713FE94A5109E29C137100C2" + }, + "coins": [ + { + "denom": "ETH", + "amount": 654321 + } + ] + }], + "plugin_options": [ + "plugin1/key1", "value1", + "profile/set", {"name": "john", age: 37} + ] + } + } + +Note that there are two subfields under app_options. The first one "accounts" +is a special case for the coin module, which is assumed to be used by most +applications. It is simply a list of accounts with an identifier and their +initial balance. The account must be identified by EITHER an address +(20 bytes in hex) or a pubkey (in the go-crypto json format), not both as in +this example. "coins" defines the initial balance of the account. + +Configuration options for every other module should be placed under +"plugin_options" as key value pairs (there must be an even number of items). +The first value must be "/" to define the option to be set. +The second value is parsed as raw json and is the value to pass to the +application. This may be a string, an array, a map or any other valid json +structure that the module can parse. + +Note that we don't use a map for plugin_options, as we will often wish +to have many values for the same key, to run this setup many times, +just as we support setting many accounts. +*/ +package genesis diff --git a/genesis/parse.go b/genesis/parse.go new file mode 100644 index 0000000000..67bc32ef1d --- /dev/null +++ b/genesis/parse.go @@ -0,0 +1,153 @@ +package genesis + +import ( + "encoding/json" + "strings" + + sdk "github.com/cosmos/cosmos-sdk" + "github.com/pkg/errors" + + cmn "github.com/tendermint/tmlibs/common" +) + +// Option just holds module/key/value triples from +// parsing the genesis file +type Option struct { + Module string + Key string + Value string +} + +// InitStater is anything that can handle app options +// from genesis file. Setting the merkle store, config options, +// or anything else +type InitStater interface { + InitState(module, key, value string) error +} + +// Load parses the genesis file and sets the initial +// state based on that +func Load(app InitStater, filePath string) error { + opts, err := GetOptions(filePath) + if err != nil { + return err + } + + // execute all the genesis init options + // abort on any error + for _, opt := range opts { + err = app.InitState(opt.Module, opt.Key, opt.Value) + if err != nil { + return err + } + } + return nil +} + +// GetOptions parses the genesis file in a format +// that can easily be handed into InitStaters +func GetOptions(path string) ([]Option, error) { + genDoc, err := load(path) + if err != nil { + return nil, err + } + + opts := genDoc.AppOptions + cnt := 1 + len(opts.Accounts) + len(opts.pluginOptions) + res := make([]Option, cnt) + + res[0] = Option{sdk.ModuleNameBase, sdk.ChainKey, genDoc.ChainID} + i := 1 + + // set accounts + for _, acct := range opts.Accounts { + res[i] = Option{"coin", "account", string(acct)} + i++ + } + + // set plugin options + for _, kv := range opts.pluginOptions { + module, key := splitKey(kv.Key) + res[i] = Option{module, key, kv.Value} + i++ + } + + return res, nil +} + +type keyValue struct { + Key string `json:"key"` + Value string `json:"value"` +} + +// FullDoc - includes tendermint (in the json, we ignore here) +type FullDoc struct { + ChainID string `json:"chain_id"` + AppOptions *Doc `json:"app_options"` +} + +// Doc - All genesis values +type Doc struct { + Accounts []json.RawMessage `json:"accounts"` + PluginOptions []json.RawMessage `json:"plugin_options"` + + pluginOptions []keyValue // unmarshaled rawmessages +} + +func load(filePath string) (*FullDoc, error) { + bytes, err := cmn.ReadFile(filePath) + if err != nil { + return nil, errors.Wrap(err, "loading genesis file") + } + + // the basecoin genesis go-wire/data :) + genDoc := new(FullDoc) + err = json.Unmarshal(bytes, genDoc) + if err != nil { + return nil, errors.Wrap(err, "unmarshaling genesis file") + } + + if genDoc.AppOptions == nil { + genDoc.AppOptions = new(Doc) + } + + pluginOpts, err := parseList(genDoc.AppOptions.PluginOptions) + if err != nil { + return nil, err + } + genDoc.AppOptions.pluginOptions = pluginOpts + return genDoc, nil +} + +func parseList(kvzIn []json.RawMessage) (kvz []keyValue, err error) { + if len(kvzIn)%2 != 0 { + return nil, errors.New("genesis cannot have an odd number of items. Format = [key1, value1, key2, value2, ...]") + } + + for i := 0; i < len(kvzIn); i += 2 { + kv := keyValue{} + rawK := []byte(kvzIn[i]) + err := json.Unmarshal(rawK, &(kv.Key)) + if err != nil { + return nil, errors.Errorf("Non-string key: %s", string(rawK)) + } + // convert value to string if possible (otherwise raw json) + rawV := kvzIn[i+1] + err = json.Unmarshal(rawV, &(kv.Value)) + if err != nil { + kv.Value = string(rawV) + } + kvz = append(kvz, kv) + } + return kvz, nil +} + +// Splits the string at the first '/'. +// if there are none, assign default module ("base"). +func splitKey(key string) (string, string) { + if strings.Contains(key, "/") { + keyParts := strings.SplitN(key, "/", 2) + return keyParts[0], keyParts[1] + } + return sdk.ModuleNameBase, key +} diff --git a/genesis/parse_test.go b/genesis/parse_test.go new file mode 100644 index 0000000000..822e2630ac --- /dev/null +++ b/genesis/parse_test.go @@ -0,0 +1,78 @@ +package genesis + +import ( + "encoding/json" + "testing" + + sdk "github.com/cosmos/cosmos-sdk" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + cmn "github.com/tendermint/tmlibs/common" +) + +const genesisFilepath = "./testdata/genesis.json" + +func TestParseList(t *testing.T) { + assert, require := assert.New(t), require.New(t) + + bytes, err := cmn.ReadFile(genesisFilepath) + require.Nil(err, "loading genesis file %+v", err) + + // the basecoin genesis go-wire/data :) + genDoc := new(FullDoc) + err = json.Unmarshal(bytes, genDoc) + require.Nil(err, "unmarshaling genesis file %+v", err) + + pluginOpts, err := parseList(genDoc.AppOptions.PluginOptions) + require.Nil(err, "%+v", err) + genDoc.AppOptions.pluginOptions = pluginOpts + + assert.Equal(genDoc.AppOptions.pluginOptions[0].Key, "plugin1/key1") + assert.Equal(genDoc.AppOptions.pluginOptions[1].Key, "plugin1/key2") + assert.Equal(genDoc.AppOptions.pluginOptions[0].Value, "value1") + assert.Equal(genDoc.AppOptions.pluginOptions[1].Value, "value2") +} + +func TestGetOptions(t *testing.T) { + assert, require := assert.New(t), require.New(t) + + opts, err := GetOptions(genesisFilepath) + require.Nil(err, "loading genesis file %+v", err) + + require.Equal(4, len(opts)) + chain := opts[0] + assert.Equal(sdk.ModuleNameBase, chain.Module) + assert.Equal(sdk.ChainKey, chain.Key) + assert.Equal("foo_bar_chain", chain.Value) + + acct := opts[1] + assert.Equal("coin", acct.Module) + assert.Equal("account", acct.Key) + + p1 := opts[2] + assert.Equal("plugin1", p1.Module) + assert.Equal("key1", p1.Key) + assert.Equal("value1", p1.Value) + + p2 := opts[3] + assert.Equal("plugin1", p2.Module) + assert.Equal("key2", p2.Key) + assert.Equal("value2", p2.Value) +} + +func TestSplitKey(t *testing.T) { + assert := assert.New(t) + prefix, suffix := splitKey("foo/bar") + assert.EqualValues("foo", prefix) + assert.EqualValues("bar", suffix) + + prefix, suffix = splitKey("foobar") + assert.EqualValues("base", prefix) + assert.EqualValues("foobar", suffix) + + prefix, suffix = splitKey("some/complex/issue") + assert.EqualValues("some", prefix) + assert.EqualValues("complex/issue", suffix) + +} diff --git a/genesis/testdata/genesis.json b/genesis/testdata/genesis.json new file mode 100644 index 0000000000..ee8879fd28 --- /dev/null +++ b/genesis/testdata/genesis.json @@ -0,0 +1,22 @@ +{ + "chain_id": "foo_bar_chain", + "app_options": { + "accounts": [{ + "pub_key": { + "type": "ed25519", + "data": "6880db93598e283a67c4d88fc67a8858aa2de70f713fe94a5109e29c137100c2" + }, + "coins": [ + { + "denom": "blank", + "amount": 12345 + }, + { + "denom": "ETH", + "amount": 654321 + } + ] + }], + "plugin_options": ["plugin1/key1", "value1", "plugin1/key2", "value2"] + } +} diff --git a/handler.go b/handler.go index 8047fc40af..f7d6638029 100644 --- a/handler.go +++ b/handler.go @@ -8,6 +8,13 @@ import ( "github.com/cosmos/cosmos-sdk/state" ) +const ( + // ModuleNameBase is the module name for internal functionality + ModuleNameBase = "base" + // ChainKey is the option key for setting the chain id + ChainKey = "chain_id" +) + // Handler is anything that processes a transaction type Handler interface { // Checker verifies there are valid fees and estimates work @@ -25,6 +32,18 @@ type Handler interface { // BeginBlock(store state.SimpleDB, hash []byte, header *abci.Header) } +// Ticker can be executed every block +type Ticker interface { + Tick(Context, state.SimpleDB) ([]*abci.Validator, error) +} + +// TickerFunc allows a function to implement the interface +type TickerFunc func(Context, state.SimpleDB) ([]*abci.Validator, error) + +func (t TickerFunc) Tick(ctx Context, store state.SimpleDB) ([]*abci.Validator, error) { + return t(ctx, store) +} + // Named ensures there is a name for the item type Named interface { Name() string diff --git a/modules/eyes/handler.go b/modules/eyes/handler.go index 478019b33d..34a54877e4 100644 --- a/modules/eyes/handler.go +++ b/modules/eyes/handler.go @@ -1,10 +1,12 @@ package eyes import ( + wire "github.com/tendermint/go-wire" + "github.com/tendermint/tmlibs/log" + sdk "github.com/cosmos/cosmos-sdk" "github.com/cosmos/cosmos-sdk/errors" "github.com/cosmos/cosmos-sdk/state" - wire "github.com/tendermint/go-wire" ) const ( @@ -19,7 +21,6 @@ const ( // Handler allows us to set and remove data type Handler struct { - sdk.NopInitState sdk.NopInitValidate } @@ -35,6 +36,16 @@ func (Handler) Name() string { return Name } +// InitState - sets the genesis state +func (h Handler) InitState(l log.Logger, store state.SimpleDB, + module, key, value string) (log string, err error) { + if module != Name { + return "", errors.ErrUnknownModule(module) + } + store.Set([]byte(key), []byte(value)) + return key, nil +} + // CheckTx verifies if the transaction is properly formated func (h Handler) CheckTx(ctx sdk.Context, store state.SimpleDB, tx sdk.Tx) (res sdk.CheckResult, err error) { err = tx.ValidateBasic() diff --git a/server/commands/start.go b/server/commands/start.go index 494660300a..5580ce3036 100644 --- a/server/commands/start.go +++ b/server/commands/start.go @@ -21,6 +21,8 @@ import ( sdk "github.com/cosmos/cosmos-sdk" "github.com/cosmos/cosmos-sdk/app" + "github.com/cosmos/cosmos-sdk/genesis" + "github.com/cosmos/cosmos-sdk/version" ) // StartCmd - command to start running the abci app (and tendermint)! @@ -31,7 +33,7 @@ var StartCmd = &cobra.Command{ } // GetTickStartCmd - initialize a command as the start command with tick -func GetTickStartCmd(tick app.Ticker) *cobra.Command { +func GetTickStartCmd(tick sdk.Ticker) *cobra.Command { startCmd := &cobra.Command{ Use: "start", Short: "Start this full node", @@ -70,42 +72,47 @@ func addStartFlag(startCmd *cobra.Command) { } //returns the start command which uses the tick -func tickStartCmd(tick app.Ticker) func(cmd *cobra.Command, args []string) error { +func tickStartCmd(clock sdk.Ticker) func(cmd *cobra.Command, args []string) error { return func(cmd *cobra.Command, args []string) error { rootDir := viper.GetString(cli.HomeFlag) - store, err := app.NewStore( + cmdName := cmd.Root().Name() + appName := fmt.Sprintf("%s v%v", cmdName, version.Version) + storeApp, err := app.NewStoreApp( + appName, path.Join(rootDir, "data", "merkleeyes.db"), EyesCacheSize, - logger.With("module", "store"), - ) + logger.With("module", "app")) if err != nil { return err } // Create Basecoin app - basecoinApp := app.NewBasecoinTick(Handler, store, logger.With("module", "app"), tick) - return start(rootDir, store, basecoinApp) + basecoinApp := app.NewBaseApp(storeApp, Handler, clock) + return start(rootDir, basecoinApp) } } func startCmd(cmd *cobra.Command, args []string) error { rootDir := viper.GetString(cli.HomeFlag) - store, err := app.NewStore( + cmdName := cmd.Root().Name() + appName := fmt.Sprintf("%s v%v", cmdName, version.Version) + storeApp, err := app.NewStoreApp( + appName, path.Join(rootDir, "data", "merkleeyes.db"), EyesCacheSize, - logger.With("module", "store"), - ) + logger.With("module", "app")) if err != nil { return err } + // Create Basecoin app - basecoinApp := app.NewBasecoin(Handler, store, logger.With("module", "app")) - return start(rootDir, store, basecoinApp) + basecoinApp := app.NewBaseApp(storeApp, Handler, nil) + return start(rootDir, basecoinApp) } -func start(rootDir string, store *app.Store, basecoinApp *app.Basecoin) error { +func start(rootDir string, basecoinApp *app.BaseApp) error { // if chain_id has not been set yet, load the genesis. // else, assume it's been loaded @@ -113,7 +120,7 @@ func start(rootDir string, store *app.Store, basecoinApp *app.Basecoin) error { // If genesis file exists, set key-value options genesisFile := path.Join(rootDir, "genesis.json") if _, err := os.Stat(genesisFile); err == nil { - err := basecoinApp.LoadGenesis(genesisFile) + err = genesis.Load(basecoinApp, genesisFile) if err != nil { return errors.Errorf("Error in LoadGenesis: %v\n", err) } diff --git a/state/merkle.go b/state/merkle.go index 14ad530024..2373d27d3b 100644 --- a/state/merkle.go +++ b/state/merkle.go @@ -5,19 +5,17 @@ import "github.com/tendermint/iavl" // State represents the app states, separating the commited state (for queries) // from the working state (for CheckTx and AppendTx) type State struct { - committed *Bonsai - deliverTx SimpleDB - checkTx SimpleDB - persistent bool + committed *Bonsai + deliverTx SimpleDB + checkTx SimpleDB } -func NewState(tree *iavl.VersionedTree) State { +func NewState(tree *iavl.VersionedTree) *State { base := NewBonsai(tree) - return State{ - committed: base, - deliverTx: base.Checkpoint(), - checkTx: base.Checkpoint(), - persistent: true, + return &State{ + committed: base, + deliverTx: base.Checkpoint(), + checkTx: base.Checkpoint(), } } @@ -45,14 +43,6 @@ func (s State) LatestHash() []byte { return s.committed.Tree.Hash() } -// BatchSet is used for some weird magic in storing the new height -func (s *State) BatchSet(key, value []byte) { - if s.persistent { - // This is in the batch with the Save, but not in the tree - s.committed.Tree.BatchSet(key, value) - } -} - // Commit save persistent nodes to the database and re-copies the trees func (s *State) Commit(version uint64) ([]byte, error) { // commit (if we didn't do hash earlier) @@ -62,15 +52,11 @@ func (s *State) Commit(version uint64) ([]byte, error) { } var hash []byte - if s.persistent { - if s.committed.Tree.Size() > 0 || s.committed.Tree.LatestVersion() > 0 { - hash, err = s.committed.Tree.SaveVersion(version) - if err != nil { - return nil, err - } + if s.committed.Tree.Size() > 0 || s.committed.Tree.LatestVersion() > 0 { + hash, err = s.committed.Tree.SaveVersion(version) + if err != nil { + return nil, err } - } else { - hash = s.committed.Tree.Hash() } s.deliverTx = s.committed.Checkpoint()