From 6d56891a0fffb19de6a2eab32199c9d5daa0d7f5 Mon Sep 17 00:00:00 2001 From: Ethan Frey Date: Tue, 4 Jul 2017 13:43:25 +0200 Subject: [PATCH] Re-implement counter plugin --- cmd/basecoin/commands/start.go | 27 +-- cmd/basecoin/main.go | 3 + docs/guide/counter/cmd/counter/main.go | 4 + docs/guide/counter/plugins/counter/counter.go | 206 ++++++++++++------ .../counter/plugins/counter/counter_test.go | 87 +++----- 5 files changed, 179 insertions(+), 148 deletions(-) diff --git a/cmd/basecoin/commands/start.go b/cmd/basecoin/commands/start.go index aeb49a1c35..791e810a62 100644 --- a/cmd/basecoin/commands/start.go +++ b/cmd/basecoin/commands/start.go @@ -22,8 +22,6 @@ import ( "github.com/tendermint/tendermint/types" "github.com/tendermint/basecoin/app" - "github.com/tendermint/basecoin/modules/coin" - "github.com/tendermint/basecoin/stack" ) var StartCmd = &cobra.Command{ @@ -42,6 +40,12 @@ const ( FlagWithoutTendermint = "without-tendermint" ) +var ( + // use a global to store the handler, so we can set it in main. + // TODO: figure out a cleaner way to register plugins + Handler basecoin.Handler +) + func init() { flags := StartCmd.Flags() flags.String(FlagAddress, "tcp://0.0.0.0:46658", "Listen address") @@ -51,22 +55,6 @@ func init() { tcmd.AddNodeFlags(StartCmd) } -// TODO: setup handler instead of Plugins -func getHandler() basecoin.Handler { - // use the default stack - h := coin.NewHandler() - app := stack.NewDefault().Use(h) - return app - - // register IBC plugn - // basecoinApp.RegisterPlugin(NewIBCPlugin()) - - // register all other plugins - // for _, p := range plugins { - // basecoinApp.RegisterPlugin(p.newPlugin()) - // } -} - func startCmd(cmd *cobra.Command, args []string) error { rootDir := viper.GetString(cli.HomeFlag) meyes := viper.GetString(FlagEyes) @@ -85,8 +73,7 @@ func startCmd(cmd *cobra.Command, args []string) error { } // Create Basecoin app - h := app.DefaultHandler() - basecoinApp := app.NewBasecoin(h, eyesCli, logger.With("module", "app")) + basecoinApp := app.NewBasecoin(Handler, eyesCli, logger.With("module", "app")) // if chain_id has not been set yet, load the genesis. // else, assume it's been loaded diff --git a/cmd/basecoin/main.go b/cmd/basecoin/main.go index 7190a8ee59..51c8ab4917 100644 --- a/cmd/basecoin/main.go +++ b/cmd/basecoin/main.go @@ -3,6 +3,7 @@ package main import ( "os" + "github.com/tendermint/basecoin/app" "github.com/tendermint/basecoin/cmd/basecoin/commands" "github.com/tendermint/tmlibs/cli" ) @@ -10,6 +11,8 @@ import ( func main() { rt := commands.RootCmd + commands.Handler = app.DefaultHandler() + rt.AddCommand( commands.InitCmd, commands.StartCmd, diff --git a/docs/guide/counter/cmd/counter/main.go b/docs/guide/counter/cmd/counter/main.go index c877a51eda..854cd0efe3 100644 --- a/docs/guide/counter/cmd/counter/main.go +++ b/docs/guide/counter/cmd/counter/main.go @@ -7,6 +7,7 @@ import ( "github.com/tendermint/tmlibs/cli" + "github.com/tendermint/basecoin/app" "github.com/tendermint/basecoin/cmd/basecoin/commands" "github.com/tendermint/basecoin/docs/guide/counter/plugins/counter" "github.com/tendermint/basecoin/types" @@ -18,6 +19,9 @@ func main() { Short: "demo plugin for basecoin", } + // TODO: register the counter here + commands.Handler = app.DefaultHandler() + RootCmd.AddCommand( commands.InitCmd, commands.StartCmd, diff --git a/docs/guide/counter/plugins/counter/counter.go b/docs/guide/counter/plugins/counter/counter.go index 79694c4052..71050862e2 100644 --- a/docs/guide/counter/plugins/counter/counter.go +++ b/docs/guide/counter/plugins/counter/counter.go @@ -1,16 +1,31 @@ package counter import ( - "fmt" + rawerr "errors" abci "github.com/tendermint/abci/types" - "github.com/tendermint/basecoin/types" "github.com/tendermint/go-wire" + + "github.com/tendermint/basecoin" + "github.com/tendermint/basecoin/errors" + "github.com/tendermint/basecoin/modules/coin" + "github.com/tendermint/basecoin/types" ) -type CounterPluginState struct { - Counter int - TotalFees types.Coins +// CounterTx +//-------------------------------------------------------------------------------- + +// register the tx type with it's validation logic +// make sure to use the name of the handler as the prefix in the tx type, +// so it gets routed properly +const ( + NameCounter = "cntr" + ByteTx = 0x21 + TypeTx = NameCounter + "/count" +) + +func init() { + basecoin.TxMapper.RegisterImplementation(CounterTx{}, TypeTx, ByteTx) } type CounterTx struct { @@ -18,81 +33,130 @@ type CounterTx struct { Fee types.Coins } +func NewCounterTx(valid bool, fee types.Coins) basecoin.Tx { + return CounterTx{ + Valid: valid, + Fee: fee, + }.Wrap() +} + +func (c CounterTx) Wrap() basecoin.Tx { + return basecoin.Tx{c} +} + +// ValidateBasic just makes sure the Fee is a valid, non-negative value +func (c CounterTx) ValidateBasic() error { + if !c.Fee.IsValid() { + return coin.ErrInvalidCoins() + } + if !c.Fee.IsNonnegative() { + return coin.ErrInvalidCoins() + } + return nil +} + +// Custom errors //-------------------------------------------------------------------------------- -type CounterPlugin struct { +var ( + errInvalidCounter = rawerr.New("Counter Tx marked invalid") +) + +// This is a custom error class +func ErrInvalidCounter() error { + return errors.WithCode(errInvalidCounter, abci.CodeType_BaseInvalidInput) +} +func IsInvalidCounterErr(err error) bool { + return errors.IsSameError(errInvalidCounter, err) } -func (cp *CounterPlugin) Name() string { - return "counter" +// This is just a helper function to return a generic "internal error" +func ErrDecoding() error { + return errors.ErrInternal("Error decoding state") } -func (cp *CounterPlugin) StateKey() []byte { - return []byte(fmt.Sprintf("CounterPlugin.State")) +// CounterHandler +//-------------------------------------------------------------------------------- + +type CounterHandler struct { + basecoin.NopOption } -func New() *CounterPlugin { - return &CounterPlugin{} +var _ basecoin.Handler = CounterHandler{} + +func (_ CounterHandler) Name() string { + return NameCounter } -func (cp *CounterPlugin) SetOption(store types.KVStore, key, value string) (log string) { - return "" -} - -func (cp *CounterPlugin) RunTx(store types.KVStore, ctx types.CallContext, txBytes []byte) (res abci.Result) { - // Decode tx - var tx CounterTx - err := wire.ReadBinaryBytes(txBytes, &tx) - if err != nil { - return abci.ErrBaseEncodingError.AppendLog("Error decoding tx: " + err.Error()).PrependLog("CounterTx Error: ") - } - - // Validate tx - if !tx.Valid { - return abci.ErrInternalError.AppendLog("CounterTx.Valid must be true") - } - if !tx.Fee.IsValid() { - return abci.ErrInternalError.AppendLog("CounterTx.Fee is not sorted or has zero amounts") - } - if !tx.Fee.IsNonnegative() { - return abci.ErrInternalError.AppendLog("CounterTx.Fee must be nonnegative") - } - - // Did the caller provide enough coins? - if !ctx.Coins.IsGTE(tx.Fee) { - return abci.ErrInsufficientFunds.AppendLog("CounterTx.Fee was not provided") - } - - // TODO If there are any funds left over, return funds. - // e.g. !ctx.Coins.Minus(tx.Fee).IsZero() - // ctx.CallerAccount is synced w/ store, so just modify that and store it. - - // Load CounterPluginState - var cpState CounterPluginState - cpStateBytes := store.Get(cp.StateKey()) - if len(cpStateBytes) > 0 { - err = wire.ReadBinaryBytes(cpStateBytes, &cpState) - if err != nil { - return abci.ErrInternalError.AppendLog("Error decoding state: " + err.Error()) - } - } - - // Update CounterPluginState - cpState.Counter += 1 - cpState.TotalFees = cpState.TotalFees.Plus(tx.Fee) - - // Save CounterPluginState - store.Set(cp.StateKey(), wire.BinaryBytes(cpState)) - - return abci.OK -} - -func (cp *CounterPlugin) InitChain(store types.KVStore, vals []*abci.Validator) { -} - -func (cp *CounterPlugin) BeginBlock(store types.KVStore, hash []byte, header *abci.Header) { -} - -func (cp *CounterPlugin) EndBlock(store types.KVStore, height uint64) (res abci.ResponseEndBlock) { +// CheckTx checks if the tx is properly structured +func (h CounterHandler) CheckTx(ctx basecoin.Context, store types.KVStore, tx basecoin.Tx) (res basecoin.Result, err error) { + _, err = checkTx(ctx, tx) return } + +// DeliverTx executes the tx if valid +func (h CounterHandler) DeliverTx(ctx basecoin.Context, store types.KVStore, tx basecoin.Tx) (res basecoin.Result, err error) { + ctr, err := checkTx(ctx, tx) + if err != nil { + return res, err + } + // note that we don't assert this on CheckTx (ValidateBasic), + // as we allow them to be writen to the chain + if !ctr.Valid { + return res, ErrInvalidCounter() + } + + // TODO: handle coin movement.... ugh, need sequence to do this, right? + + // update the counter + state, err := LoadState(store) + if err != nil { + return res, err + } + state.Counter += 1 + state.TotalFees = state.TotalFees.Plus(ctr.Fee) + err = StoreState(store, state) + + return res, err +} + +func checkTx(ctx basecoin.Context, tx basecoin.Tx) (ctr CounterTx, err error) { + ctr, ok := tx.Unwrap().(CounterTx) + if !ok { + return ctr, errors.ErrInvalidFormat(tx) + } + err = ctr.ValidateBasic() + if err != nil { + return ctr, err + } + return ctr, nil +} + +// CounterStore +//-------------------------------------------------------------------------------- + +type CounterPluginState struct { + Counter int + TotalFees types.Coins +} + +func StateKey() []byte { + return []byte(NameCounter + "/state") +} + +func LoadState(store types.KVStore) (state CounterPluginState, err error) { + bytes := store.Get(StateKey()) + if len(bytes) > 0 { + err = wire.ReadBinaryBytes(bytes, &state) + if err != nil { + return state, errors.ErrDecoding() + } + } + return state, nil +} + +func StoreState(store types.KVStore, state CounterPluginState) error { + bytes := wire.BinaryBytes(state) + store.Set(StateKey(), bytes) + return nil +} diff --git a/docs/guide/counter/plugins/counter/counter_test.go b/docs/guide/counter/plugins/counter/counter_test.go index 73f5ffb112..3fa84849a3 100644 --- a/docs/guide/counter/plugins/counter/counter_test.go +++ b/docs/guide/counter/plugins/counter/counter_test.go @@ -11,6 +11,7 @@ import ( "github.com/tendermint/basecoin/app" "github.com/tendermint/basecoin/modules/coin" "github.com/tendermint/basecoin/stack" + "github.com/tendermint/basecoin/txs" "github.com/tendermint/basecoin/types" "github.com/tendermint/go-wire" eyescli "github.com/tendermint/merkleeyes/client" @@ -18,10 +19,15 @@ import ( ) // TODO: actually handle the counter here... -func CounterHandler() basecoin.Handler { +func NewCounterHandler() basecoin.Handler { // use the default stack - h := coin.NewHandler() - return stack.NewDefault().Use(h) + coin := coin.NewHandler() + counter := CounterHandler{} + dispatcher := stack.NewDispatcher( + stack.WrapHandler(coin), + stack.WrapHandler(counter), + ) + return stack.NewDefault().Use(dispatcher) } func TestCounterPlugin(t *testing.T) { @@ -30,8 +36,11 @@ func TestCounterPlugin(t *testing.T) { // Basecoin initialization eyesCli := eyescli.NewLocalClient("", 0) chainID := "test_chain_id" - bcApp := app.NewBasecoin(CounterHandler(), eyesCli, - log.TestingLogger().With("module", "app")) + bcApp := app.NewBasecoin( + NewCounterHandler(), + eyesCli, + log.TestingLogger().With("module", "app"), + ) bcApp.SetOption("base/chain_id", chainID) // t.Log(bcApp.Info()) @@ -43,68 +52,32 @@ func TestCounterPlugin(t *testing.T) { test1Acc.Balance = types.Coins{{"", 1000}, {"gold", 1000}} accOpt, err := json.Marshal(test1Acc) require.Nil(t, err) - bcApp.SetOption("base/account", string(accOpt)) + log := bcApp.SetOption("coin/account", string(accOpt)) + require.Equal(t, "Success", log) // Deliver a CounterTx - DeliverCounterTx := func(gas int64, fee types.Coin, inputCoins types.Coins, inputSequence int, appFee types.Coins) abci.Result { - // Construct an AppTx signature - tx := &types.AppTx{ - Gas: gas, - Fee: fee, - Name: "counter", - Input: types.NewTxInput(test1Acc.PubKey, inputCoins, inputSequence), - Data: wire.BinaryBytes(CounterTx{Valid: true, Fee: appFee}), - } - - // Sign request - signBytes := tx.SignBytes(chainID) - // t.Logf("Sign bytes: %X\n", signBytes) - tx.Input.Signature = test1PrivAcc.Sign(signBytes) - // t.Logf("Signed TX bytes: %X\n", wire.BinaryBytes(struct{ types.Tx }{tx})) - - // Write request - txBytes := wire.BinaryBytes(struct{ types.Tx }{tx}) + DeliverCounterTx := func(valid bool, counterFee types.Coins, inputSequence int) abci.Result { + tx := NewCounterTx(valid, counterFee) + tx = txs.NewChain(chainID, tx) + stx := txs.NewSig(tx) + txs.Sign(stx, test1PrivAcc.PrivKey) + txBytes := wire.BinaryBytes(stx.Wrap()) return bcApp.DeliverTx(txBytes) } - // REF: DeliverCounterTx(gas, fee, inputCoins, inputSequence, appFee) { - // Test a basic send, no fee - res := DeliverCounterTx(0, types.Coin{}, types.Coins{{"", 1}}, 1, types.Coins{}) + res := DeliverCounterTx(true, types.Coins{}, 1) assert.True(res.IsOK(), res.String()) - // Test fee prevented transaction - res = DeliverCounterTx(0, types.Coin{"", 2}, types.Coins{{"", 1}}, 2, types.Coins{}) + // Test an invalid send, no fee + res = DeliverCounterTx(false, types.Coins{}, 1) assert.True(res.IsErr(), res.String()) - // Test input equals fee - res = DeliverCounterTx(0, types.Coin{"", 2}, types.Coins{{"", 2}}, 2, types.Coins{}) + // Test the fee + res = DeliverCounterTx(true, types.Coins{{"gold", 100}}, 2) assert.True(res.IsOK(), res.String()) - // Test more input than fee - res = DeliverCounterTx(0, types.Coin{"", 2}, types.Coins{{"", 3}}, 3, types.Coins{}) - assert.True(res.IsOK(), res.String()) - - // Test input equals fee+appFee - res = DeliverCounterTx(0, types.Coin{"", 1}, types.Coins{{"", 3}, {"gold", 1}}, 4, types.Coins{{"", 2}, {"gold", 1}}) - assert.True(res.IsOK(), res.String()) - - // Test fee+appFee prevented transaction, not enough "" - res = DeliverCounterTx(0, types.Coin{"", 1}, types.Coins{{"", 2}, {"gold", 1}}, 5, types.Coins{{"", 2}, {"gold", 1}}) - assert.True(res.IsErr(), res.String()) - - // Test fee+appFee prevented transaction, not enough "gold" - res = DeliverCounterTx(0, types.Coin{"", 1}, types.Coins{{"", 3}, {"gold", 1}}, 5, types.Coins{{"", 2}, {"gold", 2}}) - assert.True(res.IsErr(), res.String()) - - // Test more input than fee, more "" - res = DeliverCounterTx(0, types.Coin{"", 1}, types.Coins{{"", 4}, {"gold", 1}}, 6, types.Coins{{"", 2}, {"gold", 1}}) - assert.True(res.IsOK(), res.String()) - - // Test more input than fee, more "gold" - res = DeliverCounterTx(0, types.Coin{"", 1}, types.Coins{{"", 3}, {"gold", 2}}, 7, types.Coins{{"", 2}, {"gold", 1}}) - assert.True(res.IsOK(), res.String()) - - // REF: DeliverCounterTx(gas, fee, inputCoins, inputSequence, appFee) {w - + // TODO: Test unsupported fee + // res = DeliverCounterTx(true, types.Coins{{"silver", 100}}, 3) + // assert.True(res.IsErr(), res.String()) }