From 1b4a3d24ff2883adea5a5147291cff140ada10b0 Mon Sep 17 00:00:00 2001 From: Christopher Goes Date: Thu, 22 Mar 2018 18:47:13 +0100 Subject: [PATCH] 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 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]") +}