Merge old PoW module into Democoin

This commit is contained in:
Christopher Goes 2018-04-05 13:45:58 +02:00
commit 21fe81720a
No known key found for this signature in database
GPG Key ID: E828D98232D328D3
9 changed files with 577 additions and 0 deletions

View File

@ -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,11 +64,13 @@ 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))

View File

@ -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
},
}
}

View File

@ -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)
}

View File

@ -0,0 +1,38 @@
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()
}
if ctx.IsCheckTx() {
return sdk.Result{} // TODO
}
err = pk.ApplyValid(ctx, msg.Sender, newDiff, newCount)
if err != nil {
return err.Result()
}
return sdk.Result{}
}

View File

@ -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)
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)
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{})
}

View File

@ -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
}

View File

@ -0,0 +1,46 @@
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)
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))
}

View File

@ -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
}

View File

@ -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]")
}