From 1b4a3d24ff2883adea5a5147291cff140ada10b0 Mon Sep 17 00:00:00 2001 From: Christopher Goes Date: Thu, 22 Mar 2018 18:47:13 +0100 Subject: [PATCH 1/4] Implement basic proof-of-work module & add to basecoin example Module users specify a coin denomination and proof-of-work reward. Blockchain clients can then submit SHA256 Hashcash solutions and receive the reward, with a constantly increasing difficulty. Includes replay protection to prevent the same solution being submitted multiple times, and inclusion of the rewardee in the hash data to prevent others from submitting the same solution once they see it in the tx pool. Reasonably comprehensive testsuite --- examples/basecoin/app/app.go | 5 ++ examples/basecoin/x/pow/commands/tx.go | 74 ++++++++++++++++ examples/basecoin/x/pow/errors.go | 82 +++++++++++++++++ examples/basecoin/x/pow/handler.go | 70 +++++++++++++++ examples/basecoin/x/pow/handler_test.go | 51 +++++++++++ examples/basecoin/x/pow/mapper.go | 51 +++++++++++ examples/basecoin/x/pow/mapper_test.go | 41 +++++++++ examples/basecoin/x/pow/types.go | 75 ++++++++++++++++ examples/basecoin/x/pow/types_test.go | 111 ++++++++++++++++++++++++ 9 files changed, 560 insertions(+) create mode 100644 examples/basecoin/x/pow/commands/tx.go create mode 100644 examples/basecoin/x/pow/errors.go create mode 100644 examples/basecoin/x/pow/handler.go create mode 100644 examples/basecoin/x/pow/handler_test.go create mode 100644 examples/basecoin/x/pow/mapper.go create mode 100644 examples/basecoin/x/pow/mapper_test.go create mode 100644 examples/basecoin/x/pow/types.go create mode 100644 examples/basecoin/x/pow/types_test.go diff --git a/examples/basecoin/app/app.go b/examples/basecoin/app/app.go index 1d31b0edc..d4394e772 100644 --- a/examples/basecoin/app/app.go +++ b/examples/basecoin/app/app.go @@ -19,6 +19,7 @@ import ( "github.com/cosmos/cosmos-sdk/examples/basecoin/types" "github.com/cosmos/cosmos-sdk/examples/basecoin/x/cool" + "github.com/cosmos/cosmos-sdk/examples/basecoin/x/pow" "github.com/cosmos/cosmos-sdk/examples/basecoin/x/sketchy" ) @@ -59,11 +60,13 @@ func NewBasecoinApp(logger log.Logger, db dbm.DB) *BasecoinApp { // add handlers coinKeeper := bank.NewCoinKeeper(app.accountMapper) coolMapper := cool.NewMapper(app.capKeyMainStore) + powMapper := pow.NewMapper(app.capKeyMainStore) ibcMapper := ibc.NewIBCMapper(app.cdc, app.capKeyIBCStore) stakingMapper := staking.NewMapper(app.capKeyStakingStore) app.Router(). AddRoute("bank", bank.NewHandler(coinKeeper)). AddRoute("cool", cool.NewHandler(coinKeeper, coolMapper)). + AddRoute("pow", pow.NewHandler(coinKeeper, powMapper, pow.NewPowConfig("pow", int64(1)))). AddRoute("sketchy", sketchy.NewHandler()). AddRoute("ibc", ibc.NewHandler(ibcMapper, coinKeeper)). AddRoute("staking", staking.NewHandler(stakingMapper, coinKeeper)) @@ -92,6 +95,7 @@ func MakeCodec() *wire.Codec { const msgTypeIBCReceiveMsg = 0x6 const msgTypeBondMsg = 0x7 const msgTypeUnbondMsg = 0x8 + const msgTypeMineMsg = 0x9 var _ = oldwire.RegisterInterface( struct{ sdk.Msg }{}, oldwire.ConcreteType{bank.SendMsg{}, msgTypeSend}, @@ -102,6 +106,7 @@ func MakeCodec() *wire.Codec { oldwire.ConcreteType{ibc.IBCReceiveMsg{}, msgTypeIBCReceiveMsg}, oldwire.ConcreteType{staking.BondMsg{}, msgTypeBondMsg}, oldwire.ConcreteType{staking.UnbondMsg{}, msgTypeUnbondMsg}, + oldwire.ConcreteType{pow.MineMsg{}, msgTypeMineMsg}, ) const accTypeApp = 0x1 diff --git a/examples/basecoin/x/pow/commands/tx.go b/examples/basecoin/x/pow/commands/tx.go new file mode 100644 index 000000000..24db80236 --- /dev/null +++ b/examples/basecoin/x/pow/commands/tx.go @@ -0,0 +1,74 @@ +package commands + +import ( + "fmt" + "strconv" + + "github.com/pkg/errors" + "github.com/spf13/cobra" + "github.com/spf13/viper" + + "github.com/cosmos/cosmos-sdk/client" + "github.com/cosmos/cosmos-sdk/client/builder" + + "github.com/cosmos/cosmos-sdk/examples/basecoin/x/pow" + "github.com/cosmos/cosmos-sdk/wire" +) + +func MineCmd(cdc *wire.Codec) *cobra.Command { + return &cobra.Command{ + Use: "mine [difficulty] [count] [nonce] [solution]", + Short: "Mine some coins with proof-of-work!", + RunE: func(cmd *cobra.Command, args []string) error { + if len(args) != 4 { + return errors.New("You must provide a difficulty, a solution, and a nonce (in that order)") + } + + // get from address and parse arguments + + from, err := builder.GetFromAddress() + if err != nil { + return err + } + + difficulty, err := strconv.ParseUint(args[0], 0, 64) + if err != nil { + return err + } + + count, err := strconv.ParseUint(args[1], 0, 64) + if err != nil { + return err + } + + nonce, err := strconv.ParseUint(args[2], 0, 64) + if err != nil { + return err + } + + solution := []byte(args[3]) + + msg := pow.NewMineMsg(from, difficulty, count, nonce, solution) + + // get account name + name := viper.GetString(client.FlagName) + + // get password + buf := client.BufferStdin() + prompt := fmt.Sprintf("Password to sign with '%s':", name) + passphrase, err := client.GetPassword(prompt, buf) + if err != nil { + return err + } + + // build and sign the transaction, then broadcast to Tendermint + res, err := builder.SignBuildBroadcast(name, passphrase, msg, cdc) + if err != nil { + return err + } + + fmt.Printf("Committed at block %d. Hash: %s\n", res.Height, res.Hash.String()) + return nil + }, + } +} diff --git a/examples/basecoin/x/pow/errors.go b/examples/basecoin/x/pow/errors.go new file mode 100644 index 000000000..b44eb93d6 --- /dev/null +++ b/examples/basecoin/x/pow/errors.go @@ -0,0 +1,82 @@ +package pow + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" +) + +type CodeType = sdk.CodeType + +const ( + CodeInvalidDifficulty CodeType = 201 + CodeNonexistentDifficulty CodeType = 202 + CodeNonexistentReward CodeType = 203 + CodeNonexistentCount CodeType = 204 + CodeInvalidProof CodeType = 205 + CodeNotBelowTarget CodeType = 206 + CodeInvalidCount CodeType = 207 + CodeUnknownRequest CodeType = sdk.CodeUnknownRequest +) + +func codeToDefaultMsg(code CodeType) string { + switch code { + case CodeInvalidDifficulty: + return "Insuffient difficulty" + case CodeNonexistentDifficulty: + return "Nonexistent difficulty" + case CodeNonexistentReward: + return "Nonexistent reward" + case CodeNonexistentCount: + return "Nonexistent count" + case CodeInvalidProof: + return "Invalid proof" + case CodeNotBelowTarget: + return "Not below target" + case CodeInvalidCount: + return "Invalid count" + case CodeUnknownRequest: + return "Unknown request" + default: + return sdk.CodeToDefaultMsg(code) + } +} + +func ErrInvalidDifficulty(msg string) sdk.Error { + return newError(CodeInvalidDifficulty, msg) +} + +func ErrNonexistentDifficulty() sdk.Error { + return newError(CodeNonexistentDifficulty, "") +} + +func ErrNonexistentReward() sdk.Error { + return newError(CodeNonexistentReward, "") +} + +func ErrNonexistentCount() sdk.Error { + return newError(CodeNonexistentCount, "") +} + +func ErrInvalidProof(msg string) sdk.Error { + return newError(CodeInvalidProof, msg) +} + +func ErrNotBelowTarget(msg string) sdk.Error { + return newError(CodeNotBelowTarget, msg) +} + +func ErrInvalidCount(msg string) sdk.Error { + return newError(CodeInvalidCount, msg) +} + +func msgOrDefaultMsg(msg string, code CodeType) string { + if msg != "" { + return msg + } else { + return codeToDefaultMsg(code) + } +} + +func newError(code CodeType, msg string) sdk.Error { + msg = msgOrDefaultMsg(msg, code) + return sdk.NewError(code, msg) +} diff --git a/examples/basecoin/x/pow/handler.go b/examples/basecoin/x/pow/handler.go new file mode 100644 index 000000000..1d0b42252 --- /dev/null +++ b/examples/basecoin/x/pow/handler.go @@ -0,0 +1,70 @@ +package pow + +import ( + "fmt" + "reflect" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/x/bank" +) + +// module users must specify coin denomination and reward (constant) per PoW solution +type PowConfig struct { + Denomination string + Reward int64 +} + +func NewPowConfig(denomination string, reward int64) PowConfig { + return PowConfig{denomination, reward} +} + +func NewHandler(ck bank.CoinKeeper, pm Mapper, config PowConfig) sdk.Handler { + return func(ctx sdk.Context, msg sdk.Msg) sdk.Result { + switch msg := msg.(type) { + case MineMsg: + return handleMineMsg(ctx, ck, pm, config, msg) + default: + errMsg := "Unrecognized pow Msg type: " + reflect.TypeOf(msg).Name() + return sdk.ErrUnknownRequest(errMsg).Result() + } + } +} + +func handleMineMsg(ctx sdk.Context, ck bank.CoinKeeper, pm Mapper, config PowConfig, msg MineMsg) sdk.Result { + + // precondition: msg has passed ValidateBasic + + // will this function always be applied atomically? + + lastDifficulty, err := pm.GetLastDifficulty(ctx) + if err != nil { + return ErrNonexistentDifficulty().Result() + } + + newDifficulty := lastDifficulty + 1 + + lastCount, err := pm.GetLastCount(ctx) + if err != nil { + return ErrNonexistentCount().Result() + } + + newCount := lastCount + 1 + + if msg.Count != newCount { + return ErrInvalidCount(fmt.Sprintf("invalid count: was %d, should have been %d", msg.Count, newCount)).Result() + } + + if msg.Difficulty != newDifficulty { + return ErrInvalidDifficulty(fmt.Sprintf("invalid difficulty: was %d, should have been %d", msg.Difficulty, newDifficulty)).Result() + } + + _, ckErr := ck.AddCoins(ctx, msg.Sender, []sdk.Coin{sdk.Coin{config.Denomination, config.Reward}}) + if ckErr != nil { + return ckErr.Result() + } + + pm.SetLastDifficulty(ctx, newDifficulty) + pm.SetLastCount(ctx, newCount) + + return sdk.Result{} +} diff --git a/examples/basecoin/x/pow/handler_test.go b/examples/basecoin/x/pow/handler_test.go new file mode 100644 index 000000000..8fa859244 --- /dev/null +++ b/examples/basecoin/x/pow/handler_test.go @@ -0,0 +1,51 @@ +package pow + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + abci "github.com/tendermint/abci/types" + + sdk "github.com/cosmos/cosmos-sdk/types" + auth "github.com/cosmos/cosmos-sdk/x/auth" + bank "github.com/cosmos/cosmos-sdk/x/bank" +) + +func TestPowHandler(t *testing.T) { + ms, capKey := setupMultiStore() + + am := auth.NewAccountMapper(capKey, &auth.BaseAccount{}) + ctx := sdk.NewContext(ms, abci.Header{}, false, nil) + mapper := NewMapper(capKey) + config := NewPowConfig("pow", int64(1)) + ck := bank.NewCoinKeeper(am) + + handler := NewHandler(ck, mapper, config) + + addr := sdk.Address([]byte("sender")) + count := uint64(1) + difficulty := uint64(2) + nonce, proof := mine(addr, count, difficulty) + msg := NewMineMsg(addr, difficulty, count, nonce, proof) + + result := handler(ctx, msg) + assert.Equal(t, result, sdk.Result{}) + + newDiff, err := mapper.GetLastDifficulty(ctx) + assert.Nil(t, err) + assert.Equal(t, newDiff, uint64(2)) + + newCount, err := mapper.GetLastCount(ctx) + assert.Nil(t, err) + assert.Equal(t, newCount, uint64(1)) + + // todo assert correct coin change, awaiting https://github.com/cosmos/cosmos-sdk/pull/691 + + difficulty = uint64(4) + nonce, proof = mine(addr, count, difficulty) + msg = NewMineMsg(addr, difficulty, count, nonce, proof) + + result = handler(ctx, msg) + assert.NotEqual(t, result, sdk.Result{}) +} diff --git a/examples/basecoin/x/pow/mapper.go b/examples/basecoin/x/pow/mapper.go new file mode 100644 index 000000000..4dd226bf7 --- /dev/null +++ b/examples/basecoin/x/pow/mapper.go @@ -0,0 +1,51 @@ +package pow + +import ( + "strconv" + + sdk "github.com/cosmos/cosmos-sdk/types" +) + +type Mapper struct { + key sdk.StoreKey +} + +func NewMapper(key sdk.StoreKey) Mapper { + return Mapper{key} +} + +var lastDifficultyKey = []byte("lastDifficultyKey") + +func (pm Mapper) GetLastDifficulty(ctx sdk.Context) (uint64, error) { + store := ctx.KVStore(pm.key) + stored := store.Get(lastDifficultyKey) + if stored == nil { + // return the default difficulty of 1 if not set + // this works OK for this module, but a way to initalize the store (a "genesis block" for the module) might be better in general + return uint64(1), nil + } else { + return strconv.ParseUint(string(stored), 0, 64) + } +} + +func (pm Mapper) SetLastDifficulty(ctx sdk.Context, diff uint64) { + store := ctx.KVStore(pm.key) + store.Set(lastDifficultyKey, []byte(strconv.FormatUint(diff, 16))) +} + +var countKey = []byte("count") + +func (pm Mapper) GetLastCount(ctx sdk.Context) (uint64, error) { + store := ctx.KVStore(pm.key) + stored := store.Get(countKey) + if stored == nil { + return uint64(0), nil + } else { + return strconv.ParseUint(string(stored), 0, 64) + } +} + +func (pm Mapper) SetLastCount(ctx sdk.Context, count uint64) { + store := ctx.KVStore(pm.key) + store.Set(countKey, []byte(strconv.FormatUint(count, 16))) +} diff --git a/examples/basecoin/x/pow/mapper_test.go b/examples/basecoin/x/pow/mapper_test.go new file mode 100644 index 000000000..1a190b502 --- /dev/null +++ b/examples/basecoin/x/pow/mapper_test.go @@ -0,0 +1,41 @@ +package pow + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + abci "github.com/tendermint/abci/types" + dbm "github.com/tendermint/tmlibs/db" + + "github.com/cosmos/cosmos-sdk/store" + sdk "github.com/cosmos/cosmos-sdk/types" +) + +// possibly share this kind of setup functionality between module testsuites? +func setupMultiStore() (sdk.MultiStore, *sdk.KVStoreKey) { + db := dbm.NewMemDB() + capKey := sdk.NewKVStoreKey("capkey") + ms := store.NewCommitMultiStore(db) + ms.MountStoreWithDB(capKey, sdk.StoreTypeIAVL, db) + ms.LoadLatestVersion() + + return ms, capKey +} + +func TestPowMapperGetSet(t *testing.T) { + ms, capKey := setupMultiStore() + + ctx := sdk.NewContext(ms, abci.Header{}, false, nil) + mapper := NewMapper(capKey) + + res, err := mapper.GetLastDifficulty(ctx) + assert.Nil(t, err) + assert.Equal(t, res, uint64(1)) + + mapper.SetLastDifficulty(ctx, 2) + + res, err = mapper.GetLastDifficulty(ctx) + assert.Nil(t, err) + assert.Equal(t, res, uint64(2)) +} diff --git a/examples/basecoin/x/pow/types.go b/examples/basecoin/x/pow/types.go new file mode 100644 index 000000000..70c456d84 --- /dev/null +++ b/examples/basecoin/x/pow/types.go @@ -0,0 +1,75 @@ +package pow + +import ( + "bytes" + "encoding/hex" + "encoding/json" + "fmt" + "math" + "strconv" + + crypto "github.com/tendermint/go-crypto" + + sdk "github.com/cosmos/cosmos-sdk/types" +) + +// MineMsg - mine some coins with PoW +type MineMsg struct { + Sender sdk.Address `json:"sender"` + Difficulty uint64 `json:"difficulty"` + Count uint64 `json:"count"` + Nonce uint64 `json:"nonce"` + Proof []byte `json:"proof"` +} + +// NewMineMsg - construct mine message +func NewMineMsg(sender sdk.Address, difficulty uint64, count uint64, nonce uint64, proof []byte) MineMsg { + return MineMsg{sender, difficulty, count, nonce, proof} +} + +func (msg MineMsg) Type() string { return "mine" } +func (msg MineMsg) Get(key interface{}) (value interface{}) { return nil } +func (msg MineMsg) GetSigners() []sdk.Address { return []sdk.Address{msg.Sender} } +func (msg MineMsg) String() string { + return fmt.Sprintf("MineMsg{Sender: %v, Difficulty: %d, Count: %d, Nonce: %d, Proof: %s}", msg.Sender, msg.Difficulty, msg.Count, msg.Nonce, msg.Proof) +} + +func (msg MineMsg) ValidateBasic() sdk.Error { + // check hash + var data []byte + // hash must include sender, so no other users can race the tx + data = append(data, []byte(msg.Sender)...) + countBytes := strconv.FormatUint(msg.Count, 16) + // hash must include count so proof-of-work solutions cannot be replayed + data = append(data, countBytes...) + nonceBytes := strconv.FormatUint(msg.Nonce, 16) + data = append(data, nonceBytes...) + hash := crypto.Sha256(data) + hashHex := make([]byte, hex.EncodedLen(len(hash))) + hex.Encode(hashHex, hash) + hashHex = hashHex[:16] + if !bytes.Equal(hashHex, msg.Proof) { + return ErrInvalidProof(fmt.Sprintf("hashHex: %s, proof: %s", hashHex, msg.Proof)) + } + + // check proof below difficulty + // difficulty is linear - 1 = all hashes, 2 = half of hashes, 3 = third of hashes, etc + target := math.MaxUint64 / msg.Difficulty + hashUint, err := strconv.ParseUint(string(msg.Proof), 16, 64) + if err != nil { + return ErrInvalidProof(fmt.Sprintf("proof: %s", msg.Proof)) + } + if hashUint >= target { + return ErrNotBelowTarget(fmt.Sprintf("hashuint: %d, target: %d", hashUint, target)) + } + + return nil +} + +func (msg MineMsg) GetSignBytes() []byte { + b, err := json.Marshal(msg) + if err != nil { + panic(err) + } + return b +} diff --git a/examples/basecoin/x/pow/types_test.go b/examples/basecoin/x/pow/types_test.go new file mode 100644 index 000000000..04360c3fd --- /dev/null +++ b/examples/basecoin/x/pow/types_test.go @@ -0,0 +1,111 @@ +package pow + +import ( + "encoding/hex" + "fmt" + "math" + "strconv" + "testing" + + "github.com/stretchr/testify/assert" + + sdk "github.com/cosmos/cosmos-sdk/types" + crypto "github.com/tendermint/go-crypto" +) + +func TestNewMineMsg(t *testing.T) { + addr := sdk.Address([]byte("sender")) + msg := MineMsg{addr, 0, 0, 0, []byte("")} + equiv := NewMineMsg(addr, 0, 0, 0, []byte("")) + assert.Equal(t, msg, equiv, "%s != %s", msg, equiv) +} + +func TestMineMsgType(t *testing.T) { + addr := sdk.Address([]byte("sender")) + msg := MineMsg{addr, 0, 0, 0, []byte("")} + assert.Equal(t, msg.Type(), "mine") +} + +func hash(sender sdk.Address, count uint64, nonce uint64) []byte { + var bytes []byte + bytes = append(bytes, []byte(sender)...) + countBytes := strconv.FormatUint(count, 16) + bytes = append(bytes, countBytes...) + nonceBytes := strconv.FormatUint(nonce, 16) + bytes = append(bytes, nonceBytes...) + hash := crypto.Sha256(bytes) + // uint64, so we just use the first 8 bytes of the hash + // this limits the range of possible difficulty values (as compared to uint256), but fine for proof-of-concept + ret := make([]byte, hex.EncodedLen(len(hash))) + hex.Encode(ret, hash) + return ret[:16] +} + +func mine(sender sdk.Address, count uint64, difficulty uint64) (uint64, []byte) { + target := math.MaxUint64 / difficulty + for nonce := uint64(0); ; nonce++ { + hash := hash(sender, count, nonce) + hashuint, err := strconv.ParseUint(string(hash), 16, 64) + if err != nil { + panic(err) + } + if hashuint < target { + return nonce, hash + } + } +} + +func TestMineMsgValidation(t *testing.T) { + addr := sdk.Address([]byte("sender")) + otherAddr := sdk.Address([]byte("another")) + count := uint64(0) + for difficulty := uint64(1); difficulty < 1000; difficulty += 100 { + count += 1 + nonce, proof := mine(addr, count, difficulty) + msg := MineMsg{addr, difficulty, count, nonce, proof} + err := msg.ValidateBasic() + assert.Nil(t, err, "error with difficulty %d - %+v", difficulty, err) + + msg.Count += 1 + err = msg.ValidateBasic() + assert.NotNil(t, err, "count was wrong, should have thrown error with msg %s", msg) + + msg.Count -= 1 + msg.Nonce += 1 + err = msg.ValidateBasic() + assert.NotNil(t, err, "nonce was wrong, should have thrown error with msg %s", msg) + + msg.Nonce -= 1 + msg.Sender = otherAddr + err = msg.ValidateBasic() + assert.NotNil(t, err, "sender was wrong, should have thrown error with msg %s", msg) + } +} + +func TestMineMsgString(t *testing.T) { + addr := sdk.Address([]byte("sender")) + msg := MineMsg{addr, 0, 0, 0, []byte("abc")} + res := msg.String() + assert.Equal(t, res, "MineMsg{Sender: 73656E646572, Difficulty: 0, Count: 0, Nonce: 0, Proof: abc}") +} + +func TestMineMsgGet(t *testing.T) { + addr := sdk.Address([]byte("sender")) + msg := MineMsg{addr, 0, 0, 0, []byte("")} + res := msg.Get(nil) + assert.Nil(t, res) +} + +func TestMineMsgGetSignBytes(t *testing.T) { + addr := sdk.Address([]byte("sender")) + msg := MineMsg{addr, 1, 1, 1, []byte("abc")} + res := msg.GetSignBytes() + assert.Equal(t, string(res), `{"sender":"73656E646572","difficulty":1,"count":1,"nonce":1,"proof":"YWJj"}`) +} + +func TestMineMsgGetSigners(t *testing.T) { + addr := sdk.Address([]byte("sender")) + msg := MineMsg{addr, 1, 1, 1, []byte("abc")} + res := msg.GetSigners() + assert.Equal(t, fmt.Sprintf("%v", res), "[73656E646572]") +} From 47b87672086fea01f58e141203bfd7c78ed96d2f Mon Sep 17 00:00:00 2001 From: Christopher Goes Date: Sat, 24 Mar 2018 21:41:26 +0100 Subject: [PATCH 2/4] Refactor to keeper-based module (#689) --- examples/basecoin/app/app.go | 4 +- examples/basecoin/x/pow/handler.go | 62 +++-------- examples/basecoin/x/pow/handler_test.go | 8 +- examples/basecoin/x/pow/keeper.go | 103 ++++++++++++++++++ .../x/pow/{mapper_test.go => keeper_test.go} | 15 ++- examples/basecoin/x/pow/mapper.go | 51 --------- 6 files changed, 134 insertions(+), 109 deletions(-) create mode 100644 examples/basecoin/x/pow/keeper.go rename examples/basecoin/x/pow/{mapper_test.go => keeper_test.go} (64%) delete mode 100644 examples/basecoin/x/pow/mapper.go diff --git a/examples/basecoin/app/app.go b/examples/basecoin/app/app.go index d4394e772..2d5a57028 100644 --- a/examples/basecoin/app/app.go +++ b/examples/basecoin/app/app.go @@ -60,13 +60,13 @@ func NewBasecoinApp(logger log.Logger, db dbm.DB) *BasecoinApp { // add handlers coinKeeper := bank.NewCoinKeeper(app.accountMapper) coolMapper := cool.NewMapper(app.capKeyMainStore) - powMapper := pow.NewMapper(app.capKeyMainStore) + powKeeper := pow.NewKeeper(app.capKeyMainStore, pow.NewPowConfig("pow", int64(1)), coinKeeper) ibcMapper := ibc.NewIBCMapper(app.cdc, app.capKeyIBCStore) stakingMapper := staking.NewMapper(app.capKeyStakingStore) app.Router(). AddRoute("bank", bank.NewHandler(coinKeeper)). AddRoute("cool", cool.NewHandler(coinKeeper, coolMapper)). - AddRoute("pow", pow.NewHandler(coinKeeper, powMapper, pow.NewPowConfig("pow", int64(1)))). + AddRoute("pow", powKeeper.Handler). AddRoute("sketchy", sketchy.NewHandler()). AddRoute("ibc", ibc.NewHandler(ibcMapper, coinKeeper)). AddRoute("staking", staking.NewHandler(stakingMapper, coinKeeper)) diff --git a/examples/basecoin/x/pow/handler.go b/examples/basecoin/x/pow/handler.go index 1d0b42252..82c8a19c3 100644 --- a/examples/basecoin/x/pow/handler.go +++ b/examples/basecoin/x/pow/handler.go @@ -1,70 +1,38 @@ package pow import ( - "fmt" "reflect" sdk "github.com/cosmos/cosmos-sdk/types" - "github.com/cosmos/cosmos-sdk/x/bank" ) -// module users must specify coin denomination and reward (constant) per PoW solution -type PowConfig struct { - Denomination string - Reward int64 -} - -func NewPowConfig(denomination string, reward int64) PowConfig { - return PowConfig{denomination, reward} -} - -func NewHandler(ck bank.CoinKeeper, pm Mapper, config PowConfig) sdk.Handler { - return func(ctx sdk.Context, msg sdk.Msg) sdk.Result { - switch msg := msg.(type) { - case MineMsg: - return handleMineMsg(ctx, ck, pm, config, msg) - default: - errMsg := "Unrecognized pow Msg type: " + reflect.TypeOf(msg).Name() - return sdk.ErrUnknownRequest(errMsg).Result() - } +func (pk Keeper) Handler(ctx sdk.Context, msg sdk.Msg) sdk.Result { + switch msg := msg.(type) { + case MineMsg: + return handleMineMsg(ctx, pk, msg) + default: + errMsg := "Unrecognized pow Msg type: " + reflect.TypeOf(msg).Name() + return sdk.ErrUnknownRequest(errMsg).Result() } } -func handleMineMsg(ctx sdk.Context, ck bank.CoinKeeper, pm Mapper, config PowConfig, msg MineMsg) sdk.Result { +func handleMineMsg(ctx sdk.Context, pk Keeper, msg MineMsg) sdk.Result { // precondition: msg has passed ValidateBasic - // will this function always be applied atomically? - - lastDifficulty, err := pm.GetLastDifficulty(ctx) + newDiff, newCount, err := pk.CheckValid(ctx, msg.Difficulty, msg.Count) if err != nil { - return ErrNonexistentDifficulty().Result() + return err.Result() } - newDifficulty := lastDifficulty + 1 + if ctx.IsCheckTx() { + return sdk.Result{} // TODO + } - lastCount, err := pm.GetLastCount(ctx) + err = pk.ApplyValid(ctx, msg.Sender, newDiff, newCount) if err != nil { - return ErrNonexistentCount().Result() + return err.Result() } - newCount := lastCount + 1 - - if msg.Count != newCount { - return ErrInvalidCount(fmt.Sprintf("invalid count: was %d, should have been %d", msg.Count, newCount)).Result() - } - - if msg.Difficulty != newDifficulty { - return ErrInvalidDifficulty(fmt.Sprintf("invalid difficulty: was %d, should have been %d", msg.Difficulty, newDifficulty)).Result() - } - - _, ckErr := ck.AddCoins(ctx, msg.Sender, []sdk.Coin{sdk.Coin{config.Denomination, config.Reward}}) - if ckErr != nil { - return ckErr.Result() - } - - pm.SetLastDifficulty(ctx, newDifficulty) - pm.SetLastCount(ctx, newCount) - return sdk.Result{} } diff --git a/examples/basecoin/x/pow/handler_test.go b/examples/basecoin/x/pow/handler_test.go index 8fa859244..f9db01d0c 100644 --- a/examples/basecoin/x/pow/handler_test.go +++ b/examples/basecoin/x/pow/handler_test.go @@ -17,11 +17,11 @@ func TestPowHandler(t *testing.T) { am := auth.NewAccountMapper(capKey, &auth.BaseAccount{}) ctx := sdk.NewContext(ms, abci.Header{}, false, nil) - mapper := NewMapper(capKey) config := NewPowConfig("pow", int64(1)) ck := bank.NewCoinKeeper(am) + keeper := NewKeeper(capKey, config, ck) - handler := NewHandler(ck, mapper, config) + handler := keeper.Handler addr := sdk.Address([]byte("sender")) count := uint64(1) @@ -32,11 +32,11 @@ func TestPowHandler(t *testing.T) { result := handler(ctx, msg) assert.Equal(t, result, sdk.Result{}) - newDiff, err := mapper.GetLastDifficulty(ctx) + newDiff, err := keeper.GetLastDifficulty(ctx) assert.Nil(t, err) assert.Equal(t, newDiff, uint64(2)) - newCount, err := mapper.GetLastCount(ctx) + newCount, err := keeper.GetLastCount(ctx) assert.Nil(t, err) assert.Equal(t, newCount, uint64(1)) diff --git a/examples/basecoin/x/pow/keeper.go b/examples/basecoin/x/pow/keeper.go new file mode 100644 index 000000000..dc4494c69 --- /dev/null +++ b/examples/basecoin/x/pow/keeper.go @@ -0,0 +1,103 @@ +package pow + +import ( + "fmt" + "strconv" + + sdk "github.com/cosmos/cosmos-sdk/types" + bank "github.com/cosmos/cosmos-sdk/x/bank" +) + +// module users must specify coin denomination and reward (constant) per PoW solution +type PowConfig struct { + Denomination string + Reward int64 +} + +func NewPowConfig(denomination string, reward int64) PowConfig { + return PowConfig{denomination, reward} +} + +type Keeper struct { + key sdk.StoreKey + config PowConfig + ck bank.CoinKeeper +} + +func NewKeeper(key sdk.StoreKey, config PowConfig, ck bank.CoinKeeper) Keeper { + return Keeper{key, config, ck} +} + +var lastDifficultyKey = []byte("lastDifficultyKey") + +func (pk Keeper) GetLastDifficulty(ctx sdk.Context) (uint64, error) { + store := ctx.KVStore(pk.key) + stored := store.Get(lastDifficultyKey) + if stored == nil { + // return the default difficulty of 1 if not set + // this works OK for this module, but a way to initalize the store (a "genesis block" for the module) might be better in general + return uint64(1), nil + } else { + return strconv.ParseUint(string(stored), 0, 64) + } +} + +func (pk Keeper) SetLastDifficulty(ctx sdk.Context, diff uint64) { + store := ctx.KVStore(pk.key) + store.Set(lastDifficultyKey, []byte(strconv.FormatUint(diff, 16))) +} + +var countKey = []byte("count") + +func (pk Keeper) GetLastCount(ctx sdk.Context) (uint64, error) { + store := ctx.KVStore(pk.key) + stored := store.Get(countKey) + if stored == nil { + return uint64(0), nil + } else { + return strconv.ParseUint(string(stored), 0, 64) + } +} + +func (pk Keeper) SetLastCount(ctx sdk.Context, count uint64) { + store := ctx.KVStore(pk.key) + store.Set(countKey, []byte(strconv.FormatUint(count, 16))) +} + +func (pk Keeper) CheckValid(ctx sdk.Context, difficulty uint64, count uint64) (uint64, uint64, sdk.Error) { + + lastDifficulty, err := pk.GetLastDifficulty(ctx) + if err != nil { + return 0, 0, ErrNonexistentDifficulty() + } + + newDifficulty := lastDifficulty + 1 + + lastCount, err := pk.GetLastCount(ctx) + if err != nil { + return 0, 0, ErrNonexistentCount() + } + + newCount := lastCount + 1 + + if count != newCount { + return 0, 0, ErrInvalidCount(fmt.Sprintf("invalid count: was %d, should have been %d", count, newCount)) + } + + if difficulty != newDifficulty { + return 0, 0, ErrInvalidDifficulty(fmt.Sprintf("invalid difficulty: was %d, should have been %d", difficulty, newDifficulty)) + } + + return newDifficulty, newCount, nil + +} + +func (pk Keeper) ApplyValid(ctx sdk.Context, sender sdk.Address, newDifficulty uint64, newCount uint64) sdk.Error { + _, ckErr := pk.ck.AddCoins(ctx, sender, []sdk.Coin{sdk.Coin{pk.config.Denomination, pk.config.Reward}}) + if ckErr != nil { + return ckErr + } + pk.SetLastDifficulty(ctx, newDifficulty) + pk.SetLastCount(ctx, newCount) + return nil +} diff --git a/examples/basecoin/x/pow/mapper_test.go b/examples/basecoin/x/pow/keeper_test.go similarity index 64% rename from examples/basecoin/x/pow/mapper_test.go rename to examples/basecoin/x/pow/keeper_test.go index 1a190b502..e9ec9c6d3 100644 --- a/examples/basecoin/x/pow/mapper_test.go +++ b/examples/basecoin/x/pow/keeper_test.go @@ -10,6 +10,8 @@ import ( "github.com/cosmos/cosmos-sdk/store" sdk "github.com/cosmos/cosmos-sdk/types" + auth "github.com/cosmos/cosmos-sdk/x/auth" + bank "github.com/cosmos/cosmos-sdk/x/bank" ) // possibly share this kind of setup functionality between module testsuites? @@ -23,19 +25,22 @@ func setupMultiStore() (sdk.MultiStore, *sdk.KVStoreKey) { return ms, capKey } -func TestPowMapperGetSet(t *testing.T) { +func TestPowKeeperGetSet(t *testing.T) { ms, capKey := setupMultiStore() + am := auth.NewAccountMapper(capKey, &auth.BaseAccount{}) ctx := sdk.NewContext(ms, abci.Header{}, false, nil) - mapper := NewMapper(capKey) + config := NewPowConfig("pow", int64(1)) + ck := bank.NewCoinKeeper(am) + keeper := NewKeeper(capKey, config, ck) - res, err := mapper.GetLastDifficulty(ctx) + res, err := keeper.GetLastDifficulty(ctx) assert.Nil(t, err) assert.Equal(t, res, uint64(1)) - mapper.SetLastDifficulty(ctx, 2) + keeper.SetLastDifficulty(ctx, 2) - res, err = mapper.GetLastDifficulty(ctx) + res, err = keeper.GetLastDifficulty(ctx) assert.Nil(t, err) assert.Equal(t, res, uint64(2)) } diff --git a/examples/basecoin/x/pow/mapper.go b/examples/basecoin/x/pow/mapper.go deleted file mode 100644 index 4dd226bf7..000000000 --- a/examples/basecoin/x/pow/mapper.go +++ /dev/null @@ -1,51 +0,0 @@ -package pow - -import ( - "strconv" - - sdk "github.com/cosmos/cosmos-sdk/types" -) - -type Mapper struct { - key sdk.StoreKey -} - -func NewMapper(key sdk.StoreKey) Mapper { - return Mapper{key} -} - -var lastDifficultyKey = []byte("lastDifficultyKey") - -func (pm Mapper) GetLastDifficulty(ctx sdk.Context) (uint64, error) { - store := ctx.KVStore(pm.key) - stored := store.Get(lastDifficultyKey) - if stored == nil { - // return the default difficulty of 1 if not set - // this works OK for this module, but a way to initalize the store (a "genesis block" for the module) might be better in general - return uint64(1), nil - } else { - return strconv.ParseUint(string(stored), 0, 64) - } -} - -func (pm Mapper) SetLastDifficulty(ctx sdk.Context, diff uint64) { - store := ctx.KVStore(pm.key) - store.Set(lastDifficultyKey, []byte(strconv.FormatUint(diff, 16))) -} - -var countKey = []byte("count") - -func (pm Mapper) GetLastCount(ctx sdk.Context) (uint64, error) { - store := ctx.KVStore(pm.key) - stored := store.Get(countKey) - if stored == nil { - return uint64(0), nil - } else { - return strconv.ParseUint(string(stored), 0, 64) - } -} - -func (pm Mapper) SetLastCount(ctx sdk.Context, count uint64) { - store := ctx.KVStore(pm.key) - store.Set(countKey, []byte(strconv.FormatUint(count, 16))) -} From d47f27145907a6386fee5ba2ab90ef9544194ae6 Mon Sep 17 00:00:00 2001 From: Christopher Goes Date: Thu, 5 Apr 2018 14:13:11 +0200 Subject: [PATCH 3/4] Mount store; add testcases --- examples/democoin/app/app.go | 11 ++++-- examples/democoin/app/app_test.go | 49 +++++++++++++++++++++++++ examples/democoin/cmd/democoind/main.go | 5 +++ examples/democoin/x/pow/mine.go | 44 ++++++++++++++++++++++ examples/democoin/x/pow/types.go | 5 ++- examples/democoin/x/pow/types_test.go | 35 +----------------- 6 files changed, 110 insertions(+), 39 deletions(-) create mode 100644 examples/democoin/x/pow/mine.go diff --git a/examples/democoin/app/app.go b/examples/democoin/app/app.go index a27a37ceb..27ca7a145 100644 --- a/examples/democoin/app/app.go +++ b/examples/democoin/app/app.go @@ -80,6 +80,7 @@ func NewDemocoinApp(logger log.Logger, dbs map[string]dbm.DB) *DemocoinApp { app.SetInitChainer(app.initChainerFn(coolKeeper)) app.MountStoreWithDB(app.capKeyMainStore, sdk.StoreTypeIAVL, dbs["main"]) app.MountStoreWithDB(app.capKeyAccountStore, sdk.StoreTypeIAVL, dbs["acc"]) + app.MountStoreWithDB(app.capKeyPowStore, sdk.StoreTypeIAVL, dbs["pow"]) app.MountStoreWithDB(app.capKeyIBCStore, sdk.StoreTypeIAVL, dbs["ibc"]) app.MountStoreWithDB(app.capKeyStakingStore, sdk.StoreTypeIAVL, dbs["staking"]) // NOTE: Broken until #532 lands @@ -100,16 +101,18 @@ func MakeCodec() *wire.Codec { const msgTypeIssue = 0x2 const msgTypeQuiz = 0x3 const msgTypeSetTrend = 0x4 - const msgTypeIBCTransferMsg = 0x5 - const msgTypeIBCReceiveMsg = 0x6 - const msgTypeBondMsg = 0x7 - const msgTypeUnbondMsg = 0x8 + const msgTypeMine = 0x5 + const msgTypeIBCTransferMsg = 0x6 + const msgTypeIBCReceiveMsg = 0x7 + const msgTypeBondMsg = 0x8 + const msgTypeUnbondMsg = 0x9 var _ = oldwire.RegisterInterface( struct{ sdk.Msg }{}, oldwire.ConcreteType{bank.SendMsg{}, msgTypeSend}, oldwire.ConcreteType{bank.IssueMsg{}, msgTypeIssue}, oldwire.ConcreteType{cool.QuizMsg{}, msgTypeQuiz}, oldwire.ConcreteType{cool.SetTrendMsg{}, msgTypeSetTrend}, + oldwire.ConcreteType{pow.MineMsg{}, msgTypeMine}, oldwire.ConcreteType{ibc.IBCTransferMsg{}, msgTypeIBCTransferMsg}, oldwire.ConcreteType{ibc.IBCReceiveMsg{}, msgTypeIBCReceiveMsg}, oldwire.ConcreteType{simplestake.BondMsg{}, msgTypeBondMsg}, diff --git a/examples/democoin/app/app_test.go b/examples/democoin/app/app_test.go index bf2ddc232..74b789bea 100644 --- a/examples/democoin/app/app_test.go +++ b/examples/democoin/app/app_test.go @@ -11,6 +11,7 @@ import ( "github.com/cosmos/cosmos-sdk/examples/democoin/types" "github.com/cosmos/cosmos-sdk/examples/democoin/x/cool" + "github.com/cosmos/cosmos-sdk/examples/democoin/x/pow" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/x/auth" "github.com/cosmos/cosmos-sdk/x/bank" @@ -71,6 +72,7 @@ func loggerAndDBs() (log.Logger, map[string]dbm.DB) { dbs := map[string]dbm.DB{ "main": dbm.NewMemDB(), "acc": dbm.NewMemDB(), + "pow": dbm.NewMemDB(), "ibc": dbm.NewMemDB(), "staking": dbm.NewMemDB(), } @@ -238,6 +240,53 @@ func TestSendMsgWithAccounts(t *testing.T) { assert.Equal(t, sdk.CodeOK, res.Code, res.Log) } +func TestMineMsg(t *testing.T) { + bapp := newDemocoinApp() + + // Construct genesis state + // Construct some genesis bytes to reflect democoin/types/AppAccount + coins := sdk.Coins{} + baseAcc := auth.BaseAccount{ + Address: addr1, + Coins: coins, + } + acc1 := &types.AppAccount{baseAcc, "foobart"} + + // Construct genesis state + genesisState := map[string]interface{}{ + "accounts": []*types.GenesisAccount{ + types.NewGenesisAccount(acc1), + }, + "cool": map[string]string{ + "trend": "ice-cold", + }, + } + stateBytes, err := json.MarshalIndent(genesisState, "", "\t") + require.Nil(t, err) + + // Initialize the chain (nil) + vals := []abci.Validator{} + bapp.InitChain(abci.RequestInitChain{vals, stateBytes}) + bapp.Commit() + + // A checkTx context (true) + ctxCheck := bapp.BaseApp.NewContext(true, abci.Header{}) + res1 := bapp.accountMapper.GetAccount(ctxCheck, addr1) + assert.Equal(t, acc1, res1) + + // Mine and check for reward + mineMsg1 := pow.GenerateMineMsg(addr1, 1, 2) + SignCheckDeliver(t, bapp, mineMsg1, 0, true) + CheckBalance(t, bapp, "1pow") + // Mine again and check for reward + /* + mineMsg2 := pow.GenerateMineMsg(addr1, 2, 2) + SignCheckDeliver(t, bapp, mineMsg2, 1, true) + CheckBalance(t, bapp, "2pow") + */ + +} + func TestQuizMsg(t *testing.T) { bapp := newDemocoinApp() diff --git a/examples/democoin/cmd/democoind/main.go b/examples/democoin/cmd/democoind/main.go index 6fc593067..396774704 100644 --- a/examples/democoin/cmd/democoind/main.go +++ b/examples/democoin/cmd/democoind/main.go @@ -55,6 +55,10 @@ func generateApp(rootDir string, logger log.Logger) (abci.Application, error) { if err != nil { return nil, err } + dbPow, err := dbm.NewGoLevelDB("democoin-pow", filepath.Join(rootDir, "data")) + if err != nil { + return nil, err + } dbIBC, err := dbm.NewGoLevelDB("democoin-ibc", filepath.Join(rootDir, "data")) if err != nil { return nil, err @@ -66,6 +70,7 @@ func generateApp(rootDir string, logger log.Logger) (abci.Application, error) { dbs := map[string]dbm.DB{ "main": dbMain, "acc": dbAcc, + "pow": dbPow, "ibc": dbIBC, "staking": dbStaking, } diff --git a/examples/democoin/x/pow/mine.go b/examples/democoin/x/pow/mine.go new file mode 100644 index 000000000..ff2264aaa --- /dev/null +++ b/examples/democoin/x/pow/mine.go @@ -0,0 +1,44 @@ +package pow + +import ( + "encoding/hex" + "math" + "strconv" + + sdk "github.com/cosmos/cosmos-sdk/types" + crypto "github.com/tendermint/go-crypto" +) + +func GenerateMineMsg(sender sdk.Address, count uint64, difficulty uint64) MineMsg { + nonce, hash := mine(sender, count, difficulty) + return NewMineMsg(sender, difficulty, count, nonce, hash) +} + +func hash(sender sdk.Address, count uint64, nonce uint64) []byte { + var bytes []byte + bytes = append(bytes, []byte(sender)...) + countBytes := strconv.FormatUint(count, 16) + bytes = append(bytes, countBytes...) + nonceBytes := strconv.FormatUint(nonce, 16) + bytes = append(bytes, nonceBytes...) + hash := crypto.Sha256(bytes) + // uint64, so we just use the first 8 bytes of the hash + // this limits the range of possible difficulty values (as compared to uint256), but fine for proof-of-concept + ret := make([]byte, hex.EncodedLen(len(hash))) + hex.Encode(ret, hash) + return ret[:16] +} + +func mine(sender sdk.Address, count uint64, difficulty uint64) (uint64, []byte) { + target := math.MaxUint64 / difficulty + for nonce := uint64(0); ; nonce++ { + hash := hash(sender, count, nonce) + hashuint, err := strconv.ParseUint(string(hash), 16, 64) + if err != nil { + panic(err) + } + if hashuint < target { + return nonce, hash + } + } +} diff --git a/examples/democoin/x/pow/types.go b/examples/democoin/x/pow/types.go index 70c456d84..ea368c306 100644 --- a/examples/democoin/x/pow/types.go +++ b/examples/democoin/x/pow/types.go @@ -22,12 +22,15 @@ type MineMsg struct { Proof []byte `json:"proof"` } +// enforce the msg type at compile time +var _ sdk.Msg = MineMsg{} + // NewMineMsg - construct mine message func NewMineMsg(sender sdk.Address, difficulty uint64, count uint64, nonce uint64, proof []byte) MineMsg { return MineMsg{sender, difficulty, count, nonce, proof} } -func (msg MineMsg) Type() string { return "mine" } +func (msg MineMsg) Type() string { return "pow" } func (msg MineMsg) Get(key interface{}) (value interface{}) { return nil } func (msg MineMsg) GetSigners() []sdk.Address { return []sdk.Address{msg.Sender} } func (msg MineMsg) String() string { diff --git a/examples/democoin/x/pow/types_test.go b/examples/democoin/x/pow/types_test.go index 04360c3fd..34ab8914e 100644 --- a/examples/democoin/x/pow/types_test.go +++ b/examples/democoin/x/pow/types_test.go @@ -1,16 +1,12 @@ package pow import ( - "encoding/hex" "fmt" - "math" - "strconv" "testing" "github.com/stretchr/testify/assert" sdk "github.com/cosmos/cosmos-sdk/types" - crypto "github.com/tendermint/go-crypto" ) func TestNewMineMsg(t *testing.T) { @@ -23,36 +19,7 @@ func TestNewMineMsg(t *testing.T) { func TestMineMsgType(t *testing.T) { addr := sdk.Address([]byte("sender")) msg := MineMsg{addr, 0, 0, 0, []byte("")} - assert.Equal(t, msg.Type(), "mine") -} - -func hash(sender sdk.Address, count uint64, nonce uint64) []byte { - var bytes []byte - bytes = append(bytes, []byte(sender)...) - countBytes := strconv.FormatUint(count, 16) - bytes = append(bytes, countBytes...) - nonceBytes := strconv.FormatUint(nonce, 16) - bytes = append(bytes, nonceBytes...) - hash := crypto.Sha256(bytes) - // uint64, so we just use the first 8 bytes of the hash - // this limits the range of possible difficulty values (as compared to uint256), but fine for proof-of-concept - ret := make([]byte, hex.EncodedLen(len(hash))) - hex.Encode(ret, hash) - return ret[:16] -} - -func mine(sender sdk.Address, count uint64, difficulty uint64) (uint64, []byte) { - target := math.MaxUint64 / difficulty - for nonce := uint64(0); ; nonce++ { - hash := hash(sender, count, nonce) - hashuint, err := strconv.ParseUint(string(hash), 16, 64) - if err != nil { - panic(err) - } - if hashuint < target { - return nonce, hash - } - } + assert.Equal(t, msg.Type(), "pow") } func TestMineMsgValidation(t *testing.T) { From d99f4f3c14d7afa1d3758a50c3ba62a19f305824 Mon Sep 17 00:00:00 2001 From: Christopher Goes Date: Thu, 5 Apr 2018 15:16:54 +0200 Subject: [PATCH 4/4] PoW InitGenesis; testcases --- examples/democoin/app/app.go | 12 +++++++++--- examples/democoin/app/app_test.go | 15 ++++++++++----- examples/democoin/types/account.go | 7 ++++++- examples/democoin/x/cool/keeper.go | 15 ++------------- examples/democoin/x/cool/types.go | 5 +++++ examples/democoin/x/pow/handler.go | 10 +++++++--- examples/democoin/x/pow/handler_test.go | 4 ++++ examples/democoin/x/pow/keeper.go | 22 ++++++++++++++++------ examples/democoin/x/pow/keeper_test.go | 3 +++ 9 files changed, 62 insertions(+), 31 deletions(-) diff --git a/examples/democoin/app/app.go b/examples/democoin/app/app.go index 27ca7a145..2ee79bd5b 100644 --- a/examples/democoin/app/app.go +++ b/examples/democoin/app/app.go @@ -77,7 +77,7 @@ func NewDemocoinApp(logger log.Logger, dbs map[string]dbm.DB) *DemocoinApp { // initialize BaseApp app.SetTxDecoder(app.txDecoder) - app.SetInitChainer(app.initChainerFn(coolKeeper)) + app.SetInitChainer(app.initChainerFn(coolKeeper, powKeeper)) app.MountStoreWithDB(app.capKeyMainStore, sdk.StoreTypeIAVL, dbs["main"]) app.MountStoreWithDB(app.capKeyAccountStore, sdk.StoreTypeIAVL, dbs["acc"]) app.MountStoreWithDB(app.capKeyPowStore, sdk.StoreTypeIAVL, dbs["pow"]) @@ -151,7 +151,7 @@ func (app *DemocoinApp) txDecoder(txBytes []byte) (sdk.Tx, sdk.Error) { } // custom logic for democoin initialization -func (app *DemocoinApp) initChainerFn(coolKeeper cool.Keeper) sdk.InitChainer { +func (app *DemocoinApp) initChainerFn(coolKeeper cool.Keeper, powKeeper pow.Keeper) sdk.InitChainer { return func(ctx sdk.Context, req abci.RequestInitChain) abci.ResponseInitChain { stateJSON := req.AppStateBytes @@ -172,7 +172,13 @@ func (app *DemocoinApp) initChainerFn(coolKeeper cool.Keeper) sdk.InitChainer { } // Application specific genesis handling - err = coolKeeper.InitGenesis(ctx, stateJSON) + err = coolKeeper.InitGenesis(ctx, genesisState.CoolGenesis) + if err != nil { + panic(err) // TODO https://github.com/cosmos/cosmos-sdk/issues/468 + // return sdk.ErrGenesisParse("").TraceCause(err, "") + } + + err = powKeeper.InitGenesis(ctx, genesisState.PowGenesis) if err != nil { panic(err) // TODO https://github.com/cosmos/cosmos-sdk/issues/468 // return sdk.ErrGenesisParse("").TraceCause(err, "") diff --git a/examples/democoin/app/app_test.go b/examples/democoin/app/app_test.go index 74b789bea..1cc56bd6b 100644 --- a/examples/democoin/app/app_test.go +++ b/examples/democoin/app/app_test.go @@ -260,6 +260,10 @@ func TestMineMsg(t *testing.T) { "cool": map[string]string{ "trend": "ice-cold", }, + "pow": map[string]uint64{ + "difficulty": 1, + "count": 0, + }, } stateBytes, err := json.MarshalIndent(genesisState, "", "\t") require.Nil(t, err) @@ -279,11 +283,12 @@ func TestMineMsg(t *testing.T) { SignCheckDeliver(t, bapp, mineMsg1, 0, true) CheckBalance(t, bapp, "1pow") // Mine again and check for reward - /* - mineMsg2 := pow.GenerateMineMsg(addr1, 2, 2) - SignCheckDeliver(t, bapp, mineMsg2, 1, true) - CheckBalance(t, bapp, "2pow") - */ + mineMsg2 := pow.GenerateMineMsg(addr1, 2, 3) + SignCheckDeliver(t, bapp, mineMsg2, 1, true) + CheckBalance(t, bapp, "2pow") + // Mine again - should be invalid + SignCheckDeliver(t, bapp, mineMsg2, 1, false) + CheckBalance(t, bapp, "2pow") } diff --git a/examples/democoin/types/account.go b/examples/democoin/types/account.go index f34113fc6..ce2d6da34 100644 --- a/examples/democoin/types/account.go +++ b/examples/democoin/types/account.go @@ -4,6 +4,9 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/wire" "github.com/cosmos/cosmos-sdk/x/auth" + + "github.com/cosmos/cosmos-sdk/examples/democoin/x/cool" + "github.com/cosmos/cosmos-sdk/examples/democoin/x/pow" ) var _ sdk.Account = (*AppAccount)(nil) @@ -41,7 +44,9 @@ func GetAccountDecoder(cdc *wire.Codec) sdk.AccountDecoder { // State to Unmarshal type GenesisState struct { - Accounts []*GenesisAccount `json:"accounts"` + Accounts []*GenesisAccount `json:"accounts"` + PowGenesis pow.PowGenesis `json:"pow"` + CoolGenesis cool.CoolGenesis `json:"cool"` } // GenesisAccount doesn't need pubkey or sequence diff --git a/examples/democoin/x/cool/keeper.go b/examples/democoin/x/cool/keeper.go index 1bf342fdc..0a4fc81e1 100644 --- a/examples/democoin/x/cool/keeper.go +++ b/examples/democoin/x/cool/keeper.go @@ -1,17 +1,10 @@ package cool import ( - "encoding/json" - sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/x/bank" ) -// Cool genesis state, containing the genesis trend -type GenesisState struct { - trend string -} - // Keeper - handlers sets/gets of custom variables for your module type Keeper struct { ck bank.CoinKeeper @@ -49,11 +42,7 @@ func (k Keeper) CheckTrend(ctx sdk.Context, guessedTrend string) bool { } // InitGenesis - store the genesis trend -func (k Keeper) InitGenesis(ctx sdk.Context, data json.RawMessage) error { - var state GenesisState - if err := json.Unmarshal(data, &state); err != nil { - return err - } - k.setTrend(ctx, state.trend) +func (k Keeper) InitGenesis(ctx sdk.Context, data CoolGenesis) error { + k.setTrend(ctx, data.Trend) return nil } diff --git a/examples/democoin/x/cool/types.go b/examples/democoin/x/cool/types.go index a3fa6ca48..e24c363ac 100644 --- a/examples/democoin/x/cool/types.go +++ b/examples/democoin/x/cool/types.go @@ -15,6 +15,11 @@ type SetTrendMsg struct { Cool string } +// Genesis state - specify genesis trend +type CoolGenesis struct { + Trend string `json:"trend"` +} + // New cool message func NewSetTrendMsg(sender sdk.Address, cool string) SetTrendMsg { return SetTrendMsg{ diff --git a/examples/democoin/x/pow/handler.go b/examples/democoin/x/pow/handler.go index 82c8a19c3..d1a691139 100644 --- a/examples/democoin/x/pow/handler.go +++ b/examples/democoin/x/pow/handler.go @@ -25,9 +25,13 @@ func handleMineMsg(ctx sdk.Context, pk Keeper, msg MineMsg) sdk.Result { return err.Result() } - if ctx.IsCheckTx() { - return sdk.Result{} // TODO - } + // commented for now, makes testing difficult + // TODO figure out a better test method that allows early CheckTx return + /* + if ctx.IsCheckTx() { + return sdk.Result{} // TODO + } + */ err = pk.ApplyValid(ctx, msg.Sender, newDiff, newCount) if err != nil { diff --git a/examples/democoin/x/pow/handler_test.go b/examples/democoin/x/pow/handler_test.go index f9db01d0c..2de285371 100644 --- a/examples/democoin/x/pow/handler_test.go +++ b/examples/democoin/x/pow/handler_test.go @@ -26,6 +26,10 @@ func TestPowHandler(t *testing.T) { addr := sdk.Address([]byte("sender")) count := uint64(1) difficulty := uint64(2) + + err := keeper.InitGenesis(ctx, PowGenesis{uint64(1), uint64(0)}) + assert.Nil(t, err) + nonce, proof := mine(addr, count, difficulty) msg := NewMineMsg(addr, difficulty, count, nonce, proof) diff --git a/examples/democoin/x/pow/keeper.go b/examples/democoin/x/pow/keeper.go index dc4494c69..73558632c 100644 --- a/examples/democoin/x/pow/keeper.go +++ b/examples/democoin/x/pow/keeper.go @@ -14,8 +14,10 @@ type PowConfig struct { Reward int64 } -func NewPowConfig(denomination string, reward int64) PowConfig { - return PowConfig{denomination, reward} +// genesis info must specify starting difficulty and starting count +type PowGenesis struct { + Difficulty uint64 `json:"difficulty"` + Count uint64 `json:"count"` } type Keeper struct { @@ -24,19 +26,27 @@ type Keeper struct { ck bank.CoinKeeper } +func NewPowConfig(denomination string, reward int64) PowConfig { + return PowConfig{denomination, reward} +} + func NewKeeper(key sdk.StoreKey, config PowConfig, ck bank.CoinKeeper) Keeper { return Keeper{key, config, ck} } +func (pk Keeper) InitGenesis(ctx sdk.Context, genesis PowGenesis) error { + pk.SetLastDifficulty(ctx, genesis.Difficulty) + pk.SetLastCount(ctx, genesis.Count) + return nil +} + var lastDifficultyKey = []byte("lastDifficultyKey") func (pk Keeper) GetLastDifficulty(ctx sdk.Context) (uint64, error) { store := ctx.KVStore(pk.key) stored := store.Get(lastDifficultyKey) if stored == nil { - // return the default difficulty of 1 if not set - // this works OK for this module, but a way to initalize the store (a "genesis block" for the module) might be better in general - return uint64(1), nil + panic("no stored difficulty") } else { return strconv.ParseUint(string(stored), 0, 64) } @@ -53,7 +63,7 @@ func (pk Keeper) GetLastCount(ctx sdk.Context) (uint64, error) { store := ctx.KVStore(pk.key) stored := store.Get(countKey) if stored == nil { - return uint64(0), nil + panic("no stored count") } else { return strconv.ParseUint(string(stored), 0, 64) } diff --git a/examples/democoin/x/pow/keeper_test.go b/examples/democoin/x/pow/keeper_test.go index e9ec9c6d3..6e0d52649 100644 --- a/examples/democoin/x/pow/keeper_test.go +++ b/examples/democoin/x/pow/keeper_test.go @@ -34,6 +34,9 @@ func TestPowKeeperGetSet(t *testing.T) { ck := bank.NewCoinKeeper(am) keeper := NewKeeper(capKey, config, ck) + err := keeper.InitGenesis(ctx, PowGenesis{uint64(1), uint64(0)}) + assert.Nil(t, err) + res, err := keeper.GetLastDifficulty(ctx) assert.Nil(t, err) assert.Equal(t, res, uint64(1))