diff --git a/examples/democoin/app/app.go b/examples/democoin/app/app.go index 2fffb83241..2ee79bd5bf 100644 --- a/examples/democoin/app/app.go +++ b/examples/democoin/app/app.go @@ -19,6 +19,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" "github.com/cosmos/cosmos-sdk/examples/democoin/x/sketchy" ) @@ -34,6 +35,7 @@ type DemocoinApp struct { // keys to access the substores capKeyMainStore *sdk.KVStoreKey capKeyAccountStore *sdk.KVStoreKey + capKeyPowStore *sdk.KVStoreKey capKeyIBCStore *sdk.KVStoreKey capKeyStakingStore *sdk.KVStoreKey @@ -48,6 +50,7 @@ func NewDemocoinApp(logger log.Logger, dbs map[string]dbm.DB) *DemocoinApp { cdc: MakeCodec(), capKeyMainStore: sdk.NewKVStoreKey("main"), capKeyAccountStore: sdk.NewKVStoreKey("acc"), + capKeyPowStore: sdk.NewKVStoreKey("pow"), capKeyIBCStore: sdk.NewKVStoreKey("ibc"), capKeyStakingStore: sdk.NewKVStoreKey("stake"), } @@ -61,20 +64,23 @@ func NewDemocoinApp(logger log.Logger, dbs map[string]dbm.DB) *DemocoinApp { // add handlers coinKeeper := bank.NewCoinKeeper(app.accountMapper) coolKeeper := cool.NewKeeper(app.capKeyMainStore, coinKeeper) + powKeeper := pow.NewKeeper(app.capKeyPowStore, pow.NewPowConfig("pow", int64(1)), coinKeeper) ibcMapper := ibc.NewIBCMapper(app.cdc, app.capKeyIBCStore) stakeKeeper := simplestake.NewKeeper(app.capKeyStakingStore, coinKeeper) app.Router(). AddRoute("bank", bank.NewHandler(coinKeeper)). AddRoute("cool", cool.NewHandler(coolKeeper)). + AddRoute("pow", powKeeper.Handler). AddRoute("sketchy", sketchy.NewHandler()). AddRoute("ibc", ibc.NewHandler(ibcMapper, coinKeeper)). AddRoute("simplestake", simplestake.NewHandler(stakeKeeper)) // 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"]) app.MountStoreWithDB(app.capKeyIBCStore, sdk.StoreTypeIAVL, dbs["ibc"]) app.MountStoreWithDB(app.capKeyStakingStore, sdk.StoreTypeIAVL, dbs["staking"]) // NOTE: Broken until #532 lands @@ -95,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}, @@ -143,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 @@ -164,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 bf2ddc232f..1cc56bd6bf 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,58 @@ 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", + }, + "pow": map[string]uint64{ + "difficulty": 1, + "count": 0, + }, + } + 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, 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") + +} + func TestQuizMsg(t *testing.T) { bapp := newDemocoinApp() diff --git a/examples/democoin/cmd/democoind/main.go b/examples/democoin/cmd/democoind/main.go index 076eda248b..df94e2c323 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/types/account.go b/examples/democoin/types/account.go index 35b37c7b2f..b5d5a0d034 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 1bf342fdc2..0a4fc81e1c 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 a3fa6ca48e..e24c363ace 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/commands/tx.go b/examples/democoin/x/pow/commands/tx.go new file mode 100644 index 0000000000..77cfa621c6 --- /dev/null +++ b/examples/democoin/x/pow/commands/tx.go @@ -0,0 +1,66 @@ +package commands + +import ( + "fmt" + "strconv" + + "github.com/pkg/errors" + "github.com/spf13/cobra" + + "github.com/cosmos/cosmos-sdk/client/context" + + "github.com/cosmos/cosmos-sdk/examples/democoin/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 + + ctx := context.NewCoreContextFromViper() + + from, err := ctx.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 := ctx.FromAddressName + + // build and sign the transaction, then broadcast to Tendermint + res, err := ctx.SignBuildBroadcast(name, 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/democoin/x/pow/errors.go b/examples/democoin/x/pow/errors.go new file mode 100644 index 0000000000..b44eb93d6d --- /dev/null +++ b/examples/democoin/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/democoin/x/pow/handler.go b/examples/democoin/x/pow/handler.go new file mode 100644 index 0000000000..d1a691139d --- /dev/null +++ b/examples/democoin/x/pow/handler.go @@ -0,0 +1,42 @@ +package pow + +import ( + "reflect" + + sdk "github.com/cosmos/cosmos-sdk/types" +) + +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, pk Keeper, msg MineMsg) sdk.Result { + + // precondition: msg has passed ValidateBasic + + newDiff, newCount, err := pk.CheckValid(ctx, msg.Difficulty, msg.Count) + if err != nil { + return err.Result() + } + + // 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 { + return err.Result() + } + + return sdk.Result{} +} diff --git a/examples/democoin/x/pow/handler_test.go b/examples/democoin/x/pow/handler_test.go new file mode 100644 index 0000000000..2de2853713 --- /dev/null +++ b/examples/democoin/x/pow/handler_test.go @@ -0,0 +1,55 @@ +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) + config := NewPowConfig("pow", int64(1)) + ck := bank.NewCoinKeeper(am) + keeper := NewKeeper(capKey, config, ck) + + handler := keeper.Handler + + 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) + + result := handler(ctx, msg) + assert.Equal(t, result, sdk.Result{}) + + newDiff, err := keeper.GetLastDifficulty(ctx) + assert.Nil(t, err) + assert.Equal(t, newDiff, uint64(2)) + + newCount, err := keeper.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/democoin/x/pow/keeper.go b/examples/democoin/x/pow/keeper.go new file mode 100644 index 0000000000..73558632c4 --- /dev/null +++ b/examples/democoin/x/pow/keeper.go @@ -0,0 +1,113 @@ +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 +} + +// genesis info must specify starting difficulty and starting count +type PowGenesis struct { + Difficulty uint64 `json:"difficulty"` + Count uint64 `json:"count"` +} + +type Keeper struct { + key sdk.StoreKey + config PowConfig + 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 { + panic("no stored difficulty") + } 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 { + panic("no stored count") + } 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/democoin/x/pow/keeper_test.go b/examples/democoin/x/pow/keeper_test.go new file mode 100644 index 0000000000..6e0d526496 --- /dev/null +++ b/examples/democoin/x/pow/keeper_test.go @@ -0,0 +1,49 @@ +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" + 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? +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 TestPowKeeperGetSet(t *testing.T) { + ms, capKey := setupMultiStore() + + am := auth.NewAccountMapper(capKey, &auth.BaseAccount{}) + ctx := sdk.NewContext(ms, abci.Header{}, false, nil) + config := NewPowConfig("pow", int64(1)) + 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)) + + keeper.SetLastDifficulty(ctx, 2) + + res, err = keeper.GetLastDifficulty(ctx) + assert.Nil(t, err) + assert.Equal(t, res, uint64(2)) +} diff --git a/examples/democoin/x/pow/mine.go b/examples/democoin/x/pow/mine.go new file mode 100644 index 0000000000..ff2264aaa7 --- /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 new file mode 100644 index 0000000000..ea368c3063 --- /dev/null +++ b/examples/democoin/x/pow/types.go @@ -0,0 +1,78 @@ +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"` +} + +// 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 "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 { + 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/democoin/x/pow/types_test.go b/examples/democoin/x/pow/types_test.go new file mode 100644 index 0000000000..34ab8914eb --- /dev/null +++ b/examples/democoin/x/pow/types_test.go @@ -0,0 +1,78 @@ +package pow + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/assert" + + sdk "github.com/cosmos/cosmos-sdk/types" +) + +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(), "pow") +} + +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]") +}