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
This commit is contained in:
Christopher Goes 2018-03-22 18:47:13 +01:00
parent 946b764d7b
commit 1b4a3d24ff
9 changed files with 560 additions and 0 deletions

View File

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

View File

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

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

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

View File

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

View File

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

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