diff --git a/x/oracle/client/cli/tx.go b/x/oracle/client/cli/tx.go index 4a60f6866..9c47e93cf 100644 --- a/x/oracle/client/cli/tx.go +++ b/x/oracle/client/cli/tx.go @@ -29,6 +29,7 @@ func GetTxCmd() *cobra.Command { } cmd.AddCommand(CmdCreatePrice()) + cmd.AddCommand(CmdUpdateParams()) // this line is used by starport scaffolding # 1 return cmd diff --git a/x/oracle/client/cli/tx_create_price.go b/x/oracle/client/cli/tx_create_price.go index b308de70e..54150c078 100644 --- a/x/oracle/client/cli/tx_create_price.go +++ b/x/oracle/client/cli/tx_create_price.go @@ -18,8 +18,7 @@ func CmdCreatePrice() *cobra.Command { // TODO: support v1 single sourceID for temporary Use: "create-price feederid basedblock nonce sourceid decimal price timestamp detid optinoal(price timestamp detid) optional(desc)", Short: "Broadcast message create-price", - // Args: cobra.ExactArgs(0), - Args: cobra.MinimumNArgs(8), + Args: cobra.MinimumNArgs(8), RunE: func(cmd *cobra.Command, args []string) (err error) { clientCtx, err := client.GetClientTxContext(cmd) if err != nil { diff --git a/x/oracle/client/cli/tx_update_params.go b/x/oracle/client/cli/tx_update_params.go new file mode 100644 index 000000000..bd92f8d68 --- /dev/null +++ b/x/oracle/client/cli/tx_update_params.go @@ -0,0 +1,33 @@ +package cli + +import ( + "github.com/ExocoreNetwork/exocore/x/oracle/types" + "github.com/cosmos/cosmos-sdk/client" + "github.com/cosmos/cosmos-sdk/client/flags" + "github.com/cosmos/cosmos-sdk/client/tx" + "github.com/spf13/cobra" +) + +func CmdUpdateParams() *cobra.Command { + cmd := &cobra.Command{ + // TODO: support v1 single sourceID for temporary + Use: "update-params params", + Short: "Broadcast message update-params", + Args: cobra.MinimumNArgs(1), + RunE: func(cmd *cobra.Command, args []string) (err error) { + clientCtx, err := client.GetClientTxContext(cmd) + if err != nil { + return err + } + msg := types.NewMsgUpdateParams(clientCtx.GetFromAddress().String(), args[0]) + if err := msg.ValidateBasic(); err != nil { + return err + } + return tx.GenerateOrBroadcastTxCLI(clientCtx, cmd.Flags(), msg) + }, + } + + flags.AddTxFlagsToCmd(cmd) + + return cmd +} diff --git a/x/oracle/keeper/keeper.go b/x/oracle/keeper/keeper.go index 443d365c5..ee38eca98 100644 --- a/x/oracle/keeper/keeper.go +++ b/x/oracle/keeper/keeper.go @@ -19,7 +19,7 @@ type ( storeKey storetypes.StoreKey memKey storetypes.StoreKey paramstore paramtypes.Subspace - authority string + // authority string common.KeeperDogfood } ) diff --git a/x/oracle/keeper/msg_server_update_params.go b/x/oracle/keeper/msg_server_update_params.go index 3729e996d..a825bedba 100644 --- a/x/oracle/keeper/msg_server_update_params.go +++ b/x/oracle/keeper/msg_server_update_params.go @@ -5,18 +5,51 @@ import ( "github.com/ExocoreNetwork/exocore/x/oracle/types" sdk "github.com/cosmos/cosmos-sdk/types" - govtypes "github.com/cosmos/cosmos-sdk/x/gov/types" ) func (ms msgServer) UpdateParams(goCtx context.Context, msg *types.MsgUpdateParams) (*types.MsgUpdateParamsResponse, error) { ctx := sdk.UnwrapSDKContext(goCtx) - if ms.authority != msg.Authority { - return nil, govtypes.ErrInvalidSigner.Wrapf("invalid authority; expected %s, got %s", ms.authority, msg.Authority) - } - - // store params - ms.SetParams(ctx, msg.Params) + // TODO: skip the authority check for test + // if ms.authority != msg.Authority { + // return nil, govtypes.ErrInvalidSigner.Wrapf("invalid authority; expected %s, got %s", ms.authority, msg.Authority) + // } + p := ms.GetParams(ctx) + var err error + defer func() { + if err != nil { + ms.Logger(ctx).Error("UpdateParams failed", "error", err) + } + }() + height := uint64(ctx.BlockHeight()) + // add sources + if p, err = p.AddSources(msg.Params.Sources...); err != nil { + return nil, err + } + // add chains + if p, err = p.AddChains(msg.Params.Chains...); err != nil { + return nil, err + } + // add tokens + if p, err = p.UpdateTokens(height, msg.Params.Tokens...); err != nil { + return nil, err + } + // add rules + if p, err = p.AddRules(msg.Params.Rules...); err != nil { + return nil, err + } + // udpate tokenFeeders + for _, tokenFeeder := range msg.Params.TokenFeeders { + if p, err = p.UpdateTokenFeeder(tokenFeeder, height); err != nil { + return nil, err + } + } + // validate params + if err = p.Validate(); err != nil { + return nil, err + } + // set updated new params + ms.SetParams(ctx, p) return &types.MsgUpdateParamsResponse{}, nil } diff --git a/x/oracle/keeper/msg_server_update_params_test.go b/x/oracle/keeper/msg_server_update_params_test.go new file mode 100644 index 000000000..666e27f26 --- /dev/null +++ b/x/oracle/keeper/msg_server_update_params_test.go @@ -0,0 +1,155 @@ +package keeper_test + +import ( + "github.com/ExocoreNetwork/exocore/x/oracle/types" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("MsgUpdateParams", func() { + var defaultParams types.Params + BeforeEach(func() { + ks.Reset() + Expect(ks.ms).ToNot(BeNil()) + defaultParams = ks.k.GetParams(ks.ctx) + }) + + Context("Update Chains", func() { + inputAddChains := []string{ + `{"chains":[{"name":"Bitcoin", "desc":"-"}]}`, + `{"chains":[{"name":"Ethereum", "desc":"-"}]}`, + } + It("add chain with new name", func() { + msg := types.NewMsgUpdateParams("", inputAddChains[0]) + _, err := ks.ms.UpdateParams(ks.ctx, msg) + Expect(err).Should(BeNil()) + p := ks.k.GetParams(ks.ctx) + Expect(p.Chains[2].Name).Should(BeEquivalentTo("Bitcoin")) + + }) + It("add chain with duplicated name", func() { + _, err := ks.ms.UpdateParams(ks.ctx, types.NewMsgUpdateParams("", inputAddChains[1])) + Expect(err).Should(MatchError(types.ErrInvalidParams.Wrap("invalid source to add, duplicated"))) + }) + }) + Context("Update Sources", func() { + inputAddSources := []string{ + `{"sources":[{"name":"CoinGecko", "desc":"-", "valid":true}]}`, + `{"sources":[{"name":"CoinGecko", "desc":"-"}]}`, + `{"sources":[{"name":"Chainlink", "desc":"-", "valid":true}]}`, + } + It("add valid source with new name", func() { + _, err := ks.ms.UpdateParams(ks.ctx, types.NewMsgUpdateParams("", inputAddSources[0])) + Expect(err).Should(BeNil()) + p := ks.k.GetParams(ks.ctx) + Expect(p.Sources[2].Name).Should(BeEquivalentTo("CoinGecko")) + }) + It("add invalid source with new name", func() { + _, err := ks.ms.UpdateParams(ks.ctx, types.NewMsgUpdateParams("", inputAddSources[1])) + Expect(err).Should(MatchError(types.ErrInvalidParams.Wrap("invalid source to add, new source should be valid"))) + }) + It("add source with duplicated name", func() { + _, err := ks.ms.UpdateParams(ks.ctx, types.NewMsgUpdateParams("", inputAddSources[2])) + Expect(err).Should(MatchError(types.ErrInvalidParams.Wrap("invalid source to add, duplicated"))) + }) + }) + Context("Update Tokens", func() { + startBasedBlocks := []uint64{1, 3, 3, 3, 1, 1, 1} + inputUpdateTokens := []string{ + `{"tokens":[{"name":"UNI", "chain_id":"1"}]}`, + `{"tokens":[{"name":"ETH", "chain_id":"1", "decimal":8}]}`, + `{"tokens":[{"name":"ETH", "chain_id":"1", "asset_id":"assetID"}]}`, + `{"tokens":[{"name":"ETH", "chain_id":"1", "contract_address":"contractAddress"}]}`, + `{"tokens":[{"name":"ETH", "chain_id":"1", "decimal":8}]}`, + `{"tokens":[{"name":"ETH", "chain_id":"0"}]}`, + `{"tokens":[{"name":"ETH", "chain_id":"3"}]}`, + } + errs := []error{ + nil, + nil, + nil, + nil, + nil, + types.ErrInvalidParams.Wrap("invalid token to add, chain not found"), + types.ErrInvalidParams.Wrap("invalid token to add, chain not found"), + } + token := types.DefaultParams().Tokens[1] + token1 := *token + token1.Decimal = 8 + + token2 := *token + token2.AssetID = "assetID" + + token3 := *token + token3.ContractAddress = "0x123" + + updatedTokenETH := []*types.Token{ + nil, + &token1, + &token2, + &token3, + token, + nil, + nil, + } + + for i, input := range inputUpdateTokens { + It("", func() { //}) + if startBasedBlocks[i] > 1 { + p := defaultParams + p.TokenFeeders[1].StartBaseBlock = startBasedBlocks[i] + ks.k.SetParams(ks.ctx, p) + } + _, err := ks.ms.UpdateParams(ks.ctx, types.NewMsgUpdateParams("", input)) + if errs[i] == nil { + Expect(err).Should(BeNil()) + } else { + Expect(err).Should(MatchError(errs[i])) + } + if updatedTokenETH[i] != nil { + p := ks.k.GetParams(ks.ctx) + Expect(p.Tokens[1]).Should(BeEquivalentTo(updatedTokenETH[i])) + } + }) + } + + }) + + Context("", func() { + It("update StartBaseBlock for TokenFeeder", func() { + p := defaultParams + p.TokenFeeders[1].StartBaseBlock = 10 + ks.k.SetParams(ks.ctx, p) + p.TokenFeeders[1].StartBaseBlock = 5 + _, err := ks.ms.UpdateParams(ks.ctx, &types.MsgUpdateParams{ + Params: types.Params{ + TokenFeeders: []*types.TokenFeeder{ + { + TokenID: 1, + StartBaseBlock: 5, + }, + }, + }, + }) + Expect(err).Should(BeNil()) + p = ks.k.GetParams(ks.ctx) + Expect(p.TokenFeeders[1].StartBaseBlock).Should(BeEquivalentTo(5)) + }) + It("Add AssetID for Token", func() { + _, err := ks.ms.UpdateParams(ks.ctx, &types.MsgUpdateParams{ + Params: types.Params{ + Tokens: []*types.Token{ + { + Name: "ETH", + ChainID: 1, + AssetID: "0x83e6850591425e3c1e263c054f4466838b9bd9e4_0x9ce1", + }, + }, + }, + }) + Expect(err).Should(BeNil()) + p := ks.k.GetParams(ks.ctx) + Expect(p.Tokens[1].AssetID).Should(BeEquivalentTo("0x83e6850591425e3c1e263c054f4466838b9bd9e4_0x9ce1")) + }) + }) +}) diff --git a/x/oracle/keeper/params_test.go b/x/oracle/keeper/params_test.go index 3fb9269b8..e205f1704 100644 --- a/x/oracle/keeper/params_test.go +++ b/x/oracle/keeper/params_test.go @@ -16,3 +16,202 @@ func TestGetParams(t *testing.T) { require.EqualValues(t, params, k.GetParams(ctx)) } + +func TestUpdateTokenFeeder(t *testing.T) { + tests := []struct { + name string + tokenFeeder types.TokenFeeder + height uint64 + err error + }{ + // invalid inputs + // fail when add/update fields, before validation + { + name: "invalid update, empty fields to update", + tokenFeeder: types.TokenFeeder{ + TokenID: 1, + }, + height: 1, + err: types.ErrInvalidParams.Wrap("invalid tokenFeeder to update, no valid field set"), + }, + { + name: "invalid udpate, for not-start feeder, set StartbaseBlock to history height", + tokenFeeder: types.TokenFeeder{ + TokenID: 1, + // set current height to 100 to test fail case + StartBaseBlock: 10, + }, + height: 100, + err: types.ErrInvalidParams.Wrap("invalid tokenFeeder to update, invalid StartBaseBlock"), + }, + { + name: "invalid update, for running feeder, set EndBlock to history height", + tokenFeeder: types.TokenFeeder{ + TokenID: 1, + // set current height to 2000000 to test fail case + EndBlock: 1500000, + }, + height: 2000000, + err: types.ErrInvalidParams.Wrap("invalid tokenFeeder to update, invalid EndBlock"), + }, + { + name: "invalid update, for stopped feeder, restart a feeder with wrong StartRoundID", + tokenFeeder: types.TokenFeeder{ + TokenID: 2, + RuleID: 1, + // set current height to 100 + StartBaseBlock: 1000, + // should be 4 + StartRoundID: 5, + Interval: 10, + }, + height: 100, + err: types.ErrInvalidParams.Wrap("invalid tokenFeeder to update"), + }, + // success adding/updating, but fail validation + { + name: "invalid update, for new feeder, EndBlock is not set properly", + tokenFeeder: types.TokenFeeder{ + TokenID: 3, + StartBaseBlock: 10, + StartRoundID: 1, + Interval: 10, + EndBlock: 51, + }, + height: 1, + err: types.ErrInvalidParams.Wrap("invalid tokenFeeder, invalid EndBlock"), + }, + { + name: "invalid update, for new feeder, tokenID not exists", + tokenFeeder: types.TokenFeeder{ + TokenID: 4, + StartBaseBlock: 10, + StartRoundID: 1, + Interval: 10, + EndBlock: 58, + }, + height: 1, + err: types.ErrInvalidParams.Wrap("invalid tokenFeeder, non-exist tokenID referred"), + }, + { + name: "invalid udpate, for existing feeder, feeder not started, and set endblock to history height", + tokenFeeder: types.TokenFeeder{ + TokenID: 2, + EndBlock: 5, + }, + height: 6, + err: types.ErrInvalidParams.Wrapf("invalid tokenFeeder to update, invalid EndBlock, currentHeight: %d, set: %d", 6, 5), + }, + { + name: "invalid update, for existing feeder, feeder started, and set endblock to history height including 0", + tokenFeeder: types.TokenFeeder{ + TokenID: 2, + }, + height: 11, + err: types.ErrInvalidParams.Wrapf("invalid tokenFeeder to update, invalid EndBlock, currentHeight: %d, set: %d", 11, 0), + }, + { + name: "invalid update, add a new feeder, set endblock>0 but <=startbasedBlock", + tokenFeeder: types.TokenFeeder{ + TokenID: 3, + StartBaseBlock: 10, + StartRoundID: 1, + Interval: 10, + EndBlock: 9, + }, + height: 1, + err: types.ErrInvalidParams.Wrapf("invalid TokenFeeder, invalid EndBlock to be set, startBaseBlock: %d, endBlock: %d", 10, 9), + }, + { + name: "invalid update, resume a stopped feeder, set startroundID incorrectly", + tokenFeeder: types.TokenFeeder{ + TokenID: 2, + RuleID: 1, + StartRoundID: 3, // should be 4 + StartBaseBlock: 51, + Interval: 10, + EndBlock: 0, + }, + height: 50, + err: types.ErrInvalidParams.Wrapf("invalid tokenFeeder to update, invalid StartBaseBlock or StartRoundID, currentHeight:%d, set_startBasedBlock:%d, set_StartRoundID:%d", 50, 51, 3), + }, + { + name: "invalid update, resume a stopped feeder, set startBasedBlock in history", + tokenFeeder: types.TokenFeeder{ + TokenID: 2, + RuleID: 1, + StartRoundID: 4, // should be 4 + StartBaseBlock: 50, + Interval: 10, + EndBlock: 0, + }, + height: 51, + err: types.ErrInvalidParams.Wrapf("invalid tokenFeeder to update, invalid StartBaseBlock or StartRoundID, currentHeight:%d, set_startBasedBlock:%d, set_StartRoundID:%d", 50, 51, 3), + }, + + // valid inputs + { + name: "valid update, new feeder", + tokenFeeder: types.TokenFeeder{ + TokenID: 3, + StartBaseBlock: 10, + StartRoundID: 1, + Interval: 10, + EndBlock: 19, + }, + height: 1, + err: nil, + }, + { + name: "valid update, resume a stopped feeder", + tokenFeeder: types.TokenFeeder{ + TokenID: 2, + RuleID: 1, + StartRoundID: 4, // should be 4 + StartBaseBlock: 51, + Interval: 10, + EndBlock: 0, + }, + height: 50, + err: nil, + }, + } + p := types.DefaultParams() + p.Tokens = append(p.Tokens, &types.Token{ + Name: "TEST", + ChainID: 1, + ContractAddress: "0x", + Decimal: 8, + Active: true, + AssetID: "", + }) + p.Tokens = append(p.Tokens, &types.Token{ + Name: "TEST_NEW", + ChainID: 1, + ContractAddress: "0x", + Decimal: 8, + Active: true, + AssetID: "", + }) + p.TokenFeeders = append(p.TokenFeeders, &types.TokenFeeder{ + TokenID: 2, + RuleID: 1, + StartRoundID: 1, + StartBaseBlock: 10, + Interval: 10, + EndBlock: 38, + }) + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + p, err := p.UpdateTokenFeeder(&tt.tokenFeeder, tt.height) + if err == nil { + err = p.Validate() + } + if tt.err != nil { + require.ErrorIs(t, err, tt.err) + } else { + require.NoError(t, err) + } + }) + } +} diff --git a/x/oracle/types/errors.go b/x/oracle/types/errors.go index 3bcd67bd3..d45033ce4 100644 --- a/x/oracle/types/errors.go +++ b/x/oracle/types/errors.go @@ -10,6 +10,7 @@ const ( invalidMsg = iota + 2 priceProposalIgnored priceProposalFormatInvalid + invalidParams getPriceFailedAssetNotFound getPriceFailedRoundNotFound ) @@ -19,6 +20,7 @@ var ( ErrInvalidMsg = sdkerrors.Register(ModuleName, invalidMsg, "invalid input create price") ErrPriceProposalIgnored = sdkerrors.Register(ModuleName, priceProposalIgnored, "price proposal ignored") ErrPriceProposalFormatInvalid = sdkerrors.Register(ModuleName, priceProposalFormatInvalid, "price proposal message format invalid") + ErrInvalidParams = sdkerrors.Register(ModuleName, invalidParams, "invalid params") ErrGetPriceAssetNotFound = sdkerrors.Register(ModuleName, getPriceFailedAssetNotFound, "get price failed for asset not found") ErrGetPriceRoundNotFound = sdkerrors.Register(ModuleName, getPriceFailedRoundNotFound, "get price failed for round not found") ) diff --git a/x/oracle/types/genesis_test.go b/x/oracle/types/genesis_test.go index 5447a4fe6..a0f72a457 100644 --- a/x/oracle/types/genesis_test.go +++ b/x/oracle/types/genesis_test.go @@ -48,6 +48,13 @@ func TestGenesisState_Validate(t *testing.T) { Block: 1, }, }, + Params: types.Params{ + MaxNonce: 3, + ThresholdA: 2, + ThresholdB: 3, + Mode: 1, + MaxDetId: 5, + }, // this line is used by starport scaffolding # types/genesis/validField }, valid: true, diff --git a/x/oracle/types/message_update_params.go b/x/oracle/types/message_update_params.go index 84822971f..0ce67e30f 100644 --- a/x/oracle/types/message_update_params.go +++ b/x/oracle/types/message_update_params.go @@ -2,6 +2,7 @@ package types import ( sdkerrors "cosmossdk.io/errors" + "github.com/cometbft/cometbft/libs/json" sdk "github.com/cosmos/cosmos-sdk/types" ) @@ -25,11 +26,12 @@ func (msg *MsgUpdateParams) GetSignBytes() []byte { } // ValidateBasic executes sanity validation on the provided data +// MsgUpdateParams is used to update params, the validation will mostly be stateful which is done by service func (msg *MsgUpdateParams) ValidateBasic() error { if _, err := sdk.AccAddressFromBech32(msg.Authority); err != nil { return sdkerrors.Wrap(err, "invalid authority address") } - return msg.Params.Validate() + return nil } // GetSigners returns the expected signers for a MsgUpdateParams message @@ -37,3 +39,14 @@ func (msg *MsgUpdateParams) GetSigners() []sdk.AccAddress { addr, _ := sdk.AccAddressFromBech32(msg.Authority) return []sdk.AccAddress{addr} } + +func NewMsgUpdateParams(creator, paramsJSON string) *MsgUpdateParams { + var p Params + if err := json.Unmarshal([]byte(paramsJSON), &p); err != nil { + panic("invalid json for params") + } + return &MsgUpdateParams{ + Authority: creator, + Params: p, + } +} diff --git a/x/oracle/types/params.go b/x/oracle/types/params.go index 9a69ccf36..71de7f9c6 100644 --- a/x/oracle/types/params.go +++ b/x/oracle/types/params.go @@ -24,7 +24,23 @@ func ParamKeyTable() paramtypes.KeyTable { // NewParams creates a new Params instance func NewParams() Params { - return Params{} + return Params{ + // maximum number of transactions be submitted in one round from a validator + MaxNonce: 1, + // maximum number of deteministic-source price can be submitted in one round from a validator + MaxDetId: 1, + // Mode is set to 1 for V1, means: + // For deteministic source, use consensus to find out valid final price, for non-deteministic source, use the latest price + // Final price will be confirmed as soon as the threshold is reached, and will ignore any furthur messages submitted with prices + Mode: 1, + ThresholdA: 2, + ThresholdB: 3, + Chains: []*Chain{{}}, + Tokens: []*Token{{}}, + Sources: []*Source{{}}, + Rules: []*RuleSource{{}}, + TokenFeeders: []*TokenFeeder{{}}, + } } // DefaultParams returns a default set of parameters @@ -45,6 +61,7 @@ func DefaultParams() Params { AssetID: "0x0b34c4d876cd569129cf56bafabb3f9e97a4ff42_0x9ce1", }, }, + // source defines where to fetch the prices Sources: []*Source{ { Name: "0 position is reserved", @@ -58,6 +75,7 @@ func DefaultParams() Params { Deterministic: true, }, }, + // rules defines price from which sources are accepted, could be used to proof malicious Rules: []*RuleSource{ // 0 is reserved {}, @@ -66,21 +84,23 @@ func DefaultParams() Params { SourceIDs: []uint64{0}, }, }, + // TokenFeeder describes when a token start to be updated with its price, and the frequency, endTime. TokenFeeders: []*TokenFeeder{ {}, { TokenID: 1, RuleID: 1, StartRoundID: 1, - StartBaseBlock: 100000000, + StartBaseBlock: 1000000, Interval: 10, }, }, MaxNonce: 3, ThresholdA: 2, ThresholdB: 3, - Mode: 1, - MaxDetId: 5, + // V1 set mode to 1 + Mode: 1, + MaxDetId: 5, } } @@ -95,17 +115,343 @@ func (p *Params) ParamSetPairs() paramtypes.ParamSetPairs { } } +// TODO: consider to parallel verifications of chains, tokens, rules, ..., to improve performance. // Validate validates the set of params func (p Params) Validate() error { + // Some basic configure params validation: + // Maxnonce: This tells how many transactions for one round can a validator send, This also restrict how many blocks a window lasts for one round to collect transactions + // MaxDetID: This only works for DS, to tell how many continuous roundID_from_DS could be accept at most for one round of exorcore_oracle + // ThresholdA/ThresholdB: represents the threshold of voting power to confirm a price as final price + // Mode: tells how and when to confirm a final price, expect for voting power threshold, v1 set this value to 1 means final price will be confirmed as soon as it has reached the threshold of total voting power, and just ignore any remaining transactions followed for current round. + if p.MaxNonce < 1 || p.MaxDetId < 1 || p.ThresholdA < 1 || p.ThresholdB < p.ThresholdA || p.Mode != 1 { + return ErrInvalidParams.Wrapf("invalid maxNonce/maxDetID/Threshold/Mode: %d, %d, %d, %d, %d", p.MaxNonce, p.MaxDetId, p.ThresholdA, p.ThresholdB, p.Mode) + } + + // validate tokenFeeders + for fID, feeder := range p.TokenFeeders { + // id==0 is reserved + if fID == 0 { + continue + } + if err := feeder.validate(); err != nil { + return err + } + // If Endblock is set, it should not be in the window of one round + if feeder.EndBlock > 0 && (feeder.EndBlock-feeder.StartBaseBlock)%feeder.Interval < uint64(p.MaxNonce) { + return ErrInvalidParams.Wrap("invalid tokenFeeder, invalid EndBlock") + } + // Interval should be long enough, make it more than twice pricing window of one round + if feeder.Interval < 2*uint64(p.MaxNonce) { + return ErrInvalidParams.Wrap("invalid tokenFeeder, invalid interval") + } + // cross validation with tokens + if feeder.TokenID >= uint64(len(p.Tokens)) { + return ErrInvalidParams.Wrap("invalid tokenFeeder, non-exist tokenID referred") + } + // cross validation with rules + if feeder.RuleID >= uint64(len(p.Rules)) { + return ErrInvalidParams.Wrap("invalid tokenFeeder, non-exist ruleID referred") + } + } + + // validate chain + for cID, chain := range p.Chains { + // id==0 is reserved + if cID == 0 { + continue + } + if err := chain.validate(); err != nil { + return err + } + } + + // validate token + for tID, token := range p.Tokens { + // id==0 is reserved + if tID == 0 { + continue + } + if err := token.validate(); err != nil { + return err + } + // cross validation with chain + if token.ChainID >= uint64(len(p.Chains)) { + return ErrInvalidParams.Wrap("invalid token, non-exist chainID referred") + } + } + + // validate rules + for rID, rule := range p.Rules { + if rID == 0 { + continue + } + if err := rule.validate(); err != nil { + return err + } + // cross validation with sources + for _, id := range rule.SourceIDs { + if id >= uint64(len(p.Rules)) { + return ErrInvalidParams.Wrap("invalid rule") + } + } + if rule.Nom != nil { + for _, id := range rule.Nom.SourceIDs { + if id < 1 || id >= uint64(len(p.Rules)) { + return ErrInvalidParams.Wrap("invalid rule") + } + } + } + } + // validete sources + for sID, source := range p.Sources { + if sID == 0 { + continue + } + if err := source.validate(); err != nil { + return err + } + } return nil } +// AddSources adds new sources to tell where to fetch prices +func (p Params) AddSources(sources ...*Source) (Params, error) { + sNames := make(map[string]struct{}) + for _, source := range p.Sources { + sNames[source.Name] = struct{}{} + } + for _, s := range sources { + if !s.Valid { + return p, ErrInvalidParams.Wrap("invalid source to add, new source should be valid") + } + if _, exists := sNames[s.Name]; exists { + return p, ErrInvalidParams.Wrap("invalid source to add, duplicated") + } + sNames[s.Name] = struct{}{} + p.Sources = append(p.Sources, s) + } + return p, nil +} + +// AddChains adds new chains on which tokens are deployed +func (p Params) AddChains(chains ...*Chain) (Params, error) { + cNames := make(map[string]struct{}) + for _, chain := range p.Chains { + cNames[chain.Name] = struct{}{} + } + for _, c := range chains { + if _, exists := cNames[c.Name]; exists { + return p, ErrInvalidParams.Wrap("invalid chain to add, duplicated") + } + p.Chains = append(p.Chains, c) + } + return p, nil +} + +// UpdateTokens upates token info +// Since we don't allow to add any new token with the same Name&ChainID existed, so all fileds except the those are able to be modified +// contractAddress and decimal are only allowed before any tokenFeeder of that token had been started +// assetID is allowed to be modified no matter any tokenFeeder is started +func (p Params) UpdateTokens(currentHeight uint64, tokens ...*Token) (Params, error) { + for _, t := range tokens { + update := false + for tokenID := 1; tokenID < len(p.Tokens); tokenID++ { + token := p.Tokens[tokenID] + if token.ChainID == t.ChainID && token.Name == t.Name { + // modify existing token + update = true + // update assetID + if len(t.AssetID) > 0 { + token.AssetID = t.AssetID + } + if !p.TokenStarted(uint64(tokenID), currentHeight) { + // contractAddres is mainly used as a description information + if len(t.ContractAddress) > 0 { + token.ContractAddress = t.ContractAddress + } + // update Decimal, token.Decimal is allowed to modified to at least 1 + if t.Decimal > 0 { + token.Decimal = t.Decimal + } + } + // any other modification will be ignored + break + } + } + // add a new token + if !update { + p.Tokens = append(p.Tokens, t) + } + } + return p, nil +} + +// TokenStarted returns if any tokenFeeder had been started for the specified token identified by tokenID +func (p Params) TokenStarted(tokenID, height uint64) bool { + for _, feeder := range p.TokenFeeders { + if feeder.TokenID == tokenID && height >= feeder.StartBaseBlock { + return true + } + } + return false +} + +// AddRules adds a new RuleSource defining which sources and how many of the defined source are needed to be valid for a price to be provided +func (p Params) AddRules(rules ...*RuleSource) (Params, error) { + p.Rules = append(p.Rules, rules...) + return p, nil +} + +// UpdateTokenFeeder updates tokenfeeder info, validation first +func (p Params) UpdateTokenFeeder(tf *TokenFeeder, currentHeight uint64) (Params, error) { + tfIDs := p.GetFeederIDsByTokenID(tf.TokenID) + if len(tfIDs) == 0 { + // first tokenfeeder for this token + p.TokenFeeders = append(p.TokenFeeders, tf) + return p, nil + } + tfIdx := tfIDs[len(tfIDs)-1] + tokenFeeder := p.TokenFeeders[tfIdx] + + // latest feeder is not started yet + if tokenFeeder.StartBaseBlock > currentHeight { + // fields can be modified: startBaseBlock, interval, endBlock + update := false + if tf.StartBaseBlock > 0 { + // Set startBlock to some height in history is not allowed + if tf.StartBaseBlock <= currentHeight { + return p, ErrInvalidParams.Wrapf("invalid tokenFeeder to update, invalid StartBaseBlock, currentHeight: %d, set: %d", currentHeight, tf.StartBaseBlock) + } + update = true + tokenFeeder.StartBaseBlock = tf.StartBaseBlock + } + if tf.Interval > 0 { + tokenFeeder.Interval = tf.Interval + update = true + } + if tf.EndBlock > 0 { + // EndBlock must be set to some height in the future + if tf.EndBlock <= currentHeight { + return p, ErrInvalidParams.Wrapf("invalid tokenFeeder to update, invalid EndBlock, currentHeight: %d, set: %d", currentHeight, tf.EndBlock) + } + tokenFeeder.EndBlock = tf.EndBlock + update = true + } + // TODO: or we can just ignore this case instead of report an error + if !update { + return p, ErrInvalidParams.Wrap("invalid tokenFeeder to update, no valid field set") + } + p.TokenFeeders[tfIdx] = tokenFeeder + return p, nil + } + + // latest feeder is running + if tokenFeeder.EndBlock == 0 || tokenFeeder.EndBlock > currentHeight { + // fields can be modified: endBlock + if tf.EndBlock == 0 || tf.EndBlock <= currentHeight { + return p, ErrInvalidParams.Wrapf("invalid tokenFeeder to update, invalid EndBlock, currentHeight: %d, set: %d", currentHeight, tf.EndBlock) + } + tokenFeeder.EndBlock = tf.EndBlock + p.TokenFeeders[tfIdx] = tokenFeeder + return p, nil + } + + // latest feeder is stopped, this is actually a new feeder to resume the latest one for the same token + latestRoundID := tokenFeeder.StartRoundID + (tokenFeeder.EndBlock-tokenFeeder.StartBaseBlock)/tokenFeeder.Interval + if tf.StartBaseBlock <= currentHeight || tf.StartRoundID != latestRoundID+1 { + return p, ErrInvalidParams.Wrapf("invalid tokenFeeder to update, invalid StartBaseBlock or StartRoundID, currentHeight:%d, set_startBasedBlock:%d, set_StartRoundID:%d", currentHeight, tf.StartBaseBlock, tf.StartRoundID) + } + p.TokenFeeders = append(p.TokenFeeders, tf) + + return p, nil +} + // String implements the Stringer interface. func (p Params) String() string { out, _ := yaml.Marshal(p) return string(out) } +// GetSourceIDByName returns sourceID related to the specified name +func (p Params) GetSourceIDByName(n string) int { + for i, s := range p.Sources { + if n == s.Name { + return i + } + } + return 0 +} + +// GetFeederIDsByTokenID returns all feederIDs related to the specified token +func (p Params) GetFeederIDsByTokenID(tID uint64) []int { + ret := make([]int, 0) + for fID, f := range p.TokenFeeders { + // feeder list is ordered, so the slice returned is in the order of the feeders updated for the same token + if f.TokenID == tID { + ret = append(ret, fID) + } + } + return ret +} + +// validate validation on field Chain +func (c Chain) validate() error { + // Name must be set + if len(c.Name) == 0 { + return ErrInvalidParams.Wrap("invalid Chain, name not set") + } + return nil +} + +// validation on field Token +func (t Token) validate() error { + // Name must be set, and chainID must start from 1 + if len(t.Name) == 0 || t.ChainID < 1 { + return ErrInvalidParams.Wrap("invalid Token, name not set or ChainID<1") + } + return nil +} + +// validate validation on field RuleSource +func (r RuleSource) validate() error { + // at least one of SourceIDs and Nom has to be set + if len(r.SourceIDs) == 0 && (r.Nom == nil || len(r.Nom.SourceIDs) == 0) { + return ErrInvalidParams.Wrap("invalid RuleSource") + } + if r.Nom != nil && + r.Nom.Minimum > uint64(len(r.Nom.SourceIDs)) { + return ErrInvalidParams.Wrap("invalid RuleSource") + } + return nil +} + +// validate validation on filed Source +func (s Source) validate() error { + // Name must be set + if len(s.Name) == 0 { + return ErrInvalidParams.Wrap("invalid Source, name not set") + } + return nil +} + +// validate validation on field TokenFeeder +func (f TokenFeeder) validate() error { + // IDs must start from 1 + if f.TokenID < 1 || + f.StartRoundID < 1 || + f.Interval < 1 || + f.StartBaseBlock < 1 { + return ErrInvalidParams.Wrapf("invalid TokenFeeder, tokenID/startRoundID/interval/startBaseBlock: %d, %d, %d, %d", f.TokenID, f.StartRoundID, f.Interval, f.StartBaseBlock) + } + + // if EndBlock is set, it must be bigger than startBaseBlock + if f.EndBlock > 0 && f.StartBaseBlock >= f.EndBlock { + return ErrInvalidParams.Wrapf("invalid TokenFeeder, invalid EndBlock to be set, startBaseBlock: %d, endBlock: %d", f.StartBaseBlock, f.EndBlock) + } + + return nil +} + func (p Params) GetTokenIDFromAssetID(assetID string) int { for id, token := range p.Tokens { if token.AssetID == assetID {