diff --git a/CHANGELOG.md b/CHANGELOG.md index a60d162ee2..2c55365cf9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -36,6 +36,7 @@ FEATURES * [tests] Add WaitForNextNBlocksTM helper method * [types] Switches internal representation of Int/Uint/Rat to use pointers * [gaiad] unsafe_reset_all now resets addrbook.json +* [democoin] add x/oracle, x/assoc FIXES * [gaia] Added self delegation for validators in the genesis creation diff --git a/examples/democoin/mock/validator.go b/examples/democoin/mock/validator.go new file mode 100644 index 0000000000..29cdd8b165 --- /dev/null +++ b/examples/democoin/mock/validator.go @@ -0,0 +1,122 @@ +package mock + +import ( + "bytes" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/tendermint/go-crypto" +) + +// Validator implements sdk.Validator +type Validator struct { + Address sdk.Address + Power sdk.Rat +} + +// Implements sdk.Validator +func (v Validator) GetStatus() sdk.BondStatus { + return sdk.Bonded +} + +// Implements sdk.Validator +func (v Validator) GetOwner() sdk.Address { + return v.Address +} + +// Implements sdk.Validator +func (v Validator) GetPubKey() crypto.PubKey { + return nil +} + +// Implements sdk.Validator +func (v Validator) GetPower() sdk.Rat { + return v.Power +} + +// Implements sdk.Validator +func (v Validator) GetDelegatorShares() sdk.Rat { + return sdk.ZeroRat() +} + +// Implements sdk.Validator +func (v Validator) GetBondHeight() int64 { + return 0 +} + +// Implements sdk.Validator +func (v Validator) GetMoniker() string { + return "" +} + +// Implements sdk.Validator +type ValidatorSet struct { + Validators []Validator +} + +// IterateValidators implements sdk.ValidatorSet +func (vs *ValidatorSet) IterateValidators(ctx sdk.Context, fn func(index int64, Validator sdk.Validator) bool) { + for i, val := range vs.Validators { + if fn(int64(i), val) { + break + } + } +} + +// IterateValidatorsBonded implements sdk.ValidatorSet +func (vs *ValidatorSet) IterateValidatorsBonded(ctx sdk.Context, fn func(index int64, Validator sdk.Validator) bool) { + vs.IterateValidators(ctx, fn) +} + +// Validator implements sdk.ValidatorSet +func (vs *ValidatorSet) Validator(ctx sdk.Context, addr sdk.Address) sdk.Validator { + for _, val := range vs.Validators { + if bytes.Equal(val.Address, addr) { + return val + } + } + return nil +} + +// TotalPower implements sdk.ValidatorSet +func (vs *ValidatorSet) TotalPower(ctx sdk.Context) sdk.Rat { + res := sdk.ZeroRat() + for _, val := range vs.Validators { + res = res.Add(val.Power) + } + return res +} + +// Helper function for adding new validator +func (vs *ValidatorSet) AddValidator(val Validator) { + vs.Validators = append(vs.Validators, val) +} + +// Helper function for removing exsting validator +func (vs *ValidatorSet) RemoveValidator(addr sdk.Address) { + pos := -1 + for i, val := range vs.Validators { + if bytes.Equal(val.Address, addr) { + pos = i + break + } + } + if pos == -1 { + return + } + vs.Validators = append(vs.Validators[:pos], vs.Validators[pos+1:]...) +} + +// Implements sdk.ValidatorSet +func (vs *ValidatorSet) Slash(ctx sdk.Context, pubkey crypto.PubKey, height int64, amt sdk.Rat) { + panic("not implemented") +} + +// Implements sdk.ValidatorSet +func (vs *ValidatorSet) Revoke(ctx sdk.Context, pubkey crypto.PubKey) { + panic("not implemented") +} + +// Implements sdk.ValidatorSet +func (vs *ValidatorSet) Unrevoke(ctx sdk.Context, pubkey crypto.PubKey) { + panic("not implemented") +} diff --git a/examples/democoin/x/assoc/validator_set.go b/examples/democoin/x/assoc/validator_set.go new file mode 100644 index 0000000000..019ac87952 --- /dev/null +++ b/examples/democoin/x/assoc/validator_set.go @@ -0,0 +1,107 @@ +package assoc + +import ( + "bytes" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/wire" +) + +// ValidatorSet defines +type ValidatorSet struct { + sdk.ValidatorSet + + key sdk.KVStoreGetter + cdc *wire.Codec + + maxAssoc int + addrLen int +} + +var _ sdk.ValidatorSet = ValidatorSet{} + +// NewValidatorSet returns new ValidatorSet with underlying ValidatorSet +func NewValidatorSet(cdc *wire.Codec, key sdk.KVStoreGetter, valset sdk.ValidatorSet, maxAssoc int, addrLen int) ValidatorSet { + if maxAssoc < 0 || addrLen < 0 { + panic("Cannot use negative integer for NewValidatorSet") + } + return ValidatorSet{ + ValidatorSet: valset, + + key: key, + cdc: cdc, + + maxAssoc: maxAssoc, + addrLen: addrLen, + } +} + +// Implements sdk.ValidatorSet +func (valset ValidatorSet) Validator(ctx sdk.Context, addr sdk.Address) (res sdk.Validator) { + store := valset.key.KVStore(ctx) + base := store.Get(GetBaseKey(addr)) + res = valset.ValidatorSet.Validator(ctx, base) + if res == nil { + res = valset.ValidatorSet.Validator(ctx, addr) + } + return +} + +// GetBaseKey :: sdk.Address -> sdk.Address +func GetBaseKey(addr sdk.Address) []byte { + return append([]byte{0x00}, addr...) +} + +// GetAssocPrefix :: sdk.Address -> (sdk.Address -> byte) +func GetAssocPrefix(base sdk.Address) []byte { + return append([]byte{0x01}, base...) +} + +// GetAssocKey :: (sdk.Address, sdk.Address) -> byte +func GetAssocKey(base sdk.Address, assoc sdk.Address) []byte { + return append(append([]byte{0x01}, base...), assoc...) +} + +// Associate associates new address with validator address +func (valset ValidatorSet) Associate(ctx sdk.Context, base sdk.Address, assoc sdk.Address) bool { + if len(base) != valset.addrLen || len(assoc) != valset.addrLen { + return false + } + store := valset.key.KVStore(ctx) + // If someone already owns the associated address + if store.Get(GetBaseKey(assoc)) != nil { + return false + } + store.Set(GetBaseKey(assoc), base) + store.Set(GetAssocKey(base, assoc), []byte{0x00}) + return true +} + +// Dissociate removes association between addresses +func (valset ValidatorSet) Dissociate(ctx sdk.Context, base sdk.Address, assoc sdk.Address) bool { + if len(base) != valset.addrLen || len(assoc) != valset.addrLen { + return false + } + store := valset.key.KVStore(ctx) + // No associated address found for given validator + if !bytes.Equal(store.Get(GetBaseKey(assoc)), base) { + return false + } + store.Delete(GetBaseKey(assoc)) + store.Delete(GetAssocKey(base, assoc)) + return true +} + +// Associations returns all associated addresses with a validator +func (valset ValidatorSet) Associations(ctx sdk.Context, base sdk.Address) (res []sdk.Address) { + store := valset.key.KVStore(ctx) + res = make([]sdk.Address, valset.maxAssoc) + iter := sdk.KVStorePrefixIterator(store, GetAssocPrefix(base)) + i := 0 + for ; iter.Valid(); iter.Next() { + key := iter.Key() + res[i] = key[len(key)-valset.addrLen:] + i++ + } + return res[:i] +} diff --git a/examples/democoin/x/assoc/validator_set_test.go b/examples/democoin/x/assoc/validator_set_test.go new file mode 100644 index 0000000000..014f5650c6 --- /dev/null +++ b/examples/democoin/x/assoc/validator_set_test.go @@ -0,0 +1,71 @@ +package assoc + +import ( + "bytes" + "testing" + + "github.com/stretchr/testify/assert" + + abci "github.com/tendermint/abci/types" + dbm "github.com/tendermint/tmlibs/db" + + "github.com/cosmos/cosmos-sdk/examples/democoin/mock" + "github.com/cosmos/cosmos-sdk/store" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/wire" +) + +func defaultContext(key sdk.StoreKey) sdk.Context { + db := dbm.NewMemDB() + cms := store.NewCommitMultiStore(db) + cms.MountStoreWithDB(key, sdk.StoreTypeIAVL, db) + cms.LoadLatestVersion() + ctx := sdk.NewContext(cms, abci.Header{}, false, nil) + return ctx +} + +func TestValidatorSet(t *testing.T) { + key := sdk.NewKVStoreKey("test") + ctx := defaultContext(key) + + addr1 := []byte("addr1") + addr2 := []byte("addr2") + + base := &mock.ValidatorSet{[]mock.Validator{ + {addr1, sdk.NewRat(1)}, + {addr2, sdk.NewRat(2)}, + }} + + valset := NewValidatorSet(wire.NewCodec(), sdk.NewPrefixStoreGetter(key, []byte("assoc")), base, 1, 5) + + assert.Equal(t, base.Validator(ctx, addr1), valset.Validator(ctx, addr1)) + assert.Equal(t, base.Validator(ctx, addr2), valset.Validator(ctx, addr2)) + + assoc1 := []byte("asso1") + assoc2 := []byte("asso2") + + assert.True(t, valset.Associate(ctx, addr1, assoc1)) + assert.True(t, valset.Associate(ctx, addr2, assoc2)) + + assert.Equal(t, base.Validator(ctx, addr1), valset.Validator(ctx, assoc1)) + assert.Equal(t, base.Validator(ctx, addr2), valset.Validator(ctx, assoc2)) + + assert.Equal(t, base.Validator(ctx, addr1), valset.Validator(ctx, addr1)) + assert.Equal(t, base.Validator(ctx, addr2), valset.Validator(ctx, addr2)) + + assocs := valset.Associations(ctx, addr1) + assert.Equal(t, 1, len(assocs)) + assert.True(t, bytes.Equal(assoc1, assocs[0])) + + assert.False(t, valset.Associate(ctx, addr1, assoc2)) + assert.False(t, valset.Associate(ctx, addr2, assoc1)) + + valset.Dissociate(ctx, addr1, assoc1) + valset.Dissociate(ctx, addr2, assoc2) + + assert.Equal(t, base.Validator(ctx, addr1), valset.Validator(ctx, addr1)) + assert.Equal(t, base.Validator(ctx, addr2), valset.Validator(ctx, addr2)) + + assert.Nil(t, valset.Validator(ctx, assoc1)) + assert.Nil(t, valset.Validator(ctx, assoc2)) +} diff --git a/examples/democoin/x/oracle/README.md b/examples/democoin/x/oracle/README.md new file mode 100644 index 0000000000..eec02d7249 --- /dev/null +++ b/examples/democoin/x/oracle/README.md @@ -0,0 +1,58 @@ +# Oracle Module + +`x/oracle` provides a way to receive external information(real world price, events from other chains, etc.) with validators' vote. Each validator make transaction which contains those informations, and Oracle aggregates them until the supermajority signed on it. After then, Oracle sends the information to the actual module that processes the information, and prune the votes from the state. + +## Integration + +See `x/oracle/oracle_test.go` for the code that using Oracle + +To use Oracle in your module, first define a `payload`. It should implement `oracle.Payload` and contain nessesary information for your module. Including nonce is recommended. + +```go +type MyPayload struct { + Data int + Nonce int +} +``` + +When you write a payload, its `.Type()` should return same name with your module is registered on the router. It is because `oracle.Msg` inherits `.Type()` from its embedded payload and it should be handled on the user modules. + +Then route every incoming `oracle.Msg` to `oracle.Keeper.Handler()` with the function that implements `oracle.Handler`. + +```go +func NewHandler(keeper Keeper) sdk.Handler { + return func(ctx sdk.Context, msg sdk.Msg) sdk.Result { + switch msg := msg.(type) { + case oracle.Msg: + return keeper.oracle.Handle(ctx sdk.Context, p oracle.Payload) sdk.Error { + switch p := p.(type) { + case MyPayload: + return handleMyPayload(ctx, keeper, p) + } + } + } + } +} +``` + +In the previous example, the keeper has an `oracle.Keeper`. `oracle.Keeper`s are generated by `NewKeeper`. + +```go +func NewKeeper(key sdk.StoreKey, cdc *wire.Codec, valset sdk.ValidatorSet, supermaj sdk.Rat, timeout int64) Keeper { + return Keeper { + cdc: cdc, + key: key, + + // ValidatorSet to get validators infor + valset: valset, + + // The keeper will pass payload + // when more than 2/3 signed on it + supermaj: supermaj, + // The keeper will prune votes after 100 blocks from last sign + timeout: timeout, + } +} +``` + +Now the validators can send `oracle.Msg`s with `MyPayload` when they want to witness external events. diff --git a/examples/democoin/x/oracle/errors.go b/examples/democoin/x/oracle/errors.go new file mode 100644 index 0000000000..0507ed2266 --- /dev/null +++ b/examples/democoin/x/oracle/errors.go @@ -0,0 +1,31 @@ +package oracle + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" +) + +// Oracle errors reserve 1101-1199 +const ( + CodeNotValidator sdk.CodeType = 1101 + CodeAlreadyProcessed sdk.CodeType = 1102 + CodeAlreadySigned sdk.CodeType = 1103 + CodeUnknownRequest sdk.CodeType = sdk.CodeUnknownRequest +) + +// ---------------------------------------- +// Error constructors + +// ErrNotValidator called when the signer of a Msg is not a validator +func ErrNotValidator(codespace sdk.CodespaceType, address sdk.Address) sdk.Error { + return sdk.NewError(codespace, CodeNotValidator, address.String()) +} + +// ErrAlreadyProcessed called when a payload is already processed +func ErrAlreadyProcessed(codespace sdk.CodespaceType) sdk.Error { + return sdk.NewError(codespace, CodeAlreadyProcessed, "") +} + +// ErrAlreadySigned called when the signer is trying to double signing +func ErrAlreadySigned(codespace sdk.CodespaceType) sdk.Error { + return sdk.NewError(codespace, CodeAlreadySigned, "") +} diff --git a/examples/democoin/x/oracle/handler.go b/examples/democoin/x/oracle/handler.go new file mode 100644 index 0000000000..8b94a18940 --- /dev/null +++ b/examples/democoin/x/oracle/handler.go @@ -0,0 +1,105 @@ +package oracle + +import ( + "bytes" + + sdk "github.com/cosmos/cosmos-sdk/types" +) + +// Handler handles payload after it passes voting process +type Handler func(ctx sdk.Context, p Payload) sdk.Error + +func (keeper Keeper) update(ctx sdk.Context, val sdk.Validator, valset sdk.ValidatorSet, p Payload, info Info) Info { + info.Power = info.Power.Add(val.GetPower()) + + // Return if the voted power is not bigger than required power + totalPower := valset.TotalPower(ctx) + requiredPower := totalPower.Mul(keeper.supermaj) + if !info.Power.GT(requiredPower) { + return info + } + + // Check if the validators hash has been changed during the vote process + // and recalculate voted power + hash := ctx.BlockHeader().ValidatorsHash + if !bytes.Equal(hash, info.Hash) { + info.Power = sdk.ZeroRat() + info.Hash = hash + prefix := GetSignPrefix(p, keeper.cdc) + store := keeper.key.KVStore(ctx) + iter := sdk.KVStorePrefixIterator(store, prefix) + for ; iter.Valid(); iter.Next() { + if valset.Validator(ctx, iter.Value()) != nil { + store.Delete(iter.Key()) + continue + } + info.Power = info.Power.Add(val.GetPower()) + } + if !info.Power.GT(totalPower.Mul(keeper.supermaj)) { + return info + } + } + + info.Status = Processed + return info +} + +// Handle is used by other modules to handle Msg +func (keeper Keeper) Handle(h Handler, ctx sdk.Context, o Msg, codespace sdk.CodespaceType) sdk.Result { + valset := keeper.valset + + signer := o.Signer + payload := o.Payload + + // Check the oracle is not in process + info := keeper.Info(ctx, payload) + if info.Status != Pending { + return ErrAlreadyProcessed(codespace).Result() + } + + // Check if it is reporting timeout + now := ctx.BlockHeight() + if now > info.LastSigned+keeper.timeout { + info = Info{Status: Timeout} + keeper.setInfo(ctx, payload, info) + keeper.clearSigns(ctx, payload) + return sdk.Result{} + } + info.LastSigned = ctx.BlockHeight() + + // Check the signer is a validater + val := valset.Validator(ctx, signer) + if val == nil { + return ErrNotValidator(codespace, signer).Result() + } + + // Check double signing + if keeper.signed(ctx, payload, signer) { + return ErrAlreadySigned(codespace).Result() + } + + keeper.sign(ctx, payload, signer) + + info = keeper.update(ctx, val, valset, payload, info) + if info.Status == Processed { + info = Info{Status: Processed} + } + + keeper.setInfo(ctx, payload, info) + + if info.Status == Processed { + keeper.clearSigns(ctx, payload) + cctx, write := ctx.CacheContext() + err := h(cctx, payload) + if err != nil { + return sdk.Result{ + Code: sdk.ABCICodeOK, + Log: err.ABCILog(), + } + } + write() + + } + + return sdk.Result{} +} diff --git a/examples/democoin/x/oracle/keeper.go b/examples/democoin/x/oracle/keeper.go new file mode 100644 index 0000000000..cc15ffdc80 --- /dev/null +++ b/examples/democoin/x/oracle/keeper.go @@ -0,0 +1,111 @@ +package oracle + +import ( + "github.com/cosmos/cosmos-sdk/wire" + + sdk "github.com/cosmos/cosmos-sdk/types" +) + +// Keeper of the oracle store +type Keeper struct { + key sdk.KVStoreGetter + cdc *wire.Codec + + valset sdk.ValidatorSet + + supermaj sdk.Rat + timeout int64 +} + +// NewKeeper constructs a new keeper +func NewKeeper(key sdk.KVStoreGetter, cdc *wire.Codec, valset sdk.ValidatorSet, supermaj sdk.Rat, timeout int64) Keeper { + if timeout < 0 { + panic("Timeout should not be negative") + } + + return Keeper{ + key: key, + cdc: cdc, + + valset: valset, + + supermaj: supermaj, + timeout: timeout, + } +} + +// InfoStatus - current status of an Info +type InfoStatus int8 + +// Define InfoStatus +const ( + Pending = InfoStatus(iota) + Processed + Timeout +) + +// Info for each payload +type Info struct { + Power sdk.Rat + Hash []byte + LastSigned int64 + Status InfoStatus +} + +// EmptyInfo construct an empty Info +func EmptyInfo(ctx sdk.Context) Info { + return Info{ + Power: sdk.ZeroRat(), + Hash: ctx.BlockHeader().ValidatorsHash, + LastSigned: ctx.BlockHeight(), + Status: Pending, + } +} + +// Info returns the information about a payload +func (keeper Keeper) Info(ctx sdk.Context, p Payload) (res Info) { + store := keeper.key.KVStore(ctx) + + key := GetInfoKey(p, keeper.cdc) + bz := store.Get(key) + if bz == nil { + return EmptyInfo(ctx) + } + keeper.cdc.MustUnmarshalBinary(bz, &res) + + return +} + +func (keeper Keeper) setInfo(ctx sdk.Context, p Payload, info Info) { + store := keeper.key.KVStore(ctx) + + key := GetInfoKey(p, keeper.cdc) + bz := keeper.cdc.MustMarshalBinary(info) + store.Set(key, bz) +} + +func (keeper Keeper) sign(ctx sdk.Context, p Payload, signer sdk.Address) { + store := keeper.key.KVStore(ctx) + + key := GetSignKey(p, signer, keeper.cdc) + store.Set(key, signer) +} + +func (keeper Keeper) signed(ctx sdk.Context, p Payload, signer sdk.Address) bool { + store := keeper.key.KVStore(ctx) + + key := GetSignKey(p, signer, keeper.cdc) + return store.Has(key) +} + +func (keeper Keeper) clearSigns(ctx sdk.Context, p Payload) { + store := keeper.key.KVStore(ctx) + + prefix := GetSignPrefix(p, keeper.cdc) + + iter := sdk.KVStorePrefixIterator(store, prefix) + for ; iter.Valid(); iter.Next() { + store.Delete(iter.Key()) + } + iter.Close() +} diff --git a/examples/democoin/x/oracle/keeper_keys.go b/examples/democoin/x/oracle/keeper_keys.go new file mode 100644 index 0000000000..8c62c95de8 --- /dev/null +++ b/examples/democoin/x/oracle/keeper_keys.go @@ -0,0 +1,23 @@ +package oracle + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/wire" +) + +// GetInfoKey returns the key for OracleInfo +func GetInfoKey(p Payload, cdc *wire.Codec) []byte { + bz := cdc.MustMarshalBinary(p) + return append([]byte{0x00}, bz...) +} + +// GetSignPrefix returns the prefix for signs +func GetSignPrefix(p Payload, cdc *wire.Codec) []byte { + bz := cdc.MustMarshalBinary(p) + return append([]byte{0x01}, bz...) +} + +// GetSignKey returns the key for sign +func GetSignKey(p Payload, signer sdk.Address, cdc *wire.Codec) []byte { + return append(GetSignPrefix(p, cdc), signer...) +} diff --git a/examples/democoin/x/oracle/oracle_test.go b/examples/democoin/x/oracle/oracle_test.go new file mode 100644 index 0000000000..27c5aa08b2 --- /dev/null +++ b/examples/democoin/x/oracle/oracle_test.go @@ -0,0 +1,221 @@ +package oracle + +import ( + "encoding/json" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + abci "github.com/tendermint/abci/types" + dbm "github.com/tendermint/tmlibs/db" + + "github.com/cosmos/cosmos-sdk/examples/democoin/mock" + "github.com/cosmos/cosmos-sdk/store" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/wire" +) + +func defaultContext(keys ...sdk.StoreKey) sdk.Context { + db := dbm.NewMemDB() + cms := store.NewCommitMultiStore(db) + for _, key := range keys { + cms.MountStoreWithDB(key, sdk.StoreTypeIAVL, db) + } + cms.LoadLatestVersion() + ctx := sdk.NewContext(cms, abci.Header{}, false, nil) + return ctx +} + +type seqOracle struct { + Seq int + Nonce int +} + +func (o seqOracle) Type() string { + return "seq" +} + +func (o seqOracle) ValidateBasic() sdk.Error { + return nil +} + +func makeCodec() *wire.Codec { + var cdc = wire.NewCodec() + + cdc.RegisterInterface((*sdk.Msg)(nil), nil) + cdc.RegisterConcrete(Msg{}, "test/Oracle", nil) + + cdc.RegisterInterface((*Payload)(nil), nil) + cdc.RegisterConcrete(seqOracle{}, "test/oracle/seqOracle", nil) + + return cdc +} + +func seqHandler(ork Keeper, key sdk.StoreKey, codespace sdk.CodespaceType) sdk.Handler { + return func(ctx sdk.Context, msg sdk.Msg) sdk.Result { + switch msg := msg.(type) { + case Msg: + return ork.Handle(func(ctx sdk.Context, p Payload) sdk.Error { + switch p := p.(type) { + case seqOracle: + return handleSeqOracle(ctx, key, p) + default: + return sdk.ErrUnknownRequest("") + } + }, ctx, msg, codespace) + default: + return sdk.ErrUnknownRequest("").Result() + } + } +} + +func getSequence(ctx sdk.Context, key sdk.StoreKey) int { + store := ctx.KVStore(key) + seqbz := store.Get([]byte("seq")) + + var seq int + if seqbz == nil { + seq = 0 + } else { + wire.NewCodec().MustUnmarshalBinary(seqbz, &seq) + } + + return seq +} + +func handleSeqOracle(ctx sdk.Context, key sdk.StoreKey, o seqOracle) sdk.Error { + store := ctx.KVStore(key) + + seq := getSequence(ctx, key) + if seq != o.Seq { + return sdk.NewError(sdk.CodespaceUndefined, 1, "") + } + + bz := wire.NewCodec().MustMarshalBinary(seq + 1) + store.Set([]byte("seq"), bz) + + return nil +} + +func TestOracle(t *testing.T) { + cdc := makeCodec() + + addr1 := []byte("addr1") + addr2 := []byte("addr2") + addr3 := []byte("addr3") + addr4 := []byte("addr4") + valset := &mock.ValidatorSet{[]mock.Validator{ + mock.Validator{addr1, sdk.NewRat(7)}, + mock.Validator{addr2, sdk.NewRat(7)}, + mock.Validator{addr3, sdk.NewRat(1)}, + }} + + key := sdk.NewKVStoreKey("testkey") + ctx := defaultContext(key) + + bz, err := json.Marshal(valset) + require.Nil(t, err) + ctx = ctx.WithBlockHeader(abci.Header{ValidatorsHash: bz}) + + ork := NewKeeper(sdk.NewPrefixStoreGetter(key, []byte("oracle")), cdc, valset, sdk.NewRat(2, 3), 100) + h := seqHandler(ork, key, sdk.CodespaceUndefined) + + // Nonmock.Validator signed, transaction failed + msg := Msg{seqOracle{0, 0}, []byte("randomguy")} + res := h(ctx, msg) + assert.False(t, res.IsOK()) + assert.Equal(t, 0, getSequence(ctx, key)) + + // Less than 2/3 signed, msg not processed + msg.Signer = addr1 + res = h(ctx, msg) + assert.True(t, res.IsOK()) + assert.Equal(t, 0, getSequence(ctx, key)) + + // Double signed, transaction failed + res = h(ctx, msg) + assert.False(t, res.IsOK()) + assert.Equal(t, 0, getSequence(ctx, key)) + + // More than 2/3 signed, msg processed + msg.Signer = addr2 + res = h(ctx, msg) + assert.True(t, res.IsOK()) + assert.Equal(t, 1, getSequence(ctx, key)) + + // Already processed, transaction failed + msg.Signer = addr3 + res = h(ctx, msg) + assert.False(t, res.IsOK()) + assert.Equal(t, 1, getSequence(ctx, key)) + + // Less than 2/3 signed, msg not processed + msg = Msg{seqOracle{100, 1}, addr1} + res = h(ctx, msg) + assert.True(t, res.IsOK()) + assert.Equal(t, 1, getSequence(ctx, key)) + + // More than 2/3 signed but payload is invalid + msg.Signer = addr2 + res = h(ctx, msg) + assert.True(t, res.IsOK()) + assert.NotEqual(t, "", res.Log) + assert.Equal(t, 1, getSequence(ctx, key)) + + // Already processed, transaction failed + msg.Signer = addr3 + res = h(ctx, msg) + assert.False(t, res.IsOK()) + assert.Equal(t, 1, getSequence(ctx, key)) + + // Should handle mock.Validator set change + valset.AddValidator(mock.Validator{addr4, sdk.NewRat(12)}) + bz, err = json.Marshal(valset) + require.Nil(t, err) + ctx = ctx.WithBlockHeader(abci.Header{ValidatorsHash: bz}) + + // Less than 2/3 signed, msg not processed + msg = Msg{seqOracle{1, 2}, addr1} + res = h(ctx, msg) + assert.True(t, res.IsOK()) + assert.Equal(t, 1, getSequence(ctx, key)) + + // Less than 2/3 signed, msg not processed + msg.Signer = addr2 + res = h(ctx, msg) + assert.True(t, res.IsOK()) + assert.Equal(t, 1, getSequence(ctx, key)) + + // More than 2/3 signed, msg processed + msg.Signer = addr4 + res = h(ctx, msg) + assert.True(t, res.IsOK()) + assert.Equal(t, 2, getSequence(ctx, key)) + + // Should handle mock.Validator set change while oracle process is happening + msg = Msg{seqOracle{2, 3}, addr4} + + // Less than 2/3 signed, msg not processed + res = h(ctx, msg) + assert.True(t, res.IsOK()) + assert.Equal(t, 2, getSequence(ctx, key)) + + // Signed mock.Validator is kicked out + valset.RemoveValidator(addr4) + bz, err = json.Marshal(valset) + require.Nil(t, err) + ctx = ctx.WithBlockHeader(abci.Header{ValidatorsHash: bz}) + + // Less than 2/3 signed, msg not processed + msg.Signer = addr1 + res = h(ctx, msg) + assert.True(t, res.IsOK()) + assert.Equal(t, 2, getSequence(ctx, key)) + + // More than 2/3 signed, msg processed + msg.Signer = addr2 + res = h(ctx, msg) + assert.True(t, res.IsOK()) + assert.Equal(t, 3, getSequence(ctx, key)) +} diff --git a/examples/democoin/x/oracle/types.go b/examples/democoin/x/oracle/types.go new file mode 100644 index 0000000000..041b9ab349 --- /dev/null +++ b/examples/democoin/x/oracle/types.go @@ -0,0 +1,33 @@ +package oracle + +import ( + "encoding/json" + + sdk "github.com/cosmos/cosmos-sdk/types" +) + +// Msg - struct for voting on payloads +type Msg struct { + Payload + Signer sdk.Address +} + +// GetSignBytes implements sdk.Msg +func (msg Msg) GetSignBytes() []byte { + bz, err := json.Marshal(msg) + if err != nil { + panic(err) + } + return bz +} + +// GetSigners implements sdk.Msg +func (msg Msg) GetSigners() []sdk.Address { + return []sdk.Address{msg.Signer} +} + +// Payload defines inner data for actual execution +type Payload interface { + Type() string + ValidateBasic() sdk.Error +} diff --git a/store/prefixstore.go b/store/prefixstore.go index 8c0624f35b..c9f124a889 100644 --- a/store/prefixstore.go +++ b/store/prefixstore.go @@ -46,14 +46,24 @@ func (s prefixStore) Prefix(prefix []byte) KVStore { // Implements KVStore func (s prefixStore) Iterator(start, end []byte) Iterator { + if end == nil { + end = sdk.PrefixEndBytes(s.prefix) + } else { + end = append(s.prefix, end...) + } return prefixIterator{ prefix: s.prefix, - iter: s.store.Iterator(start, end), + iter: s.store.Iterator(append(s.prefix, start...), end), } } // Implements KVStore func (s prefixStore) ReverseIterator(start, end []byte) Iterator { + if end == nil { + end = sdk.PrefixEndBytes(s.prefix) + } else { + end = append(s.prefix, end...) + } return prefixIterator{ prefix: s.prefix, iter: s.store.ReverseIterator(start, end),