diff --git a/examples/basecoin/app/app.go b/examples/basecoin/app/app.go index 1d31b0edc4..d4394e7727 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 0000000000..24db802362 --- /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 0000000000..b44eb93d6d --- /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 0000000000..1d0b422526 --- /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 0000000000..8fa8592449 --- /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 0000000000..4dd226bf7b --- /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 0000000000..1a190b502a --- /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 0000000000..70c456d840 --- /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 0000000000..04360c3fda --- /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]") +}