diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index dd26da68f..52c72c115 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -44,3 +44,21 @@ jobs: run: | make test-unit-cover if: env.GIT_DIFF + test-unit-e2e: + runs-on: ubuntu-latest + steps: + - uses: actions/setup-go@v4 + with: + go-version: '1.21' + check-latest: true + - uses: actions/checkout@v4 + - uses: technote-space/get-diff-action@v6.1.2 + with: + PATTERNS: | + **/**.go + go.mod + go.sum + - name: Test e2e cases + run: | + make test-unit-e2e + if: env.GIT_DIFF diff --git a/Makefile b/Makefile index 1ccac8b32..bb536c034 100644 --- a/Makefile +++ b/Makefile @@ -186,7 +186,7 @@ all: build build-all: tools build lint test vulncheck -.PHONY: distclean clean build-all +.PHONY: distclean clean build-all build ############################################################################### ### makTools & Dependencies ### @@ -305,23 +305,35 @@ test-all: test-unit test-race # we want to include all unit tests in the subfolders (tests/e2e/*) # We also want to exclude the testutil folder because it contains only # helper functions for the tests. -PACKAGES_UNIT=$(shell go list ./... | grep -v '/tests/e2e$$' | grep -v 'testutil') +PACKAGES_UNIT=$(shell go list ./... | grep -v '/tests/e2e' | grep -v 'testutil') +PACKAGES_UNIT_E2E=$(shell go list ./... | grep '/tests/e2e') TEST_PACKAGES=./... -TEST_TARGETS := test-unit test-unit-cover test-race +TEST_TARGETS := test-unit test-unit-cover test-race test-unit-e2e test-unit-cover-local test-unit-e2e-local # Test runs-specific rules. To add a new test target, just add # a new rule, customise ARGS or TEST_PACKAGES ad libitum, and # append the new rule to the TEST_TARGETS list. -test-unit: ARGS=-timeout=15m -gcflags=all=-l +test-unit: ARGS=-timeout=15m -gcflags=all=-l --tags devmode test-unit: TEST_PACKAGES=$(PACKAGES_UNIT) test-race: ARGS=-race test-race: TEST_PACKAGES=$(PACKAGES_NOSIMULATION) $(TEST_TARGETS): run-tests -test-unit-cover: ARGS=-timeout=15m -coverprofile=cover.out -covermode=atomic -gcflags=all=-l +test-unit-cover: ARGS=-timeout=15m -coverprofile=cover.out -covermode=atomic -gcflags=all=-l --tags devmode test-unit-cover: TEST_PACKAGES=$(PACKAGES_UNIT) +test-unit-e2e: ARGS=-timeout=15m --tags devmode +test-unit-e2e: TEST_PACKAGES=$(PACKAGES_UNIT_E2E) + +test-unit-cover-local: ARGS=-timeout=30m -coverprofile=cover.out -covermode=atomic -gcflags=all=-l --tags 'devmode local' +test-unit-cover-local: TEST_PACKAGES=$(PACKAGES_UNIT) + +test-unit-e2e-local: TEST_OPTION=local +test-unit-e2e-local: ARGS=-timeout=30m --tags devmode +test-unit-e2e-local: TEST_PACKAGES=$(PACKAGES_UNIT_E2E) + + test-e2e: @if [ -z "$(TARGET_VERSION)" ]; then \ echo "Building docker image from local codebase"; \ @@ -335,9 +347,9 @@ test-e2e: run-tests: ifneq (,$(shell which tparse 2>/dev/null)) - go test -mod=readonly -json $(ARGS) $(EXTRA_ARGS) $(TEST_PACKAGES) | tparse + TEST_OPTION=$(TEST_OPTION) go test -mod=readonly -json $(ARGS) $(EXTRA_ARGS) $(TEST_PACKAGES) | tparse else - go test -mod=readonly $(ARGS) $(EXTRA_ARGS) $(TEST_PACKAGES) + TEST_OPTION=$(TEST_OPTION) go test -mod=readonly $(ARGS) $(EXTRA_ARGS) $(TEST_PACKAGES) endif test-import: @@ -634,4 +646,4 @@ check-licenses: @python3 scripts/check_licenses.py . swagger-ui: - docker run -p 8080:8080 -e SWAGGER_JSON=/app/swagger.json -v $(pwd)/client/docs/swagger-ui:/app swaggerapi/swagger-ui \ No newline at end of file + docker run -p 8080:8080 -e SWAGGER_JSON=/app/swagger.json -v $(pwd)/client/docs/swagger-ui:/app swaggerapi/swagger-ui diff --git a/app/ante/cosmos/context.go b/app/ante/cosmos/context.go index 3203c0ec6..27cff0953 100644 --- a/app/ante/cosmos/context.go +++ b/app/ante/cosmos/context.go @@ -39,9 +39,9 @@ func (sud SetUpContextDecorator) AnteHandle(ctx sdk.Context, tx sdk.Tx, simulate return newCtx, sdkerrors.ErrTxDecode.Wrap("Tx must be GasTx") } - // If the transaction is an OracleCreatePriceTx, we set the gas meter to infinite - // NOTE: this works fine with cosmossdk-noopmempool+tendermint-mempool, since tenderminit used gasMeter.Limit() to check the gaswanted of tx insteand of tx.GetGas(), but if works with application mempool, we need to be caution of the tx.GetGas() since oracle create-price tx doesn't really pay. - if anteutils.IsOracleCreatePriceTx(tx) { + // If the transaction is an OraclePriceFeedTx, we set the gas meter to infinite + // NOTE: this works fine with cosmossdk-noopmempool+tendermint-mempool, since tenderminit used gasMeter.Limit() to check the gaswanted of tx insteand of tx.GetGas(), but if works with application mempool, we need to be caution of the tx.GetGas() since oracle price-feed tx doesn't really pay. + if anteutils.IsOraclePriceFeedTx(tx) { newCtx = ctx.WithGasMeter(evmostypes.NewInfiniteGasMeterWithLimit(0)) return next(newCtx, tx, simulate) } diff --git a/app/ante/cosmos/fees.go b/app/ante/cosmos/fees.go index b4147f81d..37c38c198 100644 --- a/app/ante/cosmos/fees.go +++ b/app/ante/cosmos/fees.go @@ -59,7 +59,7 @@ func (dfd DeductFeeDecorator) AnteHandle(ctx sdk.Context, tx sdk.Tx, simulate bo return ctx, errorsmod.Wrap(errortypes.ErrTxDecode, "Tx must be a FeeTx") } - if anteutils.IsOracleCreatePriceTx(tx) { + if anteutils.IsOraclePriceFeedTx(tx) { newCtx := ctx.WithPriority(math.MaxInt64) return next(newCtx, tx, simulate) } diff --git a/app/ante/cosmos/min_price.go b/app/ante/cosmos/min_price.go index b5111fd27..9f1647263 100644 --- a/app/ante/cosmos/min_price.go +++ b/app/ante/cosmos/min_price.go @@ -32,7 +32,7 @@ func (mpd MinGasPriceDecorator) AnteHandle(ctx sdk.Context, tx sdk.Tx, simulate return ctx, errorsmod.Wrapf(errortypes.ErrInvalidType, "invalid transaction type %T, expected sdk.FeeTx", tx) } - if anteutils.IsOracleCreatePriceTx(tx) { + if anteutils.IsOraclePriceFeedTx(tx) { return next(ctx, tx, simulate) } diff --git a/app/ante/cosmos/sigverify.go b/app/ante/cosmos/sigverify.go index 3d91e1cdd..dd239c40f 100644 --- a/app/ante/cosmos/sigverify.go +++ b/app/ante/cosmos/sigverify.go @@ -58,8 +58,8 @@ func NewSetPubKeyDecorator(ak authante.AccountKeeper) SetPubKeyDecorator { } func (spkd SetPubKeyDecorator) AnteHandle(ctx sdk.Context, tx sdk.Tx, simulate bool, next sdk.AnteHandler) (sdk.Context, error) { - // skip publickkey set for oracle create-price message - if utils.IsOracleCreatePriceTx(tx) { + // skip publickkey set for oracle price-feed message + if utils.IsOraclePriceFeedTx(tx) { sigTx, ok := tx.(authsigning.SigVerifiableTx) if !ok { return ctx, sdkerrors.ErrTxDecode.Wrap("invalid transaction type, expected SigVerifiableTx") @@ -173,7 +173,7 @@ func (sgcd SigGasConsumeDecorator) AnteHandle(ctx sdk.Context, tx sdk.Tx, simula return ctx, sdkerrors.ErrTxDecode.Wrap("invalid transaction type, expected SigVerifiableTx") } - if utils.IsOracleCreatePriceTx(tx) { + if utils.IsOraclePriceFeedTx(tx) { return next(ctx, tx, simulate) } @@ -258,7 +258,7 @@ func OnlyLegacyAminoSigners(sigData signing.SignatureData) bool { } func (svd SigVerificationDecorator) AnteHandle(ctx sdk.Context, tx sdk.Tx, simulate bool, next sdk.AnteHandler) (newCtx sdk.Context, err error) { - if utils.IsOracleCreatePriceTx(tx) { + if utils.IsOraclePriceFeedTx(tx) { sigTx, ok := tx.(authsigning.SigVerifiableTx) if !ok { return ctx, sdkerrors.ErrTxDecode.Wrap("invalid transaction type, expected SigVerifiableTx") @@ -385,12 +385,14 @@ func NewIncrementSequenceDecorator(ak authante.AccountKeeper, oracleKeeper utils } func (isd IncrementSequenceDecorator) AnteHandle(ctx sdk.Context, tx sdk.Tx, simulate bool, next sdk.AnteHandler) (sdk.Context, error) { - // oracle create-price message dont need to increment sequence, check its nonce instead - if utils.IsOracleCreatePriceTx(tx) { + // oracle price-feed message dont need to increment sequence, check its nonce instead + if utils.IsOraclePriceFeedTx(tx) { for _, msg := range tx.GetMsgs() { - msg := msg.(*oracletypes.MsgCreatePrice) + msg := msg.(*oracletypes.MsgPriceFeed) if accAddress, err := sdk.AccAddressFromBech32(msg.Creator); err != nil { return ctx, errors.New("invalid address") + // #nosec G115 // safe conversion + // TODO: define msg.Nonce as uint32 to avoid conversion } else if _, err := isd.oracleKeeper.CheckAndIncreaseNonce(ctx, sdk.ConsAddress(accAddress).String(), msg.FeederID, uint32(msg.Nonce)); err != nil { return ctx, err } @@ -445,6 +447,7 @@ func (vscd ValidateSigCountDecorator) AnteHandle(ctx sdk.Context, tx sdk.Tx, sim sigCount := 0 for _, pk := range pubKeys { sigCount += CountSubKeys(pk) + // #nosec G115 if uint64(sigCount) > params.TxSigLimit { return ctx, sdkerrors.ErrTooManySignatures.Wrapf("signatures: %d, limit: %d", sigCount, params.TxSigLimit) } diff --git a/app/ante/cosmos/txsize_gas.go b/app/ante/cosmos/txsize_gas.go index b9684cd11..08a8014f0 100644 --- a/app/ante/cosmos/txsize_gas.go +++ b/app/ante/cosmos/txsize_gas.go @@ -37,10 +37,10 @@ func (cgts ConsumeTxSizeGasDecorator) AnteHandle(ctx sdk.Context, tx sdk.Tx, sim return ctx, sdkerrors.ErrTxDecode.Wrap("invalid tx type") } - // Skip gas consumption if tx is an OracleCreatePriceTx - if anteutils.IsOracleCreatePriceTx(tx) { + // Skip gas consumption if tx is an OraclePriceFeedTx + if anteutils.IsOraclePriceFeedTx(tx) { if len(ctx.TxBytes()) > anteutils.TxSizeLimit { - return ctx, sdkerrors.ErrTxTooLarge.Wrapf("oracle create-price tx has exceeds size limit, limit:%d, got:%d", anteutils.TxSizeLimit, len(ctx.TxBytes())) + return ctx, sdkerrors.ErrTxTooLarge.Wrapf("oracle price-feed tx has exceeds size limit, limit:%d, got:%d", anteutils.TxSizeLimit, len(ctx.TxBytes())) } return next(ctx, tx, simulate) } @@ -76,12 +76,14 @@ func (cgts ConsumeTxSizeGasDecorator) AnteHandle(ctx sdk.Context, tx sdk.Tx, sim } // use stdsignature to mock the size of a full signature + // #nosec G115 simSig := legacytx.StdSignature{ // nolint:staticcheck // this will be removed when proto is ready Signature: simSecp256k1Sig[:], PubKey: pubkey, } sigBz := legacy.Cdc.MustMarshal(simSig) + // #nosec G115 cost := sdk.Gas(len(sigBz) + 6) // If the pubkey is a multi-signature pubkey, then we estimate for the maximum diff --git a/app/ante/evm/fee_market.go b/app/ante/evm/fee_market.go index 704c4f719..8f05dd01b 100644 --- a/app/ante/evm/fee_market.go +++ b/app/ante/evm/fee_market.go @@ -30,7 +30,7 @@ func NewGasWantedDecorator( } func (gwd GasWantedDecorator) AnteHandle(ctx sdk.Context, tx sdk.Tx, simulate bool, next sdk.AnteHandler) (newCtx sdk.Context, err error) { - if anteutils.IsOracleCreatePriceTx(tx) { + if anteutils.IsOraclePriceFeedTx(tx) { return next(ctx, tx, simulate) } evmParams := gwd.evmKeeper.GetParams(ctx) diff --git a/app/ante/utils/oracle.go b/app/ante/utils/oracle.go index df7b1be0a..a5b797fab 100644 --- a/app/ante/utils/oracle.go +++ b/app/ante/utils/oracle.go @@ -5,16 +5,16 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" ) -// TxSizeLimit limits max size of a create-price tx, this is calculated based on one nativeTokenbalance message of worst case(max size), which will need 576 bytes for balance update +// TxSizeLimit limits max size of a price-feed tx, this is calculated based on one nativeTokenbalance message of worst case(max size), which will need 576 bytes for balance update const TxSizeLimit = 1000 -func IsOracleCreatePriceTx(tx sdk.Tx) bool { +func IsOraclePriceFeedTx(tx sdk.Tx) bool { msgs := tx.GetMsgs() if len(msgs) == 0 { return false } for _, msg := range msgs { - if _, ok := msg.(*oracletypes.MsgCreatePrice); !ok { + if _, ok := msg.(*oracletypes.MsgPriceFeed); !ok { return false } } diff --git a/app/app.go b/app/app.go index 4879a8a5c..ae59a01b9 100644 --- a/app/app.go +++ b/app/app.go @@ -399,7 +399,7 @@ func NewExocoreApp( // Setup Mempool and Proposal Handlers baseAppOptions = append(baseAppOptions, func(app *baseapp.BaseApp) { - // NOTE: we use a NoOpMempool here, for oracle create-price, it works fine since we have set a infinitgasmeterwithlimit in the ante handler to avoid the out-of-gas error no matter what the amount/gas is set by tx builder, and we set the highest priority for oracle create-price txs to work properly with tendermint mempool to make sure oracle creat-prie tx will be included in the mempool if received. And if we want to use some other application mempool, we need to take care of the gas limit and gas price in the oracle create-price txs.(we don't need to bother this since tendermint mempool use gasMeter.limit() instead of tx.Gas()) + // NOTE: we use a NoOpMempool here, for oracle price-feed, it works fine since we have set a infinitgasmeterwithlimit in the ante handler to avoid the out-of-gas error no matter what the amount/gas is set by tx builder, and we set the highest priority for oracle price-feed txs to work properly with tendermint mempool to make sure oracle creat-prie tx will be included in the mempool if received. And if we want to use some other application mempool, we need to take care of the gas limit and gas price in the oracle price-feed txs.(we don't need to bother this since tendermint mempool use gasMeter.limit() instead of tx.Gas()) mempool := mempool.NoOpMempool{} app.SetMempool(mempool) handler := baseapp.NewDefaultProposalHandler(mempool, app) @@ -915,14 +915,17 @@ func NewExocoreApp( app.mm.SetOrderBeginBlockers( upgradetypes.ModuleName, // to upgrade the chain capabilitytypes.ModuleName, // before any module with capabilities like IBC - epochstypes.ModuleName, // to update the epoch - feemarkettypes.ModuleName, // set EIP-1559 gas prices - evmtypes.ModuleName, // stores chain id in memory - slashingtypes.ModuleName, // TODO after reward - evidencetypes.ModuleName, // TODO after reward - stakingtypes.ModuleName, // track historical info - ibcexported.ModuleName, // handles upgrades of chain and hence client - authz.ModuleName, // clear expired approvals + // being block of oracle will fill params cache, need to be put before epochs will use the params + // it need to put before other modules to fill params cahce before access + oracleTypes.ModuleName, + epochstypes.ModuleName, // to update the epoch + feemarkettypes.ModuleName, // set EIP-1559 gas prices + evmtypes.ModuleName, // stores chain id in memory + slashingtypes.ModuleName, // TODO after reward + evidencetypes.ModuleName, // TODO after reward + stakingtypes.ModuleName, // track historical info + ibcexported.ModuleName, // handles upgrades of chain and hence client + authz.ModuleName, // clear expired approvals // no-op modules ibctransfertypes.ModuleName, icatypes.ModuleName, @@ -943,7 +946,6 @@ func NewExocoreApp( rewardTypes.ModuleName, exoslashTypes.ModuleName, avsManagerTypes.ModuleName, - oracleTypes.ModuleName, distrtypes.ModuleName, ) diff --git a/client/docs/swagger-ui/swagger.json b/client/docs/swagger-ui/swagger.json index f21c728a9..c38521be0 100644 --- a/client/docs/swagger-ui/swagger.json +++ b/client/docs/swagger-ui/swagger.json @@ -18574,11 +18574,6 @@ "type": "string", "title": "oracle_malicious_jail_duration defines the duratin one validator should be jailed for malicious behavior" }, - "slash_fraction_miss": { - "type": "string", - "format": "byte", - "title": "slash_fraction_miss defines the fraction one validator should be punished for msissing reporting price" - }, "slash_fraction_malicious": { "type": "string", "format": "byte", @@ -18816,10 +18811,10 @@ }, "validator": { "type": "string", - "title": "validator tells which validator create this price" + "title": "validator tells which validator provide this price" } }, - "title": "MsgItem represents the message info of createPrice" + "title": "MsgItem represents the message info of priceFeed" }, "title": "cached messages" } @@ -19004,10 +18999,10 @@ }, "validator": { "type": "string", - "title": "validator tells which validator create this price" + "title": "validator tells which validator provide this price" } }, - "title": "MsgItem represents the message info of createPrice" + "title": "MsgItem represents the message info of priceFeed" }, "title": "cached messages" } @@ -19321,11 +19316,6 @@ "type": "string", "title": "oracle_malicious_jail_duration defines the duratin one validator should be jailed for malicious behavior" }, - "slash_fraction_miss": { - "type": "string", - "format": "byte", - "title": "slash_fraction_miss defines the fraction one validator should be punished for msissing reporting price" - }, "slash_fraction_malicious": { "type": "string", "format": "byte", @@ -19693,11 +19683,6 @@ "type": "string", "title": "oracle_malicious_jail_duration defines the duratin one validator should be jailed for malicious behavior" }, - "slash_fraction_miss": { - "type": "string", - "format": "byte", - "title": "slash_fraction_miss defines the fraction one validator should be punished for msissing reporting price" - }, "slash_fraction_malicious": { "type": "string", "format": "byte", @@ -19774,6 +19759,11 @@ "schema": { "type": "object", "properties": { + "version": { + "type": "string", + "format": "int64", + "title": "version of the staker validator list changes" + }, "staker_info": { "title": "all staker infos under the specified asset", "type": "object", @@ -19907,6 +19897,11 @@ "schema": { "type": "object", "properties": { + "version": { + "type": "string", + "format": "int64", + "title": "version of the staker validator list changes" + }, "staker_infos": { "type": "array", "items": { @@ -20037,6 +20032,11 @@ "schema": { "type": "object", "properties": { + "version": { + "type": "string", + "format": "int64", + "title": "version of the staker validator list changes" + }, "staker_list": { "title": "staker list including all stakers of request asset", "type": "object", @@ -40922,10 +40922,10 @@ }, "validator": { "type": "string", - "title": "validator tells which validator create this price" + "title": "validator tells which validator provide this price" } }, - "title": "MsgItem represents the message info of createPrice" + "title": "MsgItem represents the message info of priceFeed" }, "exocore.oracle.v1.NOMSource": { "type": "object", @@ -41179,11 +41179,6 @@ "type": "string", "title": "oracle_malicious_jail_duration defines the duratin one validator should be jailed for malicious behavior" }, - "slash_fraction_miss": { - "type": "string", - "format": "byte", - "title": "slash_fraction_miss defines the fraction one validator should be punished for msissing reporting price" - }, "slash_fraction_malicious": { "type": "string", "format": "byte", @@ -41398,10 +41393,10 @@ }, "validator": { "type": "string", - "title": "validator tells which validator create this price" + "title": "validator tells which validator provide this price" } }, - "title": "MsgItem represents the message info of createPrice" + "title": "MsgItem represents the message info of priceFeed" }, "title": "cached messages" } @@ -41677,11 +41672,6 @@ "type": "string", "title": "oracle_malicious_jail_duration defines the duratin one validator should be jailed for malicious behavior" }, - "slash_fraction_miss": { - "type": "string", - "format": "byte", - "title": "slash_fraction_miss defines the fraction one validator should be punished for msissing reporting price" - }, "slash_fraction_malicious": { "type": "string", "format": "byte", @@ -41906,10 +41896,10 @@ }, "validator": { "type": "string", - "title": "validator tells which validator create this price" + "title": "validator tells which validator provide this price" } }, - "title": "MsgItem represents the message info of createPrice" + "title": "MsgItem represents the message info of priceFeed" }, "title": "cached messages" } @@ -42164,11 +42154,6 @@ "type": "string", "title": "oracle_malicious_jail_duration defines the duratin one validator should be jailed for malicious behavior" }, - "slash_fraction_miss": { - "type": "string", - "format": "byte", - "title": "slash_fraction_miss defines the fraction one validator should be punished for msissing reporting price" - }, "slash_fraction_malicious": { "type": "string", "format": "byte", @@ -42439,11 +42424,6 @@ "type": "string", "title": "oracle_malicious_jail_duration defines the duratin one validator should be jailed for malicious behavior" }, - "slash_fraction_miss": { - "type": "string", - "format": "byte", - "title": "slash_fraction_miss defines the fraction one validator should be punished for msissing reporting price" - }, "slash_fraction_malicious": { "type": "string", "format": "byte", @@ -42459,6 +42439,11 @@ "exocore.oracle.v1.QueryStakerInfoResponse": { "type": "object", "properties": { + "version": { + "type": "string", + "format": "int64", + "title": "version of the staker validator list changes" + }, "staker_info": { "title": "all staker infos under the specified asset", "type": "object", @@ -42529,6 +42514,11 @@ "exocore.oracle.v1.QueryStakerInfosResponse": { "type": "object", "properties": { + "version": { + "type": "string", + "format": "int64", + "title": "version of the staker validator list changes" + }, "staker_infos": { "type": "array", "items": { @@ -42603,6 +42593,11 @@ "exocore.oracle.v1.QueryStakerListResponse": { "type": "object", "properties": { + "version": { + "type": "string", + "format": "int64", + "title": "version of the staker validator list changes" + }, "staker_list": { "title": "staker list including all stakers of request asset", "type": "object", @@ -42710,10 +42705,10 @@ }, "validator": { "type": "string", - "title": "validator tells which validator create this price" + "title": "validator tells which validator provide this price" } }, - "title": "MsgItem represents the message info of createPrice" + "title": "MsgItem represents the message info of priceFeed" }, "title": "cached messages" } @@ -42962,11 +42957,6 @@ "type": "string", "title": "oracle_malicious_jail_duration defines the duratin one validator should be jailed for malicious behavior" }, - "slash_fraction_miss": { - "type": "string", - "format": "byte", - "title": "slash_fraction_miss defines the fraction one validator should be punished for msissing reporting price" - }, "slash_fraction_malicious": { "type": "string", "format": "byte", @@ -43034,11 +43024,6 @@ "type": "string", "title": "oracle_malicious_jail_duration defines the duratin one validator should be jailed for malicious behavior" }, - "slash_fraction_miss": { - "type": "string", - "format": "byte", - "title": "slash_fraction_miss defines the fraction one validator should be punished for msissing reporting price" - }, "slash_fraction_malicious": { "type": "string", "format": "byte", diff --git a/precompiles/avs/events.go b/precompiles/avs/events.go index 0bf1a5234..8eac48bf3 100644 --- a/precompiles/avs/events.go +++ b/precompiles/avs/events.go @@ -271,6 +271,8 @@ func (p Precompile) EmitTaskSubmittedByOperator(ctx sdk.Context, stateDB vm.Stat } // Prepare the event data:sender,TaskResponse, BlsSignature, Phase arguments := abi.Arguments{event.Inputs[2], event.Inputs[3], event.Inputs[4], event.Inputs[5]} + // #nosec G115 + // TODO: consider modify define of Phase to uint8 packed, err := arguments.Pack(params.CallerAddress.String(), params.TaskResponse, params.BlsSignature, uint8(params.Phase)) if err != nil { return err diff --git a/precompiles/delegation/tx.go b/precompiles/delegation/tx.go index 0ca524589..e98bc0393 100644 --- a/precompiles/delegation/tx.go +++ b/precompiles/delegation/tx.go @@ -52,7 +52,6 @@ func (p Precompile) Delegate( if err != nil { return nil, err } - err = p.delegationKeeper.DelegateTo(ctx, delegationParams) if err != nil { return nil, err diff --git a/proto/exocore/oracle/v1/genesis.proto b/proto/exocore/oracle/v1/genesis.proto index 421434dd3..cf2275625 100644 --- a/proto/exocore/oracle/v1/genesis.proto +++ b/proto/exocore/oracle/v1/genesis.proto @@ -55,6 +55,8 @@ message StakerInfosAssets { string asset_id = 1; // stakerInfos repeated StakerInfo staker_infos = 2; + // nst_version is the version of nst to track validator list changes + int64 nst_version = 3; } // stakerListAssets bond stakerList to their related assets id @@ -63,6 +65,8 @@ message StakerListAssets { string asset_id = 1; // stakerList StakerList staker_list = 2; + // nst_version is the version of nst to track validator list changes + int64 nst_version = 3; } // ValidatorMissedRounds record missed rounds indexes for a validator which consAddr corresponding to the address diff --git a/proto/exocore/oracle/v1/native_token.proto b/proto/exocore/oracle/v1/native_token.proto index bf06f5e23..1626ced7c 100644 --- a/proto/exocore/oracle/v1/native_token.proto +++ b/proto/exocore/oracle/v1/native_token.proto @@ -50,3 +50,12 @@ message StakerList { // staker's address repeated string staker_addrs = 1; } + +// NSTVersion used to track the validator list changes for each NST +message NSTVersion { + // asset_id of the NST + string asset_id = 1; + // version of the validator list + int64 version = 2; +} + diff --git a/proto/exocore/oracle/v1/params.proto b/proto/exocore/oracle/v1/params.proto index a9f8c785b..bc8ad4faf 100644 --- a/proto/exocore/oracle/v1/params.proto +++ b/proto/exocore/oracle/v1/params.proto @@ -71,14 +71,8 @@ message SlashingParams { (amino.dont_omitempty) = true, (gogoproto.stdduration) = true ]; - // slash_fraction_miss defines the fraction one validator should be punished for msissing reporting price - bytes slash_fraction_miss = 5 [ - (gogoproto.customtype) = "github.com/cosmos/cosmos-sdk/types.Dec", - (gogoproto.nullable) = false, - (amino.dont_omitempty) = true - ]; // slash_fraction_malicious defines the fraction one validator should be punished for malicious behavior - bytes slash_fraction_malicious = 6 [ + bytes slash_fraction_malicious = 5 [ (gogoproto.customtype) = "github.com/cosmos/cosmos-sdk/types.Dec", (gogoproto.nullable) = false, (amino.dont_omitempty) = true diff --git a/proto/exocore/oracle/v1/query.proto b/proto/exocore/oracle/v1/query.proto index c52a81271..8c13999e2 100644 --- a/proto/exocore/oracle/v1/query.proto +++ b/proto/exocore/oracle/v1/query.proto @@ -120,8 +120,10 @@ message QueryStakerListRequest { // QueryStakerListResponse is response type for Query/StakerList RPC method message QueryStakerListResponse { + // version of the staker validator list changes + int64 version = 1; // staker list including all stakers of request asset - StakerList staker_list = 1; + StakerList staker_list = 2; } // QueryStakerInfoRequest is request type for Query/StakerInfo RCP method @@ -134,8 +136,10 @@ message QueryStakerInfoRequest { // QueryStakerInfoResponse is response type for Query/StakerInfo RCP method message QueryStakerInfoResponse { + // version of the staker validator list changes + int64 version = 1; // all staker infos under the specified asset - StakerInfo staker_info = 1; + StakerInfo staker_info = 2; } // QueryStakerInfosRequest is request type for Query/StakerInfos RCP method @@ -146,8 +150,10 @@ message QueryStakerInfosRequest { // QueryStakerInfosResponse is response type for Query/StakerInfo RCP method message QueryStakerInfosResponse { + // version of the staker validator list changes + int64 version = 1; // all staker infos under the specified asset - repeated StakerInfo staker_infos = 1; + repeated StakerInfo staker_infos = 2; } // QueryParamsRequest is request type for the Query/Params RPC method. diff --git a/proto/exocore/oracle/v1/recent_msg.proto b/proto/exocore/oracle/v1/recent_msg.proto index c018279f1..b4376c0c7 100644 --- a/proto/exocore/oracle/v1/recent_msg.proto +++ b/proto/exocore/oracle/v1/recent_msg.proto @@ -14,12 +14,12 @@ message RecentMsg { repeated MsgItem msgs = 2; } -// MsgItem represents the message info of createPrice +// MsgItem represents the message info of priceFeed message MsgItem { // feeder_id tells of wich feeder this price if corresponding to uint64 feeder_id = 2 [(gogoproto.customname) = "FeederID"]; // p_source price with its source info repeated PriceSource p_sources = 3; - // validator tells which validator create this price + // validator tells which validator provide this price string validator = 4; } diff --git a/proto/exocore/oracle/v1/tx.proto b/proto/exocore/oracle/v1/tx.proto index 86fab5421..e3534a943 100644 --- a/proto/exocore/oracle/v1/tx.proto +++ b/proto/exocore/oracle/v1/tx.proto @@ -13,14 +13,14 @@ option go_package = "github.com/ExocoreNetwork/exocore/x/oracle/types"; // Msg defines the Msg service. service Msg { - // CreatePrice creates price for a new oracle round - rpc CreatePrice(MsgCreatePrice) returns (MsgCreatePriceResponse); + // PriceFeed creates price for a new oracle round + rpc PriceFeed(MsgPriceFeed) returns (MsgPriceFeedResponse); // UpdateParams update params value rpc UpdateParams(MsgUpdateParams) returns (MsgUpdateParamsResponse); } -// MsgCreatePrice provide the price updating message -message MsgCreatePrice { +// MsgPriceFeed provide the price updating message +message MsgPriceFeed { // creator tells which is the message sender and should sign this message string creator = 1 [(cosmos_proto.scalar) = "cosmos.ValidatorAddressString"]; // refer to id from Params.TokenFeeders, 0 is reserved, invalid to use @@ -34,8 +34,8 @@ message MsgCreatePrice { int32 nonce = 5; } -// MsgCreatePriceResponse -message MsgCreatePriceResponse {} +// MsgPriceFeedResponse +message MsgPriceFeedResponse {} // MsgUpdateParms message MsgUpdateParams { diff --git a/tests/e2e/bank/bank_test.go b/tests/e2e/bank/bank_test.go deleted file mode 100644 index 773cf5681..000000000 --- a/tests/e2e/bank/bank_test.go +++ /dev/null @@ -1,16 +0,0 @@ -package bank - -import ( - "testing" - - "github.com/ExocoreNetwork/exocore/testutil/network" - "github.com/stretchr/testify/suite" -) - -func TestE2ETestSuite(t *testing.T) { - cfg := network.DefaultConfig() - cfg.NumValidators = 1 - cfg.CleanupDir = true - cfg.EnableTMLogging = false - suite.Run(t, NewE2ETestSuite(cfg)) -} diff --git a/tests/e2e/bank/query.go b/tests/e2e/bank/query.go deleted file mode 100644 index a4a7f237b..000000000 --- a/tests/e2e/bank/query.go +++ /dev/null @@ -1,18 +0,0 @@ -package bank - -import ( - "github.com/ExocoreNetwork/exocore/tests/e2e" - sdk "github.com/cosmos/cosmos-sdk/types" -) - -// TestQueryBalance verifies that the native coin balance query returns the expected -// account balance for validators. It checks that: -// - The balance matches the network configuration -// - The returned coin denomination and amount are correct -// - The balance can be properly parsed into a native coin -func (s *E2ETestSuite) TestQueryBalance() { - res, err := e2e.QueryNativeCoinBalance(s.network.Validators[0].Address, s.network) - s.Require().NoError(err) - s.Require().Equal(sdk.NewCoin(s.network.Config.NativeDenom, s.network.Config.AccountTokens), *res.Balance) - s.Require().Equal(e2e.NewNativeCoin(s.network.Config.AccountTokens, s.network), *res.Balance) -} diff --git a/tests/e2e/bank/suite.go b/tests/e2e/bank/suite.go deleted file mode 100644 index 1566f26fd..000000000 --- a/tests/e2e/bank/suite.go +++ /dev/null @@ -1,26 +0,0 @@ -package bank - -import ( - "github.com/ExocoreNetwork/exocore/testutil/network" - "github.com/stretchr/testify/suite" -) - -type E2ETestSuite struct { - suite.Suite - - cfg network.Config - network *network.Network -} - -func NewE2ETestSuite(cfg network.Config) *E2ETestSuite { - return &E2ETestSuite{cfg: cfg} -} - -func (s *E2ETestSuite) SetupSuite() { - s.T().Log("setting up e2e test suite") - var err error - s.network, err = network.New(s.T(), s.T().TempDir(), s.cfg) - s.Require().NoError(err) - _, err = s.network.WaitForHeight(2) - s.Require().NoError(err) -} diff --git a/tests/e2e/bank/test.result b/tests/e2e/bank/test.result deleted file mode 100644 index e69de29bb..000000000 diff --git a/tests/e2e/bank/tx.go b/tests/e2e/bank/tx.go deleted file mode 100644 index e84020f09..000000000 --- a/tests/e2e/bank/tx.go +++ /dev/null @@ -1,50 +0,0 @@ -package bank - -import ( - sdkmath "cosmossdk.io/math" - "github.com/ExocoreNetwork/exocore/tests/e2e" - sdk "github.com/cosmos/cosmos-sdk/types" - banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" -) - -func (s *E2ETestSuite) TestSendCoin() { - kr := s.network.Validators[0].ClientCtx.Keyring - // generate a new account with ethsecp256k1 to receive/send native coins (hua) - toAddr, err := e2e.GenerateAccAddress(kr, "user1") - s.Require().NoError(err) - // generate sendCoin msg - fromAddr := s.network.Validators[0].Address - msg := banktypes.NewMsgSend(fromAddr, toAddr, sdk.NewCoins(sdk.NewCoin(s.network.Config.NativeDenom, sdkmath.NewInt(2000000)))) - - // send sendCoinMsg - err = s.network.SendTx([]sdk.Msg{msg}, s.network.Validators[0].ClientCtx.FromName, kr) - s.Require().NoError(err) - - // wait to next block for tx to be included - err = s.network.WaitForNextBlock() - s.Require().NoError(err) - - // check user1's balance - res, err := e2e.QueryNativeCoinBalance(toAddr, s.network) - s.Require().NoError(err) - s.Require().Equal(e2e.NewNativeCoin(sdkmath.NewInt(2000000), s.network), *res.Balance) - - toAddr2, err := e2e.GenerateAccAddress(kr, "user2") - s.Require().NoError(err) - - msg = banktypes.NewMsgSend(toAddr, toAddr2, sdk.NewCoins(sdk.NewCoin(s.network.Config.NativeDenom, sdkmath.NewInt(100)))) - // send sendCoinMsg - err = s.network.SendTx([]sdk.Msg{msg}, "user1", kr) - s.Require().NoError(err) - - // wait to next block for tx to be included - err = s.network.WaitForNextBlock() - s.Require().NoError(err) - err = s.network.WaitForNextBlock() - s.Require().NoError(err) - - // check user2's balance - res, err = e2e.QueryNativeCoinBalance(toAddr2, s.network) - s.Require().NoError(err) - s.Require().Equal(e2e.NewNativeCoin(sdkmath.NewInt(100), s.network), *res.Balance) -} diff --git a/tests/e2e/basesuite.go b/tests/e2e/basesuite.go deleted file mode 100644 index aa19de429..000000000 --- a/tests/e2e/basesuite.go +++ /dev/null @@ -1,26 +0,0 @@ -package e2e - -import ( - "github.com/ExocoreNetwork/exocore/testutil/network" - "github.com/stretchr/testify/suite" -) - -type BaseSuite struct { - suite.Suite - - Cfg network.Config - Network *network.Network -} - -func NewBaseSuite(cfg network.Config) *BaseSuite { - return &BaseSuite{Cfg: cfg} -} - -func (s *BaseSuite) SetupSuite() { - s.T().Log("setting up e2e test suite") - var err error - s.Network, err = network.New(s.T(), s.T().TempDir(), s.Cfg) - s.Require().NoError(err) - _, err = s.Network.WaitForHeight(2) - s.Require().NoError(err) -} diff --git a/tests/e2e/oracle/create_price.go b/tests/e2e/oracle/create_price.go index c5ac70280..313e38dc4 100644 --- a/tests/e2e/oracle/create_price.go +++ b/tests/e2e/oracle/create_price.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "math/big" + "os" "time" sdkmath "cosmossdk.io/math" @@ -18,6 +19,7 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" "google.golang.org/grpc/codes" + "google.golang.org/grpc/metadata" "google.golang.org/grpc/status" ) @@ -26,13 +28,13 @@ var ( creator0, creator1, creator2, creator3 sdk.AccAddress ) -// TestCreatePrice run test cases for oracle module including related workflow from other module(assets) -// create-price for LST -// create-price for NST +// TestPriceFeed run test cases for oracle module including related workflow from other module(assets) +// price-feed for LST +// price-feed for NST // registerToken automatically through assets module when precompiled is called // slashing for downtime // slashing for malicious price -func (s *E2ETestSuite) TestCreatePrice() { +func (s *E2ETestSuite) TestPriceFeed() { kr0 = s.network.Validators[0].ClientCtx.Keyring creator0 = sdk.AccAddress(s.network.Validators[0].PubKey.Address()) @@ -45,25 +47,92 @@ func (s *E2ETestSuite) TestCreatePrice() { kr3 = s.network.Validators[3].ClientCtx.Keyring creator3 = sdk.AccAddress(s.network.Validators[3].PubKey.Address()) - // we combine all test cases into one big case to avoid reset the network multiple times - s.testRegisterTokenThroughPrecompile() - s.testCreatePriceNST() - s.testCreatePriceLST() - s.testSlashing() + // we combine all test cases into one big case to avoid reset the network multiple times, the order can't be changed + + option := os.Getenv("TEST_OPTION") + if option == "local" { + s.testRecoveryCases(10) + } else { + s.testRegisterTokenThroughPrecompile() + s.testPriceFeedNST() + s.testPriceFeedLST() + s.testSlashing() + s.testPriceFeedLSTAfterDelegationChangePower() + } +} + +func (s *E2ETestSuite) testPriceFeedLSTAfterDelegationChangePower() { + s.moveToAndCheck(80) + priceTest1R1 := price2.updateTimestamp() + priceTimeDetID1R1 := priceTest1R1.getPriceTimeDetID("9") + priceSource1R1 := oracletypes.PriceSource{ + SourceID: 1, + Prices: []*oracletypes.PriceTimeDetID{ + &priceTimeDetID1R1, + }, + } + + // send price-feed from validator-0 + msg0 := oracletypes.NewMsgPriceFeed(creator0.String(), 1, []*oracletypes.PriceSource{&priceSource1R1}, 80, 1) + err := s.network.SendTxOraclePriceFeed([]sdk.Msg{msg0}, "valconskey0", kr0) + s.Require().NoError(err) + + // send price-feed from validator-1 + msg1 := oracletypes.NewMsgPriceFeed(creator1.String(), 1, []*oracletypes.PriceSource{&priceSource1R1}, 80, 1) + err = s.network.SendTxOraclePriceFeed([]sdk.Msg{msg1}, "valconskey1", kr1) + s.Require().NoError(err) + + s.moveToAndCheck(82) + res, err := s.network.QueryOracle().LatestPrice(ctxWithHeight(81), &oracletypes.QueryGetLatestPriceRequest{TokenId: 1}) + s.NoError(err) + s.Require().Equal(res.Price.Price, price1.Price) + + s.moveToAndCheck(85) + clientChainID := uint32(101) + lzNonce := uint64(0) + assetAddr, _ := hexutil.Decode(network.ETHAssetAddress) + stakerAddr := []byte(s.network.Validators[0].Address) + operatorAddr := []byte(s.network.Validators[0].Address.String()) + opAmount := big.NewInt(90000000) + // deposit 32 NSTETH to staker from beaconchain_validatro_1 + err = s.network.SendPrecompileTx(network.DELEGATION, "delegate", clientChainID, lzNonce, assetAddr, stakerAddr, operatorAddr, opAmount) + s.Require().NoError(err) + + // wait for validator set update + s.moveToAndCheck(120) + + // send price-feed from validator-0 + msg0 = oracletypes.NewMsgPriceFeed(creator0.String(), 1, []*oracletypes.PriceSource{&priceSource1R1}, 120, 1) + err = s.network.SendTxOraclePriceFeed([]sdk.Msg{msg0}, "valconskey0", kr0) + s.Require().NoError(err) + + // send price-feed from validator-1 + msg1 = oracletypes.NewMsgPriceFeed(creator1.String(), 1, []*oracletypes.PriceSource{&priceSource1R1}, 120, 1) + err = s.network.SendTxOraclePriceFeed([]sdk.Msg{msg1}, "valconskey1", kr1) + s.Require().NoError(err) + + s.moveToAndCheck(122) + // query final price. query state of 11 on height 12 + res, err = s.network.QueryOracle().LatestPrice(ctxWithHeight(121), &oracletypes.QueryGetLatestPriceRequest{TokenId: 1}) + s.Require().NoError(err) + + ret := priceTest1R1.getPriceTimeRound(12) + ret.Timestamp = res.Price.Timestamp + s.Require().Equal(ret, res.Price) } /* cases: - we need more than 2/3 power, so that at least 3 out of 4 validators power should be enough - 1. block_1_1: v1 sendPrice{p1}, [no round_1 price after block_1_1 committed], block_1_2:v2&v3 sendPrice{p1}, [got round_1 price{p1} after block_1_2 committed] - 2. block_2_1: v3 sendPrice{p2}, block_2_2: v1 sendPrice{p2}, [no round_2 price after block_2_2 committed], block_2_3:nothing, [got round_2 price{p1} equals to round_1 after block_2_3 committed] - 3. block_3_1: v1 sendPrice{p1}, block_3_2: v2&v3 sendPrice{p2}, block_3_3: v3 sendPrice{p2}, [got final price{p2} after block_3_3 committed] - 4. block_4_1: v1&v2&v3 sendPrice{p1}, [got round_4 price{p1} after block_4_1 committed]] +we need more than 2/3 power, so that at least 3 out of 4 validators power should be enough +1. block_1_1: v1 sendPrice{p1}, [no round_1 price after block_1_1 committed], block_1_2:v2&v3 sendPrice{p1}, [got round_1 price{p1} after block_1_2 committed] +2. block_2_1: v3 sendPrice{p2}, block_2_2: v1 sendPrice{p2}, [no round_2 price after block_2_2 committed], block_2_3:nothing, [got round_2 price{p1} equals to round_1 after block_2_3 committed] +3. block_3_1: v1 sendPrice{p1}, block_3_2: v2&v3 sendPrice{p2}, block_3_3: v3 sendPrice{p2}, [got final price{p2} after block_3_3 committed] +4. block_4_1: v1&v2&v3 sendPrice{p1}, [got round_4 price{p1} after block_4_1 committed]] - --- nonce: +--- nonce: */ -func (s *E2ETestSuite) testCreatePriceLST() { +func (s *E2ETestSuite) testPriceFeedLST() { priceTest1R1 := price1.updateTimestamp() priceTimeDetID1R1 := priceTest1R1.getPriceTimeDetID("9") priceSource1R1 := oracletypes.PriceSource{ @@ -76,39 +145,45 @@ func (s *E2ETestSuite) testCreatePriceLST() { // case_1. slashing_{miss_v3:1, window:2} [1.0] s.moveToAndCheck(10) - // send create-price from validator-0 - msg0 := oracletypes.NewMsgCreatePrice(creator0.String(), 1, []*oracletypes.PriceSource{&priceSource1R1}, 10, 1) - err := s.network.SendTxOracleCreateprice([]sdk.Msg{msg0}, "valconskey0", kr0) + // send price-feed from validator-0 + msg0 := oracletypes.NewMsgPriceFeed(creator0.String(), 1, []*oracletypes.PriceSource{&priceSource1R1}, 10, 1) + err := s.network.SendTxOraclePriceFeed([]sdk.Msg{msg0}, "valconskey0", kr0) s.Require().NoError(err) - s.moveNAndCheck(1) - // query final price - _, err = s.network.QueryOracle().LatestPrice(context.Background(), &oracletypes.QueryGetLatestPriceRequest{TokenId: 1}) - errStatus, _ := status.FromError(err) - s.Require().Equal(codes.NotFound, errStatus.Code()) + s.moveToAndCheck(11) - // send create-price from validator-1 - msg1 := oracletypes.NewMsgCreatePrice(creator1.String(), 1, []*oracletypes.PriceSource{&priceSource1R1}, 10, 1) - err = s.network.SendTxOracleCreateprice([]sdk.Msg{msg1}, "valconskey1", kr1) + // send price-feed from validator-1 + msg1 := oracletypes.NewMsgPriceFeed(creator1.String(), 1, []*oracletypes.PriceSource{&priceSource1R1}, 10, 1) + err = s.network.SendTxOraclePriceFeed([]sdk.Msg{msg1}, "valconskey1", kr1) s.Require().NoError(err) - // send create-price from validator-2 - msg2 := oracletypes.NewMsgCreatePrice(creator2.String(), 1, []*oracletypes.PriceSource{&priceSource1R1}, 10, 1) - err = s.network.SendTxOracleCreateprice([]sdk.Msg{msg2}, "valconskey2", kr2) + // send price-feed from validator-2 + msg2 := oracletypes.NewMsgPriceFeed(creator2.String(), 1, []*oracletypes.PriceSource{&priceSource1R1}, 10, 1) + err = s.network.SendTxOraclePriceFeed([]sdk.Msg{msg2}, "valconskey2", kr2) s.Require().NoError(err) - s.moveNAndCheck(1) - // query final price - res, err := s.network.QueryOracle().LatestPrice(context.Background(), &oracletypes.QueryGetLatestPriceRequest{TokenId: 1}) - s.Require().NoError(err) - s.Require().Equal(priceTest1R1.getPriceTimeRound(1), res.Price) + s.moveToAndCheck(12) // TODO: there might be a small chance that the blockHeight grows to more than 13, try bigger price window(nonce>3) to be more confident - // send create-price from validator3 to avoid being slashed for downtime - msg3 := oracletypes.NewMsgCreatePrice(creator3.String(), 1, []*oracletypes.PriceSource{&priceSource1R1}, 10, 1) - err = s.network.SendTxOracleCreateprice([]sdk.Msg{msg3}, "valconskey3", kr3) + // send price-feed from validator3 to avoid being slashed for downtime + msg3 := oracletypes.NewMsgPriceFeed(creator3.String(), 1, []*oracletypes.PriceSource{&priceSource1R1}, 10, 1) + err = s.network.SendTxOraclePriceFeed([]sdk.Msg{msg3}, "valconskey3", kr3) s.Require().NoError(err) + // query final price. query state of 11 on height 12 + _, err = s.network.QueryOracle().LatestPrice(ctxWithHeight(11), &oracletypes.QueryGetLatestPriceRequest{TokenId: 1}) + errStatus, _ := status.FromError(err) + s.Require().Equal(codes.NotFound, errStatus.Code()) + + s.moveToAndCheck(13) + // query final price. query state of 12 on height 13 + res, err := s.network.QueryOracle().LatestPrice(ctxWithHeight(12), &oracletypes.QueryGetLatestPriceRequest{TokenId: 1}) + s.Require().NoError(err) + // NOTE: update timestamp manually to ignore + ret := priceTest1R1.getPriceTimeRound(1) + ret.Timestamp = res.Price.Timestamp + s.Require().Equal(ret, res.Price) + // case_2. slashing{miss_v3:1, window:2} [1.0] // timestamp need to be updated priceTest2R2 := price2.updateTimestamp() @@ -119,23 +194,24 @@ func (s *E2ETestSuite) testCreatePriceLST() { &priceTimeDetID2R2, }, } - msg0 = oracletypes.NewMsgCreatePrice(creator0.String(), 1, []*oracletypes.PriceSource{&priceSource2R2}, 20, 1) - msg2 = oracletypes.NewMsgCreatePrice(creator2.String(), 1, []*oracletypes.PriceSource{&priceSource2R2}, 20, 1) - + msg0 = oracletypes.NewMsgPriceFeed(creator0.String(), 1, []*oracletypes.PriceSource{&priceSource2R2}, 20, 1) + msg2 = oracletypes.NewMsgPriceFeed(creator2.String(), 1, []*oracletypes.PriceSource{&priceSource2R2}, 20, 1) s.moveToAndCheck(20) // send price{p2} from validator-2 - err = s.network.SendTxOracleCreateprice([]sdk.Msg{msg2}, "valconskey2", kr2) + err = s.network.SendTxOraclePriceFeed([]sdk.Msg{msg2}, "valconskey2", kr2) s.Require().NoError(err) - s.moveNAndCheck(1) + s.moveToAndCheck(21) // send price{p2} from validator-0 - err = s.network.SendTxOracleCreateprice([]sdk.Msg{msg0}, "valconskey0", kr0) + err = s.network.SendTxOraclePriceFeed([]sdk.Msg{msg0}, "valconskey0", kr0) s.Require().NoError(err) - s.moveToAndCheck(23) - res, err = s.network.QueryOracle().LatestPrice(context.Background(), &oracletypes.QueryGetLatestPriceRequest{TokenId: 1}) + s.moveToAndCheck(24) + res, err = s.network.QueryOracle().LatestPrice(ctxWithHeight(23), &oracletypes.QueryGetLatestPriceRequest{TokenId: 1}) s.Require().NoError(err) // price update fail, round 2 still have price{p1} - s.Require().Equal(priceTest1R1.getPriceTimeRound(2), res.Price) - + // NOTE: update timestamp manually to ignore + ret = priceTest1R1.getPriceTimeRound(2) + ret.Timestamp = res.Price.Timestamp + s.Require().Equal(ret, res.Price) // case_3. slashing_{miss_v3:2, window:3} [1.0.1] // update timestamp priceTest2R3 := price2.updateTimestamp() @@ -146,54 +222,63 @@ func (s *E2ETestSuite) testCreatePriceLST() { &priceTimeDetID2R3, }, } - - msg0 = oracletypes.NewMsgCreatePrice(creator0.String(), 1, []*oracletypes.PriceSource{&priceSource2R3}, 30, 1) - msg1 = oracletypes.NewMsgCreatePrice(creator1.String(), 1, []*oracletypes.PriceSource{&priceSource2R3}, 30, 1) - msg2 = oracletypes.NewMsgCreatePrice(creator2.String(), 1, []*oracletypes.PriceSource{&priceSource2R3}, 30, 1) + msg0 = oracletypes.NewMsgPriceFeed(creator0.String(), 1, []*oracletypes.PriceSource{&priceSource2R3}, 30, 1) + msg1 = oracletypes.NewMsgPriceFeed(creator1.String(), 1, []*oracletypes.PriceSource{&priceSource2R3}, 30, 1) + msg2 = oracletypes.NewMsgPriceFeed(creator2.String(), 1, []*oracletypes.PriceSource{&priceSource2R3}, 30, 1) s.moveToAndCheck(30) // send price{p2} from validator-0 - err = s.network.SendTxOracleCreateprice([]sdk.Msg{msg0}, "valconskey0", kr0) + err = s.network.SendTxOraclePriceFeed([]sdk.Msg{msg0}, "valconskey0", kr0) s.Require().NoError(err) - s.moveNAndCheck(1) + s.moveToAndCheck(31) // send price{p2} from validator-1 - err = s.network.SendTxOracleCreateprice([]sdk.Msg{msg1}, "valconskey1", kr1) + err = s.network.SendTxOraclePriceFeed([]sdk.Msg{msg1}, "valconskey1", kr1) s.Require().NoError(err) // send price{p2} from validator-2 - err = s.network.SendTxOracleCreateprice([]sdk.Msg{msg2}, "valconskey2", kr2) + err = s.network.SendTxOraclePriceFeed([]sdk.Msg{msg2}, "valconskey2", kr2) s.Require().NoError(err) - s.moveNAndCheck(1) - res, err = s.network.QueryOracle().LatestPrice(context.Background(), &oracletypes.QueryGetLatestPriceRequest{TokenId: 1}) + s.moveToAndCheck(33) + res, err = s.network.QueryOracle().LatestPrice(ctxWithHeight(32), &oracletypes.QueryGetLatestPriceRequest{TokenId: 1}) s.Require().NoError(err) // price updated, round 3 has price{p2} - s.Require().Equal(priceTest2R3.getPriceTimeRound(3), res.Price) + // NOTE: update timestamp manually to ignore + ret = priceTest2R3.getPriceTimeRound(3) + ret.Timestamp = res.Price.Timestamp + s.Require().Equal(ret, res.Price) // case_4. slashing_{miss_v3:2, window:4}.maxWindow=4 [1.0.1.0] // update timestamp s.moveToAndCheck(40) priceTest1R4, priceSource1R4 := price1.generateRealTimeStructs("12", 1) - msg0 = oracletypes.NewMsgCreatePrice(creator0.String(), 1, []*oracletypes.PriceSource{&priceSource1R4}, 40, 1) - msg1 = oracletypes.NewMsgCreatePrice(creator1.String(), 1, []*oracletypes.PriceSource{&priceSource1R4}, 40, 1) - msg2 = oracletypes.NewMsgCreatePrice(creator2.String(), 1, []*oracletypes.PriceSource{&priceSource1R4}, 40, 1) - err = s.network.SendTxOracleCreateprice([]sdk.Msg{msg0}, "valconskey0", kr0) + msg0 = oracletypes.NewMsgPriceFeed(creator0.String(), 1, []*oracletypes.PriceSource{&priceSource1R4}, 40, 1) + msg1 = oracletypes.NewMsgPriceFeed(creator1.String(), 1, []*oracletypes.PriceSource{&priceSource1R4}, 40, 1) + msg2 = oracletypes.NewMsgPriceFeed(creator2.String(), 1, []*oracletypes.PriceSource{&priceSource1R4}, 40, 1) + err = s.network.SendTxOraclePriceFeed([]sdk.Msg{msg0}, "valconskey0", kr0) s.Require().NoError(err) - err = s.network.SendTxOracleCreateprice([]sdk.Msg{msg1}, "valconskey1", kr1) + err = s.network.SendTxOraclePriceFeed([]sdk.Msg{msg1}, "valconskey1", kr1) s.Require().NoError(err) - err = s.network.SendTxOracleCreateprice([]sdk.Msg{msg2}, "valconskey2", kr2) + err = s.network.SendTxOraclePriceFeed([]sdk.Msg{msg2}, "valconskey2", kr2) s.Require().NoError(err) - s.moveNAndCheck(1) - res, err = s.network.QueryOracle().LatestPrice(context.Background(), &oracletypes.QueryGetLatestPriceRequest{TokenId: 1}) + + s.moveToAndCheck(41) + // send price-feed from validator3 to avoid being slashed for downtime + msg3 = oracletypes.NewMsgPriceFeed(creator3.String(), 1, []*oracletypes.PriceSource{&priceSource1R4}, 40, 1) + err = s.network.SendTxOraclePriceFeed([]sdk.Msg{msg3}, "valconskey3", kr3) s.Require().NoError(err) - // price updated, round 4 has price{p1} - s.Require().Equal(priceTest1R4.getPriceTimeRound(4), res.Price) - // send create-price from validator3 to avoid being slashed for downtime - msg3 = oracletypes.NewMsgCreatePrice(creator3.String(), 1, []*oracletypes.PriceSource{&priceSource1R4}, 40, 1) - err = s.network.SendTxOracleCreateprice([]sdk.Msg{msg3}, "valconskey3", kr3) + + s.moveToAndCheck(42) + + res, err = s.network.QueryOracle().LatestPrice(ctxWithHeight(41), &oracletypes.QueryGetLatestPriceRequest{TokenId: 1}) s.Require().NoError(err) + // price updated, round 4 has price{p1} + // NOTE: update timestamp manually to ignore + ret = priceTest1R4.getPriceTimeRound(4) + ret.Timestamp = res.Price.Timestamp + s.Require().Equal(ret, res.Price) } -func (s *E2ETestSuite) testCreatePriceNST() { +func (s *E2ETestSuite) testPriceFeedNST() { clientChainID := uint32(101) validatorPubkey := []byte{1} // this is just a fake address @@ -207,28 +292,38 @@ func (s *E2ETestSuite) testCreatePriceNST() { // deposit 32 NSTETH to staker from beaconchain_validatro_1 err = s.network.SendPrecompileTx(network.ASSETS, "depositNST", clientChainID, validatorPubkey, stakerAddr, opAmount) s.Require().NoError(err) - s.moveNAndCheck(1) ctx := context.Background() - // check stakerAssetInfo + + // slashing_{miss_v3:1, window:1} [1] + s.moveToAndCheck(7) + _, ps := priceNST1.generateRealTimeStructs("100_1", 1) + msg0 := oracletypes.NewMsgPriceFeed(creator0.String(), 2, []*oracletypes.PriceSource{&ps}, 7, 1) + msg1 := oracletypes.NewMsgPriceFeed(creator1.String(), 2, []*oracletypes.PriceSource{&ps}, 7, 1) + msg2 := oracletypes.NewMsgPriceFeed(creator2.String(), 2, []*oracletypes.PriceSource{&ps}, 7, 1) + err = s.network.SendTxOraclePriceFeed([]sdk.Msg{msg0}, "valconskey0", kr0) + s.Require().NoError(err) + err = s.network.SendTxOraclePriceFeed([]sdk.Msg{msg1}, "valconskey1", kr1) + s.Require().NoError(err) + err = s.network.SendTxOraclePriceFeed([]sdk.Msg{msg2}, "valconskey2", kr2) + s.Require().NoError(err) + + // on height 7, the state from 6 is committed and confirmed res, err := s.network.QueryAssets().QueStakerSpecifiedAssetAmount(ctx, &assetstypes.QuerySpecifiedAssetAmountReq{StakerId: stakerID, AssetId: network.NativeAssetID}) + resStakerList, err2 := s.network.QueryOracle().StakerList(ctx, &oracletypes.QueryStakerListRequest{AssetId: network.NativeAssetID}) + resStakerInfo, err3 := s.network.QueryOracle().StakerInfo(ctx, &oracletypes.QueryStakerInfoRequest{AssetId: network.NativeAssetID, StakerAddr: stakerAddrStr}) s.Require().NoError(err) s.Require().Equal(assetstypes.StakerAssetInfo{ TotalDepositAmount: sdkmath.NewInt(32), WithdrawableAmount: sdkmath.NewInt(32), PendingUndelegationAmount: sdkmath.ZeroInt(), }, *res) - // check stakerList from oracle had been updated successfully - resStakerList, err := s.network.QueryOracle().StakerList(ctx, &oracletypes.QueryStakerListRequest{AssetId: network.NativeAssetID}) - s.Require().NoError(err) + s.Require().NoError(err2) s.Require().Equal(oracletypes.StakerList{ StakerAddrs: []string{ stakerAddrStr, }, }, *resStakerList.StakerList) - - // check stakerInfo from oracle had been updated successfully - resStakerInfo, err := s.network.QueryOracle().StakerInfo(ctx, &oracletypes.QueryStakerInfoRequest{AssetId: network.NativeAssetID, StakerAddr: stakerAddrStr}) - s.Require().NoError(err) + s.Require().NoError(err3) s.Require().Equal(oracletypes.StakerInfo{ StakerAddr: stakerAddrStr, StakerIndex: 0, @@ -246,31 +341,21 @@ func (s *E2ETestSuite) testCreatePriceNST() { }, }, *resStakerInfo.StakerInfo) - // slashing_{miss_v3:1, window:1} [1] - s.moveToAndCheck(7) - _, ps := priceNST1.generateRealTimeStructs("100", 1) - msg0 := oracletypes.NewMsgCreatePrice(creator0.String(), 2, []*oracletypes.PriceSource{&ps}, 7, 1) - msg1 := oracletypes.NewMsgCreatePrice(creator1.String(), 2, []*oracletypes.PriceSource{&ps}, 7, 1) - msg2 := oracletypes.NewMsgCreatePrice(creator2.String(), 2, []*oracletypes.PriceSource{&ps}, 7, 1) - err = s.network.SendTxOracleCreateprice([]sdk.Msg{msg0}, "valconskey0", kr0) - s.Require().NoError(err) - err = s.network.SendTxOracleCreateprice([]sdk.Msg{msg1}, "valconskey1", kr1) - s.Require().NoError(err) - err = s.network.SendTxOracleCreateprice([]sdk.Msg{msg2}, "valconskey2", kr2) - s.Require().NoError(err) - - s.moveNAndCheck(1) + // new block - 9, state of 8 is committed + s.moveToAndCheck(9) resStakerInfo, err = s.network.QueryOracle().StakerInfo(ctx, &oracletypes.QueryStakerInfoRequest{AssetId: network.NativeAssetID, StakerAddr: stakerAddrStr}) s.Require().NoError(err) s.Require().Equal(2, len(resStakerInfo.StakerInfo.BalanceList)) s.Require().Equal([]*oracletypes.BalanceInfo{ { Block: 6, + Index: 0, Balance: 32, Change: oracletypes.Action_ACTION_DEPOSIT, }, { RoundID: 1, + Index: 1, Block: 8, Balance: 28, Change: oracletypes.Action_ACTION_SLASH_REFUND, @@ -286,34 +371,39 @@ func (s *E2ETestSuite) testSlashing() { s.moveToAndCheck(50) // slashing_{miss_v3:3, window:5}[1.0.1.0.1] -> {miss_v3:2, window:4} [0.1.0.1] priceTest1R5, priceSource1R5 := price1.generateRealTimeStructs("13", 1) - msg0 := oracletypes.NewMsgCreatePrice(creator0.String(), 1, []*oracletypes.PriceSource{&priceSource1R5}, 50, 1) - msg1 := oracletypes.NewMsgCreatePrice(creator1.String(), 1, []*oracletypes.PriceSource{&priceSource1R5}, 50, 1) - msg2 := oracletypes.NewMsgCreatePrice(creator2.String(), 1, []*oracletypes.PriceSource{&priceSource1R5}, 50, 1) - err := s.network.SendTxOracleCreateprice([]sdk.Msg{msg0}, "valconskey0", kr0) + msg0 := oracletypes.NewMsgPriceFeed(creator0.String(), 1, []*oracletypes.PriceSource{&priceSource1R5}, 50, 1) + msg1 := oracletypes.NewMsgPriceFeed(creator1.String(), 1, []*oracletypes.PriceSource{&priceSource1R5}, 50, 1) + msg2 := oracletypes.NewMsgPriceFeed(creator2.String(), 1, []*oracletypes.PriceSource{&priceSource1R5}, 50, 1) + err := s.network.SendTxOraclePriceFeed([]sdk.Msg{msg0}, "valconskey0", kr0) s.Require().NoError(err) - err = s.network.SendTxOracleCreateprice([]sdk.Msg{msg1}, "valconskey1", kr1) + err = s.network.SendTxOraclePriceFeed([]sdk.Msg{msg1}, "valconskey1", kr1) s.Require().NoError(err) - err = s.network.SendTxOracleCreateprice([]sdk.Msg{msg2}, "valconskey2", kr2) + err = s.network.SendTxOraclePriceFeed([]sdk.Msg{msg2}, "valconskey2", kr2) s.Require().NoError(err) - s.moveNAndCheck(1) - res, err := s.network.QueryOracle().LatestPrice(context.Background(), &oracletypes.QueryGetLatestPriceRequest{TokenId: 1}) + s.moveToAndCheck(52) + // query state of 51 on height 52 + res, err := s.network.QueryOracle().LatestPrice(ctxWithHeight(51), &oracletypes.QueryGetLatestPriceRequest{TokenId: 1}) s.Require().NoError(err) // price updated, round 4 has price{p1} - s.Require().Equal(priceTest1R5.getPriceTimeRound(5), res.Price) + // NOTE: update timestamp manually to ignore + ret := priceTest1R5.getPriceTimeRound(5) + ret.Timestamp = res.Price.Timestamp + s.Require().Equal(ret, res.Price) s.moveToAndCheck(60) // slashing_{miss_v3:3, window:5} [0.1.0.1.1] -> {miss_v3:2, window:4} [1.0.1.1] _, priceSource1R6 := price1.generateRealTimeStructs("14", 1) - msg0 = oracletypes.NewMsgCreatePrice(creator0.String(), 1, []*oracletypes.PriceSource{&priceSource1R6}, 60, 1) - msg1 = oracletypes.NewMsgCreatePrice(creator1.String(), 1, []*oracletypes.PriceSource{&priceSource1R6}, 60, 1) - msg2 = oracletypes.NewMsgCreatePrice(creator2.String(), 1, []*oracletypes.PriceSource{&priceSource1R6}, 60, 1) - err = s.network.SendTxOracleCreateprice([]sdk.Msg{msg0}, "valconskey0", kr0) + msg0 = oracletypes.NewMsgPriceFeed(creator0.String(), 1, []*oracletypes.PriceSource{&priceSource1R6}, 60, 1) + msg1 = oracletypes.NewMsgPriceFeed(creator1.String(), 1, []*oracletypes.PriceSource{&priceSource1R6}, 60, 1) + msg2 = oracletypes.NewMsgPriceFeed(creator2.String(), 1, []*oracletypes.PriceSource{&priceSource1R6}, 60, 1) + err = s.network.SendTxOraclePriceFeed([]sdk.Msg{msg0}, "valconskey0", kr0) s.Require().NoError(err) - err = s.network.SendTxOracleCreateprice([]sdk.Msg{msg1}, "valconskey1", kr1) + err = s.network.SendTxOraclePriceFeed([]sdk.Msg{msg1}, "valconskey1", kr1) s.Require().NoError(err) - err = s.network.SendTxOracleCreateprice([]sdk.Msg{msg2}, "valconskey2", kr2) + err = s.network.SendTxOraclePriceFeed([]sdk.Msg{msg2}, "valconskey2", kr2) s.Require().NoError(err) - s.moveToAndCheck(63) - resSigningInfo, err := s.network.QuerySlashing().SigningInfo(context.Background(), &slashingtypes.QuerySigningInfoRequest{ConsAddress: sdk.ConsAddress(s.network.Validators[3].PubKey.Address()).String()}) + s.moveToAndCheck(64) + // query state of 63 on height 64 + resSigningInfo, err := s.network.QuerySlashing().SigningInfo(ctxWithHeight(63), &slashingtypes.QuerySigningInfoRequest{ConsAddress: sdk.ConsAddress(s.network.Validators[3].PubKey.Address()).String()}) s.Require().NoError(err) // validator3 is jailed s.Require().True(resSigningInfo.ValSigningInfo.JailedUntil.After(time.Now())) @@ -323,7 +413,8 @@ func (s *E2ETestSuite) testSlashing() { s.Require().NoError(err) s.Require().True(resOperator.Jailed) // wait for validator3 to pass jail duration - time.Sleep(35 * time.Second) + // timeout commit is set to 2 seconds, 10 blocks about 20 seconds + s.moveToAndCheck(75) msgUnjail := slashingtypes.NewMsgUnjail(s.network.Validators[3].ValAddress) // unjail validator3 err = s.network.SendTx([]sdk.Msg{msgUnjail}, "node3", kr3) @@ -331,7 +422,6 @@ func (s *E2ETestSuite) testSlashing() { s.moveNAndCheck(2) resOperator, err = s.network.QueryOperator().QueryOptInfo(context.Background(), &operatortypes.QueryOptInfoRequest{OperatorAVSAddress: &operatortypes.OperatorAVSAddress{OperatorAddr: s.network.Validators[3].Address.String(), AvsAddress: avsAddr}}) s.Require().NoError(err) - fmt.Println("debug----->jailed:", resOperator.Jailed) s.Require().False(resOperator.Jailed) } @@ -349,15 +439,15 @@ func (s *E2ETestSuite) testRegisterTokenThroughPrecompile() { err := s.network.SendPrecompileTx(network.ASSETS, "registerToken", clientChainID, token, decimal, name, metaData, oracleInfo) s.Require().NoError(err) - s.moveNAndCheck(1) + s.moveToAndCheck(4) // registerToken will automaticlly register that token into oracle module - res, err := s.network.QueryOracle().Params(context.Background(), &oracletypes.QueryParamsRequest{}) + res, err := s.network.QueryOracle().Params(ctxWithHeight(3), &oracletypes.QueryParamsRequest{}) s.Require().NoError(err) - s.Require().Equal(name, res.Params.Tokens[3].Name) + s.Require().Equal(name, res.Params.Tokens[len(res.Params.Tokens)-1].Name) } func (s *E2ETestSuite) moveToAndCheck(height int64) { - _, err := s.network.WaitForStateHeightWithTimeout(height, 30*time.Second) + _, err := s.network.WaitForStateHeightWithTimeout(height, 120*time.Second) s.Require().NoError(err) } @@ -367,3 +457,8 @@ func (s *E2ETestSuite) moveNAndCheck(n int64) { s.Require().NoError(err) } } + +func ctxWithHeight(height int64) context.Context { + md := metadata.Pairs("x-cosmos-block-height", fmt.Sprintf("%d", height)) + return metadata.NewOutgoingContext(context.Background(), md) +} diff --git a/tests/e2e/oracle/data.go b/tests/e2e/oracle/data.go index 32bdbf871..55bb1b71e 100644 --- a/tests/e2e/oracle/data.go +++ b/tests/e2e/oracle/data.go @@ -1,12 +1,10 @@ package oracle import ( - "time" - oracletypes "github.com/ExocoreNetwork/exocore/x/oracle/types" ) -const layout = "2006-01-02 15:04:05" +var now = "2025-01-01 00:00:00" type priceTime struct { Price string @@ -33,8 +31,7 @@ func (p priceTime) getPriceTimeRound(roundID uint64) oracletypes.PriceTimeRound } func (p priceTime) updateTimestamp() priceTime { - t := time.Now().UTC().Format(layout) - p.Timestamp = t + p.Timestamp = now return p } @@ -55,22 +52,100 @@ func generateNSTPriceTime(sc [][]int) priceTime { return priceTime{ Price: string(rawBytes), Decimal: 0, - Timestamp: time.Now().UTC().Format(layout), + Timestamp: now, } } var ( price1 = priceTime{ - Price: "199999", - Decimal: 18, - Timestamp: time.Now().UTC().Format(layout), + Price: "1900000000", + Decimal: 8, + Timestamp: now, } price2 = priceTime{ - Price: "299999", - Decimal: 18, - Timestamp: time.Now().UTC().Format(layout), + Price: "290000000", + Decimal: 8, + Timestamp: now, } stakerChanges1 = [][]int{{0, -4}} priceNST1 = generateNSTPriceTime(stakerChanges1) + + // 1. detID:1, price: 123 + // 2. detID:1, price: 129 + // 3. detID:2, price: 127 + priceRecovery1 = oracletypes.PriceSource{ + SourceID: 1, + Prices: []*oracletypes.PriceTimeDetID{ + { + Price: "12300000000", + Decimal: 8, + DetID: "1", + Timestamp: now, + }, + }, + } + priceRecovery1_2 = oracletypes.PriceSource{ + SourceID: 1, + Prices: []*oracletypes.PriceTimeDetID{ + { + Price: "12300000000", + Decimal: 8, + DetID: "1", + Timestamp: now, + }, + { + Price: "12700000000", + Decimal: 8, + DetID: "2", + Timestamp: now, + }, + }, + } + + priceRecovery1_3 = oracletypes.PriceSource{ + SourceID: 1, + Prices: []*oracletypes.PriceTimeDetID{ + { + Price: "12300000000", + Decimal: 8, + DetID: "1", + Timestamp: now, + }, + { + Price: "12700000000", + Decimal: 8, + DetID: "2", + Timestamp: now, + }, + { + Price: "12900000000", + Decimal: 8, + DetID: "3", + Timestamp: now, + }, + }, + } + priceRecovery2 = oracletypes.PriceSource{ + SourceID: 1, + Prices: []*oracletypes.PriceTimeDetID{ + { + Price: "12700000000", + Decimal: 8, + DetID: "2", + Timestamp: now, + }, + }, + } + priceRecovery3 = oracletypes.PriceSource{ + SourceID: 1, + Prices: []*oracletypes.PriceTimeDetID{ + { + Price: "12900000000", + Decimal: 8, + DetID: "3", + Timestamp: now, + }, + }, + } ) diff --git a/tests/e2e/oracle/helper_nstconvert.go b/tests/e2e/oracle/helper_nstconvert.go index 57dcb6910..3cae1a4ff 100644 --- a/tests/e2e/oracle/helper_nstconvert.go +++ b/tests/e2e/oracle/helper_nstconvert.go @@ -2,6 +2,7 @@ package oracle import ( "encoding/binary" + "math" "strings" "github.com/imroc/biu" @@ -10,8 +11,7 @@ import ( func convertBalanceChangeToBytes(stakerChanges [][]int) []byte { if len(stakerChanges) == 0 { // length equals to 0 means that alls takers have efb of 32 with 0 changes - ret := make([]byte, 32) - return ret + return make([]byte, 32) } str := "" index := 0 @@ -23,6 +23,10 @@ func convertBalanceChangeToBytes(stakerChanges [][]int) []byte { // change amount -> bytes change := stakerChange[1] + if (change > 0 && change > math.MaxUint16) || + (change < 0 && (-1*change) > math.MaxUint16) { + return make([]byte, 32) + } var changeBytes []byte symbol := 1 if change < 0 { @@ -47,6 +51,7 @@ func convertBalanceChangeToBytes(stakerChanges [][]int) []byte { } else { // 2 byte changeBytes = make([]byte, 2) + // #nosec G115 // change has been checked to make sure no overflow binary.BigEndian.PutUint16(changeBytes, uint16(change)) moveLength := 16 - bits changeBytes[0] <<= moveLength diff --git a/tests/e2e/oracle/recovery.go b/tests/e2e/oracle/recovery.go new file mode 100644 index 000000000..7dfbaabb9 --- /dev/null +++ b/tests/e2e/oracle/recovery.go @@ -0,0 +1,530 @@ +package oracle + +import ( + "math/big" + + "github.com/ExocoreNetwork/exocore/testutil/network" + oracletypes "github.com/ExocoreNetwork/exocore/x/oracle/types" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/ethereum/go-ethereum/common/hexutil" +) + +// the test cases run with 'devmode' flag, we try to elaborate all cases to check the recovery logic works fine in each scenario +// this could take some time since we will run for many tokenfeeder rounds to cover many cases +// +// comments explain: +// +// 1{1} means the first block includes one valid quote +// 1{1-} means the first block includes on invalid quote which is different with the expected final price(detID, price) +func (s *E2ETestSuite) testRecoveryCases(start int64) { + // 1.successfully aggregated, + // 1.1 all prices provided are the same(detID, price, decimal) + // 1{3}, 2{1}, 3 + s.moveToAndCheck(start) + // #nosec G115 -- block height is positive + startUint := uint64(start) + msg0 := oracletypes.NewMsgPriceFeed(creator0.String(), 1, []*oracletypes.PriceSource{&priceRecovery1}, startUint, 1) + msg1 := oracletypes.NewMsgPriceFeed(creator1.String(), 1, []*oracletypes.PriceSource{&priceRecovery1}, startUint, 1) + msg2 := oracletypes.NewMsgPriceFeed(creator2.String(), 1, []*oracletypes.PriceSource{&priceRecovery1}, startUint, 1) + msg3 := oracletypes.NewMsgPriceFeed(creator3.String(), 1, []*oracletypes.PriceSource{&priceRecovery1}, startUint, 1) + + err := s.network.SendTxOraclePriceFeed([]sdk.Msg{msg0}, "valconsKey0", kr0) + s.Require().NoError(err) + + err = s.network.SendTxOraclePriceFeed([]sdk.Msg{msg1}, "valconsKey1", kr1) + s.Require().NoError(err) + + err = s.network.SendTxOraclePriceFeed([]sdk.Msg{msg2}, "valconsKey2", kr2) + s.Require().NoError(err) + + s.moveToAndCheck(start + 1) + err = s.network.SendTxOraclePriceFeed([]sdk.Msg{msg3}, "valconsKey3", kr3) + s.Require().NoError(err) + + s.moveToAndCheck(start + 2) + res, err := s.network.QueryOracle().LatestPrice(ctxWithHeight(start+1), &oracletypes.QueryGetLatestPriceRequest{TokenId: 1}) + s.NoError(err) + s.Require().Equal(res.Price.Price, priceRecovery1.Prices[0].Price) + + // 1{1}, 2{3}, 3 + // init_start + 10 + start += 10 + startUint = uint64(start) + msg0_1 := oracletypes.NewMsgPriceFeed(creator0.String(), 1, []*oracletypes.PriceSource{&priceRecovery2}, startUint, 1) + msg1_1 := oracletypes.NewMsgPriceFeed(creator1.String(), 1, []*oracletypes.PriceSource{&priceRecovery2}, startUint, 1) + msg2_1 := oracletypes.NewMsgPriceFeed(creator2.String(), 1, []*oracletypes.PriceSource{&priceRecovery2}, startUint, 1) + msg3_1 := oracletypes.NewMsgPriceFeed(creator3.String(), 1, []*oracletypes.PriceSource{&priceRecovery2}, startUint, 1) + + s.moveToAndCheck(start) + + err = s.network.SendTxOraclePriceFeed([]sdk.Msg{msg0_1}, "valconsKey0", kr0) + s.Require().NoError(err) + + s.moveToAndCheck(start + 1) + + err = s.network.SendTxOraclePriceFeed([]sdk.Msg{msg1_1}, "valconsKey1", kr1) + s.Require().NoError(err) + + err = s.network.SendTxOraclePriceFeed([]sdk.Msg{msg2_1}, "valconsKey2", kr2) + s.Require().NoError(err) + + err = s.network.SendTxOraclePriceFeed([]sdk.Msg{msg3_1}, "valconsKey3", kr3) + s.Require().NoError(err) + + s.moveToAndCheck(start + 3) + + res, err = s.network.QueryOracle().LatestPrice(ctxWithHeight(start+1), &oracletypes.QueryGetLatestPriceRequest{TokenId: 1}) + s.NoError(err) + // price not updated yet + s.Require().Equal(res.Price.Price, priceRecovery1.Prices[0].Price) + + res, err = s.network.QueryOracle().LatestPrice(ctxWithHeight(start+2), &oracletypes.QueryGetLatestPriceRequest{TokenId: 1}) + s.NoError(err) + // price updated from priceRecovery1 to priceRecovery2 + s.Require().Equal(res.Price.Price, priceRecovery2.Prices[0].Price) + + // 1{1}, 2, 3{3} + // init_start + 20 + start += 10 + startUint = uint64(start) + + s.moveToAndCheck(start) + msg0.BasedBlock = startUint + msg1.BasedBlock = startUint + msg2.BasedBlock = startUint + msg3.BasedBlock = startUint + + err = s.network.SendTxOraclePriceFeed([]sdk.Msg{msg0}, "valconsKey0", kr0) + s.Require().NoError(err) + + s.moveToAndCheck(start + 2) + + err = s.network.SendTxOraclePriceFeed([]sdk.Msg{msg1}, "valconsKey1", kr1) + s.Require().NoError(err) + + err = s.network.SendTxOraclePriceFeed([]sdk.Msg{msg2}, "valconsKey2", kr2) + s.Require().NoError(err) + + err = s.network.SendTxOraclePriceFeed([]sdk.Msg{msg3}, "valconsKey3", kr3) + s.Require().NoError(err) + + s.moveToAndCheck(start + 4) + + res, err = s.network.QueryOracle().LatestPrice(ctxWithHeight(start+2), &oracletypes.QueryGetLatestPriceRequest{TokenId: 1}) + s.NoError(err) + // price not updated yet + s.Require().Equal(res.Price.Price, priceRecovery2.Prices[0].Price) + + res, err = s.network.QueryOracle().LatestPrice(ctxWithHeight(start+3), &oracletypes.QueryGetLatestPriceRequest{TokenId: 1}) + s.NoError(err) + // price updated from priceRecovery2 to priceRecovery1 + s.Require().Equal(res.Price.Price, priceRecovery1.Prices[0].Price) + + // 1{1}, 2{1}, 3{1} + // init_start + 30 + start += 10 + startUint = uint64(start) + s.moveToAndCheck(start) + msg1_1.BasedBlock = startUint + msg2_1.BasedBlock = startUint + msg3_1.BasedBlock = startUint + + err = s.network.SendTxOraclePriceFeed([]sdk.Msg{msg1_1}, "valconsKey1", kr1) + s.Require().NoError(err) + + s.moveToAndCheck(start + 1) + + err = s.network.SendTxOraclePriceFeed([]sdk.Msg{msg2_1}, "valconsKey2", kr2) + s.Require().NoError(err) + + s.moveToAndCheck(start + 2) + + err = s.network.SendTxOraclePriceFeed([]sdk.Msg{msg3_1}, "valconsKey3", kr3) + s.Require().NoError(err) + + s.moveToAndCheck(start + 4) + + res, err = s.network.QueryOracle().LatestPrice(ctxWithHeight(start+2), &oracletypes.QueryGetLatestPriceRequest{TokenId: 1}) + s.Require().NoError(err) + // price not updated yet + s.Require().Equal(res.Price.Price, priceRecovery1.Prices[0].Price) + + res, err = s.network.QueryOracle().LatestPrice(ctxWithHeight(start+3), &oracletypes.QueryGetLatestPriceRequest{TokenId: 1}) + s.Require().NoError(err) + // price updated from priceRecovery1 to priceRecovery2 + s.Require().Equal(res.Price.Price, priceRecovery2.Prices[0].Price) + + // 1{1}, 2{1}, 3{2} + // init_start+40 + start += 10 + startUint = uint64(start) + s.moveToAndCheck(start) + msg1.BasedBlock = startUint + msg2.BasedBlock = startUint + msg3.BasedBlock = startUint + msg0.BasedBlock = startUint + + err = s.network.SendTxOraclePriceFeed([]sdk.Msg{msg1}, "valconsKey1", kr1) + s.Require().NoError(err) + + s.moveToAndCheck(start + 1) + + err = s.network.SendTxOraclePriceFeed([]sdk.Msg{msg2}, "valconsKey2", kr2) + s.Require().NoError(err) + + s.moveToAndCheck(start + 2) + + err = s.network.SendTxOraclePriceFeed([]sdk.Msg{msg3}, "valconsKey3", kr3) + s.Require().NoError(err) + + err = s.network.SendTxOraclePriceFeed([]sdk.Msg{msg0}, "valconsKey0", kr0) + s.Require().NoError(err) + + s.moveToAndCheck(start + 4) + + res, err = s.network.QueryOracle().LatestPrice(ctxWithHeight(start+2), &oracletypes.QueryGetLatestPriceRequest{TokenId: 1}) + s.Require().NoError(err) + // price not updated yet + s.Require().Equal(res.Price.Price, priceRecovery2.Prices[0].Price) + + res, err = s.network.QueryOracle().LatestPrice(ctxWithHeight(start+3), &oracletypes.QueryGetLatestPriceRequest{TokenId: 1}) + s.Require().NoError(err) + // price updated from priceRecovery2 to priceRecovery1 + s.Require().Equal(res.Price.Price, priceRecovery1.Prices[0].Price) + + // 1{1}, 2{2}, 3{1} + // init_start+50 + start += 10 + startUint = uint64(start) + s.moveToAndCheck(start) + msg1_1.BasedBlock = startUint + msg2_1.BasedBlock = startUint + msg3_1.BasedBlock = startUint + msg0_1.BasedBlock = startUint + + err = s.network.SendTxOraclePriceFeed([]sdk.Msg{msg1_1}, "valconsKey1", kr1) + s.Require().NoError(err) + + s.moveToAndCheck(start + 1) + + err = s.network.SendTxOraclePriceFeed([]sdk.Msg{msg2_1}, "valconsKey2", kr2) + s.Require().NoError(err) + + s.moveToAndCheck(start + 2) + + err = s.network.SendTxOraclePriceFeed([]sdk.Msg{msg3_1}, "valconsKey3", kr3) + s.Require().NoError(err) + + err = s.network.SendTxOraclePriceFeed([]sdk.Msg{msg0_1}, "valconsKey0", kr0) + s.Require().NoError(err) + + s.moveToAndCheck(start + 4) + + res, err = s.network.QueryOracle().LatestPrice(ctxWithHeight(start+2), &oracletypes.QueryGetLatestPriceRequest{TokenId: 1}) + s.Require().NoError(err) + // price not updated yet + s.Require().Equal(res.Price.Price, priceRecovery1.Prices[0].Price) + + res, err = s.network.QueryOracle().LatestPrice(ctxWithHeight(start+3), &oracletypes.QueryGetLatestPriceRequest{TokenId: 1}) + s.Require().NoError(err) + // price updated from priceRecovery1 to priceRecovery2 + s.Require().Equal(res.Price.Price, priceRecovery2.Prices[0].Price) + + // 1{1}, 2{2}, 3{1} + // init_start+60 + start += 10 + startUint = uint64(start) + s.moveToAndCheck(start) + msg1.BasedBlock = startUint + msg2.BasedBlock = startUint + msg0.BasedBlock = startUint + msg3.BasedBlock = startUint + + err = s.network.SendTxOraclePriceFeed([]sdk.Msg{msg1}, "valconsKey1", kr1) + s.Require().NoError(err) + + s.moveToAndCheck(start + 1) + + err = s.network.SendTxOraclePriceFeed([]sdk.Msg{msg2}, "valconsKey2", kr2) + s.Require().NoError(err) + + err = s.network.SendTxOraclePriceFeed([]sdk.Msg{msg0}, "valconsKey0", kr0) + s.Require().NoError(err) + + s.moveToAndCheck(start + 2) + + err = s.network.SendTxOraclePriceFeed([]sdk.Msg{msg3}, "valconsKey3", kr3) + s.Require().NoError(err) + + s.moveToAndCheck(start + 3) + + res, err = s.network.QueryOracle().LatestPrice(ctxWithHeight(start+1), &oracletypes.QueryGetLatestPriceRequest{TokenId: 1}) + s.Require().NoError(err) + // price not updated yet + s.Require().Equal(res.Price.Price, priceRecovery2.Prices[0].Price) + + res, err = s.network.QueryOracle().LatestPrice(ctxWithHeight(start+2), &oracletypes.QueryGetLatestPriceRequest{TokenId: 1}) + s.Require().NoError(err) + // price updated from priceRecovery2 to priceRecovery1 + s.Require().Equal(res.Price.Price, priceRecovery1.Prices[0].Price) + + // 1{2}, 2{2}, mixed prices + // init_start+70 + start += 10 + startUint = uint64(start) + s.moveToAndCheck(start) + msg1_2 := oracletypes.NewMsgPriceFeed(creator1.String(), 1, []*oracletypes.PriceSource{&priceRecovery1_3}, startUint, 1) + msg2_2 := oracletypes.NewMsgPriceFeed(creator2.String(), 1, []*oracletypes.PriceSource{&priceRecovery1_2}, startUint, 1) + msg3_2 := oracletypes.NewMsgPriceFeed(creator3.String(), 1, []*oracletypes.PriceSource{&priceRecovery3}, startUint, 1) + // msg0_2 := oracletypes.NewMsgPriceFeed(creator0.String(), 1, []*oracletypes.PriceSource{&priceRecovery3}, startUint, 1) + msg0_1.BasedBlock = startUint + + // id:1,2,3 + err = s.network.SendTxOraclePriceFeed([]sdk.Msg{msg1_2}, "valconsKey1", kr1) + s.Require().NoError(err) + + // id:3 + err = s.network.SendTxOraclePriceFeed([]sdk.Msg{msg3_2}, "valconsKey3", kr3) + s.Require().NoError(err) + + s.moveToAndCheck(start + 1) + + // id:1,2 + err = s.network.SendTxOraclePriceFeed([]sdk.Msg{msg2_2}, "valconsKey2", kr2) + s.Require().NoError(err) + + // id:2 + err = s.network.SendTxOraclePriceFeed([]sdk.Msg{msg0_1}, "valconsKey0", kr0) + s.Require().NoError(err) + + s.moveToAndCheck(start + 3) + + res, err = s.network.QueryOracle().LatestPrice(ctxWithHeight(start+1), &oracletypes.QueryGetLatestPriceRequest{TokenId: 1}) + s.Require().NoError(err) + // price not updated yet + s.Require().Equal(res.Price.Price, priceRecovery1.Prices[0].Price) + + res, err = s.network.QueryOracle().LatestPrice(ctxWithHeight(start+2), &oracletypes.QueryGetLatestPriceRequest{TokenId: 1}) + s.Require().NoError(err) + // price updated from priceRecovery1 to priceRecovery2 + s.Require().Equal(res.Price.Price, priceRecovery2.Prices[0].Price) + + // 1{2}, 2, 3{2} + // init_start+80 + start += 10 + startUint = uint64(start) + s.moveToAndCheck(start) + msg3_2.BasedBlock = startUint + msg1_2.BasedBlock = startUint + msg2_2.BasedBlock = startUint + msg0.BasedBlock = startUint + // id:3 + err = s.network.SendTxOraclePriceFeed([]sdk.Msg{msg3_2}, "valconsKey3", kr3) + s.Require().NoError(err) + + // id:1,2,3 + err = s.network.SendTxOraclePriceFeed([]sdk.Msg{msg1_2}, "valconsKey1", kr1) + s.Require().NoError(err) + + s.moveToAndCheck(start + 2) + + // id:1,2 + err = s.network.SendTxOraclePriceFeed([]sdk.Msg{msg2_2}, "valconsKey2", kr2) + s.Require().NoError(err) + + // id:1 + err = s.network.SendTxOraclePriceFeed([]sdk.Msg{msg0}, "valconsKey0", kr0) + s.Require().NoError(err) + + s.moveToAndCheck(start + 4) + + res, err = s.network.QueryOracle().LatestPrice(ctxWithHeight(start+1), &oracletypes.QueryGetLatestPriceRequest{TokenId: 1}) + s.Require().NoError(err) + // price not updated yet + s.Require().Equal(res.Price.Price, priceRecovery2.Prices[0].Price) + + res, err = s.network.QueryOracle().LatestPrice(ctxWithHeight(start+3), &oracletypes.QueryGetLatestPriceRequest{TokenId: 1}) + s.Require().NoError(err) + // price updated from priceRecovery2 to priceRecovery1 + s.Require().Equal(res.Price.Price, priceRecovery1.Prices[0].Price) + + // 1{2}, 2{2}, 3. mixed prices + // init_start+90 + start += 10 + startUint = uint64(start) + s.moveToAndCheck(start) + msg3_2.BasedBlock = startUint + msg1_2.BasedBlock = startUint + msg2_2.BasedBlock = startUint + msg0_2 := oracletypes.NewMsgPriceFeed(creator0.String(), 1, []*oracletypes.PriceSource{&priceRecovery3}, startUint, 1) + // msg0_2.BasedBlock = startUint + // id:3 + err = s.network.SendTxOraclePriceFeed([]sdk.Msg{msg3_2}, "valconsKey3", kr3) + s.Require().NoError(err) + + // id:1,2,3 + err = s.network.SendTxOraclePriceFeed([]sdk.Msg{msg1_2}, "valconsKey1", kr1) + s.Require().NoError(err) + + s.moveToAndCheck(start + 1) + + // id:1,2 + err = s.network.SendTxOraclePriceFeed([]sdk.Msg{msg2_2}, "valconsKey2", kr2) + s.Require().NoError(err) + + // id:3 + err = s.network.SendTxOraclePriceFeed([]sdk.Msg{msg0_2}, "valconsKey0", kr0) + s.Require().NoError(err) + + s.moveToAndCheck(start + 3) + + res, err = s.network.QueryOracle().LatestPrice(ctxWithHeight(start+1), &oracletypes.QueryGetLatestPriceRequest{TokenId: 1}) + s.Require().NoError(err) + // price not updated yet + s.Require().Equal(res.Price.Price, priceRecovery1.Prices[0].Price) + + res, err = s.network.QueryOracle().LatestPrice(ctxWithHeight(start+2), &oracletypes.QueryGetLatestPriceRequest{TokenId: 1}) + s.Require().NoError(err) + // price updated from priceRecovery1 to priceRecovery3 + s.Require().Equal(res.Price.Price, priceRecovery3.Prices[0].Price) + + // 2.failed to aggregate + // 2.1 all prices provided are the same(detID, price, decimal), failed for not enough power + // init_start+100 + start += 10 + startUint = uint64(start) + s.moveToAndCheck(start) + msg3.BasedBlock = startUint + msg1.BasedBlock = startUint + msg2_1.BasedBlock = startUint + msg0_1.BasedBlock = startUint + + err = s.network.SendTxOraclePriceFeed([]sdk.Msg{msg3}, "valconsKey3", kr3) + s.Require().NoError(err) + + err = s.network.SendTxOraclePriceFeed([]sdk.Msg{msg1}, "valconsKey1", kr1) + s.Require().NoError(err) + + s.moveToAndCheck(start + 1) + + err = s.network.SendTxOraclePriceFeed([]sdk.Msg{msg2_1}, "valconsKey2", kr2) + s.Require().NoError(err) + + err = s.network.SendTxOraclePriceFeed([]sdk.Msg{msg0_1}, "valconsKey0", kr0) + s.Require().NoError(err) + + s.moveToAndCheck(start + 4) + + res, err = s.network.QueryOracle().LatestPrice(ctxWithHeight(start+3), &oracletypes.QueryGetLatestPriceRequest{TokenId: 1}) + s.Require().NoError(err) + // price not updated yet + s.Require().Equal(res.Price.Price, priceRecovery3.Prices[0].Price) + + // 2.2 mixed with some different prices(detID, price) + // init_start+110 + start += 10 + startUint = uint64(start) + s.moveToAndCheck(start) + msg3_2.BasedBlock = startUint + msg1_2.BasedBlock = startUint + msg2_2.BasedBlock = startUint + msg0_2.BasedBlock = startUint + // msg0_2.BasedBlock = startUint + // id:3 + err = s.network.SendTxOraclePriceFeed([]sdk.Msg{msg3_2}, "valconsKey3", kr3) + s.Require().NoError(err) + + // id:1,2,3 + err = s.network.SendTxOraclePriceFeed([]sdk.Msg{msg1_2}, "valconsKey1", kr1) + s.Require().NoError(err) + + s.moveToAndCheck(start + 1) + + // id:1,2 + err = s.network.SendTxOraclePriceFeed([]sdk.Msg{msg2_2}, "valconsKey2", kr2) + s.Require().NoError(err) + + // id:3 + // err = s.network.SendTxOraclePriceFeed([]sdk.Msg{msg0_2}, "valconsKey0", kr0) + // s.Require().NoError(err) + + s.moveToAndCheck(start + 3) + + res, err = s.network.QueryOracle().LatestPrice(ctxWithHeight(start+2), &oracletypes.QueryGetLatestPriceRequest{TokenId: 1}) + s.Require().NoError(err) + // price not updated yet + s.Require().Equal(res.Price.Price, priceRecovery3.Prices[0].Price) + + // 2.3 failed for forceSeal by paramsUpdate + // TODO: for now all paramsUpdate related forceSeal are not supported (the related fields are not allowed to be updated by msgUpdateParms) + // we comment out this case for now + // start += 10 + // startUint = uint64(start) + // msg0.BasedBlock = startUint + // msg1.BasedBlock = startUint + // msg2.BasedBlock = startUint + // msgUpdateParams := oracletypes.NewMsgUpdateParams("creator", `{"max_nonce":5}`) + // s.moveNAndCheck(start) + // + // err = s.network.SendTxOraclePriceFeed([]sdk.Msg{msg0}, "valconsKey0", kr0) + // s.Require().NoError(err) + // + // err = s.network.SendTxOraclePriceFeed([]sdk.Msg{msg1}, "valconsKey1", kr1) + // s.Require().NoError(err) + // + // // send updateParams msg to forceSeal current round + // err = s.network.SendTx([]sdk.Msg{msgUpdateParams}, s.network.Validators[0].ClientCtx.FromName, kr0) + // s.Require().NoError(err) + // s.moveToAndCheck(start + 1) + // + // err = s.network.SendTxOraclePriceFeed([]sdk.Msg{msg2}, "valconsKey2", kr2) + // s.Require().NoError(err) + // + // s.moveToAndCheck(start + 3) + // + // res, err = s.network.QueryOracle().LatestPrice(ctxWithHeight(start+2), &oracletypes.QueryGetLatestPriceRequest{TokenId: 1}) + // s.Require().NoError(err) + // // price failed to update + // s.Require().Equal(res.Price.Price, priceRecovery3.Prices[0].Price) + + // 2.4 failed for forceSeal by validatorSetUpdate: we use an old timestamp in genesisfile to setup the network so that the epoch end will be triggered on each block + start += 10 + startUint = uint64(start) + + msg0.BasedBlock = startUint + msg1.BasedBlock = startUint + msg2.BasedBlock = startUint + // msgUpdateParams := oracletypes.NewMsgUpdateParams(s.network.Validators[0].Address.String(), `{"max_nonce":5}`) + s.moveToAndCheck(start) + + err = s.network.SendTxOraclePriceFeed([]sdk.Msg{msg0}, "valconsKey0", kr0) + s.Require().NoError(err) + + err = s.network.SendTxOraclePriceFeed([]sdk.Msg{msg1}, "valconsKey1", kr1) + s.Require().NoError(err) + + // delegate to change validator set, we set genesis time to a history time so that the validator set update will be triggered every block + clientChainID := uint32(101) + lzNonce := uint64(0) + assetAddr, _ := hexutil.Decode(network.ETHAssetAddress) + stakerAddr := []byte(s.network.Validators[0].Address) + operatorAddr := []byte(s.network.Validators[0].Address.String()) + opAmount := big.NewInt(90000000) + // deposit 32 NSTETH to staker from beaconchain_validatro_1 + err = s.network.SendPrecompileTx(network.DELEGATION, "delegate", clientChainID, lzNonce, assetAddr, stakerAddr, operatorAddr, opAmount) + s.Require().NoError(err) + + // power will be updated at endBlock of start+2, it would force seal this round + s.moveToAndCheck(start + 2) + + err = s.network.SendTxOraclePriceFeed([]sdk.Msg{msg2}, "valconsKey2", kr2) + s.Require().NotNil(err) + + s.moveToAndCheck(start + 3) + + res, err = s.network.QueryOracle().LatestPrice(ctxWithHeight(start+2), &oracletypes.QueryGetLatestPriceRequest{TokenId: 1}) + s.NoError(err) + s.Require().Equal(res.Price.Price, priceRecovery3.Prices[0].Price) + + s.moveToAndCheck(start + 20) +} diff --git a/tests/e2e/util.go b/tests/e2e/util.go deleted file mode 100644 index 777e584ad..000000000 --- a/tests/e2e/util.go +++ /dev/null @@ -1,37 +0,0 @@ -package e2e - -import ( - "context" - - sdkmath "cosmossdk.io/math" - "github.com/ExocoreNetwork/exocore/testutil/network" - "github.com/cosmos/cosmos-sdk/crypto/keyring" - sdk "github.com/cosmos/cosmos-sdk/types" - banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" - "github.com/evmos/evmos/v16/crypto/hd" -) - -// func (s *E2ETestSuite) queryNativeCoinBalance(address sdk.AccAddress, n *network.Network) (*banktypes.QueryBalanceResponse, error) { -func QueryNativeCoinBalance(address sdk.AccAddress, n *network.Network) (*banktypes.QueryBalanceResponse, error) { - return n.QueryBank().Balance(context.Background(), &banktypes.QueryBalanceRequest{ - Address: address.String(), - // Denom: s.network.Config.NativeDenom, - Denom: n.Config.NativeDenom, - }) -} - -// func (s *E2ETestSuite) newNativeCoin(amount sdkmath.Int, n *network.Network) sdk.Coin { -func NewNativeCoin(amount sdkmath.Int, n *network.Network) sdk.Coin { - // return sdk.NewCoin(s.network.Config.NativeDenom, amount) - return sdk.NewCoin(n.Config.NativeDenom, amount) -} - -func GenerateAccAddress(kr keyring.Keyring, name string) (sdk.AccAddress, error) { - // generate a new account with ethsecp256k1 - r, _, err := kr.NewMnemonic(name, keyring.English, sdk.GetConfig().GetFullBIP44Path(), "", hd.EthSecp256k1) - if err != nil { - return nil, err - } - addr, _ := r.GetAddress() - return addr, nil -} diff --git a/testutil/keeper/oracle.go b/testutil/keeper/oracle.go index 1a7eb0ee2..9005e4dca 100644 --- a/testutil/keeper/oracle.go +++ b/testutil/keeper/oracle.go @@ -70,6 +70,7 @@ func OracleKeeper(t testing.TB) (*keeper.Keeper, sdk.Context) { p4Test := types.DefaultParams() p4Test.TokenFeeders[1].StartBaseBlock = 1 k.SetParams(ctx, p4Test) + k.FeederManager.InitCachesForTest(k, &p4Test, nil) return &k, ctx } diff --git a/testutil/network/genesis_data.go b/testutil/network/genesis_data.go index 6fc68971c..bd52893c3 100644 --- a/testutil/network/genesis_data.go +++ b/testutil/network/genesis_data.go @@ -40,8 +40,8 @@ var ( }, }, Tokens: []assetstypes.StakingAssetInfo{ - NewTestToken("ETH", "Ethereum native token", ETHAssetAddress, TestEVMChainID, 5000), - NewTestToken("NST ETH", "native restaking ETH", NativeAssetAddress, TestEVMChainID, 5000), + NewTestToken("ETH", "Ethereum native token", ETHAssetAddress, TestEVMChainID, 0, 5000), + NewTestToken("NST ETH", "native restaking ETH", NativeAssetAddress, TestEVMChainID, 0, 5000), }, } @@ -79,11 +79,11 @@ func init() { }) // set slashing_miss window to 4 DefaultGenStateOracle.Params.Slashing.ReportedRoundsWindow = 4 - // set jailduration of oracle report downtime to 30 seconds for test - DefaultGenStateOracle.Params.Slashing.OracleMissJailDuration = 30 * time.Second + // set jailduration of oracle report downtime to 15 seconds for test + DefaultGenStateOracle.Params.Slashing.OracleMissJailDuration = 15 * time.Second } -func NewTestToken(name, metaInfo, address string, chainID uint64, amount int64) assetstypes.StakingAssetInfo { +func NewTestToken(name, metaInfo, address string, chainID uint64, decimal uint32, amount int64) assetstypes.StakingAssetInfo { if name == "" { panic("token name cannot be empty") } @@ -94,6 +94,7 @@ func NewTestToken(name, metaInfo, address string, chainID uint64, amount int64) AssetBasicInfo: assetstypes.AssetInfo{ Name: name, MetaInfo: metaInfo, + Decimals: decimal, Address: address, LayerZeroChainID: chainID, }, diff --git a/testutil/network/network.go b/testutil/network/network.go index c393f2c50..1a4260c6f 100644 --- a/testutil/network/network.go +++ b/testutil/network/network.go @@ -118,12 +118,13 @@ func DefaultConfig() Config { NumValidators: 4, NativeDenom: "hua", MinGasPrices: "10hua", - AccountTokens: sdk.TokensFromConsensusPower(1000, evmostypes.PowerReduction), - DepositedTokens: sdk.TokensFromConsensusPower(500, evmostypes.PowerReduction), - StakingTokens: sdk.TokensFromConsensusPower(200, evmostypes.PowerReduction), - PruningStrategy: pruningtypes.PruningOptionNothing, - CleanupDir: true, - SigningAlgo: string(hd.EthSecp256k1Type), + AccountTokens: sdk.NewInt(150000000), + DepositedTokens: sdk.NewInt(110000000), + StakingTokens: sdk.NewInt(20000000), + + PruningStrategy: pruningtypes.PruningOptionNothing, + CleanupDir: true, + SigningAlgo: string(hd.EthSecp256k1Type), // KeyringOptions: []keyring.Option{hd.EthSecp256k1Option()}, KeyringOptions: []keyring.Option{exocorecrypto.Ed25519Option()}, PrintMnemonic: false, diff --git a/testutil/network/tx.go b/testutil/network/tx.go index 372823653..bf7726ff2 100644 --- a/testutil/network/tx.go +++ b/testutil/network/tx.go @@ -22,9 +22,9 @@ func (n *Network) SendTx(msgs []sdk.Msg, keyName string, keyring keyring.Keyring return tx.BroadcastTx(ctx, txf, msgs...) } -// SendTxOracleCreatePrice consturct and sign that tx with input msgs, it's different from SendTx, since when we use ed25519 for oracle senario, we allowed that signer is an unexists account, this implementation skip the 'accoutn exists' related checks -// Also, if you want to sign some normal message (not oracle-create-price) with ed25519, just use SendTx is fine, we support ed25519 signing in keyring -func (n *Network) SendTxOracleCreateprice(msgs []sdk.Msg, keyName string, keyring keyring.Keyring) error { +// SendTxOraclePriceFeed consturct and sign that tx with input msgs, it's different from SendTx, since when we use ed25519 for oracle senario, we allowed that signer is an unexists account, this implementation skip the 'accoutn exists' related checks +// Also, if you want to sign some normal message (not oracle-price-feed) with ed25519, just use SendTx is fine, we support ed25519 signing in keyring +func (n *Network) SendTxOraclePriceFeed(msgs []sdk.Msg, keyName string, keyring keyring.Keyring) error { txf, ctx, err := generateTxf(n.Validators[0].ClientCtx, keyName, keyring, 1.5, n.Config.MinGasPrices) if err != nil { return err diff --git a/testutil/network/util.go b/testutil/network/util.go index 79e45f5f4..d3fb868ba 100644 --- a/testutil/network/util.go +++ b/testutil/network/util.go @@ -3,6 +3,7 @@ package network import ( "encoding/json" "fmt" + "os" "path/filepath" "time" @@ -36,7 +37,6 @@ import ( cmttime "github.com/cometbft/cometbft/types/time" "github.com/ethereum/go-ethereum/common/hexutil" "github.com/evmos/evmos/v16/server" - evmostypes "github.com/evmos/evmos/v16/types" evmtypes "github.com/evmos/evmos/v16/x/evm/types" feemarkettypes "github.com/evmos/evmos/v16/x/feemarket/types" ) @@ -259,6 +259,10 @@ func initGenFiles(cfg Config, genAccounts []authtypes.GenesisAccount, genBalance // generate empty genesis files for each validator and save gTime := cmttime.Now() + if os.Getenv("TEST_OPTION") == "local" { + gTime = gTime.Add(-300 * time.Minute) + } + // we use a time 100 minutes before now, to trigger epoch change for each block in the early blocks(more than 100 blocks) for i := 0; i < cfg.NumValidators; i++ { if genDoc.InitialHeight == 0 { genDoc.InitialHeight = 1 @@ -397,7 +401,7 @@ func NewGenStateOperator(operatorAccAddresses []sdk.AccAddress, consPubKeys []st }) // OperatorUSDValues // the price unit of assets is 1 not decimal 18 - stakingValue := sdk.TokensToConsensusPower(stakingAmount, evmostypes.PowerReduction) + stakingValue := stakingAmount.Int64() DefaultGenStateOperator.OperatorUSDValues = append(DefaultGenStateOperator.OperatorUSDValues, operatortypes.OperatorUSDValue{ Key: AVSAddress + "/" + operatorAccAddress.String(), OptedUSDValue: operatortypes.OperatorOptedUSDValue{ @@ -414,7 +418,7 @@ func NewGenStateOperator(operatorAccAddresses []sdk.AccAddress, consPubKeys []st AVSAddr: AVSAddress, Value: operatortypes.DecValueField{ // the price unit of assets is 1 not decimal 18 - Amount: sdkmath.LegacyNewDec(sdk.TokensToConsensusPower(totalStakingAmount, evmostypes.PowerReduction)), + Amount: sdkmath.LegacyNewDec(totalStakingAmount.Int64()), }, }) } @@ -425,7 +429,7 @@ func NewGenStateOperator(operatorAccAddresses []sdk.AccAddress, consPubKeys []st // stakingAmount is the amount each operator have for every single asset defined in assets module, so for a single operator the total stakingAmount they have is stakingAmount*count(assets) // assets genesis state is required as input argument to provide assets information. It should be called with NewGenStateAssets to update default assets genesis state for test func NewGenStateDogfood(consPubKeys []string, stakingAmount sdkmath.Int, genStateAssets assetstypes.GenesisState) (dogfoodtypes.GenesisState, error) { - power := sdk.TokensToConsensusPower(stakingAmount.Mul(sdkmath.NewInt(int64(len(genStateAssets.Tokens)))), evmostypes.PowerReduction) + power := stakingAmount.Mul(sdkmath.NewInt(int64(len(genStateAssets.Tokens)))).Int64() DefaultGenStateDogfood.Params.EpochIdentifier = "minute" DefaultGenStateDogfood.Params.EpochsUntilUnbonded = 5 DefaultGenStateDogfood.Params.MinSelfDelegation = sdkmath.NewInt(100) diff --git a/x/avs/client/cli/tx.go b/x/avs/client/cli/tx.go index b73524a26..7623857b7 100644 --- a/x/avs/client/cli/tx.go +++ b/x/avs/client/cli/tx.go @@ -116,7 +116,7 @@ func newBuildMsg( taskContractAddress, _ := fs.GetString(FlagTaskContractAddress) taskID, _ := fs.GetUint64(FlagTaskID) - phase, _ := fs.GetUint32(FlagPhase) + phase, _ := fs.GetInt32(FlagPhase) if err := types.ValidatePhase(types.Phase(phase)); err != nil { return nil, err } diff --git a/x/avs/keeper/keeper.go b/x/avs/keeper/keeper.go index 6a29a4da1..99233cb41 100644 --- a/x/avs/keeper/keeper.go +++ b/x/avs/keeper/keeper.go @@ -447,12 +447,14 @@ func (k Keeper) RaiseAndResolveChallenge(ctx sdk.Context, params *types.Challeng return errorsmod.Wrap(types.ErrEpochNotFound, fmt.Sprintf("epoch info not found %s", avsInfo.EpochIdentifier)) } + // #nosec G115 if epoch.CurrentEpoch <= int64(taskInfo.StartingEpoch)+int64(taskInfo.TaskResponsePeriod)+int64(taskInfo.TaskStatisticalPeriod) { return errorsmod.Wrap( types.ErrSubmitTooSoonError, fmt.Sprintf("SetTaskResultInfo:the challenge period has not started , CurrentEpoch:%d", epoch.CurrentEpoch), ) } + // #nosec G115 if epoch.CurrentEpoch > int64(taskInfo.StartingEpoch)+int64(taskInfo.TaskResponsePeriod)+int64(taskInfo.TaskStatisticalPeriod)+int64(taskInfo.TaskChallengePeriod) { return errorsmod.Wrap( types.ErrSubmitTooLateError, @@ -589,6 +591,7 @@ func (k Keeper) SubmitTaskResult(ctx sdk.Context, addr string, info *types.TaskR fmt.Sprintf("SetTaskResultInfo:the TaskResponse period has not started , CurrentEpoch:%d", epoch.CurrentEpoch), ) } + // #nosec G115 if epoch.CurrentEpoch > int64(task.StartingEpoch)+int64(task.TaskResponsePeriod)+int64(task.TaskStatisticalPeriod) { return errorsmod.Wrap( types.ErrSubmitTooLateError, diff --git a/x/delegation/keeper/delegation.go b/x/delegation/keeper/delegation.go index ac0081420..5eb8d8182 100644 --- a/x/delegation/keeper/delegation.go +++ b/x/delegation/keeper/delegation.go @@ -37,7 +37,6 @@ func (k *Keeper) delegateTo( if notGenesis && k.slashKeeper.IsOperatorFrozen(ctx, params.OperatorAddress) { return delegationtype.ErrOperatorIsFrozen } - stakerID, assetID := assetstype.GetStakerIDAndAssetID(params.ClientChainID, params.StakerAddress, params.AssetsAddress) if assetID != assetstype.ExocoreAssetID { // check if the staker asset has been deposited and the canWithdraw amount is bigger than the delegation amount diff --git a/x/evm/keeper/grpc_query.go b/x/evm/keeper/grpc_query.go index 851ce1dd0..518611326 100644 --- a/x/evm/keeper/grpc_query.go +++ b/x/evm/keeper/grpc_query.go @@ -472,6 +472,7 @@ func (k Keeper) TraceTx(c context.Context, req *types.QueryTraceTxRequest) (*typ continue } txConfig.TxHash = ethTx.Hash() + // #nosec G115 txConfig.TxIndex = uint(i) // reset gas meter for each transaction ctx = ctx.WithGasMeter(evmostypes.NewInfiniteGasMeterWithLimit(msg.Gas())) @@ -565,6 +566,7 @@ func (k Keeper) TraceBlock(c context.Context, req *types.QueryTraceBlockRequest) result := types.TxTraceResult{} ethTx := tx.AsTransaction() txConfig.TxHash = ethTx.Hash() + // #nosec G115 txConfig.TxIndex = uint(i) traceResult, logIndex, err := k.traceTx(ctx, cfg, txConfig, signer, ethTx, req.TraceConfig, true, nil) if err != nil { diff --git a/x/operator/keeper/slash.go b/x/operator/keeper/slash.go index ec7634117..80634d9a5 100644 --- a/x/operator/keeper/slash.go +++ b/x/operator/keeper/slash.go @@ -19,6 +19,7 @@ import ( // GetSlashIDForDogfood It use infractionType+'_'+'infractionHeight' as the slashID, because /* the slash */event occurs in dogfood doesn't have a TxID. It isn't submitted through an external transaction. func GetSlashIDForDogfood(infraction stakingtypes.Infraction, infractionHeight int64) string { // #nosec G701 + // #nosec G115 return strings.Join([]string{hexutil.EncodeUint64(uint64(infraction)), hexutil.EncodeUint64(uint64(infractionHeight))}, utils.DelimiterForID) } diff --git a/x/operator/types/keys.go b/x/operator/types/keys.go index ad63c5de5..24b0da57a 100644 --- a/x/operator/types/keys.go +++ b/x/operator/types/keys.go @@ -157,7 +157,7 @@ func KeyForVotingPowerSnapshot(avs common.Address, height int64) []byte { return utils.AppendMany( avs.Bytes(), // Append the height - sdk.Uint64ToBigEndian(uint64(height)), + sdk.Uint64ToBigEndian(uint64(height)), // #nosec G115 // height is not negative ) } diff --git a/x/oracle/client/cli/tx.go b/x/oracle/client/cli/tx.go index 9c47e93cf..6326c4701 100644 --- a/x/oracle/client/cli/tx.go +++ b/x/oracle/client/cli/tx.go @@ -28,7 +28,7 @@ func GetTxCmd() *cobra.Command { RunE: client.ValidateCmd, } - cmd.AddCommand(CmdCreatePrice()) + cmd.AddCommand(CmdPriceFeed()) cmd.AddCommand(CmdUpdateParams()) // this line is used by starport scaffolding # 1 diff --git a/x/oracle/client/cli/tx_create_price.go b/x/oracle/client/cli/tx_create_price.go index c84780786..306c1aa09 100644 --- a/x/oracle/client/cli/tx_create_price.go +++ b/x/oracle/client/cli/tx_create_price.go @@ -13,11 +13,11 @@ import ( var _ = strconv.Itoa(0) -func CmdCreatePrice() *cobra.Command { +func CmdPriceFeed() *cobra.Command { cmd := &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", + Use: "price-feed feederid basedblock nonce sourceid decimal price timestamp detid optinoal(price timestamp detid) optional(desc)", + Short: "Broadcast message price-feed", Args: cobra.MinimumNArgs(8), RunE: func(cmd *cobra.Command, args []string) (err error) { clientCtx, err := client.GetClientTxContext(cmd) @@ -71,7 +71,7 @@ func CmdCreatePrice() *cobra.Command { prices[0].Desc = args[i+1] } - msg := types.NewMsgCreatePrice( + msg := types.NewMsgPriceFeed( clientCtx.GetFromAddress().String(), feederID, prices, diff --git a/x/oracle/genesis.go b/x/oracle/genesis.go index 3dc8994e9..6461bf7cc 100644 --- a/x/oracle/genesis.go +++ b/x/oracle/genesis.go @@ -14,7 +14,7 @@ func InitGenesis(ctx sdk.Context, k keeper.Keeper, genState types.GenesisState) } // Set if defined if genState.ValidatorUpdateBlock != nil { - k.SetValidatorUpdateBlock(ctx, *genState.ValidatorUpdateBlock) + k.SetValidatorUpdateForCache(ctx, *genState.ValidatorUpdateBlock) } // Set if defined if genState.IndexRecentParams != nil { @@ -39,6 +39,7 @@ func InitGenesis(ctx sdk.Context, k keeper.Keeper, genState types.GenesisState) // Set all stakerInfos for assetIDs for _, elem := range genState.StakerInfosAssets { k.SetStakerInfos(ctx, elem.AssetId, elem.StakerInfos) + k.SetNSTVersion(ctx, elem.AssetId, elem.NstVersion) } // set validatorReportInfos for _, elem := range genState.ValidatorReportInfos { diff --git a/x/oracle/keeper/aggregator/aggregator.go b/x/oracle/keeper/aggregator/aggregator.go deleted file mode 100644 index 1d21ceb00..000000000 --- a/x/oracle/keeper/aggregator/aggregator.go +++ /dev/null @@ -1,240 +0,0 @@ -package aggregator - -import ( - "math/big" - "sort" - - "github.com/ExocoreNetwork/exocore/x/oracle/keeper/common" - "github.com/ExocoreNetwork/exocore/x/oracle/types" -) - -type priceWithTimeAndRound struct { - price string - decimal int32 - timestamp string - detRoundID string // roundId from source if exists -} - -type reportPrice struct { - validator string - // final price, set to -1 as initial - price string - // sourceId->priceWithTimeAndRound - prices map[uint64]*priceWithTimeAndRound - power *big.Int -} - -func (r *reportPrice) aggregate() string { - if len(r.price) > 0 { - return r.price - } - tmp := make([]*big.Int, 0, len(r.prices)) - for _, p := range r.prices { - priceInt, ok := new(big.Int).SetString(p.price, 10) - // price is not a number (NST), we will just return instead of calculation - if !ok { - return p.price - } - tmp = append(tmp, priceInt) - } - r.price = common.BigIntList(tmp).Median().String() - return r.price -} - -type aggregator struct { - finalPrice string - reports []*reportPrice - // total valiadtor power who has submitted price - reportPower *big.Int - totalPower *big.Int - // validator set total power - // totalPower string - // sourceId->roundId used to track the confirmed DS roundId - // updated by calculator, detId use string - dsPrices map[uint64]string -} - -func (agg *aggregator) copy4CheckTx() *aggregator { - ret := &aggregator{ - finalPrice: agg.finalPrice, - reportPower: copyBigInt(agg.reportPower), - totalPower: copyBigInt(agg.totalPower), - - reports: make([]*reportPrice, 0, len(agg.reports)), - dsPrices: make(map[uint64]string), - } - for k, v := range agg.dsPrices { - ret.dsPrices[k] = v - } - for _, report := range agg.reports { - rTmp := *report - rTmp.price = report.price - rTmp.power = copyBigInt(report.power) - - for k, v := range report.prices { - // prices are information submitted by validators, these data will not change under deterministic sources, but with non-deterministic sources they might be overwrite by later prices - tmpV := *v - tmpV.price = v.price - rTmp.prices[k] = &tmpV - } - - ret.reports = append(ret.reports, &rTmp) - } - - return ret -} - -// fill price from validator submitting into aggregator, and calculation the voting power and check with the consensus status of deterministic source value to decide when to do the aggregation -// TODO: currently apply mode=1 in V1, add swith modes -func (agg *aggregator) fillPrice(pSources []*types.PriceSource, validator string, power *big.Int) { - report := agg.getReport(validator) - if report == nil { - report = &reportPrice{ - validator: validator, - prices: make(map[uint64]*priceWithTimeAndRound), - power: power, - } - agg.reports = append(agg.reports, report) - agg.reportPower = new(big.Int).Add(agg.reportPower, power) - } - - for _, pSource := range pSources { - if len(pSource.Prices[0].DetID) == 0 { - // this is an NS price report, price will just be updated instead of append - if pTR := report.prices[pSource.SourceID]; pTR == nil { - pTmp := pSource.Prices[0] - pTR = &priceWithTimeAndRound{ - price: pTmp.Price, - decimal: pTmp.Decimal, - timestamp: pTmp.Timestamp, - } - report.prices[pSource.SourceID] = pTR - } else { - pTR.price = pSource.Prices[0].Price - } - } else { - // this is an DS price report - if pTR := report.prices[pSource.SourceID]; pTR == nil { - pTmp := pSource.Prices[0] - pTR = &priceWithTimeAndRound{ - decimal: pTmp.Decimal, - } - if len(agg.dsPrices[pSource.SourceID]) > 0 { - for _, reportTmp := range agg.reports { - if priceTmp := reportTmp.prices[pSource.SourceID]; priceTmp != nil && len(priceTmp.price) > 0 { - pTR.price = priceTmp.price - pTR.detRoundID = priceTmp.detRoundID - pTR.timestamp = priceTmp.timestamp - break - } - } - } - report.prices[pSource.SourceID] = pTR - } - // skip if this DS's slot exists, DS's value only updated by calculator - } - } -} - -// TODO: for v1 use mode=1, which means agg.dsPrices with each key only be updated once, switch modes -func (agg *aggregator) confirmDSPrice(confirmedRounds []*confirmedPrice) { - for _, priceSourceRound := range confirmedRounds { - // update the latest round-detId for DS, TODO: in v1 we only update this value once since calculator will just ignore any further value once a detId has reached consensus - // agg.dsPrices[priceSourceRound.sourceId] = priceSourceRound.detId - // this id's comparison need to format id to make sure them be the same length - if id := agg.dsPrices[priceSourceRound.sourceID]; len(id) == 0 || (len(id) > 0 && id < priceSourceRound.detID) { - agg.dsPrices[priceSourceRound.sourceID] = priceSourceRound.detID - for _, report := range agg.reports { - if len(report.price) > 0 { - // price of IVA has completed - continue - } - if price := report.prices[priceSourceRound.sourceID]; price != nil { - price.detRoundID = priceSourceRound.detID - price.timestamp = priceSourceRound.timestamp - price.price = priceSourceRound.price - } // else TODO: panic in V1 - } - } - } -} - -func (agg *aggregator) getReport(validator string) *reportPrice { - for _, r := range agg.reports { - if r.validator == validator { - return r - } - } - return nil -} - -func (agg *aggregator) aggregate() string { - if len(agg.finalPrice) > 0 { - return agg.finalPrice - } - // TODO: implemetn different MODE for definition of consensus, - // currently: use rule_1+MODE_1: {rule:specified source:`chainlink`, MODE: asap when power exceeds the threshold} - // 1. check OVA threshold - // 2. check IVA consensus with rule, TODO: for v1 we only implement with mode=1&rule=1 - if common.ExceedsThreshold(agg.reportPower, agg.totalPower) { - // TODO: this is kind of a mock way to suite V1, need update to check with params.rule - // check if IVA all reached consensus - if len(agg.dsPrices) > 0 { - validatorPrices := make([]*big.Int, 0, len(agg.reports)) - // do the aggregation to find out the 'final price' - for _, validatorReport := range agg.reports { - priceInt, ok := new(big.Int).SetString(validatorReport.aggregate(), 10) - if !ok { - // price is not number, we just return the price when power exceeds threshold - agg.finalPrice = validatorReport.aggregate() - return agg.finalPrice - } - validatorPrices = append(validatorPrices, priceInt) - } - // vTmp := bigIntList(validatorPrices) - agg.finalPrice = common.BigIntList(validatorPrices).Median().String() - // clear relative aggregator for this feeder, all the aggregator,calculator, filter can be removed since this round has been sealed - } - } - return agg.finalPrice -} - -// TODO: this only suites for DS. check source type for extension -// GetFinaPriceListForFeederIDs retrieve final price info as an array ordered by sourceID asc -func (agg *aggregator) getFinalPriceList(feederID uint64) []*types.AggFinalPrice { - sourceIDs := make([]uint64, 0, len(agg.dsPrices)) - for sID := range agg.dsPrices { - sourceIDs = append(sourceIDs, sID) - } - sort.Slice(sourceIDs, func(i, j int) bool { - return sourceIDs[i] < sourceIDs[j] - }) - ret := make([]*types.AggFinalPrice, 0, len(sourceIDs)) - for _, sID := range sourceIDs { - for _, report := range agg.reports { - price := report.prices[sID] - if price == nil || price.detRoundID != agg.dsPrices[sID] { - // the DetID mismatch should not happen - continue - } - ret = append(ret, &types.AggFinalPrice{ - FeederID: feederID, - SourceID: sID, - DetID: price.detRoundID, - Price: price.price, - }) - // {feederID, sourceID} has been found, skip rest reports - break - } - } - return ret -} - -func newAggregator(validatorSetLength int, totalPower *big.Int) *aggregator { - return &aggregator{ - reports: make([]*reportPrice, 0, validatorSetLength), - reportPower: big.NewInt(0), - dsPrices: make(map[uint64]string), - totalPower: totalPower, - } -} diff --git a/x/oracle/keeper/aggregator/aggregator_test.go b/x/oracle/keeper/aggregator/aggregator_test.go deleted file mode 100644 index 35adf0aa1..000000000 --- a/x/oracle/keeper/aggregator/aggregator_test.go +++ /dev/null @@ -1,68 +0,0 @@ -package aggregator - -import ( - "math/big" - "testing" - - . "github.com/smartystreets/goconvey/convey" -) - -func TestAggregator(t *testing.T) { - Convey("fill prices into aggregator", t, func() { - a := newAggregator(5, big.NewInt(4)) - // a.fillPrice(pS1, "v1", one) //v1:{1, 2} - - Convey("fill v1's report", func() { - a.fillPrice(pS1, "v1", one) // v1:{1, 2} - report := a.getReport("v1") - So(report.prices[1].price, ShouldEqual, "") - Convey("fill v2's report", func() { - a.fillPrice(pS2, "v2", one) - report := a.getReport("v2") - So(report.prices[1].price, ShouldEqual, "") - Convey("fill more v1's report", func() { - a.fillPrice(pS21, "v1", one) - report := a.getReport("v1") - So(report.prices[1].price, ShouldEqual, "") - So(report.prices[2].price, ShouldEqual, "") - Convey("confirm deterministic source_1 and source 2", func() { - a.confirmDSPrice([]*confirmedPrice{ - { - sourceID: 1, - detID: "9", - price: "10", - timestamp: "-", - }, - { - sourceID: 2, - detID: "3", - price: "20", - timestamp: "-", - }, - }) - reportV1 := a.getReport("v1") - reportV2 := a.getReport("v2") - So(reportV1.prices[1].price, ShouldResemble, "10") - So(reportV1.prices[1].detRoundID, ShouldEqual, "9") - - So(reportV2.prices[1].price, ShouldResemble, "10") - So(reportV2.prices[1].detRoundID, ShouldEqual, "9") - - So(reportV1.prices[2].price, ShouldResemble, "20") - So(reportV1.prices[2].detRoundID, ShouldEqual, "3") - - // current implementation only support v1's single source - Convey("aggregate after all source confirmed", func() { - a.fillPrice(pS6, "v3", one) - a.aggregate() // v1:{s1:9-10, s2:3-20}:15, v2:{s1:9-10}:10 - So(a.getReport("v1").price, ShouldEqual, "15") - So(a.getReport("v2").price, ShouldEqual, "10") - So(a.getReport("v3").price, ShouldEqual, "20") - So(a.finalPrice, ShouldEqual, "15") - }) - }) - }) - }) - }) - }) -} diff --git a/x/oracle/keeper/aggregator/calculator.go b/x/oracle/keeper/aggregator/calculator.go deleted file mode 100644 index 5e1b8fd5c..000000000 --- a/x/oracle/keeper/aggregator/calculator.go +++ /dev/null @@ -1,197 +0,0 @@ -package aggregator - -import ( - "math/big" - - "github.com/ExocoreNetwork/exocore/x/oracle/keeper/common" - "github.com/ExocoreNetwork/exocore/x/oracle/types" -) - -type confirmedPrice struct { - sourceID uint64 - detID string - price string - timestamp string -} - -// internal struct -type priceAndPower struct { - price string - power *big.Int -} - -// for a specific DS round, it could have multiple values provided by different validators(should not be true if there's no malicious validator) -type roundPrices struct { // 0 means NS - detID string - prices []*priceAndPower - price string - timestamp string - // confirmed bool -} - -// udpate priceAndPower for a specific DSRoundID, if the price exists, increase its power with provided data -// return confirmed=true, when detect power exceeds the threshold -func (r *roundPrices) updatePriceAndPower(pw *priceAndPower, totalPower *big.Int) (updated bool, confirmed bool) { - if len(r.price) > 0 { - confirmed = true - return - } - for _, item := range r.prices { - if item.price == pw.price { - item.power = new(big.Int).Add(item.power, pw.power) - updated = true - if common.ExceedsThreshold(item.power, totalPower) { - r.price = item.price - confirmed = true - } - return - } - } - if len(r.prices) < cap(r.prices) { - r.prices = append(r.prices, pw) - updated = true - if common.ExceedsThreshold(pw.power, totalPower) { - r.price = pw.price - confirmed = true - } - } - return -} - -// each DS corresponding a roundPriceList to represent its multiple rounds(DS round) in one oracle-round -type roundPricesList struct { - roundPricesList []*roundPrices - // each round can have at most roundPricesCount priceAndPower - roundPricesCount int -} - -func (r *roundPricesList) copy4CheckTx() *roundPricesList { - ret := &roundPricesList{ - roundPricesList: make([]*roundPrices, 0, len(r.roundPricesList)), - roundPricesCount: r.roundPricesCount, - } - - for _, v := range r.roundPricesList { - tmpRP := &roundPrices{ - detID: v.detID, - price: v.price, - prices: make([]*priceAndPower, 0, len(v.prices)), - timestamp: v.timestamp, - } - for _, pNP := range v.prices { - tmpPNP := *pNP - // power will be modified during execution - tmpPNP.power = copyBigInt(pNP.power) - tmpRP.prices = append(tmpRP.prices, &tmpPNP) - } - - ret.roundPricesList = append(ret.roundPricesList, tmpRP) - } - return ret -} - -// to tell if any round of this DS has reached consensus/confirmed -func (r *roundPricesList) hasConfirmedDetID() bool { - for _, round := range r.roundPricesList { - if len(round.price) > 0 { - return true - } - } - return false -} - -// get the roundPriceList correspond to specifid detID of a DS -// if no required data and the pricesList has not reach its limitation, we will add a new slot for this detId -func (r *roundPricesList) getOrNewRound(detID string, timestamp string) (round *roundPrices) { - for _, round = range r.roundPricesList { - if round.detID == detID { - if len(round.price) > 0 { - round = nil - } - return - } - } - - if len(r.roundPricesList) < cap(r.roundPricesList) { - round = &roundPrices{ - detID: detID, - prices: make([]*priceAndPower, 0, r.roundPricesCount), - timestamp: timestamp, - } - r.roundPricesList = append(r.roundPricesList, round) - return - } - return -} - -// calculator used to get consensus on deterministic source based data from validator set reports of price -type calculator struct { - // sourceId->{[]{roundId, []{price,power}, confirmed}}, confirmed value will be set in [0] - deterministicSource map[uint64]*roundPricesList - validatorLength int - totalPower *big.Int -} - -func (c *calculator) copy4CheckTx() *calculator { - ret := newCalculator(c.validatorLength, c.totalPower) - - // copy deterministicSource - for k, v := range c.deterministicSource { - ret.deterministicSource[k] = v.copy4CheckTx() - } - - return ret -} - -func (c *calculator) newRoundPricesList() *roundPricesList { - return &roundPricesList{ - roundPricesList: make([]*roundPrices, 0, int(common.MaxDetID)*c.validatorLength), - // for each DS-roundId, the count of prices provided is the number of validators at most - roundPricesCount: c.validatorLength, - } -} - -func (c *calculator) getOrNewSourceID(sourceID uint64) *roundPricesList { - rounds := c.deterministicSource[sourceID] - if rounds == nil { - rounds = c.newRoundPricesList() - c.deterministicSource[sourceID] = rounds - } - return rounds -} - -// fillPrice called upon new MsgCreatPrice arrived, to trigger the calculation to get to consensus on the same roundID_of_deterministic_source -// v1 use mode1, TODO: switch modes -func (c *calculator) fillPrice(pSources []*types.PriceSource, _ string, power *big.Int) (confirmedRounds []*confirmedPrice) { - for _, pSource := range pSources { - rounds := c.getOrNewSourceID(pSource.SourceID) - if rounds.hasConfirmedDetID() { - // TODO: this skip is just for V1 to do fast calculation and release EndBlocker pressure, may lead to 'not latest detId' be chosen - break - } - for _, pDetID := range pSource.Prices { - round := rounds.getOrNewRound(pDetID.DetID, pDetID.Timestamp) - if round == nil { - // this sourceId has reach the limitation of different detId, or has confirmed - continue - } - - updated, confirmed := round.updatePriceAndPower(&priceAndPower{pDetID.Price, power}, c.totalPower) - if updated && confirmed { - // sourceId, detId, price - confirmedRounds = append(confirmedRounds, &confirmedPrice{pSource.SourceID, round.detID, round.price, round.timestamp}) // TODO: just in v1 with mode==1, we use asap, so we just ignore any further data from this DS, even higher detId may get to consensus, in this way, in most case, we can complete the calculation in the transaction execution process. Release the pressure in EndBlocker - // TODO: this may delay to current block finish - break - } - } - } - return -} - -func newCalculator(validatorSetLength int, totalPower *big.Int) *calculator { - return &calculator{ - deterministicSource: make(map[uint64]*roundPricesList), - validatorLength: validatorSetLength, - totalPower: totalPower, - } -} diff --git a/x/oracle/keeper/aggregator/calculator_test.go b/x/oracle/keeper/aggregator/calculator_test.go deleted file mode 100644 index e5b85a46e..000000000 --- a/x/oracle/keeper/aggregator/calculator_test.go +++ /dev/null @@ -1,70 +0,0 @@ -package aggregator - -import ( - "math/big" - "testing" - - . "github.com/smartystreets/goconvey/convey" -) - -/* - 1-10, 2-12, 3-15 - -ps1: 1-10, 2-12 -ps2: 2-12, 3-15 -ps3: 1-10, 2-11(m) ---- -ps4: 2-12, 3-19(m) -ps5: 1-10, 3-19(m) ----- -ps1, ps2, ps3, ps4 ---> 2-12 -ps2, ps2, ps3, ps5 ---> 1-10 -*/ -func TestCalculator(t *testing.T) { - one := big.NewInt(1) - Convey("fill prices into calculator", t, func() { - c := newCalculator(5, big.NewInt(4)) - Convey("fill prices from single deterministic source", func() { - c.fillPrice(pS1, "v1", one) // 1-10, 2-12 - c.fillPrice(pS2, "v2", one) // 2-12, 3-15 - c.fillPrice(pS3, "v3", one) // 1-10, 2-11 - Convey("consensus on detid=2 and price=12", func() { - confirmed := c.fillPrice(pS4, "v4", one) // 2-12, 3-19 - So(confirmed[0].detID, ShouldEqual, "2") - So(confirmed[0].price, ShouldResemble, "12") - }) - Convey("consensus on detid=1 and price=10", func() { - confirmed := c.fillPrice(pS5, "v5", one) // 1-10, 3-19 - So(confirmed[0].detID, ShouldEqual, "1") - So(confirmed[0].price, ShouldResemble, "10") - - confirmed = c.fillPrice(pS4, "v4", one) - So(confirmed, ShouldBeNil) - }) - }) - Convey("fill prices from multiple deterministic sources", func() { - c.fillPrice(pS21, "v1", one) - c.fillPrice(pS22, "v2", one) - c.fillPrice(pS23, "v3", one) - Convey("consensus on both source 1 and source 2", func() { - confirmed := c.fillPrice(pS24, "v4", one) - So(len(confirmed), ShouldEqual, 2) - i := 0 - if confirmed[0].sourceID == 2 { - i = 1 - } - So(confirmed[i].detID, ShouldEqual, "2") - So(confirmed[i].price, ShouldResemble, "12") - - So(confirmed[1-i].detID, ShouldEqual, "3") - So(confirmed[1-i].price, ShouldResemble, "15") - }) - Convey("consenus on source 1 only", func() { - confirmed := c.fillPrice(pS25, "v5", one) - So(len(confirmed), ShouldEqual, 1) - So(confirmed[0].detID, ShouldEqual, "1") - So(confirmed[0].price, ShouldResemble, "10") - }) - }) - }) -} diff --git a/x/oracle/keeper/aggregator/context.go b/x/oracle/keeper/aggregator/context.go deleted file mode 100644 index ffa3cedda..000000000 --- a/x/oracle/keeper/aggregator/context.go +++ /dev/null @@ -1,450 +0,0 @@ -package aggregator - -import ( - "errors" - "fmt" - "math/big" - "sort" - - "github.com/ExocoreNetwork/exocore/x/oracle/keeper/cache" - "github.com/ExocoreNetwork/exocore/x/oracle/keeper/common" - "github.com/ExocoreNetwork/exocore/x/oracle/types" - sdk "github.com/cosmos/cosmos-sdk/types" -) - -type PriceItemKV struct { - TokenID uint64 - PriceTR types.PriceTimeRound -} - -type roundInfo struct { - // this round of price will start from block basedBlock+1, the basedBlock served as a trigger to notify validators to submit prices - basedBlock uint64 - // next round id of the price oracle service, price with the id will be record on block basedBlock+1 if all prices submitted by validators(for v1, validators serve as oracle nodes) get to consensus immediately - nextRoundID uint64 - // indicate if this round is open for collecting prices or closed in either condition that success with a consensused price or not - // 1: open, 2: closed - status roundStatus -} - -// roundStatus is an enum type to indicate the status of a roundInfo -type roundStatus int32 - -const ( - // roundStatusOpen indicates the round is open for collecting prices - roundStatusOpen roundStatus = iota + 1 - // roundStatusClosed indicates the round is closed, either success with a consensused price or not - roundStatusClosed -) - -// AggregatorContext keeps memory cache for state params, validatorset, and updatedthese values as they updated on chain. And it keeps the information to track all tokenFeeders' status and data collection -// nolint -type AggregatorContext struct { - params *types.Params - - // validator->power - validatorsPower map[string]*big.Int - totalPower *big.Int - - // each active feederToken has a roundInfo - rounds map[uint64]*roundInfo - - // each roundInfo has a worker - aggregators map[uint64]*worker -} - -func (agc *AggregatorContext) Copy4CheckTx() *AggregatorContext { - ret := &AggregatorContext{ - // params, validatorsPower, totalPower, these values won't change during block executing - params: agc.params, - validatorsPower: agc.validatorsPower, - totalPower: agc.totalPower, - - rounds: make(map[uint64]*roundInfo), - aggregators: make(map[uint64]*worker), - } - - for k, v := range agc.rounds { - vTmp := *v - ret.rounds[k] = &vTmp - } - - for k, v := range agc.aggregators { - w := newWorker(k, ret) - w.sealed = v.sealed - w.price = v.price - - w.f = v.f.copy4CheckTx() - w.c = v.c.copy4CheckTx() - w.a = v.a.copy4CheckTx() - } - - return ret -} - -// sanity check for the msgCreatePrice -func (agc *AggregatorContext) sanityCheck(msg *types.MsgCreatePrice) error { - // sanity check - // TODO: check the msgCreatePrice's Decimal is correct with params setting - // TODO: check len(price.prices)>0, len(price.prices._range_eachPriceSource.Prices)>0, at least has one source, and for each source has at least one price - - if accAddress, err := sdk.AccAddressFromBech32(msg.Creator); err != nil { - return errors.New("invalid address") - } else if _, ok := agc.validatorsPower[sdk.ConsAddress(accAddress).String()]; !ok { - return errors.New("signer is not validator") - } - - if len(msg.Prices) == 0 { - return errors.New("msg should provide at least one price") - } - - for _, pSource := range msg.Prices { - if len(pSource.Prices) == 0 || len(pSource.Prices) > int(common.MaxDetID) || !agc.params.IsValidSource(pSource.SourceID) { - return errors.New("source should be valid and provide at least one price") - } - // check with params is coressponding source is deteministic - if agc.params.IsDeterministicSource(pSource.SourceID) { - for _, pDetID := range pSource.Prices { - // TODO: verify the format of DetId is correct, since this is string, and we will make consensus with validator's power, so it's ok not to verify the format - // just make sure the DetId won't mess up with NS's placeholder id, the limitation of maximum count one validator can submit will be check by filter - if len(pDetID.DetID) == 0 { - // deterministic must have specified deterministicId - return errors.New("ds should have roundid") - } - // DS's price value will go through consensus process, so it's safe to skip the check here - } - // sanity check: NS submit only one price with detId=="" - } else if len(pSource.Prices) > 1 || len(pSource.Prices[0].DetID) > 0 { - return errors.New("ns should not have roundid") - } - } - return nil -} - -func (agc *AggregatorContext) checkMsg(msg *types.MsgCreatePrice, isCheckMode bool) error { - if err := agc.sanityCheck(msg); err != nil { - return err - } - - // check feeder is active - feederContext := agc.rounds[msg.FeederID] - if feederContext == nil { - return fmt.Errorf("context not exist for feederID:%d", msg.FeederID) - } - // This round had been sealed but current window not closed - if feederContext.status != roundStatusOpen { - if feederWorker := agc.aggregators[msg.FeederID]; feederWorker != nil { - if _, list4Aggregator := feederWorker.filtrate(msg); list4Aggregator != nil { - // record this message for performance evaluation(used for slashing) - feederWorker.recordMessage(msg.Creator, msg.FeederID, list4Aggregator) - } - } - // if the validator send a tx inside an alive window but the status had been changed to closed by enough power collected - // we should ignore the error for simulation to complete - if !isCheckMode { - return fmt.Errorf("context is not available for feederID:%d", msg.FeederID) - } - } - - // senity check on basedBlock - if msg.BasedBlock != feederContext.basedBlock { - return errors.New("baseblock not match") - } - - // check sources rule matches - if ok, err := agc.params.CheckRules(msg.FeederID, msg.Prices); !ok { - return err - } - - for _, pSource := range msg.Prices { - for _, pTimeDetID := range pSource.Prices { - if ok := agc.params.CheckDecimal(msg.FeederID, pTimeDetID.Decimal); !ok { - return fmt.Errorf("decimal not match for source ID %d and price ID %s", pSource.SourceID, pTimeDetID.DetID) - } - } - } - return nil -} - -func (agc *AggregatorContext) FillPrice(msg *types.MsgCreatePrice) (*PriceItemKV, *cache.ItemM, error) { - feederWorker := agc.aggregators[msg.FeederID] - // worker initialzed here reduce workload for Endblocker - if feederWorker == nil { - feederWorker = newWorker(msg.FeederID, agc) - agc.aggregators[msg.FeederID] = feederWorker - } - - if feederWorker.sealed { - if _, list4Aggregator := feederWorker.filtrate(msg); list4Aggregator != nil { - // record this message for performance evaluation(used for slashing) - feederWorker.recordMessage(msg.Creator, msg.FeederID, list4Aggregator) - } - return nil, nil, types.ErrPriceProposalIgnored.Wrap("price aggregation for this round has sealed") - } - - if listFilled := feederWorker.do(msg); listFilled != nil { - feederWorker.recordMessage(msg.Creator, msg.FeederID, listFilled) - if finalPrice := feederWorker.aggregate(); len(finalPrice) > 0 { - agc.rounds[msg.FeederID].status = roundStatusClosed - feederWorker.seal() - return &PriceItemKV{agc.params.GetTokenFeeder(msg.FeederID).TokenID, types.PriceTimeRound{ - Price: finalPrice, - Decimal: agc.params.GetTokenInfo(msg.FeederID).Decimal, - // TODO: check the format - Timestamp: msg.Prices[0].Prices[0].Timestamp, - RoundID: agc.rounds[msg.FeederID].nextRoundID, - }}, &cache.ItemM{FeederID: msg.FeederID}, nil - } - return nil, &cache.ItemM{FeederID: msg.FeederID, PSources: listFilled, Validator: msg.Creator}, nil - } - - // return nil, nil, errors.New("no valid price proposal to add for aggregation") - return nil, nil, types.ErrPriceProposalIgnored -} - -// NewCreatePrice receives msgCreatePrice message, and goes process: filter->aggregator, filter->calculator->aggregator -// non-deterministic data will goes directly into aggregator, and deterministic data will goes into calculator first to get consensus on the deterministic id. -func (agc *AggregatorContext) NewCreatePrice(ctx sdk.Context, msg *types.MsgCreatePrice) (*PriceItemKV, *cache.ItemM, error) { - if err := agc.checkMsg(msg, ctx.IsCheckTx()); err != nil { - return nil, nil, types.ErrInvalidMsg.Wrap(err.Error()) - } - return agc.FillPrice(msg) -} - -// prepare for new roundInfo, just update the status kept in memory -// executed at EndBlock stage, seall all success or expired roundInfo -// including possible aggregation and state update -// when validatorSet update, set force to true, to seal all alive round -// returns: 1st successful sealed, need to be written to KVStore, 2nd: failed sealed tokenID, use previous price to write to KVStore -func (agc *AggregatorContext) SealRound(ctx sdk.Context, force bool) (success []*PriceItemKV, failed []uint64, sealed []uint64, windowClosed []uint64) { - logger := ctx.Logger() - feederIDs := make([]uint64, 0, len(agc.rounds)) - for fID := range agc.rounds { - feederIDs = append(feederIDs, fID) - } - sort.Slice(feederIDs, func(i, j int) bool { - return feederIDs[i] < feederIDs[j] - }) - height := uint64(ctx.BlockHeight()) - // make sure feederIDs are accessed in order to calculate the indexOffset for slashing - windowClosedMap := make(map[uint64]bool) - for _, feederID := range feederIDs { - if agc.windowEnd(feederID, height) { - windowClosed = append(windowClosed, feederID) - windowClosedMap[feederID] = true - } - round := agc.rounds[feederID] - if round.status == roundStatusOpen { - feeder := agc.params.GetTokenFeeder(feederID) - // TODO: for mode=1, we don't do aggregate() here, since if it donesn't success in the transaction execution stage, it won't success here - // but it's not always the same for other modes, switch modes - switch common.Mode { - case types.ConsensusModeASAP: - offset := height - round.basedBlock - expired := feeder.EndBlock > 0 && height >= feeder.EndBlock - outOfWindow := offset >= uint64(common.MaxNonce) - - // an open round reach its end of window, increase offsetIndex for active valdiator and chech the performance(missing/malicious) - - if expired || outOfWindow || force { - failed = append(failed, feeder.TokenID) - if !expired { - logger.Debug("set round status from open to closed", "feederID", feederID, "force", force, "block", height) - round.status = roundStatusClosed - } - // TODO: optimize operformance - sealed = append(sealed, feederID) - if !windowClosedMap[feederID] { - logger.Debug("remove aggregators(workers) force/expired", "feederID", feederID) - agc.RemoveWorker(feederID) - } - } - default: - logger.Info("mode other than 1 is not support now") - } - } - // all status: 1->2, remove its aggregator - if agc.aggregators[feederID] != nil && agc.aggregators[feederID].sealed { - sealed = append(sealed, feederID) - } - } - return success, failed, sealed, windowClosed -} - -// PrepareEndBlock is called at EndBlock stage, to prepare the roundInfo for the next block(of input block) -func (agc *AggregatorContext) PrepareRoundEndBlock(ctx sdk.Context, block int64, forceSealHeight uint64) (newRoundFeederIDs []uint64) { - if block < 1 { - return newRoundFeederIDs - } - logger := ctx.Logger() - blockUint64 := uint64(block) - - for feederID, feeder := range agc.params.GetTokenFeeders() { - if feederID == 0 { - continue - } - if (feeder.EndBlock > 0 && feeder.EndBlock <= blockUint64) || feeder.StartBaseBlock > blockUint64 { - // this feeder is inactive - continue - } - - delta := blockUint64 - feeder.StartBaseBlock - left := delta % feeder.Interval - count := delta / feeder.Interval - latestBasedblock := blockUint64 - left - latestNextRoundID := feeder.StartRoundID + count - - logger.Info("PrepareRoundEndBlock", "feederID", feederID, "block", block, "latestBasedblock", latestBasedblock, "forceSealHeight", forceSealHeight, "position_in_round", left) - - feederIDUint64 := uint64(feederID) - round := agc.rounds[feederIDUint64] - if round == nil { - logger.Info("PrepareRoundEndBlock: initialize round info") - round = &roundInfo{ - basedBlock: latestBasedblock, - nextRoundID: latestNextRoundID, - } - if left >= uint64(common.MaxNonce) { - // since do sealround properly before prepareRound, this only possible happens in node restart, and nonce has been taken care of in kvStore - round.status = roundStatusClosed - logger.Info("PrepareRoundEndBlock: status_closed") - } else { - round.status = roundStatusOpen - logger.Info("PrepareRoundEndBlock: status_open") - if latestBasedblock < forceSealHeight { - // debug - logger.Debug("PrepareRoundEndBlock: status_closed due to forceseal") - round.status = roundStatusClosed - } - if left == 0 { - logger.Info("PrepareRoundEndBlock: add a new round") - // set nonce for corresponding feederID for new roud start - newRoundFeederIDs = append(newRoundFeederIDs, feederIDUint64) - } - } - agc.rounds[feederIDUint64] = round - } else { - // prepare a new round for exist roundInfo - if left == 0 { - logger.Info("PrepareRoundEndBlock: set existing round status to open") - round.basedBlock = latestBasedblock - round.nextRoundID = latestNextRoundID - round.status = roundStatusOpen - // set nonce for corresponding feederID for new roud start - newRoundFeederIDs = append(newRoundFeederIDs, feederIDUint64) - // drop previous worker - agc.RemoveWorker(feederIDUint64) - } else if round.status == roundStatusOpen && left >= uint64(common.MaxNonce) { - logger.Info("PrepareRoundEndBlock: set existing round status to closed") - // this shouldn't happen, if do sealround properly before prepareRound, basically for test only - // TODO: print error log here - round.status = roundStatusClosed - // TODO: just modify the status here, since sealRound should do all the related seal actions already when parepare invoked - } - } - } - return newRoundFeederIDs -} - -// SetParams sets the params field of aggregatorContext“ -func (agc *AggregatorContext) SetParams(p *types.Params) { - agc.params = p -} - -// SetValidatorPowers sets the map of validator's power for aggreagtorContext -func (agc *AggregatorContext) SetValidatorPowers(vp map[string]*big.Int) { - // t := big.NewInt(0) - agc.totalPower = big.NewInt(0) - agc.validatorsPower = make(map[string]*big.Int) - for addr, power := range vp { - agc.validatorsPower[addr] = power - agc.totalPower = new(big.Int).Add(agc.totalPower, power) - } -} - -// GetValidatorPowers returns the map of validator's power stored in aggregatorContext -func (agc *AggregatorContext) GetValidatorPowers() (vp map[string]*big.Int) { - return agc.validatorsPower -} - -func (agc *AggregatorContext) GetValidators() (validators []string) { - for k := range agc.validatorsPower { - validators = append(validators, k) - } - return -} - -// GetTokenIDFromAssetID returns tokenID for corresponding tokenID, it returns 0 if agc.params is nil or assetID not found in agc.params -func (agc *AggregatorContext) GetTokenIDFromAssetID(assetID string) int { - if agc.params == nil { - return 0 - } - return agc.params.GetTokenIDFromAssetID(assetID) -} - -// GetParams returns the params field of aggregatorContext -func (agc *AggregatorContext) GetParams() types.Params { - return *agc.params -} - -func (agc *AggregatorContext) GetParamsMaxSizePrices() uint64 { - return uint64(agc.params.MaxSizePrices) -} - -// GetFinalPriceListForFeederIDs get final price list for required feederIDs in format []{feederID, sourceID, detID, price} with asc of {feederID, sourceID} -// feederIDs is required to be ordered asc -func (agc *AggregatorContext) GetFinalPriceListForFeederIDs(feederIDs []uint64) []*types.AggFinalPrice { - ret := make([]*types.AggFinalPrice, 0, len(feederIDs)) - for _, feederID := range feederIDs { - feederWorker := agc.aggregators[feederID] - if feederWorker != nil { - if pList := feederWorker.getFinalPriceList(feederID); len(pList) > 0 { - ret = append(ret, pList...) - } - } - } - return ret -} - -// PerformanceReview compare results to decide whether the validator is effective, honest -func (agc *AggregatorContext) PerformanceReview(ctx sdk.Context, finalPrice *types.AggFinalPrice, validator string) (exist, matched bool) { - feederWorker := agc.aggregators[finalPrice.FeederID] - if feederWorker == nil { - // Log unexpected nil feederWorker for debugging - ctx.Logger().Error( - "unexpected nil feederWorker in PerformanceReview", - "feederID", finalPrice.FeederID, - "validator", validator, - ) - // Treat validator as effective & honest to avoid unfair penalties - exist = true - matched = true - return - } - exist, matched = feederWorker.check(validator, finalPrice.FeederID, finalPrice.SourceID, finalPrice.Price, finalPrice.DetID) - return -} - -func (agc AggregatorContext) windowEnd(feederID, height uint64) bool { - feeder := agc.params.TokenFeeders[feederID] - if (feeder.EndBlock > 0 && feeder.EndBlock <= height) || feeder.StartBaseBlock > height { - return false - } - delta := height - feeder.StartBaseBlock - left := delta % feeder.Interval - return left == uint64(common.MaxNonce) -} - -func (agc *AggregatorContext) RemoveWorker(feederID uint64) { - delete(agc.aggregators, feederID) -} - -// NewAggregatorContext returns a new instance of AggregatorContext -func NewAggregatorContext() *AggregatorContext { - return &AggregatorContext{ - validatorsPower: make(map[string]*big.Int), - totalPower: big.NewInt(0), - rounds: make(map[uint64]*roundInfo), - aggregators: make(map[uint64]*worker), - } -} diff --git a/x/oracle/keeper/aggregator/context_test.go b/x/oracle/keeper/aggregator/context_test.go deleted file mode 100644 index ef97c5b73..000000000 --- a/x/oracle/keeper/aggregator/context_test.go +++ /dev/null @@ -1,93 +0,0 @@ -package aggregator - -import ( - "math/big" - - . "github.com/agiledragon/gomonkey/v2" - sdk "github.com/cosmos/cosmos-sdk/types" - // . "github.com/smartystreets/goconvey/convey" -) - -// func TestAggregatorContext(t *testing.T) { -// Convey("init aggregatorContext with default params", t, func() { -// agc := initAggregatorContext4Test() -// var ctx sdk.Context -// Convey("prepare round to gengerate round info of feeders for next block", func() { -// Convey("pepare within the window", func() { -// p := patchBlockHeight(12) -// agc.PrepareRoundEndBlock(ctx, 11, 0) -// -// Convey("for empty round list", func() { -// So(*agc.rounds[1], ShouldResemble, roundInfo{10, 2, 1}) -// }) -// -// Convey("update already exist round info", func() { -// p.Reset() -// time.Sleep(1 * time.Second) -// patchBlockHeight(10 + int64(common.MaxNonce) + 1) -// -// agc.PrepareRoundEndBlock(ctx, 10+int64(common.MaxNonce), 0) -// So(agc.rounds[1].status, ShouldEqual, 2) -// }) -// p.Reset() -// time.Sleep(1 * time.Second) -// }) -// Convey("pepare outside the window", func() { -// Convey("for empty round list", func() { -// p := patchBlockHeight(10 + int64(common.MaxNonce) + 1) -// agc.PrepareRoundEndBlock(ctx, 10+int64(common.MaxNonce), 0) -// So(agc.rounds[1].status, ShouldEqual, 2) -// p.Reset() -// time.Sleep(1 * time.Second) -// }) -// }) -// }) -// -// Convey("seal existing round without any msg recieved", func() { -// p := patchBlockHeight(11) -// agc.PrepareRoundEndBlock(ctx, 10, 0) -// Convey("seal when exceed the window", func() { -// So(agc.rounds[1].status, ShouldEqual, 1) -// p.Reset() -// time.Sleep(1 * time.Second) -// patchBlockHeight(13) -// agc.SealRound(ctx, false) -// So(agc.rounds[1].status, ShouldEqual, 2) -// }) -// -// Convey("force seal by required", func() { -// p.Reset() -// time.Sleep(1 * time.Second) -// patchBlockHeight(12) -// agc.SealRound(ctx, false) -// So(agc.rounds[1].status, ShouldEqual, 1) -// agc.SealRound(ctx, true) -// So(agc.rounds[1].status, ShouldEqual, 2) -// }) -// p.Reset() -// time.Sleep(1 * time.Second) -// }) -// }) -// } - -func initAggregatorContext4Test() *AggregatorContext { - agc := NewAggregatorContext() - - validatorPowers := map[string]*big.Int{ - "v1": big.NewInt(1), - "v2": big.NewInt(1), - "v3": big.NewInt(1), - } - - p := defaultParams - - agc.SetValidatorPowers(validatorPowers) - agc.SetParams(&p) - return agc -} - -func patchBlockHeight(h int64) *Patches { - return ApplyMethod(sdk.Context{}, "BlockHeight", func(sdk.Context) int64 { - return h - }) -} diff --git a/x/oracle/keeper/aggregator/filter.go b/x/oracle/keeper/aggregator/filter.go deleted file mode 100644 index a179373a3..000000000 --- a/x/oracle/keeper/aggregator/filter.go +++ /dev/null @@ -1,102 +0,0 @@ -package aggregator - -import ( - "strconv" - - "github.com/ExocoreNetwork/exocore/x/oracle/keeper/common" - "github.com/ExocoreNetwork/exocore/x/oracle/types" -) - -type filter struct { - maxNonce int - maxDetID int - // nonce start from 1 - validatorNonce map[string]*common.Set[int32] - // validator_sourceId -> roundID, NS use 0 - validatorSource map[string]*common.Set[string] -} - -func newFilter(maxNonce, maxDetID int) *filter { - return &filter{ - maxNonce: maxNonce, - maxDetID: maxDetID, - validatorNonce: make(map[string]*common.Set[int32]), - validatorSource: make(map[string]*common.Set[string]), - } -} - -func (f *filter) copy4CheckTx() *filter { - ret := *f - ret.validatorNonce = make(map[string]*common.Set[int32], len(f.validatorNonce)) - ret.validatorSource = make(map[string]*common.Set[string], len(f.validatorSource)) - - for k, v := range f.validatorNonce { - ret.validatorNonce[k] = v.Copy() - } - - for k, v := range f.validatorSource { - ret.validatorSource[k] = v.Copy() - } - - return &ret -} - -func (f *filter) newVNSet() *common.Set[int32] { - return common.NewSet[int32](f.maxNonce) -} - -func (f *filter) newVSSet() *common.Set[string] { - return common.NewSet[string](f.maxDetID) -} - -// add priceWithSource into calculator list and aggregator list depends on the source type(deterministic/non-deterministic) -func (f *filter) addPSource(pSources []*types.PriceSource, validator string) (list4Calculator []*types.PriceSource, list4Aggregator []*types.PriceSource) { - for _, pSource := range pSources { - // check conflicts or duplicate data for the same roundID within the same source - if len(pSource.Prices[0].DetID) > 0 { - // #nosec G115 - k := validator + strconv.Itoa(int(pSource.SourceID)) - detIDs := f.validatorSource[k] - if detIDs == nil { - detIDs = f.newVSSet() - f.validatorSource[k] = detIDs - } - - pSourceTmp := &types.PriceSource{ - SourceID: pSource.SourceID, - Prices: make([]*types.PriceTimeDetID, 0, len(pSource.Prices)), - Desc: pSource.Desc, - } - - for _, pDetID := range pSource.Prices { - if ok := detIDs.Add(pDetID.DetID); ok { - // deterministic id has not seen in filter and limitation of ids this souce has not reached - pSourceTmp.Prices = append(pSourceTmp.Prices, pDetID) - } - } - if len(pSourceTmp.Prices) > 0 { - list4Calculator = append(list4Calculator, pSourceTmp) - list4Aggregator = append(list4Aggregator, pSourceTmp) - } - } else { - // add non-deterministic pSource value into aggregator list - list4Aggregator = append(list4Aggregator, pSource) - } - } - return list4Calculator, list4Aggregator -} - -// filtrate checks data from MsgCreatePrice, and will drop the conflict or duplicate data, it will then fill data into calculator(for deterministic source data to get to consensus) and aggregator (for both deterministic and non0-deterministic source data run 2-layers aggregation to get the final price) -func (f *filter) filtrate(price *types.MsgCreatePrice) (list4Calculator []*types.PriceSource, list4Aggregator []*types.PriceSource) { - validator := price.Creator - nonces := f.validatorNonce[validator] - if nonces == nil { - nonces = f.newVNSet() - f.validatorNonce[validator] = nonces - } - - if ok := nonces.Add(price.Nonce); ok { - list4Calculator, list4Aggregator = f.addPSource(price.Prices, validator) - } - return -} diff --git a/x/oracle/keeper/aggregator/filter_test.go b/x/oracle/keeper/aggregator/filter_test.go deleted file mode 100644 index 085251f9b..000000000 --- a/x/oracle/keeper/aggregator/filter_test.go +++ /dev/null @@ -1,104 +0,0 @@ -package aggregator - -import ( - "testing" - - "github.com/ExocoreNetwork/exocore/x/oracle/types" - . "github.com/smartystreets/goconvey/convey" -) - -func TestFilter(t *testing.T) { - Convey("test aggregator_filter", t, func() { - f := newFilter(3, 5) - ptd1 := newPTD("1", "600000") - ptd2 := newPTD("2", "600050") - ptd3 := newPTD("3", "600070") - ptd4 := newPTD("4", "601000") - ptd5 := newPTD("5", "602000") - ptd6 := newPTD("6", "603000") - - ps1 := &types.PriceSource{ - SourceID: 1, - Prices: []*types.PriceTimeDetID{ - ptd1, - ptd2, - }, - } - - ps := []*types.PriceSource{ps1} - msg := &types.MsgCreatePrice{ - Creator: "v1", - FeederID: 1, - Prices: ps, - BasedBlock: 10, - Nonce: 1, - } - l4c, l4a := f.filtrate(msg) - - Convey("add first valid msg", func() { - So(l4c, ShouldResemble, ps) - So(l4a, ShouldResemble, ps) - }) - - Convey("add duplicate nonce msg", func() { - ps1.Prices[0] = ptd3 - l4c, l4a = f.filtrate(msg) - So(l4c, ShouldBeNil) - So(l4a, ShouldBeNil) - }) - - Convey("add duplicate detId", func() { - msg.Nonce = 2 - l4c, l4a = f.filtrate(msg) - Convey("add with new nonce", func() { - So(l4c, ShouldBeNil) - So(l4a, ShouldBeNil) - }) - Convey("update with new detId but use duplicate nonce", func() { - msg.Nonce = 2 - ps1.Prices[0] = ptd3 - l4c, l4a := f.filtrate(msg) - So(l4c, ShouldBeNil) - So(l4a, ShouldBeNil) - }) - }) - - Convey("add new detId with new nonce", func() { - msg.Nonce = 2 - ps1.Prices[0] = ptd3 - l4c, l4a = f.filtrate(msg) - ps1.Prices = ps1.Prices[:1] - ps1.Prices[0] = ptd3 - psReturn := []*types.PriceSource{ps1} - So(l4c, ShouldResemble, psReturn) - So(l4a, ShouldResemble, psReturn) - }) - - Convey("add too many nonce", func() { - msg.Nonce = 2 - ps1.Prices[0] = ptd3 - f.filtrate(msg) - - msg.Nonce = 3 - ps1.Prices[0] = ptd4 - l4c, _ = f.filtrate(msg) - So(l4c[0].Prices, ShouldContain, ptd4) - - msg.Nonce = 4 - ps1.Prices[0] = ptd5 - l4c, _ = f.filtrate(msg) - So(l4c, ShouldBeNil) - }) - - Convey("add too many DetIds", func() { - msg.Nonce = 2 - ps1.Prices = []*types.PriceTimeDetID{ptd3, ptd4, ptd5, ptd6} - l4c, l4a = f.filtrate(msg) - So(l4c, ShouldResemble, l4a) - So(l4c[0].Prices, ShouldContain, ptd3) - So(l4c[0].Prices, ShouldContain, ptd4) - So(l4c[0].Prices, ShouldContain, ptd5) - So(l4c[0].Prices, ShouldNotContain, ptd6) - }) - }) -} diff --git a/x/oracle/keeper/aggregator/helper_test.go b/x/oracle/keeper/aggregator/helper_test.go deleted file mode 100644 index f993c6b8e..000000000 --- a/x/oracle/keeper/aggregator/helper_test.go +++ /dev/null @@ -1,19 +0,0 @@ -package aggregator - -import "github.com/ExocoreNetwork/exocore/x/oracle/types" - -func newPTD(detID, price string) *types.PriceTimeDetID { - return &types.PriceTimeDetID{ - Price: price, - Decimal: 1, - Timestamp: "-", - DetID: detID, - } -} - -func newPS(sourceID uint64, prices ...*types.PriceTimeDetID) *types.PriceSource { - return &types.PriceSource{ - SourceID: sourceID, - Prices: prices, - } -} diff --git a/x/oracle/keeper/aggregator/info_test.go b/x/oracle/keeper/aggregator/info_test.go deleted file mode 100644 index dbab8f01c..000000000 --- a/x/oracle/keeper/aggregator/info_test.go +++ /dev/null @@ -1,60 +0,0 @@ -package aggregator - -import ( - "math/big" - - "github.com/ExocoreNetwork/exocore/x/oracle/types" -) - -var ( - one = big.NewInt(1) - zero = big.NewInt(0) - ten = big.NewInt(10) - eleven = big.NewInt(11) - fifteen = big.NewInt(15) - twenty = big.NewInt(20) -) - -var ( - pTD1 = newPTD("1", "10") - pTD2 = newPTD("2", "12") - pTD3 = newPTD("3", "15") - pTD2M = newPTD("2", "11") - pTD3M = newPTD("3", "19") - // 1-10, 2-12 - pS1 = []*types.PriceSource{newPS(1, pTD1, pTD2)} - // 2-12, 3-1 - pS2 = []*types.PriceSource{newPS(1, pTD3, pTD2)} - // 1-10, 2-11(m) - pS3 = []*types.PriceSource{newPS(1, pTD1, pTD2M)} - // 2-12, 3-19(m) - pS4 = []*types.PriceSource{newPS(1, pTD2, pTD3M)} - // 1-10, 3-19(m) - pS5 = []*types.PriceSource{newPS(1, pTD1, pTD3M)} - - pS6 = []*types.PriceSource{newPS(2, pTD1)} - - // 1-10, 2-12 - pS21 = []*types.PriceSource{newPS(1, pTD1, pTD2), newPS(2, pTD1, pTD3)} - // 2-12, 3-15 - pS22 = []*types.PriceSource{newPS(1, pTD3, pTD2), newPS(2, pTD2, pTD3)} - // 1-10, 2-11(m) - pS23 = []*types.PriceSource{newPS(1, pTD1, pTD2M), newPS(2, pTD2M, pTD1)} - // 2-12, 3-19(m) - pS24 = []*types.PriceSource{newPS(1, pTD2, pTD3M), newPS(2, pTD3, pTD2M)} - // 1-10, 3-19(m) - pS25 = []*types.PriceSource{newPS(1, pTD1, pTD3M), newPS(2, pTD2M, pTD3M)} -) - -var defaultParams = types.Params{ - Chains: []*types.Chain{{Name: "-", Desc: "-"}, {Name: "Ethereum", Desc: "-"}}, - Tokens: []*types.Token{{}, {Name: "eth", ChainID: 1, ContractAddress: "0xabc", Decimal: 18, Active: true, AssetID: ""}}, - Sources: []*types.Source{{}, {Name: "chainLink", Entry: &types.Endpoint{}, Valid: true, Deterministic: true}}, - Rules: []*types.RuleSource{{}, {SourceIDs: []uint64{1}}}, - TokenFeeders: []*types.TokenFeeder{{}, {TokenID: 1, RuleID: 1, StartRoundID: 1, StartBaseBlock: 0, Interval: 10, EndBlock: 0}}, - MaxNonce: 3, - ThresholdA: 2, - ThresholdB: 3, - Mode: types.ConsensusModeASAP, - MaxDetId: 5, -} diff --git a/x/oracle/keeper/aggregator/util.go b/x/oracle/keeper/aggregator/util.go deleted file mode 100644 index 0c1bbc47b..000000000 --- a/x/oracle/keeper/aggregator/util.go +++ /dev/null @@ -1,11 +0,0 @@ -package aggregator - -import "math/big" - -func copyBigInt(i *big.Int) *big.Int { - if i == nil { - return nil - } - - return big.NewInt(0).Set(i) -} diff --git a/x/oracle/keeper/aggregator/worker.go b/x/oracle/keeper/aggregator/worker.go deleted file mode 100644 index a676fd2f4..000000000 --- a/x/oracle/keeper/aggregator/worker.go +++ /dev/null @@ -1,121 +0,0 @@ -package aggregator - -import ( - "github.com/ExocoreNetwork/exocore/x/oracle/keeper/common" - "github.com/ExocoreNetwork/exocore/x/oracle/types" - sdk "github.com/cosmos/cosmos-sdk/types" -) - -// worker is the actual instance used to calculate final price for each tokenFeeder's round. Which means, every tokenFeeder corresponds to a specified token, and for that tokenFeeder, each round we use a worker instance to calculate the final price -type worker struct { - sealed bool - price string - decimal int32 - // mainly used for deterministic source data to check conflicts and validation - f *filter - // used to get to consensus on deterministic source's data - c *calculator - // when enough data(exceeds threshold) collected, aggregate to conduct the final price - a *aggregator - ctx *AggregatorContext - // TODO: move outside into context through .ctx - records recordMsg -} - -// recordKey used to retrieve messages from records to evaluate that if a validator report proper price for a specific feederID+sourceID -type recordKey struct { - validator string - feederID uint64 - sourceID uint64 -} - -// recordMsg define wrap the map for fast access to validator's message info -type recordMsg map[recordKey][]*types.PriceTimeDetID - -func newRecordMsg() recordMsg { - return make(map[recordKey][]*types.PriceTimeDetID) -} - -func (r recordMsg) get(validator string, feederID, sourceID uint64) []*types.PriceTimeDetID { - v := r[recordKey{validator, feederID, sourceID}] - return v -} - -func (r recordMsg) check(validator string, feederID, sourceID uint64, price, detID string) (exist, matched bool) { - prices := r.get(validator, feederID, sourceID) - for _, p := range prices { - if p.DetID == detID { - exist = true - if p.Price == price { - matched = true - return - } - } - } - return -} - -func (r recordMsg) set(creator string, feederID uint64, priceSources []*types.PriceSource) { - accAddress, _ := sdk.AccAddressFromBech32(creator) - validator := sdk.ConsAddress(accAddress).String() - for _, price := range priceSources { - r[recordKey{validator, feederID, price.SourceID}] = price.Prices - } -} - -// GetFinalPriceList relies requirement to aggregator inside them to get final price list -// []{feederID, sourceID, detID, price} in asc order of {soruceID} -func (w *worker) getFinalPriceList(feederID uint64) []*types.AggFinalPrice { - return w.a.getFinalPriceList(feederID) -} - -func (w *worker) filtrate(msg *types.MsgCreatePrice) (list4Calculator []*types.PriceSource, list4Aggregator []*types.PriceSource) { - return w.f.filtrate(msg) -} - -func (w *worker) recordMessage(creator string, feederID uint64, priceSources []*types.PriceSource) { - w.records.set(creator, feederID, priceSources) -} - -func (w *worker) check(validator string, feederID, sourceID uint64, price, detID string) (exist, matched bool) { - return w.records.check(validator, feederID, sourceID, price, detID) -} - -func (w *worker) do(msg *types.MsgCreatePrice) []*types.PriceSource { - list4Calculator, list4Aggregator := w.f.filtrate(msg) - if list4Aggregator != nil { - accAddress, _ := sdk.AccAddressFromBech32(msg.Creator) - validator := sdk.ConsAddress(accAddress).String() - power := w.ctx.validatorsPower[validator] - w.a.fillPrice(list4Aggregator, validator, power) - if confirmedRounds := w.c.fillPrice(list4Calculator, validator, power); confirmedRounds != nil { - w.a.confirmDSPrice(confirmedRounds) - } - } - return list4Aggregator -} - -func (w *worker) aggregate() string { - return w.a.aggregate() -} - -// not concurrency safe -func (w *worker) seal() { - if w.sealed { - return - } - w.sealed = true - w.price = w.a.aggregate() -} - -// newWorker new a instance for a tokenFeeder's specific round -func newWorker(feederID uint64, agc *AggregatorContext) *worker { - return &worker{ - f: newFilter(int(common.MaxNonce), int(common.MaxDetID)), - c: newCalculator(len(agc.validatorsPower), agc.totalPower), - a: newAggregator(len(agc.validatorsPower), agc.totalPower), - decimal: agc.params.GetTokenInfo(feederID).Decimal, - ctx: agc, - records: newRecordMsg(), - } -} diff --git a/x/oracle/keeper/cache/caches.go b/x/oracle/keeper/cache/caches.go deleted file mode 100644 index 47db24b53..000000000 --- a/x/oracle/keeper/cache/caches.go +++ /dev/null @@ -1,234 +0,0 @@ -package cache - -import ( - "math/big" - - "github.com/ExocoreNetwork/exocore/x/oracle/keeper/common" - "github.com/ExocoreNetwork/exocore/x/oracle/types" - sdk "github.com/cosmos/cosmos-sdk/types" -) - -var zeroBig = big.NewInt(0) - -type ( - ItemV map[string]*big.Int - ItemP types.Params - ItemM types.MsgItem -) - -type Cache struct { - msg *cacheMsgs - validators *cacheValidator - params *cacheParams -} - -type cacheMsgs []*ItemM - -// used to track validator change -type cacheValidator struct { - validators map[string]*big.Int - update bool -} - -// used to track params change -type cacheParams struct { - // params types.Params - params *ItemP - update bool -} - -func (c *cacheMsgs) add(item *ItemM) { - *c = append(*c, item) -} - -// remove removes all items with the same feederID -func (c *cacheMsgs) remove(item *ItemM) { - var newCache []*ItemM - for _, msg := range *c { - if msg.FeederID != item.FeederID { - newCache = append(newCache, msg) - } - } - *c = newCache -} - -func (c cacheMsgs) commit(ctx sdk.Context, k common.KeeperOracle) { - block := uint64(ctx.BlockHeight()) - - recentMsgs := types.RecentMsg{ - Block: block, - Msgs: make([]*types.MsgItem, 0), - } - - for _, msg := range c { - msgTmp := types.MsgItem(*msg) - recentMsgs.Msgs = append(recentMsgs.Msgs, &msgTmp) - } - index, _ := k.GetIndexRecentMsg(ctx) - - i := 0 - for ; i < len(index.Index); i++ { - b := index.Index[i] - if b > block-uint64(common.MaxNonce) { - break - } - k.RemoveRecentMsg(ctx, b) - } - index.Index = index.Index[i:] - - k.SetRecentMsg(ctx, recentMsgs) - - index.Index = append(index.Index, block) - k.SetIndexRecentMsg(ctx, index) -} - -func (c *cacheValidator) add(validators map[string]*big.Int) { - for operator, newPower := range validators { - if power, ok := c.validators[operator]; ok { - if newPower.Cmp(zeroBig) == 0 { - delete(c.validators, operator) - c.update = true - } else if power.Cmp(newPower) != 0 { - c.validators[operator].Set(newPower) - c.update = true - } - } else { - c.update = true - np := *newPower - c.validators[operator] = &np - } - } -} - -func (c *cacheValidator) commit(ctx sdk.Context, k common.KeeperOracle) { - block := uint64(ctx.BlockHeight()) - k.SetValidatorUpdateBlock(ctx, types.ValidatorUpdateBlock{Block: block}) -} - -func (c *cacheParams) add(p ItemP) { - // params' update is triggered when params is actually updated, so no need to do comparison here, just udpate and mark the flag - // TODO: add comparison check, that's something should be done for validation - c.params = &p - c.update = true -} - -func (c *cacheParams) commit(ctx sdk.Context, k common.KeeperOracle) { - block := uint64(ctx.BlockHeight()) - index, _ := k.GetIndexRecentParams(ctx) - i := 0 - for ; i < len(index.Index); i++ { - b := index.Index[i] - if b >= block-uint64(common.MaxNonce) { - break - } - k.RemoveRecentParams(ctx, b) - } - if i > 0 && i == len(index.Index) { - i-- - } - index.Index = index.Index[i:] - // remove and append for KVStore - index.Index = append(index.Index, block) - k.SetIndexRecentParams(ctx, index) - - p := types.Params(*c.params) - k.SetRecentParams(ctx, types.RecentParams{ - Block: block, - Params: &p, - }) -} - -// memory cache -func (c *Cache) AddCache(i any) { - switch item := i.(type) { - case *ItemM: - c.msg.add(item) - case ItemP: - c.params.add(item) - case ItemV: - c.validators.add(item) - default: - panic("no other types are support") - } -} - -// RemoveCache removes all cached msgs with the same feederID -func (c *Cache) RemoveCache(i any) { - if item, isItemM := i.(*ItemM); isItemM { - c.msg.remove(item) - } -} - -func (c *Cache) GetCache(i any) bool { - switch item := i.(type) { - case ItemV: - if item == nil { - return false - } - for addr, power := range c.validators.validators { - item[addr] = power - } - return c.validators.update - case *ItemP: - if item == nil { - return false - } - *item = *c.params.params - return c.params.update - case *([]*ItemM): - if item == nil { - return false - } - *item = *c.msg - return len(*c.msg) > 0 - default: - return false - } -} - -// SkipCommit skip real commit by setting the updage flag to false -func (c *Cache) SkipCommit() { - c.validators.update = false - c.params.update = false -} - -// CommitCache commits the cache to the KVStore -func (c *Cache) CommitCache(ctx sdk.Context, reset bool, k common.KeeperOracle) (msgUpdated, validatorsUpdated, paramsUpdated bool) { - if len(*(c.msg)) > 0 { - c.msg.commit(ctx, k) - *(c.msg) = make([]*ItemM, 0) - msgUpdated = true - } - - if c.validators.update { - c.validators.commit(ctx, k) - c.validators.update = false - validatorsUpdated = true - } - - if c.params.update { - c.params.commit(ctx, k) - c.params.update = false - paramsUpdated = true - } - if reset { - c.ResetCaches() - } - return -} - -func (c *Cache) ResetCaches() { - *c = *(NewCache()) -} - -func NewCache() *Cache { - return &Cache{ - msg: new(cacheMsgs), - validators: &cacheValidator{ - validators: make(map[string]*big.Int), - }, - params: &cacheParams{ - params: &ItemP{}, - }, - } -} diff --git a/x/oracle/keeper/cache/caches_test.go b/x/oracle/keeper/cache/caches_test.go deleted file mode 100644 index 2d9738781..000000000 --- a/x/oracle/keeper/cache/caches_test.go +++ /dev/null @@ -1,112 +0,0 @@ -package cache - -import ( - "math/big" - "testing" - - "github.com/ExocoreNetwork/exocore/x/oracle/types" - . "github.com/smartystreets/goconvey/convey" - // "go.uber.org/mock/gomock" -) - -func TestCache(t *testing.T) { - c := NewCache() - p := defaultParams - pWrapped := ItemP(p) - - // ctrl := gomock.NewController(t) - // defer ctrl.Finish() - // ko := common.NewMockKeeperOracle(ctrl) - // c.AddCache(CacheItemP(&pWrapped), ko) - - Convey("test cache", t, func() { - Convey("add pramams item", func() { - c.AddCache(pWrapped) - pReturn := &ItemP{} - c.GetCache(pReturn) - So(*pReturn, ShouldResemble, pWrapped) - }) - - Convey("add validatorPower item", func() { - validatorPowers := map[string]*big.Int{ - "v1": big.NewInt(100), - "v2": big.NewInt(109), - "v3": big.NewInt(119), - } - c.AddCache(ItemV(validatorPowers)) - vpReturn := make(map[string]*big.Int) - Convey("for empty cache", func() { - c.GetCache(ItemV(vpReturn)) - So(vpReturn, ShouldResemble, validatorPowers) - }) - Convey("then update validatorPower item for this cache", func() { - validaotrPowers := map[string]*big.Int{ - // add v5 - "v5": big.NewInt(123), - // remove v1 - "v1": big.NewInt(0), - // update v2 - "v2": big.NewInt(199), - } - c.AddCache(ItemV(validaotrPowers)) - c.GetCache(ItemV(vpReturn)) - So(vpReturn, ShouldNotContainKey, "v1") - So(vpReturn, ShouldContainKey, "v5") - So(vpReturn["v2"], ShouldResemble, big.NewInt(199)) - }) - }) - - Convey("add msg item", func() { - msgItems := []*ItemM{ - { - FeederID: 1, - PSources: []*types.PriceSource{ - { - SourceID: 1, - Prices: []*types.PriceTimeDetID{ - {Price: "600000", Decimal: 1, Timestamp: "-", DetID: "1"}, {Price: "620000", Decimal: 1, Timestamp: "-", DetID: "2"}, - }, - }, - }, - Validator: "v1", - }, - { - FeederID: 1, - PSources: []*types.PriceSource{ - {SourceID: 1, Prices: []*types.PriceTimeDetID{{Price: "600000", Decimal: 1, Timestamp: "-", DetID: "4"}, {Price: "620000", Decimal: 1, Timestamp: "-", DetID: "3"}}}, - }, - Validator: "v1", - }, - { - FeederID: 2, - PSources: []*types.PriceSource{{SourceID: 1, Prices: []*types.PriceTimeDetID{{Price: "30000", Decimal: 1, Timestamp: "-", DetID: "4"}, {Price: "32000", Decimal: 1, Timestamp: "-", DetID: "3"}}}}, - Validator: "v2", - }, - } - c.AddCache(msgItems[0]) - msgItemsReturn := make([]*ItemM, 0, 3) - Convey("add single item", func() { - c.GetCache(&msgItemsReturn) - So(msgItemsReturn, ShouldContain, msgItems[0]) - }) - Convey("add more items", func() { - c.AddCache(msgItems[1]) - c.AddCache(msgItems[2]) - - c.GetCache(&msgItemsReturn) - So(msgItemsReturn, ShouldContain, msgItems[0]) - So(msgItemsReturn, ShouldContain, msgItems[2]) - }) - Convey("remove two items with same feederID", func() { - c.AddCache(msgItems[1]) - c.AddCache(msgItems[2]) - c.RemoveCache(msgItems[0]) - - c.GetCache(&msgItemsReturn) - So(msgItemsReturn, ShouldContain, msgItems[2]) - So(msgItemsReturn, ShouldNotContain, msgItems[0]) - So(msgItemsReturn, ShouldNotContain, msgItems[1]) - }) - }) - }) -} diff --git a/x/oracle/keeper/cache/info_test.go b/x/oracle/keeper/cache/info_test.go deleted file mode 100644 index 8514b8a68..000000000 --- a/x/oracle/keeper/cache/info_test.go +++ /dev/null @@ -1,11 +0,0 @@ -package cache - -import "github.com/ExocoreNetwork/exocore/x/oracle/types" - -var defaultParams = types.Params{ - Chains: []*types.Chain{{Name: "-", Desc: "-"}, {Name: "Ethereum", Desc: "-"}}, - Tokens: []*types.Token{{}, {Name: "eth", ChainID: 1, ContractAddress: "0xabc", Decimal: 18, Active: true}}, - Sources: []*types.Source{{}, {Name: "chainLink", Entry: &types.Endpoint{}, Valid: true, Deterministic: true}}, - Rules: []*types.RuleSource{{}, {SourceIDs: []uint64{1}}}, - TokenFeeders: []*types.TokenFeeder{{}, {TokenID: 1, RuleID: 1, StartRoundID: 1, StartBaseBlock: 0, Interval: 10, EndBlock: 0}}, -} diff --git a/x/oracle/keeper/common/common_test.go b/x/oracle/keeper/common/common_test.go deleted file mode 100644 index 08944dcaf..000000000 --- a/x/oracle/keeper/common/common_test.go +++ /dev/null @@ -1,29 +0,0 @@ -package common - -import ( - "testing" - - "cosmossdk.io/math" - sdk "github.com/cosmos/cosmos-sdk/types" - . "github.com/smartystreets/goconvey/convey" - "go.uber.org/mock/gomock" -) - -//go:generate mockgen -destination mock_keeper_test.go -package common github.com/ExocoreNetwork/exocore/x/oracle/keeper/common KeeperOracle - -//go:generate mockgen -destination mock_validator_test.go -package common github.com/cosmos/cosmos-sdk/x/staking/types ValidatorI - -func TestMock(t *testing.T) { - ctrl := gomock.NewController(t) - defer ctrl.Finish() - ko := NewMockKeeperOracle(ctrl) - - ko.EXPECT().GetLastTotalPower(gomock.Any()).Return(math.NewInt(99)) - - x := ko.GetLastTotalPower(sdk.Context{}) - _ = x - - Convey("mock oracle keeper", t, func() { - Convey("GetLastTotalPower", func() { So(x, ShouldResemble, math.NewInt(99)) }) - }) -} diff --git a/x/oracle/keeper/common/expected_keepers.go b/x/oracle/keeper/common/expected_keepers.go index f668d139b..fd7e0ad45 100644 --- a/x/oracle/keeper/common/expected_keepers.go +++ b/x/oracle/keeper/common/expected_keepers.go @@ -1,11 +1,14 @@ package common import ( + "time" + sdkmath "cosmossdk.io/math" dogfoodkeeper "github.com/ExocoreNetwork/exocore/x/dogfood/keeper" dogfoodtypes "github.com/ExocoreNetwork/exocore/x/dogfood/types" "github.com/ExocoreNetwork/exocore/x/oracle/types" abci "github.com/cometbft/cometbft/abci/types" + "github.com/cometbft/cometbft/libs/log" sdk "github.com/cosmos/cosmos-sdk/types" stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" ) @@ -14,12 +17,37 @@ type Price struct { Value sdkmath.Int Decimal uint8 } - +type SlashingKeeper interface { + JailUntil(sdk.Context, sdk.ConsAddress, time.Time) +} type KeeperOracle interface { KeeperDogfood - + SlashingKeeper + + Logger(ctx sdk.Context) log.Logger + AddZeroNonceItemWithFeederIDForValidators(ctx sdk.Context, feederID uint64, validators []string) + InitValidatorReportInfo(ctx sdk.Context, validator string, height int64) + ClearAllValidatorReportInfo(ctx sdk.Context) + ClearAllValidatorMissedRoundBitArray(ctx sdk.Context) + GrowRoundID(ctx sdk.Context, tokenID uint64) (price string, roundID uint64) + AppendPriceTR(ctx sdk.Context, tokenID uint64, priceTR types.PriceTimeRound, detID string) bool + // AppendPriceTR(ctx sdk.Context, tokenID uint64, priceTR types.PriceTimeRound) bool + GetValidatorReportInfo(ctx sdk.Context, validator string) (info types.ValidatorReportInfo, found bool) + GetMaliciousJailDuration(ctx sdk.Context) (res time.Duration) + ClearValidatorMissedRoundBitArray(ctx sdk.Context, validator string) + GetReportedRoundsWindow(ctx sdk.Context) int64 + GetValidatorMissedRoundBitArray(ctx sdk.Context, validator string, index uint64) bool + SetValidatorMissedRoundBitArray(ctx sdk.Context, validator string, index uint64, missed bool) + GetMinReportedPerWindow(ctx sdk.Context) int64 + GetMissJailDuration(ctx sdk.Context) (res time.Duration) + SetValidatorReportInfo(ctx sdk.Context, validator string, info types.ValidatorReportInfo) + GetSlashFractionMalicious(ctx sdk.Context) (res sdk.Dec) + SetValidatorUpdateForCache(sdk.Context, types.ValidatorUpdateBlock) + SetParamsForCache(sdk.Context, types.RecentParams) + SetMsgItemsForCache(sdk.Context, types.RecentMsg) + GetRecentParamsWithinMaxNonce(ctx sdk.Context) (recentParamsList []*types.RecentParams, prev, latest types.RecentParams) + GetAllRecentMsg(ctx sdk.Context) (list []types.RecentMsg) GetParams(sdk.Context) types.Params - GetIndexRecentMsg(sdk.Context) (types.IndexRecentMsg, bool) GetAllRecentMsgAsMap(sdk.Context) map[int64][]*types.MsgItem @@ -34,8 +62,6 @@ type KeeperOracle interface { SetIndexRecentParams(sdk.Context, types.IndexRecentParams) SetRecentParams(sdk.Context, types.RecentParams) - SetValidatorUpdateBlock(sdk.Context, types.ValidatorUpdateBlock) - RemoveRecentParams(sdk.Context, uint64) RemoveRecentMsg(sdk.Context, uint64) diff --git a/x/oracle/keeper/common/mock_keeper_test.go b/x/oracle/keeper/common/mock_keeper_test.go deleted file mode 100644 index 0e6f96e3e..000000000 --- a/x/oracle/keeper/common/mock_keeper_test.go +++ /dev/null @@ -1,270 +0,0 @@ -// Code generated by MockGen. DO NOT EDIT. -// Source: github.com/ExocoreNetwork/exocore/x/oracle/keeper/common (interfaces: KeeperOracle) -// -// Generated by this command: -// -// mockgen -destination mock_keeper_test.go -package common github.com/ExocoreNetwork/exocore/x/oracle/keeper/common KeeperOracle -// - -// Package common is a generated GoMock package. -package common - -import ( - reflect "reflect" - - math "cosmossdk.io/math" - types "github.com/ExocoreNetwork/exocore/x/oracle/types" - types0 "github.com/cometbft/cometbft/abci/types" - types1 "github.com/cosmos/cosmos-sdk/types" - types2 "github.com/cosmos/cosmos-sdk/x/staking/types" - gomock "go.uber.org/mock/gomock" -) - -// MockKeeperOracle is a mock of KeeperOracle interface. -type MockKeeperOracle struct { - ctrl *gomock.Controller - recorder *MockKeeperOracleMockRecorder -} - -// MockKeeperOracleMockRecorder is the mock recorder for MockKeeperOracle. -type MockKeeperOracleMockRecorder struct { - mock *MockKeeperOracle -} - -// NewMockKeeperOracle creates a new mock instance. -func NewMockKeeperOracle(ctrl *gomock.Controller) *MockKeeperOracle { - mock := &MockKeeperOracle{ctrl: ctrl} - mock.recorder = &MockKeeperOracleMockRecorder{mock} - return mock -} - -// EXPECT returns an object that allows the caller to indicate expected use. -func (m *MockKeeperOracle) EXPECT() *MockKeeperOracleMockRecorder { - return m.recorder -} - -// GetAllRecentMsgAsMap mocks base method. -func (m *MockKeeperOracle) GetAllRecentMsgAsMap(arg0 types1.Context) map[int64][]*types.MsgItem { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetAllRecentMsgAsMap", arg0) - ret0, _ := ret[0].(map[int64][]*types.MsgItem) - return ret0 -} - -// GetAllRecentMsgAsMap indicates an expected call of GetAllRecentMsgAsMap. -func (mr *MockKeeperOracleMockRecorder) GetAllRecentMsgAsMap(arg0 any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAllRecentMsgAsMap", reflect.TypeOf((*MockKeeperOracle)(nil).GetAllRecentMsgAsMap), arg0) -} - -// GetAllRecentParamsAsMap mocks base method. -func (m *MockKeeperOracle) GetAllRecentParamsAsMap(arg0 types1.Context) map[uint64]*types.Params { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetAllRecentParamsAsMap", arg0) - ret0, _ := ret[0].(map[uint64]*types.Params) - return ret0 -} - -// GetAllRecentParamsAsMap indicates an expected call of GetAllRecentParamsAsMap. -func (mr *MockKeeperOracleMockRecorder) GetAllRecentParamsAsMap(arg0 any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAllRecentParamsAsMap", reflect.TypeOf((*MockKeeperOracle)(nil).GetAllRecentParamsAsMap), arg0) -} - -// GetIndexRecentMsg mocks base method. -func (m *MockKeeperOracle) GetIndexRecentMsg(arg0 types1.Context) (types.IndexRecentMsg, bool) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetIndexRecentMsg", arg0) - ret0, _ := ret[0].(types.IndexRecentMsg) - ret1, _ := ret[1].(bool) - return ret0, ret1 -} - -// GetIndexRecentMsg indicates an expected call of GetIndexRecentMsg. -func (mr *MockKeeperOracleMockRecorder) GetIndexRecentMsg(arg0 any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetIndexRecentMsg", reflect.TypeOf((*MockKeeperOracle)(nil).GetIndexRecentMsg), arg0) -} - -// GetIndexRecentParams mocks base method. -func (m *MockKeeperOracle) GetIndexRecentParams(arg0 types1.Context) (types.IndexRecentParams, bool) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetIndexRecentParams", arg0) - ret0, _ := ret[0].(types.IndexRecentParams) - ret1, _ := ret[1].(bool) - return ret0, ret1 -} - -// GetIndexRecentParams indicates an expected call of GetIndexRecentParams. -func (mr *MockKeeperOracleMockRecorder) GetIndexRecentParams(arg0 any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetIndexRecentParams", reflect.TypeOf((*MockKeeperOracle)(nil).GetIndexRecentParams), arg0) -} - -// GetLastTotalPower mocks base method. -func (m *MockKeeperOracle) GetLastTotalPower(arg0 types1.Context) math.Int { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetLastTotalPower", arg0) - ret0, _ := ret[0].(math.Int) - return ret0 -} - -// GetLastTotalPower indicates an expected call of GetLastTotalPower. -func (mr *MockKeeperOracleMockRecorder) GetLastTotalPower(arg0 any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetLastTotalPower", reflect.TypeOf((*MockKeeperOracle)(nil).GetLastTotalPower), arg0) -} - -// GetParams mocks base method. -func (m *MockKeeperOracle) GetParams(arg0 types1.Context) types.Params { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetParams", arg0) - ret0, _ := ret[0].(types.Params) - return ret0 -} - -// GetParams indicates an expected call of GetParams. -func (mr *MockKeeperOracleMockRecorder) GetParams(arg0 any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetParams", reflect.TypeOf((*MockKeeperOracle)(nil).GetParams), arg0) -} - -// GetValidatorByConsAddr mocks base method. -func (m *MockKeeperOracle) GetValidatorByConsAddr(arg0 types1.Context, arg1 types1.ConsAddress) (types2.Validator, bool) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetValidatorByConsAddr", arg0, arg1) - ret0, _ := ret[0].(types2.Validator) - ret1, _ := ret[1].(bool) - return ret0, ret1 -} - -// GetValidatorByConsAddr indicates an expected call of GetValidatorByConsAddr. -func (mr *MockKeeperOracleMockRecorder) GetValidatorByConsAddr(arg0, arg1 any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetValidatorByConsAddr", reflect.TypeOf((*MockKeeperOracle)(nil).GetValidatorByConsAddr), arg0, arg1) -} - -// GetValidatorUpdateBlock mocks base method. -func (m *MockKeeperOracle) GetValidatorUpdateBlock(arg0 types1.Context) (types.ValidatorUpdateBlock, bool) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetValidatorUpdateBlock", arg0) - ret0, _ := ret[0].(types.ValidatorUpdateBlock) - ret1, _ := ret[1].(bool) - return ret0, ret1 -} - -// GetValidatorUpdateBlock indicates an expected call of GetValidatorUpdateBlock. -func (mr *MockKeeperOracleMockRecorder) GetValidatorUpdateBlock(arg0 any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetValidatorUpdateBlock", reflect.TypeOf((*MockKeeperOracle)(nil).GetValidatorUpdateBlock), arg0) -} - -// GetValidatorUpdates mocks base method. -func (m *MockKeeperOracle) GetValidatorUpdates(arg0 types1.Context) []types0.ValidatorUpdate { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetValidatorUpdates", arg0) - ret0, _ := ret[0].([]types0.ValidatorUpdate) - return ret0 -} - -// GetValidatorUpdates indicates an expected call of GetValidatorUpdates. -func (mr *MockKeeperOracleMockRecorder) GetValidatorUpdates(arg0 any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetValidatorUpdates", reflect.TypeOf((*MockKeeperOracle)(nil).GetValidatorUpdates), arg0) -} - -// IterateBondedValidatorsByPower mocks base method. -func (m *MockKeeperOracle) IterateBondedValidatorsByPower(arg0 types1.Context, arg1 func(int64, types2.ValidatorI) bool) { - m.ctrl.T.Helper() - m.ctrl.Call(m, "IterateBondedValidatorsByPower", arg0, arg1) -} - -// IterateBondedValidatorsByPower indicates an expected call of IterateBondedValidatorsByPower. -func (mr *MockKeeperOracleMockRecorder) IterateBondedValidatorsByPower(arg0, arg1 any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IterateBondedValidatorsByPower", reflect.TypeOf((*MockKeeperOracle)(nil).IterateBondedValidatorsByPower), arg0, arg1) -} - -// RemoveRecentMsg mocks base method. -func (m *MockKeeperOracle) RemoveRecentMsg(arg0 types1.Context, arg1 uint64) { - m.ctrl.T.Helper() - m.ctrl.Call(m, "RemoveRecentMsg", arg0, arg1) -} - -// RemoveRecentMsg indicates an expected call of RemoveRecentMsg. -func (mr *MockKeeperOracleMockRecorder) RemoveRecentMsg(arg0, arg1 any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RemoveRecentMsg", reflect.TypeOf((*MockKeeperOracle)(nil).RemoveRecentMsg), arg0, arg1) -} - -// RemoveRecentParams mocks base method. -func (m *MockKeeperOracle) RemoveRecentParams(arg0 types1.Context, arg1 uint64) { - m.ctrl.T.Helper() - m.ctrl.Call(m, "RemoveRecentParams", arg0, arg1) -} - -// RemoveRecentParams indicates an expected call of RemoveRecentParams. -func (mr *MockKeeperOracleMockRecorder) RemoveRecentParams(arg0, arg1 any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RemoveRecentParams", reflect.TypeOf((*MockKeeperOracle)(nil).RemoveRecentParams), arg0, arg1) -} - -// SetIndexRecentMsg mocks base method. -func (m *MockKeeperOracle) SetIndexRecentMsg(arg0 types1.Context, arg1 types.IndexRecentMsg) { - m.ctrl.T.Helper() - m.ctrl.Call(m, "SetIndexRecentMsg", arg0, arg1) -} - -// SetIndexRecentMsg indicates an expected call of SetIndexRecentMsg. -func (mr *MockKeeperOracleMockRecorder) SetIndexRecentMsg(arg0, arg1 any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetIndexRecentMsg", reflect.TypeOf((*MockKeeperOracle)(nil).SetIndexRecentMsg), arg0, arg1) -} - -// SetIndexRecentParams mocks base method. -func (m *MockKeeperOracle) SetIndexRecentParams(arg0 types1.Context, arg1 types.IndexRecentParams) { - m.ctrl.T.Helper() - m.ctrl.Call(m, "SetIndexRecentParams", arg0, arg1) -} - -// SetIndexRecentParams indicates an expected call of SetIndexRecentParams. -func (mr *MockKeeperOracleMockRecorder) SetIndexRecentParams(arg0, arg1 any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetIndexRecentParams", reflect.TypeOf((*MockKeeperOracle)(nil).SetIndexRecentParams), arg0, arg1) -} - -// SetRecentMsg mocks base method. -func (m *MockKeeperOracle) SetRecentMsg(arg0 types1.Context, arg1 types.RecentMsg) { - m.ctrl.T.Helper() - m.ctrl.Call(m, "SetRecentMsg", arg0, arg1) -} - -// SetRecentMsg indicates an expected call of SetRecentMsg. -func (mr *MockKeeperOracleMockRecorder) SetRecentMsg(arg0, arg1 any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetRecentMsg", reflect.TypeOf((*MockKeeperOracle)(nil).SetRecentMsg), arg0, arg1) -} - -// SetRecentParams mocks base method. -func (m *MockKeeperOracle) SetRecentParams(arg0 types1.Context, arg1 types.RecentParams) { - m.ctrl.T.Helper() - m.ctrl.Call(m, "SetRecentParams", arg0, arg1) -} - -// SetRecentParams indicates an expected call of SetRecentParams. -func (mr *MockKeeperOracleMockRecorder) SetRecentParams(arg0, arg1 any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetRecentParams", reflect.TypeOf((*MockKeeperOracle)(nil).SetRecentParams), arg0, arg1) -} - -// SetValidatorUpdateBlock mocks base method. -func (m *MockKeeperOracle) SetValidatorUpdateBlock(arg0 types1.Context, arg1 types.ValidatorUpdateBlock) { - m.ctrl.T.Helper() - m.ctrl.Call(m, "SetValidatorUpdateBlock", arg0, arg1) -} - -// SetValidatorUpdateBlock indicates an expected call of SetValidatorUpdateBlock. -func (mr *MockKeeperOracleMockRecorder) SetValidatorUpdateBlock(arg0, arg1 any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetValidatorUpdateBlock", reflect.TypeOf((*MockKeeperOracle)(nil).SetValidatorUpdateBlock), arg0, arg1) -} diff --git a/x/oracle/keeper/common/mock_validator_test.go b/x/oracle/keeper/common/mock_validator_test.go deleted file mode 100644 index 6e84bfe0a..000000000 --- a/x/oracle/keeper/common/mock_validator_test.go +++ /dev/null @@ -1,343 +0,0 @@ -// Code generated by MockGen. DO NOT EDIT. -// Source: github.com/cosmos/cosmos-sdk/x/staking/types (interfaces: ValidatorI) -// -// Generated by this command: -// -// mockgen -destination mock_validator_test.go -package common github.com/cosmos/cosmos-sdk/x/staking/types ValidatorI -// - -// Package common is a generated GoMock package. -package common - -import ( - reflect "reflect" - - math "cosmossdk.io/math" - crypto "github.com/cometbft/cometbft/proto/tendermint/crypto" - types "github.com/cosmos/cosmos-sdk/crypto/types" - types0 "github.com/cosmos/cosmos-sdk/types" - types1 "github.com/cosmos/cosmos-sdk/x/staking/types" - gomock "go.uber.org/mock/gomock" -) - -// MockValidatorI is a mock of ValidatorI interface. -type MockValidatorI struct { - ctrl *gomock.Controller - recorder *MockValidatorIMockRecorder -} - -// MockValidatorIMockRecorder is the mock recorder for MockValidatorI. -type MockValidatorIMockRecorder struct { - mock *MockValidatorI -} - -// NewMockValidatorI creates a new mock instance. -func NewMockValidatorI(ctrl *gomock.Controller) *MockValidatorI { - mock := &MockValidatorI{ctrl: ctrl} - mock.recorder = &MockValidatorIMockRecorder{mock} - return mock -} - -// EXPECT returns an object that allows the caller to indicate expected use. -func (m *MockValidatorI) EXPECT() *MockValidatorIMockRecorder { - return m.recorder -} - -// ConsPubKey mocks base method. -func (m *MockValidatorI) ConsPubKey() (types.PubKey, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "ConsPubKey") - ret0, _ := ret[0].(types.PubKey) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// ConsPubKey indicates an expected call of ConsPubKey. -func (mr *MockValidatorIMockRecorder) ConsPubKey() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ConsPubKey", reflect.TypeOf((*MockValidatorI)(nil).ConsPubKey)) -} - -// GetBondedTokens mocks base method. -func (m *MockValidatorI) GetBondedTokens() math.Int { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetBondedTokens") - ret0, _ := ret[0].(math.Int) - return ret0 -} - -// GetBondedTokens indicates an expected call of GetBondedTokens. -func (mr *MockValidatorIMockRecorder) GetBondedTokens() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBondedTokens", reflect.TypeOf((*MockValidatorI)(nil).GetBondedTokens)) -} - -// GetCommission mocks base method. -func (m *MockValidatorI) GetCommission() math.LegacyDec { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetCommission") - ret0, _ := ret[0].(math.LegacyDec) - return ret0 -} - -// GetCommission indicates an expected call of GetCommission. -func (mr *MockValidatorIMockRecorder) GetCommission() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetCommission", reflect.TypeOf((*MockValidatorI)(nil).GetCommission)) -} - -// GetConsAddr mocks base method. -func (m *MockValidatorI) GetConsAddr() (types0.ConsAddress, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetConsAddr") - ret0, _ := ret[0].(types0.ConsAddress) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// GetConsAddr indicates an expected call of GetConsAddr. -func (mr *MockValidatorIMockRecorder) GetConsAddr() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetConsAddr", reflect.TypeOf((*MockValidatorI)(nil).GetConsAddr)) -} - -// GetConsensusPower mocks base method. -func (m *MockValidatorI) GetConsensusPower(arg0 math.Int) int64 { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetConsensusPower", arg0) - ret0, _ := ret[0].(int64) - return ret0 -} - -// GetConsensusPower indicates an expected call of GetConsensusPower. -func (mr *MockValidatorIMockRecorder) GetConsensusPower(arg0 any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetConsensusPower", reflect.TypeOf((*MockValidatorI)(nil).GetConsensusPower), arg0) -} - -// GetDelegatorShares mocks base method. -func (m *MockValidatorI) GetDelegatorShares() math.LegacyDec { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetDelegatorShares") - ret0, _ := ret[0].(math.LegacyDec) - return ret0 -} - -// GetDelegatorShares indicates an expected call of GetDelegatorShares. -func (mr *MockValidatorIMockRecorder) GetDelegatorShares() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetDelegatorShares", reflect.TypeOf((*MockValidatorI)(nil).GetDelegatorShares)) -} - -// GetMinSelfDelegation mocks base method. -func (m *MockValidatorI) GetMinSelfDelegation() math.Int { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetMinSelfDelegation") - ret0, _ := ret[0].(math.Int) - return ret0 -} - -// GetMinSelfDelegation indicates an expected call of GetMinSelfDelegation. -func (mr *MockValidatorIMockRecorder) GetMinSelfDelegation() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetMinSelfDelegation", reflect.TypeOf((*MockValidatorI)(nil).GetMinSelfDelegation)) -} - -// GetMoniker mocks base method. -func (m *MockValidatorI) GetMoniker() string { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetMoniker") - ret0, _ := ret[0].(string) - return ret0 -} - -// GetMoniker indicates an expected call of GetMoniker. -func (mr *MockValidatorIMockRecorder) GetMoniker() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetMoniker", reflect.TypeOf((*MockValidatorI)(nil).GetMoniker)) -} - -// GetOperator mocks base method. -func (m *MockValidatorI) GetOperator() types0.ValAddress { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetOperator") - ret0, _ := ret[0].(types0.ValAddress) - return ret0 -} - -// GetOperator indicates an expected call of GetOperator. -func (mr *MockValidatorIMockRecorder) GetOperator() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetOperator", reflect.TypeOf((*MockValidatorI)(nil).GetOperator)) -} - -// GetStatus mocks base method. -func (m *MockValidatorI) GetStatus() types1.BondStatus { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetStatus") - ret0, _ := ret[0].(types1.BondStatus) - return ret0 -} - -// GetStatus indicates an expected call of GetStatus. -func (mr *MockValidatorIMockRecorder) GetStatus() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetStatus", reflect.TypeOf((*MockValidatorI)(nil).GetStatus)) -} - -// GetTokens mocks base method. -func (m *MockValidatorI) GetTokens() math.Int { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetTokens") - ret0, _ := ret[0].(math.Int) - return ret0 -} - -// GetTokens indicates an expected call of GetTokens. -func (mr *MockValidatorIMockRecorder) GetTokens() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetTokens", reflect.TypeOf((*MockValidatorI)(nil).GetTokens)) -} - -// IsBonded mocks base method. -func (m *MockValidatorI) IsBonded() bool { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "IsBonded") - ret0, _ := ret[0].(bool) - return ret0 -} - -// IsBonded indicates an expected call of IsBonded. -func (mr *MockValidatorIMockRecorder) IsBonded() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsBonded", reflect.TypeOf((*MockValidatorI)(nil).IsBonded)) -} - -// IsJailed mocks base method. -func (m *MockValidatorI) IsJailed() bool { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "IsJailed") - ret0, _ := ret[0].(bool) - return ret0 -} - -// IsJailed indicates an expected call of IsJailed. -func (mr *MockValidatorIMockRecorder) IsJailed() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsJailed", reflect.TypeOf((*MockValidatorI)(nil).IsJailed)) -} - -// IsUnbonded mocks base method. -func (m *MockValidatorI) IsUnbonded() bool { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "IsUnbonded") - ret0, _ := ret[0].(bool) - return ret0 -} - -// IsUnbonded indicates an expected call of IsUnbonded. -func (mr *MockValidatorIMockRecorder) IsUnbonded() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsUnbonded", reflect.TypeOf((*MockValidatorI)(nil).IsUnbonded)) -} - -// IsUnbonding mocks base method. -func (m *MockValidatorI) IsUnbonding() bool { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "IsUnbonding") - ret0, _ := ret[0].(bool) - return ret0 -} - -// IsUnbonding indicates an expected call of IsUnbonding. -func (mr *MockValidatorIMockRecorder) IsUnbonding() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsUnbonding", reflect.TypeOf((*MockValidatorI)(nil).IsUnbonding)) -} - -// SharesFromTokens mocks base method. -func (m *MockValidatorI) SharesFromTokens(arg0 math.Int) (math.LegacyDec, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "SharesFromTokens", arg0) - ret0, _ := ret[0].(math.LegacyDec) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// SharesFromTokens indicates an expected call of SharesFromTokens. -func (mr *MockValidatorIMockRecorder) SharesFromTokens(arg0 any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SharesFromTokens", reflect.TypeOf((*MockValidatorI)(nil).SharesFromTokens), arg0) -} - -// SharesFromTokensTruncated mocks base method. -func (m *MockValidatorI) SharesFromTokensTruncated(arg0 math.Int) (math.LegacyDec, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "SharesFromTokensTruncated", arg0) - ret0, _ := ret[0].(math.LegacyDec) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// SharesFromTokensTruncated indicates an expected call of SharesFromTokensTruncated. -func (mr *MockValidatorIMockRecorder) SharesFromTokensTruncated(arg0 any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SharesFromTokensTruncated", reflect.TypeOf((*MockValidatorI)(nil).SharesFromTokensTruncated), arg0) -} - -// TmConsPublicKey mocks base method. -func (m *MockValidatorI) TmConsPublicKey() (crypto.PublicKey, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "TmConsPublicKey") - ret0, _ := ret[0].(crypto.PublicKey) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// TmConsPublicKey indicates an expected call of TmConsPublicKey. -func (mr *MockValidatorIMockRecorder) TmConsPublicKey() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "TmConsPublicKey", reflect.TypeOf((*MockValidatorI)(nil).TmConsPublicKey)) -} - -// TokensFromShares mocks base method. -func (m *MockValidatorI) TokensFromShares(arg0 math.LegacyDec) math.LegacyDec { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "TokensFromShares", arg0) - ret0, _ := ret[0].(math.LegacyDec) - return ret0 -} - -// TokensFromShares indicates an expected call of TokensFromShares. -func (mr *MockValidatorIMockRecorder) TokensFromShares(arg0 any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "TokensFromShares", reflect.TypeOf((*MockValidatorI)(nil).TokensFromShares), arg0) -} - -// TokensFromSharesRoundUp mocks base method. -func (m *MockValidatorI) TokensFromSharesRoundUp(arg0 math.LegacyDec) math.LegacyDec { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "TokensFromSharesRoundUp", arg0) - ret0, _ := ret[0].(math.LegacyDec) - return ret0 -} - -// TokensFromSharesRoundUp indicates an expected call of TokensFromSharesRoundUp. -func (mr *MockValidatorIMockRecorder) TokensFromSharesRoundUp(arg0 any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "TokensFromSharesRoundUp", reflect.TypeOf((*MockValidatorI)(nil).TokensFromSharesRoundUp), arg0) -} - -// TokensFromSharesTruncated mocks base method. -func (m *MockValidatorI) TokensFromSharesTruncated(arg0 math.LegacyDec) math.LegacyDec { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "TokensFromSharesTruncated", arg0) - ret0, _ := ret[0].(math.LegacyDec) - return ret0 -} - -// TokensFromSharesTruncated indicates an expected call of TokensFromSharesTruncated. -func (mr *MockValidatorIMockRecorder) TokensFromSharesTruncated(arg0 any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "TokensFromSharesTruncated", reflect.TypeOf((*MockValidatorI)(nil).TokensFromSharesTruncated), arg0) -} diff --git a/x/oracle/keeper/common/types.go b/x/oracle/keeper/common/types.go deleted file mode 100644 index 993c20674..000000000 --- a/x/oracle/keeper/common/types.go +++ /dev/null @@ -1,98 +0,0 @@ -package common - -import ( - "math/big" - "sort" - - "github.com/ExocoreNetwork/exocore/x/oracle/types" -) - -var ( - // maxNonce indicates how many messages a validator can submit in a single round to offer price - // current we use this as a mock distance - MaxNonce int32 = 3 - - // these two threshold value used to set the threshold to tell when the price had come to consensus and was able to get a final price of that round - ThresholdA int32 = 2 - ThresholdB int32 = 3 - - // maxDetId each validator can submit, so the calculator can cache maximum of maxDetId*count(validators) values, this is for resistance of malicious validator submmiting invalid detId - MaxDetID int32 = 5 - - // for each token at most MaxSizePrices round of prices will be keep in store - MaxSizePrices = 100 - - // consensus mode: v1: as soon as possbile - Mode types.ConsensusMode = types.ConsensusModeASAP -) - -type Set[T comparable] struct { - size int - slice []T -} - -func (s *Set[T]) Copy() *Set[T] { - ret := NewSet[T](s.Length()) - ret.slice = append(ret.slice, s.slice...) - return ret -} - -func (s *Set[T]) Add(value T) bool { - if len(s.slice) == s.size { - return false - } - for _, v := range s.slice { - if v == value { - return false - } - } - s.slice = append(s.slice, value) - return true -} - -func (s *Set[T]) Has(value T) bool { - for _, v := range s.slice { - if v == value { - return true - } - } - return false -} - -func (s *Set[T]) Length() int { - return s.size -} - -func NewSet[T comparable](length int) *Set[T] { - return &Set[T]{ - size: length, - slice: make([]T, 0, length), - } -} - -func ExceedsThreshold(power *big.Int, totalPower *big.Int) bool { - return new(big.Int).Mul(power, big.NewInt(int64(ThresholdB))).Cmp(new(big.Int).Mul(totalPower, big.NewInt(int64(ThresholdA)))) > 0 -} - -type BigIntList []*big.Int - -func (b BigIntList) Len() int { - return len(b) -} - -func (b BigIntList) Less(i, j int) bool { - return b[i].Cmp(b[j]) < 0 -} - -func (b BigIntList) Swap(i, j int) { - b[i], b[j] = b[j], b[i] -} - -func (b BigIntList) Median() *big.Int { - sort.Sort(b) - l := len(b) - if l%2 == 1 { - return b[l/2] - } - return new(big.Int).Div(new(big.Int).Add(b[l/2], b[l/2-1]), big.NewInt(2)) -} diff --git a/x/oracle/keeper/feedermanagement/aggregator.go b/x/oracle/keeper/feedermanagement/aggregator.go new file mode 100644 index 000000000..96b2bd107 --- /dev/null +++ b/x/oracle/keeper/feedermanagement/aggregator.go @@ -0,0 +1,485 @@ +package feedermanagement + +import ( + "fmt" + "math/big" + "reflect" + "slices" + + "golang.org/x/exp/maps" +) + +type sourceChecker interface { + IsDeterministic(sourceID int64) (bool, error) +} + +func newAggregator(t *threshold, algo AggAlgorithm) *aggregator { + return &aggregator{ + t: t, + finalPrice: nil, + v: newRecordsValidators(), + ds: newRecordsDSs(t), + algo: algo, + } +} + +func (a *aggregator) Equals(a2 *aggregator) bool { + if a == nil || a2 == nil { + return a == a2 + } + + if !reflect.DeepEqual(a.finalPrice, a2.finalPrice) { + return false + } + + if !a.t.Equals(a2.t) { + return false + } + if !a.v.Equals(a2.v) { + return false + } + if !a.ds.Equals(a2.ds) { + return false + } + + return true +} + +func (a *aggregator) CopyForCheckTx() *aggregator { + if a == nil { + return nil + } + var finalPrice *PriceResult + if a.finalPrice != nil { + tmp := *a.finalPrice + finalPrice = &tmp + } + return &aggregator{ + t: a.t.Cpy(), + finalPrice: finalPrice, + v: a.v.Cpy(), + ds: a.ds.Cpy(), + algo: a.algo, + } +} + +func (a *aggregator) GetFinalPrice() (*PriceResult, bool) { + if a.finalPrice != nil { + return a.finalPrice, true + } + if !a.exceedPowerLimit() { + return nil, false + } + finalPrice, ok := a.v.GetFinalPrice(a.algo) + if ok { + a.finalPrice = finalPrice + } + return finalPrice, ok +} + +func (a *aggregator) RecordMsg(msg *MsgItem) error { + _, err := a.v.RecordMsg(msg) + return err +} + +// AddMsg records the message in a.v and do aggregation in a.ds +func (a *aggregator) AddMsg(msg *MsgItem) error { + // record into recordsValidators, validation for duplication + addedMsg, err := a.v.RecordMsg(msg) + // all prices failed to be recorded + if err != nil { + return fmt.Errorf("failed to add quote, error:%w", err) + } + // add into recordsDSs for DS aggregation + for _, ps := range addedMsg.PriceSources { + if ps.deterministic { + if a.ds.AddPriceSource(ps, msg.Power, msg.Validator) { + finalPrice, ok := a.ds.GetFinalPriceForSourceID(ps.sourceID) + if ok { + a.v.UpdateFinalPriceForDS(ps.sourceID, finalPrice) + } + } + } + } + return nil +} + +// TODO: V2: the accumulatedPower should corresponding to all valid validators which provides all sources required by rules(defined in oracle.Params) +func (a *aggregator) exceedPowerLimit() bool { + return a.t.Exceeds(a.v.accumulatedPower) +} + +func newRecordsValidators() *recordsValidators { + return &recordsValidators{ + finalPrice: nil, + accumulatedPower: big.NewInt(0), + records: make(map[string]*priceValidator), + } +} + +func (rv *recordsValidators) Equals(rv2 *recordsValidators) bool { + if rv == nil || rv2 == nil { + return rv == rv2 + } + + if !reflect.DeepEqual(rv.finalPrice, rv2.finalPrice) { + return false + } + if rv.accumulatedPower.Cmp(rv2.accumulatedPower) != 0 { + return false + } + if !reflect.DeepEqual(rv.finalPrices, rv2.finalPrices) { + return false + } + if len(rv.records) != len(rv2.records) { + return false + } + // safe to range map, map compare + for k, v := range rv.records { + if v2, ok := rv2.records[k]; !ok || !v.Equals(v2) { + return false + } + } + + return true +} + +func (rv *recordsValidators) Cpy() *recordsValidators { + if rv == nil { + return nil + } + var finalPrice *PriceResult + if rv.finalPrice != nil { + tmp := *rv.finalPrice + finalPrice = &tmp + } + var finalPrices map[string]*PriceResult + if len(rv.finalPrices) > 0 { + finalPrices = make(map[string]*PriceResult) + // safe to range map, map copy + for v, p := range rv.finalPrices { + price := *p + finalPrices[v] = &price + } + } + records := make(map[string]*priceValidator) + // safe to range map, map copy + for v, pv := range rv.records { + records[v] = pv.Cpy() + } + return &recordsValidators{ + finalPrice: finalPrice, + finalPrices: finalPrices, + accumulatedPower: new(big.Int).Set(rv.accumulatedPower), + records: records, + } +} + +func (rv *recordsValidators) RecordMsg(msg *MsgItem) (*MsgItem, error) { + record, ok := rv.records[msg.Validator] + if !ok { + record = newPriceValidator(msg.Validator, msg.Power) + } + rets := &MsgItem{ + FeederID: msg.FeederID, + Validator: msg.Validator, + Power: msg.Power, + PriceSources: make([]*priceSource, 0), + } + + updated, added, err := record.TryAddPriceSources(msg.PriceSources) + if err != nil { + return nil, fmt.Errorf("failed to record msg, error:%w", err) + } + record.ApplyAddedPriceSources(updated) + if !ok { + rv.records[msg.Validator] = record + rv.accumulatedPower = new(big.Int).Add(rv.accumulatedPower, msg.Power) + } + rets.PriceSources = added + return rets, nil +} + +func (rv *recordsValidators) GetValidatorQuotePricesForSourceID(validator string, sourceID int64) ([]*PriceInfo, bool) { + record, ok := rv.records[validator] + if !ok { + return nil, false + } + pSource, ok := record.priceSources[sourceID] + if !ok { + return nil, false + } + return pSource.prices, true +} + +func (rv *recordsValidators) GetFinalPrice(algo AggAlgorithm) (*PriceResult, bool) { + if rv.finalPrice != nil { + return rv.finalPrice, true + } + if prices, ok := rv.GetFinalPriceForValidators(algo); ok { + keySlice := make([]string, 0, len(prices)) + // safe to range map, this is used to generate a sorted keySlice + for validator := range prices { + keySlice = append(keySlice, validator) + } + slices.Sort(keySlice) + algo.Reset() + // keys are sorted to make sure algo.Add is called in the same order for deterministic result + for _, validator := range keySlice { + if !algo.Add(prices[validator]) { + algo.Reset() + return nil, false + } + } + rv.finalPrice = algo.GetResult() + return rv.finalPrice, rv.finalPrice != nil + } + return nil, false +} + +func (rv *recordsValidators) GetFinalPriceForValidators(algo AggAlgorithm) (map[string]*PriceResult, bool) { + if len(rv.finalPrices) > 0 { + return rv.finalPrices, true + } + ret := make(map[string]*PriceResult) + // the order here is not important, so it's safe to range map here + // we only return true when all validators have finalPrice + for validator, pv := range rv.records { + finalPrice, ok := pv.GetFinalPrice(algo) + if !ok { + return nil, false + } + ret[validator] = finalPrice + } + rv.finalPrices = ret + return ret, true +} + +func (rv *recordsValidators) UpdateFinalPriceForDS(sourceID int64, finalPrice *PriceResult) bool { + if finalPrice == nil { + return false + } + // it's safe to range map here, order does not matter + for _, record := range rv.records { + // ignore the fail cases for updating some pv' DS finalPrice + record.UpdateFinalPriceForDS(sourceID, finalPrice) + } + return true +} + +func newRecordsDSs(t *threshold) *recordsDSs { + return &recordsDSs{ + t: t, + dsMap: make(map[int64]*recordsDS), + } +} + +func (rdss *recordsDSs) Equals(rdss2 *recordsDSs) bool { + if rdss == nil || rdss2 == nil { + return rdss == rdss2 + } + + if !rdss.t.Equals(rdss2.t) { + return false + } + if len(rdss.dsMap) != len(rdss2.dsMap) { + return false + } + // safe to range map, map compare + for k, v := range rdss.dsMap { + if v2, ok := rdss2.dsMap[k]; !ok || !v.Equals(v2) { + return false + } + } + + return true +} + +func (rdss *recordsDSs) Cpy() *recordsDSs { + if rdss == nil { + return nil + } + dsMap := make(map[int64]*recordsDS) + // safe to range map, map copy + for id, r := range rdss.dsMap { + dsMap[id] = r.Cpy() + } + return &recordsDSs{ + t: rdss.t.Cpy(), + dsMap: dsMap, + } +} + +// AddPriceSource adds prices for DS sources +func (rdss *recordsDSs) AddPriceSource(ps *priceSource, power *big.Int, validator string) bool { + if !ps.deterministic { + return false + } + price, ok := rdss.dsMap[ps.sourceID] + if !ok { + price = newRecordsDS() + rdss.dsMap[ps.sourceID] = price + } + for _, p := range ps.prices { + price.AddPrice(&PricePower{ + Price: p, + Power: power, + Validators: map[string]struct{}{validator: {}}, + }) + } + return true +} + +func (rdss *recordsDSs) GetFinalPriceForSourceID(sourceID int64) (*PriceResult, bool) { + rds, ok := rdss.dsMap[sourceID] + if !ok { + return nil, false + } + return rds.GetFinalPrice(rdss.t) +} + +func (rdss *recordsDSs) GetFinalPriceForSources() (map[int64]*PriceResult, bool) { + ret := make(map[int64]*PriceResult) + // safe to range map, the result is a map of 'all or none' + for sourceID, rds := range rdss.dsMap { + if finalPrice, ok := rds.GetFinalPrice(rdss.t); ok { + ret[sourceID] = finalPrice + } else { + return nil, false + } + } + return ret, true +} + +func (rdss *recordsDSs) GetFinalDetIDForSourceID(sourceID int64) string { + if rds, ok := rdss.dsMap[sourceID]; ok { + if rds.finalPrice != nil { + return rds.finalDetID + } + if _, ok := rds.GetFinalPrice(rdss.t); ok { + return rds.finalDetID + } + } + return "" +} + +func newRecordsDS() *recordsDS { + return &recordsDS{ + finalPrice: nil, + validators: make(map[string]struct{}), + finalDetID: "", + accumulatedPowers: big.NewInt(0), + records: make([]*PricePower, 0), + } +} + +func (rds *recordsDS) Equals(rds2 *recordsDS) bool { + if rds == nil || rds2 == nil { + return rds == rds2 + } + + if !reflect.DeepEqual(rds.finalPrice, rds2.finalPrice) { + return false + } + if rds.finalDetID != rds2.finalDetID { + return false + } + if rds.accumulatedPowers.Cmp(rds2.accumulatedPowers) != 0 { + return false + } + if !reflect.DeepEqual(rds.validators, rds2.validators) { + return false + } + if len(rds.records) != len(rds2.records) { + return false + } + for i, r := range rds.records { + if !r.Equals(rds2.records[i]) { + return false + } + } + + return true +} + +func (rds *recordsDS) Cpy() *recordsDS { + if rds == nil { + return nil + } + var finalPrice *PriceResult + if rds.finalPrice != nil { + tmp := *rds.finalPrice + finalPrice = &tmp + } + validators := make(map[string]struct{}) + // safe to range map, map copy + for v := range rds.validators { + validators[v] = struct{}{} + } + records := make([]*PricePower, 0, len(rds.records)) + for _, r := range rds.records { + records = append(records, r.Cpy()) + } + return &recordsDS{ + finalPrice: finalPrice, + finalDetID: rds.finalDetID, + accumulatedPowers: new(big.Int).Set(rds.accumulatedPowers), + validators: validators, + records: records, + } +} + +func (rds *recordsDS) GetFinalPrice(t *threshold) (*PriceResult, bool) { + if rds.finalPrice != nil { + return rds.finalPrice, true + } + if t.Exceeds(rds.accumulatedPowers) { + l := len(rds.records) + for i := l - 1; i >= 0; i-- { + pPower := rds.records[i] + if t.Exceeds(pPower.Power) { + rds.finalPrice = pPower.Price.PriceResult() + rds.finalDetID = pPower.Price.DetID + return rds.finalPrice, true + } + } + } + return nil, false +} + +// AddPrice adds a price into recordsDS +// NOTE: the input PricePower should be filtered by recordsValidators before calling this function to make sure the price is not duplicated by detID +func (rds *recordsDS) AddPrice(p *PricePower) { + validator := maps.Keys(p.Validators)[0] + i := 0 + l := len(rds.records) + for ; i < l; i++ { + record := rds.records[i] + if record.Price.EqualDS(p.Price) { + if _, ok := record.Validators[validator]; !ok { + record.Power.Add(record.Power, p.Power) + record.Validators[validator] = struct{}{} + } + break + } + } + if i >= l { + p = p.Cpy() + for i = 0; i < l; i++ { + record := rds.records[i] + if p.Price.DetID <= record.Price.DetID { + // insert before i + combined := append([]*PricePower{p}, rds.records[i:]...) + rds.records = append(rds.records[:i], combined...) + break + } + } + if i >= l { + rds.records = append(rds.records, p) + } + } + if _, ok := rds.validators[validator]; !ok { + rds.accumulatedPowers.Add(rds.accumulatedPowers, p.Power) + rds.validators[validator] = struct{}{} + } +} diff --git a/x/oracle/keeper/feedermanagement/aggregator_test.go b/x/oracle/keeper/feedermanagement/aggregator_test.go new file mode 100644 index 000000000..4d24dcc9b --- /dev/null +++ b/x/oracle/keeper/feedermanagement/aggregator_test.go @@ -0,0 +1,336 @@ +package feedermanagement + +import ( + "testing" + + oracletypes "github.com/ExocoreNetwork/exocore/x/oracle/types" + . "github.com/smartystreets/goconvey/convey" + gomock "go.uber.org/mock/gomock" +) + +func TestAggregation(t *testing.T) { + Convey("aggregation", t, func() { + Convey("add priceSouce in priceSource", func() { + ps := newPriceSource(1, true) + Convey("add first priceSource, success", func() { + psAdded, err := ps.Add(ps1) + So(psAdded, ShouldResemble, ps1) + So(err, ShouldBeNil) + _, ok := ps.detIDs["1"] + So(ok, ShouldBeTrue) + Convey("add different sourceID, reject", func() { + psAdded, err := ps.Add(ps3) + So(psAdded, ShouldBeNil) + So(err, ShouldNotBeNil) + }) + Convey("add same sourceID with same DetID, reject", func() { + psAdded, err := ps.Add(ps1) + So(psAdded, ShouldBeNil) + So(err, ShouldNotBeNil) + }) + Convey("add same sourceID with different DetID, success", func() { + psAdded, err := ps.Add(ps2) + So(psAdded, ShouldResemble, ps2) + So(err, ShouldBeNil) + _, ok := ps.detIDs["2"] + So(ok, ShouldBeTrue) + }) + Convey("add same sourceID with different DetID, duplicated input, return the added one value", func() { + psAdded, err := ps.Add(ps4) + So(psAdded, ShouldResemble, ps2) + So(err, ShouldBeNil) + }) + }) + }) + Convey("add priceSource in priceValidator", func() { + // Try + pv := newPriceValidator("validator1", big1) + Convey("add source1 with 2 detIDs, try:success", func() { + // duplicated detID=1 in ps1_2 will be removed in returned 'added' + updated, added, err := pv.TryAddPriceSources([]*priceSource{ps1_2, ps2}) + So(updated, ShouldResemble, map[int64]*priceSource{1: ps5}) + So(added, ShouldResemble, []*priceSource{ps1, ps2}) + So(err, ShouldBeNil) + // 'try' will not actually update pv + So(pv.priceSources, ShouldHaveLength, 0) + Convey("apply changes, success", func() { + pv.ApplyAddedPriceSources(updated) + So(pv.priceSources, ShouldHaveLength, 1) + So(pv.priceSources, ShouldResemble, map[int64]*priceSource{1: ps5}) + Convey("add source1 with detID 3, try:success", func() { + updated, added, err := pv.TryAddPriceSources([]*priceSource{ps3_2}) + So(updated, ShouldResemble, map[int64]*priceSource{1: ps6}) + So(added, ShouldResemble, []*priceSource{ps3_2}) + So(err, ShouldBeNil) + So(pv.priceSources[1].prices, ShouldHaveLength, 2) + Convey("apply changes, success", func() { + pv.ApplyAddedPriceSources(updated) + So(pv.priceSources[1].prices, ShouldHaveLength, 3) + }) + }) + }) + }) + }) + Convey("record msgs in recordsValidators", func() { + rv := newRecordsValidators() + // TODO: multiple sources(for V2) + Convey("record valid msg, success", func() { + msgAdded, err := rv.RecordMsg(msgItem1) + So(msgAdded, ShouldResemble, msgItem1_2) + So(err, ShouldBeNil) + So(rv.records["validator1"], ShouldResemble, &priceValidator{validator: "validator1", power: big1, priceSources: map[int64]*priceSource{1: ps5}}) + So(rv.accumulatedPower, ShouldResemble, big1) + Convey("record duplicated msg, reject", func() { + msgAdded, err := rv.RecordMsg(msgItem1_3) + So(msgAdded, ShouldBeNil) + So(err, ShouldNotBeNil) + }) + Convey("record msg from another validator, success", func() { + msgAdded, err := rv.RecordMsg(msgItem2) + So(msgAdded, ShouldResemble, msgItem2_2) + So(err, ShouldBeNil) + So(rv.records["validator2"], ShouldResemble, &priceValidator{validator: "validator2", power: big1, priceSources: map[int64]*priceSource{1: ps5}}) + So(rv.accumulatedPower, ShouldResemble, big2) + Convey("calculate final price without confirmed ds price, fail", func() { + finalPrice, err := rv.GetFinalPrice(defaultAggMedian) + So(finalPrice, ShouldBeNil) + So(err, ShouldBeFalse) + }) + Convey("calculate final price with confirmed ds price, success", func() { + Convey("update final price of ds, success", func() { + So(rv.records["validator1"].priceSources[1].finalPrice, ShouldBeNil) + rv.UpdateFinalPriceForDS(1, pr1) + So(rv.records["validator1"].priceSources[1].finalPrice, ShouldResemble, pr1) + So(rv.records["validator2"].priceSources[1].finalPrice, ShouldResemble, pr1) + finalPrice, err := rv.GetFinalPrice(defaultAggMedian) + So(finalPrice, ShouldResemble, pr1_2) + So(err, ShouldBeTrue) + }) + }) + }) + }) + }) + Convey("add msgs in recordsDS", func() { + rds := newRecordsDS() + + Convey("add first msg with v1-power-1 for detID2, success", func() { + rds.AddPrice(pw2) + So(rds.accumulatedPowers, ShouldResemble, big1) + So(rds.validators["validator1"], ShouldNotBeNil) + So(rds.records, ShouldHaveLength, 1) + So(rds.records[0], ShouldResemble, pw2) + Convey("add second msg with v1-power- 1 for detID1", func() { + rds.AddPrice(pw1) + So(rds.accumulatedPowers, ShouldResemble, big1) + So(rds.records, ShouldHaveLength, 2) + So(rds.records[0], ShouldResemble, pw1) + So(rds.records[1], ShouldResemble, pw2) + Convey("add 3rd msg with v2-power-1 for detID2", func() { + rds.AddPrice(pw3) + So(rds.accumulatedPowers, ShouldResemble, big2) + So(rds.validators["validator2"], ShouldNotBeNil) + So(rds.records, ShouldHaveLength, 2) + So(rds.records[0], ShouldResemble, pw1) + So(rds.records[1], ShouldResemble, pw2_2) + finalPrice, ok := rds.GetFinalPrice(th) + So(finalPrice, ShouldBeNil) + So(ok, ShouldBeFalse) + Convey("add 4th msg with v3-power-1 for detID2", func() { + rds.AddPrice(pw4) + So(rds.accumulatedPowers, ShouldResemble, big3) + So(rds.validators["validator3"], ShouldNotBeNil) + So(rds.records, ShouldHaveLength, 2) + So(rds.records[1], ShouldResemble, pw3_2) + Convey("get finalPrice, success", func() { + finalPrice, ok = rds.GetFinalPrice(th) + So(finalPrice, ShouldResemble, &PriceResult{ + Price: "999", + Decimal: 8, + DetID: "2", + Timestamp: timestamp, + }) + So(ok, ShouldBeTrue) + Convey("add 5th msg with v4-power-1 for detID2", func() { + rds.AddPrice(pw5) + So(rds.accumulatedPowers, ShouldResemble, big4) + So(rds.validators["validator4"], ShouldNotBeNil) + So(rds.records, ShouldHaveLength, 2) + finalPrice, ok = rds.GetFinalPrice(th) + So(finalPrice, ShouldResemble, &PriceResult{ + Price: "999", + Decimal: 8, + DetID: "2", + Timestamp: timestamp, + }) + }) + }) + }) + }) + }) + }) + }) + Convey("add msgs in recordsDSs", func() { + rdss := newRecordsDSs(th) + Convey("add 3 same detId=1 prices from v1,v2,v3", func() { + rdss.AddPriceSource(ps1, big1, "validator1") + rdss.AddPriceSource(ps1, big1, "validator2") + finalPrice, ok := rdss.GetFinalPriceForSourceID(1) + So(finalPrice, ShouldBeNil) + So(ok, ShouldBeFalse) + rdss.AddPriceSource(ps1, big1, "validator3") + finalPrice, ok = rdss.GetFinalPriceForSourceID(1) + So(finalPrice, ShouldNotBeNil) + So(finalPrice, ShouldResemble, ps1.prices[0].PriceResult()) + So(ok, ShouldBeTrue) + }) + Convey("add 3 same detId=1 prices and 2 same detID=2 prices from v1,v2,v3", func() { + rdss.AddPriceSource(ps1, big1, "validator1") + rdss.AddPriceSource(ps2, big1, "validator2") + finalPrice, ok := rdss.GetFinalPriceForSourceID(1) + So(finalPrice, ShouldBeNil) + So(ok, ShouldBeFalse) + rdss.AddPriceSource(ps1_3, big1, "validator3") + finalPrice, ok = rdss.GetFinalPriceForSourceID(1) + So(finalPrice, ShouldBeNil) + So(ok, ShouldBeFalse) + rdss.AddPriceSource(ps2, big1, "validator4") + finalPrice, ok = rdss.GetFinalPriceForSourceID(1) + So(finalPrice, ShouldResemble, ps2.prices[0].PriceResult()) + So(ok, ShouldBeTrue) + }) + + }) + Convey("add msgs in aggregator", func() { + a := newAggregator(th, defaultAggMedian) + err := a.AddMsg(msgItem1) + So(err, ShouldBeNil) + finalPrice, ok := a.GetFinalPrice() + So(finalPrice, ShouldBeNil) + So(ok, ShouldBeFalse) + + err = a.AddMsg(msgItem2) + So(err, ShouldBeNil) + finalPrice, ok = a.GetFinalPrice() + So(finalPrice, ShouldBeNil) + So(ok, ShouldBeFalse) + + // failed to add duplicated msg + err = a.AddMsg(msgItem2) + So(err, ShouldNotBeNil) + + // powe exceeds 2/3 on detID=2 + err = a.AddMsg(msgItem3) + So(err, ShouldBeNil) + finalPrice, ok = a.GetFinalPrice() + So(finalPrice, ShouldResemble, &PriceResult{Price: "999", Decimal: 8}) + So(ok, ShouldBeTrue) + So(a.ds.GetFinalDetIDForSourceID(1), ShouldEqual, "2") + }) + Convey("tally in round", func() { + ctrl := gomock.NewController(t) + c := NewMockCacheReader(ctrl) + c.EXPECT(). + GetPowerForValidator(gomock.Any()). + Return(big1, true). + AnyTimes() + c.EXPECT(). + IsDeterministic(gomock.Eq(int64(1))). + Return(true, nil). + AnyTimes() + c.EXPECT(). + GetThreshold(). + Return(th). + AnyTimes() + c.EXPECT(). + IsRuleV1(gomock.Any()). + Return(true). + AnyTimes() + + r := tData.NewRound(c) + r.cache = c + feederID := r.feederID + Convey("add msg in closed quoting window", func() { + pmsg1 := protoMsgItem1 + pmsg1.FeederID = uint64(feederID) + finalPrice, addedMsgItem, err := r.Tally(pmsg1) + // quoting window not open + So(err, ShouldNotBeNil) + So(finalPrice, ShouldBeNil) + So(addedMsgItem, ShouldBeNil) + }) + Convey("open quotingWindow", func() { + r.PrepareForNextBlock(int64(params.TokenFeeders[r.feederID].StartBaseBlock)) + So(r.status, ShouldEqual, roundStatusOpen) + Convey("add msg-v1-detID1 for source1", func() { + pmsg1 := protoMsgItem1 + pmsg1.FeederID = uint64(feederID) + finalPrice, addedMsgItem, err := r.Tally(pmsg1) + So(finalPrice, ShouldBeNil) + So(addedMsgItem, ShouldResemble, pmsg1) + So(err, ShouldBeNil) + Convey("add msg-v1-detID2, success ", func() { + pmsg2 := protoMsgItem2 + pmsg2.FeederID = uint64(feederID) + finalPrice, addedMsgItem, err = r.Tally(pmsg2) + So(finalPrice, ShouldBeNil) + So(addedMsgItem, ShouldResemble, pmsg2) + So(err, ShouldBeNil) + Convey("add msg-v2-detID2, success", func() { + // v2,detID=2 + pmsg3 := protoMsgItem3 + pmsg3.FeederID = uint64(feederID) + finalPrice, addedMsgItem, err = r.Tally(pmsg3) + So(finalPrice, ShouldBeNil) + So(addedMsgItem, ShouldResemble, pmsg3) + So(err, ShouldBeNil) + Convey("two cases:", func() { + Convey("add msg-v3-detID2, finalPrice", func() { + // v3,detID=2 + pmsg4 := protoMsgItem4 + pmsg4.FeederID = uint64(feederID) + finalPrice, addedMsgItem, err = r.Tally(pmsg4) + So(finalPrice, ShouldResemble, &PriceResult{ + Price: "999", + Decimal: 8, + DetID: "2", + }) + So(addedMsgItem, ShouldResemble, pmsg4) + So(err, ShouldBeNil) + Convey("add msg-v4-detID2, recordOnly", func() { + pmsg5 := protoMsgItem5 + pmsg5.FeederID = uint64(feederID) + finalPrice, addedMsgItem, err = r.Tally(pmsg5) + So(finalPrice, ShouldBeNil) + So(addedMsgItem, ShouldResemble, pmsg5) + So(err, ShouldBeError, oracletypes.ErrQuoteRecorded) + }) + }) + Convey("add msg-v3-detID2-different-price, success", func() { + pmsg4 := protoMsgItem4_2 + pmsg4.FeederID = uint64(feederID) + finalPrice, addedMsgItem, err = r.Tally(pmsg4) + So(finalPrice, ShouldBeNil) + So(addedMsgItem, ShouldResemble, pmsg4) + So(err, ShouldBeNil) + Convey("add msg-v4-detID2, success", func() { + pmsg5 := protoMsgItem5 + pmsg5.FeederID = uint64(feederID) + finalPrice, addedMsgItem, err = r.Tally(pmsg5) + So(finalPrice, ShouldResemble, &PriceResult{ + Price: "999", + Decimal: 8, + DetID: "2", + }) + So(addedMsgItem, ShouldResemble, pmsg5) + So(err, ShouldBeNil) + }) + + }) + }) + }) + }) + }) + }) + }) + }) +} diff --git a/x/oracle/keeper/feedermanagement/algo.go b/x/oracle/keeper/feedermanagement/algo.go new file mode 100644 index 000000000..7a7f35a00 --- /dev/null +++ b/x/oracle/keeper/feedermanagement/algo.go @@ -0,0 +1,132 @@ +package feedermanagement + +import ( + "math/big" + "sort" + "strings" +) + +type BigIntList []*big.Int + +func (b BigIntList) Len() int { + return len(b) +} + +func (b BigIntList) Less(i, j int) bool { + return b[i].Cmp(b[j]) < 0 +} + +func (b BigIntList) Swap(i, j int) { + b[i], b[j] = b[j], b[i] +} + +func (b BigIntList) Median() *big.Int { + sort.Sort(b) + l := len(b) + if l%2 == 1 { + return b[l/2] + } + return new(big.Int).Div(new(big.Int).Add(b[l/2], b[l/2-1]), big.NewInt(2)) +} + +type AggAlgorithm interface { + Add(*PriceResult) bool + GetResult() *PriceResult + Reset() +} + +type priceType int + +const ( + notSet priceType = iota + number + notNumber +) + +type AggMedian struct { + t priceType + finalString string + list []*big.Int + decimal int32 +} + +func NewAggMedian() *AggMedian { + return &AggMedian{ + list: make([]*big.Int, 0), + } +} + +func (a *AggMedian) Add(price *PriceResult) bool { + priceInt, ok := new(big.Int).SetString(price.Price, 10) + if ok { + if a.t == notNumber { + return false + } + if a.t == notSet { + a.t = number + a.list = append(a.list, priceInt) + a.decimal = price.Decimal + return true + } + if a.decimal != price.Decimal { + if a.decimal > price.Decimal { + price.Price += strings.Repeat("0", int(a.decimal-price.Decimal)) + priceInt, _ = new(big.Int).SetString(price.Price, 10) + } else { + delta := big.NewInt(int64(price.Decimal - a.decimal)) + for _, v := range a.list { + nv := new(big.Int).Mul(v, new(big.Int).Exp(big.NewInt(10), delta, nil)) + *v = *nv + } + a.decimal = price.Decimal + } + } + a.list = append(a.list, priceInt) + return true + } + // input is a string, not a number + if a.t == number { + return false + } + if a.t == notSet { + a.t = notNumber + a.finalString = price.Price + return true + } + if a.finalString != price.Price { + return false + } + return true +} + +func (a *AggMedian) GetResult() *PriceResult { + defer a.Reset() + if a.t == notSet { + return nil + } + if a.t == number { + result := BigIntList(a.list).Median().String() + decimal := a.decimal + return &PriceResult{ + Price: result, + Decimal: decimal, + } + } + if len(a.finalString) == 0 { + return nil + } + result := a.finalString + return &PriceResult{ + Price: result, + } +} + +func (a *AggMedian) Reset() { + a.list = make([]*big.Int, 0) + a.t = notSet + a.decimal = 0 + a.finalString = "" +} + +//nolint:unused +var defaultAggMedian = NewAggMedian() diff --git a/x/oracle/keeper/feedermanagement/caches.go b/x/oracle/keeper/feedermanagement/caches.go new file mode 100644 index 000000000..e5690ca61 --- /dev/null +++ b/x/oracle/keeper/feedermanagement/caches.go @@ -0,0 +1,378 @@ +package feedermanagement + +import ( + "errors" + "fmt" + "math/big" + "reflect" + "slices" + + oracletypes "github.com/ExocoreNetwork/exocore/x/oracle/types" + sdk "github.com/cosmos/cosmos-sdk/types" +) + +type ItemV map[string]*big.Int + +var zeroBig = big.NewInt(0) + +const v1RuleID = 1 + +func (c *caches) CpyForSimulation() *caches { + ret := *c + msg := *(c.msg) + params := *(c.params) + // it's safe to do shallow copy on msg, params + ret.msg = &msg + ret.params = ¶ms + validators := make(map[string]*big.Int) + // safe to range map, map copy + for v, p := range c.validators.validators { + validators[v] = new(big.Int).Set(p) + } + ret.validators = &cacheValidator{ + validators: validators, + update: c.validators.update, + } + + return &ret +} + +func (c *caches) Equals(c2 *caches) bool { + if c == nil || c2 == nil { + return c == c2 + } + if !c.msg.Equals(c2.msg) { + return false + } + if !c.validators.Equals(c2.validators) { + return false + } + if !c.params.Equals(c2.params) { + return false + } + return true +} + +func (c *caches) Init(k Submitter, params *oracletypes.Params, validators map[string]*big.Int) { + c.ResetCaches() + c.k = k + + c.params.add(params) + + c.validators.add(validators) +} + +func (c *caches) GetDecimalFromFeederID(feederID uint64) (int32, error) { + p := c.params.params + if feederID <= 0 || feederID > uint64(len(p.TokenFeeders)) { + return 0, errors.New("feederID not exists") + } + tf := p.TokenFeeders[feederID] + return p.Tokens[tf.TokenID].Decimal, nil +} + +func (c *caches) GetMaxNonce() int32 { + return c.params.params.GetMaxNonce() +} + +func (c *caches) GetMaxSizePrices() int32 { + return c.params.params.GetMaxSizePrices() +} + +func (c *caches) IsDeterministic(sourceID int64) (bool, error) { + sources := c.params.params.Sources + if sourceID >= int64(len(sources)) || sourceID <= 0 { + return false, errors.New("invalid sourceID") + } + return sources[sourceID].Deterministic, nil +} + +// RuleV1, we restrict the source to be Chainlink and only that source is acceptable +func (c *caches) IsRuleV1(feederID int64) bool { + ruleID := c.params.params.TokenFeeders[feederID].RuleID + return ruleID == v1RuleID && len(c.params.params.Sources) == 2 && c.params.params.Sources[1].Name == oracletypes.SourceChainlinkName +} + +func (c *caches) GetTokenIDForFeederID(feederID int64) (int64, bool) { + tf, ok := c.GetTokenFeederForFeederID(feederID) + if !ok { + return 0, false + } + // #nosec G115 // tokenID is index of slice + return int64(tf.TokenID), true +} + +func (c *caches) GetValidators() []string { + return c.validators.slice() +} + +func (cm *cacheMsgs) Equals(cm2 *cacheMsgs) bool { + if cm == nil || cm2 == nil { + return cm == cm2 + } + for idx, v := range *cm { + v2 := (*cm2)[idx] + if !reflect.DeepEqual(v, v2) { + return false + } + } + return true +} + +func (cm *cacheMsgs) Cpy() *cacheMsgs { + ret := make([]*oracletypes.MsgItem, 0, len(*cm)) + for _, msg := range *cm { + msgCpy := *msg + ret = append(ret, &msgCpy) + } + cmNew := cacheMsgs(ret) + return &cmNew +} + +func (cm *cacheMsgs) add(item *oracletypes.MsgItem) { + *cm = append(*cm, item) +} + +func (cm *cacheMsgs) commit(ctx sdk.Context, k Submitter) { + if len(*cm) == 0 { + return + } + recentMsgs := oracletypes.RecentMsg{ + // #nosec G115 // height is not negative + Block: uint64(ctx.BlockHeight()), + Msgs: *cm, + } + + k.SetMsgItemsForCache(ctx, recentMsgs) + + *cm = make([]*oracletypes.MsgItem, 0) +} + +func (cv *cacheValidator) Equals(cv2 *cacheValidator) bool { + if cv == nil || cv2 == nil { + return cv == cv2 + } + if cv.update != cv2.update { + return false + } + if len(cv.validators) != len(cv2.validators) { + return false + } + // safe to range map, map compare + for k, v := range cv.validators { + if v2, ok := cv2.validators[k]; !ok { + return false + } else if v.Cmp(v2) != 0 { + return false + } + } + return true +} + +func (cv *cacheValidator) add(validators map[string]*big.Int) { + // safe to range map, check and update all KVs with another map + for operator, newPower := range validators { + if power, ok := cv.validators[operator]; ok { + if newPower.Cmp(zeroBig) == 0 { + delete(cv.validators, operator) + cv.update = true + } else if power.Cmp(newPower) != 0 { + cv.validators[operator].Set(newPower) + cv.update = true + } + } else { + cv.update = true + np := *newPower + cv.validators[operator] = &np + } + } +} + +func (cv *cacheValidator) commit(ctx sdk.Context, k Submitter) { + if !cv.update { + return + } + // #nosec blockHeight is not negative + // TODO: consider change the define of all height types in proto to int64(since cosmossdk defined block height as int64) to get avoid all these conversion + k.SetValidatorUpdateForCache(ctx, oracletypes.ValidatorUpdateBlock{Block: uint64(ctx.BlockHeight())}) + cv.update = false +} + +func (cv *cacheValidator) size() int { + return len(cv.validators) +} + +func (cv *cacheValidator) slice() []string { + if cv.size() == 0 { + return nil + } + validators := make([]string, 0, cv.size()) + // safe to range map, this range is used to generate a sorted slice + for validator := range cv.validators { + validators = append(validators, validator) + } + slices.Sort(validators) + return validators +} + +func (cp *cacheParams) Equals(cp2 *cacheParams) bool { + if cp == nil || cp2 == nil { + return cp == cp2 + } + if cp.update != cp2.update { + return false + } + p1 := cp.params + p2 := cp2.params + return reflect.DeepEqual(p1, p2) +} + +func (cp *cacheParams) add(p *oracletypes.Params) { + cp.params = p + cp.update = true +} + +func (cp *cacheParams) commit(ctx sdk.Context, k Submitter) { + if !cp.update { + return + } + k.SetParamsForCache(ctx, oracletypes.RecentParams{ + // #nosec G115 blockheight is not negative + Block: uint64(ctx.BlockHeight()), + Params: cp.params, + }) + cp.update = false +} + +// memory cache +func (c *caches) AddCache(i any) error { + switch item := i.(type) { + case *oracletypes.MsgItem: + c.msg.add(item) + case *oracletypes.Params: + c.params.add(item) + case ItemV: + c.validators.add(item) + default: + return fmt.Errorf("unsuppported caceh type: %T", i) + } + return nil +} + +// Read reads the cache +func (c *caches) Read(i any) bool { + switch item := i.(type) { + case ItemV: + if item == nil { + return false + } + // safe to range map, map copy + for addr, power := range c.validators.validators { + item[addr] = power + } + return c.validators.update + case *oracletypes.Params: + if item == nil { + return false + } + *item = *c.params.params + return c.params.update + case *[]*oracletypes.MsgItem: + if item == nil { + return false + } + *item = *c.msg + return len(*c.msg) > 0 + default: + return false + } +} + +func (c *caches) GetThreshold() *threshold { + params := &oracletypes.Params{} + c.Read(params) + return &threshold{ + totalPower: c.GetTotalPower(), + thresholdA: big.NewInt(int64(params.ThresholdA)), + thresholdB: big.NewInt(int64(params.ThresholdB)), + } +} + +// GetPowerForValidator returns the power of a validator +func (c *caches) GetPowerForValidator(validator string) (power *big.Int, found bool) { + if c.validators != nil && + len(c.validators.validators) > 0 { + power = c.validators.validators[validator] + if power != nil { + found = true + } + } + // if caches not filled yet, we just return not-found instead of fetching from keeper + return +} + +// GetTotalPower returns the total power of all validators +func (c *caches) GetTotalPower() (totalPower *big.Int) { + totalPower = big.NewInt(0) + if c.validators == nil { + return + } + // safe to renage map, the order does not impact the result + for _, power := range c.validators.validators { + totalPower.Add(totalPower, power) + } + return +} + +// GetTokenFeederForFeederID returns the token feeder for a feederID +func (c *caches) GetTokenFeederForFeederID(feederID int64) (tokenFeeder *oracletypes.TokenFeeder, found bool) { + if c.params != nil && + c.params.params != nil && + int64(len(c.params.params.TokenFeeders)) > feederID { + tokenFeeder = c.params.params.TokenFeeders[feederID] + found = true + } + return +} + +// SkipCommit skip real commit by setting the updage flag to false +func (c *caches) SkipCommit() { + c.validators.update = false + c.params.update = false +} + +// Commit commits the cache to the KVStore +func (c *caches) Commit(ctx sdk.Context, reset bool) (msgUpdated, validatorsUpdated, paramsUpdated bool) { + if len(*(c.msg)) > 0 { + c.msg.commit(ctx, c.k) + msgUpdated = true + } + + if c.validators.update { + c.validators.commit(ctx, c.k) + validatorsUpdated = true + } + + if c.params.update { + c.params.commit(ctx, c.k) + paramsUpdated = true + } + if reset { + c.ResetCaches() + } + return +} + +func (c *caches) ResetCaches() { + *c = *(newCaches()) +} + +func newCaches() *caches { + return &caches{ + msg: new(cacheMsgs), + validators: &cacheValidator{ + validators: make(map[string]*big.Int), + }, + params: &cacheParams{}, + } +} diff --git a/x/oracle/keeper/feedermanagement/data_elaborate_test.go b/x/oracle/keeper/feedermanagement/data_elaborate_test.go new file mode 100644 index 000000000..47b172c43 --- /dev/null +++ b/x/oracle/keeper/feedermanagement/data_elaborate_test.go @@ -0,0 +1,331 @@ +package feedermanagement + +import ( + "math" + + oracletypes "github.com/ExocoreNetwork/exocore/x/oracle/types" +) + +type price oracletypes.MsgItem + +func (p *price) withFeederID(feederID uint64) *price { + ret := *p + ret.FeederID = feederID + return &ret +} + +func (p *price) withValidator(validator string) *price { + ret := *p + ret.Validator = validator + return &ret +} + +func (p *price) msgItem() *oracletypes.MsgItem { + ret := (oracletypes.MsgItem)(*p) + return &ret +} + +func newPrice(prices []*oracletypes.PriceTimeDetID) *price { + return &price{ + PSources: []*oracletypes.PriceSource{ + { + SourceID: 1, + Prices: prices, + }, + }, + } +} + +type validatorSet []*price +type powers struct { + validators map[string]struct{} + p int +} +type blocks struct { + msgItemsInBlocks [][]*price + idx int + accumulated map[string]*powers + // for test cases we use int, so this condition is set to >= (equivalent to > for real bigInt cases) + threshold int + result *oracletypes.PriceTimeDetID +} + +type Blocks struct { + MsgItemsInBlocks [][]*price + Idx int + Accumulated map[string]*powers + // for test cases we use int, so this condition is set to >= (equivalent to > for real bigInt cases) + Threshold int + Result *oracletypes.PriceTimeDetID +} + +func newBlocks(t int) *blocks { + return &blocks{ + msgItemsInBlocks: make([][]*price, 0), + accumulated: make(map[string]*powers), + threshold: t, + } +} + +func NewBlocks(t int) *Blocks { + return &Blocks{ + MsgItemsInBlocks: make([][]*price, 0), + Accumulated: make(map[string]*powers), + Threshold: t, + } +} + +func (b *Blocks) AddPrices(ps []*price) { + b.MsgItemsInBlocks = append(b.MsgItemsInBlocks, ps) +} +func (b *Blocks) Next() (ps []*price, result *oracletypes.PriceTimeDetID) { + if b.Idx >= len(b.MsgItemsInBlocks) { + return nil, nil + } + + ret := b.MsgItemsInBlocks[b.Idx] + b.Idx++ + // skip calculation, just return next msgs and result + if b.Result != nil { + return ret, b.Result + } + + // calculate the expected result(fianlPrice) + for _, pMsgItem := range ret { + // TODO: test only, we assume only first element is valid(sourceID=1) + if pMsgItem == nil { + break + } + if len(pMsgItem.PSources) < 1 || pMsgItem.PSources[0].SourceID != 1 { + panic("we support v1 test only") + } + validator := pMsgItem.Validator + pPriceTimeDetIDs := pMsgItem.PSources[0].Prices + for _, pPriceTimeDetID := range pPriceTimeDetIDs { + acPower := 0 + if item := b.Accumulated[pPriceTimeDetID.DetID]; item != nil { + if _, ok := item.validators[validator]; !ok { + item.validators[validator] = struct{}{} + item.p++ + acPower = item.p + } + // we dont update the tmp variable acPower if validator had been seen + } else { + b.Accumulated[pPriceTimeDetID.DetID] = &powers{ + validators: map[string]struct{}{validator: {}}, + p: 1, + } + acPower = 1 + } + if acPower >= b.Threshold { + if b.Result != nil && pPriceTimeDetID.DetID > b.Result.DetID { + b.Result = pPriceTimeDetID + } + } + } + } + return ret, nil +} + +func (b *blocks) AddPirces(ps []*price) { + b.msgItemsInBlocks = append(b.msgItemsInBlocks, ps) +} + +func (b *blocks) next() (ps []*price, result *oracletypes.PriceTimeDetID) { + if b.idx >= len(b.msgItemsInBlocks) { + return nil, nil + } + + ret := b.msgItemsInBlocks[b.idx] + b.idx++ + // skip calculation, just return next msgs and result + if b.result != nil { + return ret, b.result + } + + // calculate the expected result(fianlPrice) + for _, pMsgItem := range ret { + // TODO: test only, we assume only first element is valid(sourceID=1) + if len(pMsgItem.PSources) < 1 || pMsgItem.PSources[0].SourceID != 1 { + panic("we support v1 test only") + } + validator := pMsgItem.Validator + pPriceTimeDetIDs := pMsgItem.PSources[0].Prices + for _, pPriceTimeDetID := range pPriceTimeDetIDs { + acPower := 0 + if item := b.accumulated[pPriceTimeDetID.DetID]; item != nil { + if _, ok := item.validators[validator]; !ok { + item.validators[validator] = struct{}{} + item.p++ + acPower = item.p + } + // we dont update the tmp variable acPower if validator had been seen + } else { + b.accumulated[pPriceTimeDetID.DetID] = &powers{ + validators: map[string]struct{}{validator: {}}, + p: 1, + } + acPower = 1 + } + if acPower >= b.threshold { + b.result = pPriceTimeDetID + return ret, b.result + } + } + } + return ret, nil +} + +func (b *blocks) reset() { + b.idx = 0 + b.accumulated = make(map[string]*powers) +} + +func (b *Blocks) Reset() { + b.Idx = 0 + b.Accumulated = make(map[string]*powers) + b.Result = nil +} + +func generateAllValidatorSets(ps []*price, validators []string) []validatorSet { + total := len(validators) + ret := make([]validatorSet, 0, 8^total) + + vs := make([]*price, total) + var f func(int, int) + f = func(depth int, total int) { + if depth == 0 { + cpy := make([]*price, total) + // price never changed, it's fine to just copy the pointers + copy(cpy, vs) + ret = append(ret, cpy) + return + } + for _, p := range ps { + if p != nil { + vs[total-depth] = p.withFeederID(1).withValidator(validators[total-depth]) + } + f(depth-1, total) + } + } + f(total, total) + return ret +} + +// this method has some hard coded value for easy listing all cases +// it restrict validatorSet corresponding for 4 validators +// we set this sizeValidators and check for caller to notice +// func generateAllBlocks(validatorSets []validatorSet, quotingWindow int, sizeValidators int) []*blocks { +func generateAllBlocks(validatorSets []validatorSet, quotingWindow int, sizeValidators int) []*Blocks { + // TODO: support arbitrary size + if sizeValidators != 4 { + // this variable is not actually used in the following process for now + panic("only support for 4 validators for test case") + } + // count of possible combinations for one validatorSet + count := int(math.Pow(math.Pow(2, float64(sizeValidators)), float64(quotingWindow))) + ret := make([]*Blocks, 0, len(validatorSets)*count) + for _, vs := range validatorSets { + // TODO: this should be generated from seizeValidtors instead of hard code + // but it might break the defination as 'int', we just set 3 here for simplify temporary + tmpBs := make([][]*price, 0) + var f func(int, [][]int) + + f = func(depth int, idxs [][]int) { + if depth == 0 { + if len(tmpBs) != 3 { + panic("length not equal to 3") + } + // bs := newBlocks(3) + bs := NewBlocks(3) + for _, tmp := range tmpBs { + cpy := make([]*price, len(tmp)) + copy(cpy, tmp) + bs.AddPrices(cpy) + } + ret = append(ret, bs) + return + } + if idxs == nil { + // depth-- + // 1 validators, including nil(zero validator) + for i := -1; i < 4; i++ { + if i == -1 { + tmpBs = append(tmpBs, []*price{nil}) + } else { + tmpBs = append(tmpBs, []*price{vs[i]}) + } + f(depth-1, nil) + + if len(tmpBs) > 0 { + tmpBs = tmpBs[:len(tmpBs)-1] + } + + } + + // 2 validators + + f(depth, [][]int{{0, 1}, {0, 2}, {0, 3}, {1, 2}, {1, 3}, {2, 3}}) + + // 3 validators + f(depth, [][]int{{0, 1, 2}, {0, 1, 3}, {0, 2, 3}, {1, 2, 3}}) + + // 4 ovalidators + f(depth, [][]int{{0, 1, 2, 3}}) + } else { + for _, idx := range idxs { + tmp := make([]*price, 0, len(idx)) + for _, id := range idx { + tmp = append(tmp, vs[id]) + } + tmpBs = append(tmpBs, tmp) + f(depth-1, nil) + if len(tmpBs) > 0 { + tmpBs = tmpBs[:len(tmpBs)-1] + } + + } + } + } + + f(3, nil) + } + return ret +} + +var ( + // only consider about combination + // TODO: add cases as Permutation ? + prices []*price = []*price{ + newPrice([]*oracletypes.PriceTimeDetID{ + {Price: "12100000000", Decimal: 8, DetID: "1"}, + }), + newPrice([]*oracletypes.PriceTimeDetID{ + {Price: "12700000000", Decimal: 8, DetID: "2"}, + }), + newPrice([]*oracletypes.PriceTimeDetID{ + {Price: "12900000000", Decimal: 8, DetID: "3"}, + }), + + newPrice([]*oracletypes.PriceTimeDetID{ + {Price: "12100000000", Decimal: 8, DetID: "1"}, + {Price: "12700000000", Decimal: 8, DetID: "2"}, + }), + newPrice([]*oracletypes.PriceTimeDetID{ + {Price: "12100000000", Decimal: 8, DetID: "1"}, + {Price: "12900000000", Decimal: 8, DetID: "3"}, + }), + newPrice([]*oracletypes.PriceTimeDetID{ + {Price: "12700000000", Decimal: 8, DetID: "2"}, + {Price: "12900000000", Decimal: 8, DetID: "3"}, + }), + + newPrice([]*oracletypes.PriceTimeDetID{ + {Price: "12100000000", Decimal: 8, DetID: "1"}, + {Price: "12700000000", Decimal: 8, DetID: "2"}, + {Price: "12900000000", Decimal: 8, DetID: "3"}, + }), + // 0 price should be considered + nil, + } +) diff --git a/x/oracle/keeper/feedermanagement/data_test.go b/x/oracle/keeper/feedermanagement/data_test.go new file mode 100644 index 000000000..9e29ccc50 --- /dev/null +++ b/x/oracle/keeper/feedermanagement/data_test.go @@ -0,0 +1,194 @@ +package feedermanagement + +import oracletypes "github.com/ExocoreNetwork/exocore/x/oracle/types" + +var ( + ps1 = &priceSource{ + deterministic: true, + sourceID: 1, + prices: []*PriceInfo{{Price: "999", Decimal: 8, DetID: "1"}}, + } + ps1_2 = &priceSource{ + deterministic: true, + sourceID: 1, + prices: []*PriceInfo{{Price: "999", Decimal: 8, DetID: "1"}, {Price: "998", Decimal: 8, DetID: "1"}}, + } + ps1_3 = &priceSource{ + deterministic: true, + sourceID: 1, + prices: []*PriceInfo{{Price: "999", Decimal: 8, DetID: "1"}, {Price: "999", Decimal: 8, DetID: "2"}}, + } + ps2 = &priceSource{ + deterministic: true, + sourceID: 1, + prices: []*PriceInfo{{Price: "999", Decimal: 8, DetID: "2"}}, + } + ps3 = &priceSource{ + deterministic: true, + sourceID: 2, + prices: []*PriceInfo{{Price: "999", Decimal: 8, DetID: "3"}}, + } + ps3_2 = &priceSource{ + deterministic: true, + sourceID: 1, + prices: []*PriceInfo{{Price: "999", Decimal: 8, DetID: "3"}}, + } + ps4 = &priceSource{ + deterministic: true, + sourceID: 1, + prices: []*PriceInfo{{Price: "999", Decimal: 8, DetID: "2"}, {Price: "999", Decimal: 8, DetID: "2"}}, + } + ps5 = &priceSource{ + deterministic: true, + sourceID: 1, + detIDs: map[string]struct{}{"1": {}, "2": {}}, + prices: []*PriceInfo{{Price: "999", Decimal: 8, DetID: "1"}, {Price: "999", Decimal: 8, DetID: "2"}}, + } + ps6 = &priceSource{ + deterministic: true, + sourceID: 1, + detIDs: map[string]struct{}{"1": {}, "2": {}, "3": {}}, + prices: []*PriceInfo{{Price: "999", Decimal: 8, DetID: "1"}, {Price: "999", Decimal: 8, DetID: "2"}, {Price: "999", Decimal: 8, DetID: "3"}}, + } + msgItem1 = &MsgItem{ + FeederID: 1, + Validator: "validator1", + Power: big1, + PriceSources: []*priceSource{ps1_2, ps2}, + } + msgItem1_2 = &MsgItem{ + FeederID: 1, + Validator: "validator1", + Power: big1, + PriceSources: []*priceSource{ps1, ps2}, + } + msgItem1_3 = &MsgItem{ + FeederID: 1, + Validator: "validator1", + Power: big1, + PriceSources: []*priceSource{ps1}, + } + msgItem2 = &MsgItem{ + FeederID: 1, + Validator: "validator2", + Power: big1, + PriceSources: []*priceSource{ps1_2, ps2}, + } + msgItem2_2 = &MsgItem{ + FeederID: 1, + Validator: "validator2", + Power: big1, + PriceSources: []*priceSource{ps1, ps2}, + } + msgItem3 = &MsgItem{ + FeederID: 1, + Validator: "validator3", + Power: big1, + PriceSources: []*priceSource{ps2}, + } + protoMsgItem1 = newTestProtoMsgItem(1, "validator1", "999", "1") + protoMsgItem2 = newTestProtoMsgItem(1, "validator1", "999", "2") + protoMsgItem3 = newTestProtoMsgItem(1, "validator2", "999", "2") + protoMsgItem4 = newTestProtoMsgItem(1, "validator3", "999", "2") + protoMsgItem4_2 = newTestProtoMsgItem(1, "validatorr3", "777", "2") + protoMsgItem5 = newTestProtoMsgItem(1, "validator4", "999", "2") + + pr1 = &PriceResult{ + Price: "999", + Decimal: 8, + DetID: "1", + Timestamp: timestamp, + } + pr1_2 = &PriceResult{ + Price: "999", + Decimal: 8, + } + pw1 = &PricePower{ + Price: &PriceInfo{ + Price: "999", + Decimal: 8, + DetID: "1", + Timestamp: timestamp, + }, + Power: big1, + Validators: map[string]struct{}{"validator1": {}}, + } + pw2 = &PricePower{ + Price: &PriceInfo{ + Price: "999", + Decimal: 8, + DetID: "2", + Timestamp: timestamp, + }, + Power: big1, + Validators: map[string]struct{}{"validator1": {}}, + } + pw2_2 = &PricePower{ + Price: &PriceInfo{ + Price: "999", + Decimal: 8, + DetID: "2", + Timestamp: timestamp, + }, + Power: big2, + Validators: map[string]struct{}{"validator1": {}, "validator2": {}}, + } + + pw3 = &PricePower{ + Price: &PriceInfo{ + Price: "999", + Decimal: 8, + DetID: "2", + Timestamp: timestamp, + }, + Power: big1, + Validators: map[string]struct{}{"validator2": {}}, + } + pw3_2 = &PricePower{ + Price: &PriceInfo{ + Price: "999", + Decimal: 8, + DetID: "2", + Timestamp: timestamp, + }, + Power: big3, + Validators: map[string]struct{}{"validator1": {}, "validator2": {}, "validator3": {}}, + } + + pw4 = &PricePower{ + Price: &PriceInfo{ + Price: "999", + Decimal: 8, + DetID: "2", + Timestamp: timestamp, + }, + Power: big1, + Validators: map[string]struct{}{"validator3": {}}, + } + pw5 = &PricePower{ + Price: &PriceInfo{ + Price: "999", + Decimal: 8, + DetID: "2", + Timestamp: timestamp, + }, + Power: big1, + Validators: map[string]struct{}{"validator4": {}}, + } +) + +func newTestProtoMsgItem(feederID uint64, validator string, price string, detID string) *oracletypes.MsgItem { + return &oracletypes.MsgItem{ + FeederID: feederID, + PSources: []*oracletypes.PriceSource{{ + SourceID: 1, + Prices: []*oracletypes.PriceTimeDetID{{ + Price: price, + Decimal: 8, + DetID: detID, + Timestamp: timestamp, + }}, + }}, + Validator: validator, + } +} diff --git a/x/oracle/keeper/feedermanagement/elaborate_test.go b/x/oracle/keeper/feedermanagement/elaborate_test.go new file mode 100644 index 000000000..dbbf23ded --- /dev/null +++ b/x/oracle/keeper/feedermanagement/elaborate_test.go @@ -0,0 +1,80 @@ +//go:build local + +package feedermanagement + +import ( + "fmt" + "testing" + + gomock "go.uber.org/mock/gomock" +) + +// this test elaborate all combinations for {4validators, 3maxNonce, 3detIDs} +func TestRoundTallyElaborate(t *testing.T) { + ret := generateAllValidatorSets(prices, []string{"v1", "v2", "v3", "v4"}) + tests := generateAllBlocks(ret, 3, 4) + ctrl := gomock.NewController(t) + c := NewMockCacheReader(ctrl) + c.EXPECT(). + GetPowerForValidator(gomock.Any()). + Return(big1, true). + AnyTimes() + c.EXPECT(). + IsDeterministic(gomock.Eq(int64(1))). + Return(true, nil). + AnyTimes() + c.EXPECT(). + GetThreshold(). + Return(th). + AnyTimes() + c.EXPECT(). + IsRuleV1(gomock.Any()). + Return(true). + AnyTimes() + + total := len(tests) + for idx, tt := range tests { + fmt.Printf("total:%d, running:%d\r\n", total, idx) + t.Run(fmt.Sprintf("case_%d", idx), func(t *testing.T) { + r := tData.NewRoundWithFeederID(c, 1) + r.cache = c + r.PrepareForNextBlock(int64(params.TokenFeeders[1].StartBaseBlock)) + p, rslt := tt.Next() + for p != nil { + var pRslt *PriceResult + for _, pItem := range p { + if pItem == nil { + continue + } + if idx == 372751 { + fmt.Printf("tally:%v, result:%v\r\n", pItem.msgItem(), rslt) + } + if pRsltTmp, _, _ := r.Tally(pItem.msgItem()); pRsltTmp != nil { + pRslt = pRsltTmp + } + } + if rslt != nil && (pRslt == nil || rslt.Price != pRslt.Price) { + tt.Reset() + fmt.Println("fail case:", idx, "Tally.result:", pRslt) + p, rslt := tt.Next() + idx2 := 1 + for p != nil { + fmt.Printf("block_%d ", idx2) + idx2++ + for idx3, pi := range p { + fmt.Printf("msgItem_%d: %v ", idx3, pi) + } + fmt.Println() + fmt.Println(rslt) + p, rslt = tt.Next() + } + t.Fatal("failed") + } + if rslt != nil { + break + } + p, rslt = tt.Next() + } + }) + } +} diff --git a/x/oracle/keeper/feedermanagement/feedermanager.go b/x/oracle/keeper/feedermanagement/feedermanager.go new file mode 100644 index 000000000..d1bc0d16a --- /dev/null +++ b/x/oracle/keeper/feedermanagement/feedermanager.go @@ -0,0 +1,917 @@ +package feedermanagement + +import ( + "errors" + "fmt" + "math/big" + "sort" + "strconv" + "strings" + + sdkerrors "cosmossdk.io/errors" + "github.com/ExocoreNetwork/exocore/x/oracle/keeper/common" + oracletypes "github.com/ExocoreNetwork/exocore/x/oracle/types" + cryptocodec "github.com/cosmos/cosmos-sdk/crypto/codec" + sdk "github.com/cosmos/cosmos-sdk/types" + stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" +) + +func NewFeederManager(k common.KeeperOracle) *FeederManager { + return &FeederManager{ + k: k, + sortedFeederIDs: make([]int64, 0), + rounds: make(map[int64]*round), + cs: nil, + } +} + +//nolint:revive +func (f *FeederManager) GetCaches() *caches { + return f.cs +} + +func (f *FeederManager) InitCachesForTest(k Submitter, params *oracletypes.Params, validators map[string]*big.Int) { + f.cs = newCaches() + f.cs.Init(k, params, validators) +} + +func (f *FeederManager) GetParamsFromCache() *oracletypes.Params { + return f.cs.params.params +} + +func (f *FeederManager) GetMaxNonceFromCache() int32 { + return f.cs.GetMaxNonce() +} + +func (f *FeederManager) GetMaxSizePricesFromCache() int32 { + return f.cs.GetMaxSizePrices() +} + +func (f *FeederManager) GetTokenIDForFeederID(feederID int64) (int64, bool) { + return f.cs.GetTokenIDForFeederID(feederID) +} + +func (f *FeederManager) SetKeeper(k common.KeeperOracle) { + f.k = k +} + +func (f *FeederManager) SetNilCaches() { + f.cs = nil +} + +// BeginBlock initializes the caches and slashing records, and setup the rounds +func (f *FeederManager) BeginBlock(ctx sdk.Context) (recovered bool) { + // if the cache is nil and we are not in recovery mode, init the caches + if f.cs == nil { + var err error + recovered, err = f.recovery(ctx) + // it's safe to panic since this will only happen when the node is starting with something wrong in the store + if err != nil { + panic(err) + } + // init feederManager if failed to recovery, this should only happened on block_height==1 + if !recovered { + f.initCaches(ctx) + f.SetParamsUpdated() + f.SetValidatorsUpdated() + } + f.initBehaviorRecords(ctx, ctx.BlockHeight()) + // in recovery mode, snapshot of feederManager is set in the beginblock instead of in the process of replaying endblockInrecovery + f.updateCheckTx() + } + return +} + +func (f *FeederManager) EndBlock(ctx sdk.Context) { + // update params and validator set if necessary in caches and commit all updated information + addedValidators := f.updateAndCommitCaches(ctx) + + // update Slashing related records (reportInfo, missCountBitArray), handle case for 1. reseetSlashing, 2. new validators added for validatorset change + f.updateBehaviorRecordsForNextBlock(ctx, addedValidators) + + // update rounds including create new rounds based on params change, remove expired rounds + // handleQuoteBehavior for ending quotes of rounds + // commit state of mature rounds + f.updateAndCommitRounds(ctx) + + // set status to open for rounds before their quoting window + feederIDs := f.prepareRounds(ctx) + // remove nocnes for closing quoting-window and set nonces for opening quoting-window + f.setupNonces(ctx, feederIDs) + + f.ResetFlags() + + f.updateCheckTx() +} + +func (f *FeederManager) EndBlockInRecovery(ctx sdk.Context, params *oracletypes.Params) { + if params != nil { + f.SetParamsUpdated() + _ = f.cs.AddCache(params) + } + f.updateAndCommitRoundsInRecovery(ctx) + f.prepareRounds(ctx) + f.ResetFlags() +} + +func (f *FeederManager) setupNonces(ctx sdk.Context, feederIDs []int64) { + logger := f.k.Logger(ctx) + height := ctx.BlockHeight() + // the order does not matter, it's safe to update independent state in non-deterministic order + // no need to go through all 'hash' process to range sorted key slice + for _, r := range f.rounds { + // remove nonces for closed quoting windows or when forceSeal is marked + if r.IsQuotingWindowEnd(height) || f.forceSeal { + logger.Debug("clear nonces for closing quoting window or forceSeal", "feederID", r.feederID, "roundID", r.roundID, "basedBlock", r.roundBaseBlock, "height", height, "forceSeal", f.forceSeal) + // #nosec G115 // feederID is index of slice + // items will be removed from slice and keep the order, so it's safe to delete items in different order + f.k.RemoveNonceWithFeederIDForAll(ctx, uint64(r.feederID)) + } + } + if len(feederIDs) == 0 { + return + } + // setup nonces for opening quoting windows + // items need to be insert into slice in order, so feederIDs is sorted + sort.Slice(feederIDs, func(i, j int) bool { return feederIDs[i] < feederIDs[j] }) + validators := f.cs.GetValidators() + for _, feederID := range feederIDs { + r := f.rounds[feederID] + logger.Debug("init nonces for new quoting window", "feederID", feederID, "roundID", r.roundID, "basedBlock", r.roundBaseBlock, "height", height) + // #nosec G115 -- feederID is index of slice + f.k.AddZeroNonceItemWithFeederIDForValidators(ctx, uint64(feederID), validators) + } +} + +func (f *FeederManager) initBehaviorRecords(ctx sdk.Context, height int64) { + if !f.validatorsUpdated { + return + } + validators := f.cs.GetValidators() + for _, validator := range validators { + f.k.InitValidatorReportInfo(ctx, validator, height) + } +} + +func (f *FeederManager) updateBehaviorRecordsForNextBlock(ctx sdk.Context, addedValidators []string) { + height := ctx.BlockHeight() + 1 + if f.resetSlashing { + // reset all validators' reportInfo + f.k.ClearAllValidatorReportInfo(ctx) + f.k.ClearAllValidatorMissedRoundBitArray(ctx) + validators := f.cs.GetValidators() + // order does not matter for independent state update + for _, validator := range validators { + f.k.InitValidatorReportInfo(ctx, validator, height) + } + } else if f.validatorsUpdated { + // order does not matter for independent state update + for _, validator := range addedValidators { + // add possible new added validator info for slashing tracking + f.k.InitValidatorReportInfo(ctx, validator, height) + } + } +} + +// praepareRounds prepares the rounds for the next block, and returns the feederIDs of the rounds that are open on next block +func (f *FeederManager) prepareRounds(ctx sdk.Context) []int64 { + logger := f.k.Logger(ctx) + feederIDs := make([]int64, 0) + height := ctx.BlockHeight() + // it's safe to range map directly, this is just used to update memory state + for _, r := range f.rounds { + if open := r.PrepareForNextBlock(ctx.BlockHeight()); open { + feederIDs = append(feederIDs, r.feederID) + // logs might not be displayed in order, it's marked with [mem] to indicate that this is a memory state update + logger.Info("[mem] open quoting window for round", "feederID", r.feederID, "roundID", r.roundID, "basedBlock", r.roundBaseBlock, "height", height) + } + } + return feederIDs +} + +// 1. update and commit Params if updated +// 2. update and commit validatorPowers if updated +// forceSeal: 1. params has some modifications related to quoting. 2.validatorSet changed +// resetSlashing: params has some modifications related to oracle_slashing +// func (f *FeederManager) updateAndCommitCaches(ctx sdk.Context) (forceSeal, resetSlashing bool, prevValidators, addedValidators []string) { +func (f *FeederManager) updateAndCommitCaches(ctx sdk.Context) (activeValidators []string) { + // update params in caches + if f.paramsUpdated { + paramsOld := &oracletypes.Params{} + f.cs.Read(paramsOld) + params := f.k.GetParams(ctx) + if paramsOld.IsForceSealingUpdate(¶ms) { + f.SetForceSeal() + } + if paramsOld.IsSlashingResetUpdate(¶ms) { + f.SetResetSlasing() + } + _ = f.cs.AddCache(¶ms) + } + + // update validators + validatorUpdates := f.k.GetValidatorUpdates(ctx) + if len(validatorUpdates) > 0 { + f.SetValidatorsUpdated() + f.SetForceSeal() + activeValidators = make([]string, 0) + validatorMap := make(map[string]*big.Int) + for _, vu := range validatorUpdates { + pubKey, _ := cryptocodec.FromTmProtoPublicKey(vu.PubKey) + validatorStr := sdk.ConsAddress(pubKey.Address()).String() + validatorMap[validatorStr] = big.NewInt(vu.Power) + if vu.Power > 0 { + activeValidators = append(activeValidators, validatorStr) + } + } + // update validator set information in cache + _ = f.cs.AddCache(ItemV(validatorMap)) + } + + // commit caches: msgs is exists, params if updated, validatorPowers is updated + _, vUpdated, pUpdated := f.cs.Commit(ctx, false) + if vUpdated || pUpdated { + f.k.Logger(ctx).Info("update caches", "validatorUpdated", vUpdated, "paramsUpdated", pUpdated) + } + return activeValidators +} + +func (f *FeederManager) commitRoundsInRecovery() { + // safe to range map directly, this is just used to update memory state, we don't update state in recovery mode + for _, r := range f.rounds { + if r.Committable() { + r.FinalPrice() + r.status = roundStatusClosed + } + // close all quotingWindow to skip current rounds' 'handlQuotingMisBehavior' + if f.forceSeal { + r.closeQuotingWindow() + } + } +} + +func (f *FeederManager) commitRounds(ctx sdk.Context) { + logger := f.k.Logger(ctx) + height := ctx.BlockHeight() + successFeederIDs := make([]string, 0) + // it's safe to range map directly since the sate update is independent for each feederID, however we use sortedFeederIDs to keep the order of logs + // this can be replaced by map iteration directly when better performance is needed + for _, feederID := range f.sortedFeederIDs { + r := f.rounds[feederID] + if r.Committable() { + finalPrice, ok := r.FinalPrice() + if !ok { + logger.Info("commit round with price from previous", "feederID", r.feederID, "roundID", r.roundID, "baseBlock", r.roundBaseBlock, "height", height) + // #nosec G115 // tokenID is index of slice + f.k.GrowRoundID(ctx, uint64(r.tokenID)) + } else { + if f.cs.IsRuleV1(r.feederID) { + priceCommit := finalPrice.ProtoPriceTimeRound(r.roundID, ctx.BlockTime().Format(oracletypes.TimeLayout)) + logger.Info("commit round with aggregated price", "feederID", r.feederID, "roundID", r.roundID, "baseBlock", r.roundBaseBlock, "price", priceCommit, "height", height) + + // #nosec G115 // tokenID is index of slice + f.k.AppendPriceTR(ctx, uint64(r.tokenID), *priceCommit, finalPrice.DetID) + // f.k.AppendPriceTR(ctx, uint64(r.tokenID), *priceCommit) + + fstr := strconv.FormatInt(feederID, 10) + successFeederIDs = append(successFeederIDs, fstr) // there's no valid price for any round yet + } else { + logger.Error("We currently only support rules under oracle V1: only allow price from source Chainlink", "feederID", r.feederID) + } + } + // keep aggregator for possible 'handlQuotingMisBehavior' at quotingWindowEnd + r.status = roundStatusClosed + } + // close all quotingWindow to skip current rounds' 'handlQuotingMisBehavior' + if f.forceSeal { + r.closeQuotingWindow() + } + } + if len(successFeederIDs) > 0 { + feederIDsStr := strings.Join(successFeederIDs, "_") + ctx.EventManager().EmitEvent(sdk.NewEvent( + oracletypes.EventTypePriceFeed, + sdk.NewAttribute(oracletypes.AttributeKeyPriceUpdated, oracletypes.AttributeValuePriceUpdatedSuccess), + sdk.NewAttribute(oracletypes.AttributeKeyFeederIDs, feederIDsStr), + )) + } +} + +func (f *FeederManager) handleQuotingMisBehaviorInRecovery(ctx sdk.Context) { + height := ctx.BlockHeight() + logger := f.k.Logger(ctx) + // it's safe to range map directly, no state in kvStore will be updated in recovery mode, only memory state is updated + for _, r := range f.rounds { + if r.IsQuotingWindowEnd(height) && r.a != nil { + validators := f.cs.GetValidators() + for _, validator := range validators { + _, found := f.k.GetValidatorReportInfo(ctx, validator) + if !found { + logger.Error(fmt.Sprintf("Expected report info for validator %s but not found", validator)) + continue + } + _, malicious := r.PerformanceReview(validator) + if malicious { + r.getFinalDetIDForSourceID(oracletypes.SourceChainlinkID) + r.FinalPrice() + } + } + r.closeQuotingWindow() + } + } +} + +func (f *FeederManager) handleQuotingMisBehavior(ctx sdk.Context) { + height := ctx.BlockHeight() + logger := f.k.Logger(ctx) + + // it's safe to range map directly, each state update is independent for each feederID + // state to be updated: {validatorReportInfo, validatorMissedRoundBitArray, signInfo, assets} of individual validator + // we use sortedFeederIDs to keep the order of logs + // this can be replaced by map iteration directly when better performance is needed + for _, feederID := range f.sortedFeederIDs { + r := f.rounds[feederID] + if r.IsQuotingWindowEnd(height) { + if _, found := r.FinalPrice(); !found { + r.closeQuotingWindow() + continue + } + validators := f.cs.GetValidators() + for _, validator := range validators { + reportedInfo, found := f.k.GetValidatorReportInfo(ctx, validator) + if !found { + logger.Error(fmt.Sprintf("Expected report info for validator %s but not found", validator)) + continue + } + miss, malicious := r.PerformanceReview(validator) + if malicious { + detID := r.getFinalDetIDForSourceID(oracletypes.SourceChainlinkID) + finalPrice, _ := r.FinalPrice() + logger.Info( + "confirmed malicious price", + "validator", validator, + "infraction_height", height, + "infraction_time", ctx.BlockTime(), + "feederID", r.feederID, + "detID", detID, + "sourceID", oracletypes.SourceChainlinkID, + "finalPrice", finalPrice, + ) + consAddr, err := sdk.ConsAddressFromBech32(validator) + if err != nil { + f.k.Logger(ctx).Error("when do orale_performance_review, got invalid consAddr string. This should never happen", "validatorStr", validator) + continue + } + + operator := f.k.ValidatorByConsAddr(ctx, consAddr) + if operator != nil && !operator.IsJailed() { + power, _ := f.cs.GetPowerForValidator(validator) + coinsBurned := f.k.SlashWithInfractionReason(ctx, consAddr, height, power.Int64(), f.k.GetSlashFractionMalicious(ctx), stakingtypes.Infraction_INFRACTION_UNSPECIFIED) + ctx.EventManager().EmitEvent( + sdk.NewEvent( + oracletypes.EventTypeOracleSlash, + sdk.NewAttribute(oracletypes.AttributeKeyValidatorKey, validator), + sdk.NewAttribute(oracletypes.AttributeKeyPower, fmt.Sprintf("%d", power)), + sdk.NewAttribute(oracletypes.AttributeKeyReason, oracletypes.AttributeValueMaliciousReportPrice), + sdk.NewAttribute(oracletypes.AttributeKeyJailed, validator), + sdk.NewAttribute(oracletypes.AttributeKeyBurnedCoins, coinsBurned.String()), + ), + ) + f.k.Jail(ctx, consAddr) + jailUntil := ctx.BlockHeader().Time.Add(f.k.GetMaliciousJailDuration(ctx)) + f.k.JailUntil(ctx, consAddr, jailUntil) + + reportedInfo.MissedRoundsCounter = 0 + reportedInfo.IndexOffset = 0 + f.k.ClearValidatorMissedRoundBitArray(ctx, validator) + } + continue + } + reportedRoundsWindow := f.k.GetReportedRoundsWindow(ctx) + // #nosec G115 + index := uint64(reportedInfo.IndexOffset % reportedRoundsWindow) + reportedInfo.IndexOffset++ + // Update reported round bit array & counter + // This counter just tracks the sum of the bit array + // That way we avoid needing to read/write the whole array each time + previous := f.k.GetValidatorMissedRoundBitArray(ctx, validator, index) + switch { + case !previous && miss: + // Array value has changed from not missed to missed, increment counter + f.k.SetValidatorMissedRoundBitArray(ctx, validator, index, true) + reportedInfo.MissedRoundsCounter++ + case previous && !miss: + // Array value has changed from missed to not missed, decrement counter + f.k.SetValidatorMissedRoundBitArray(ctx, validator, index, false) + reportedInfo.MissedRoundsCounter-- + default: + // Array value at this index has not changed, no need to update counter + } + + minReportedPerWindow := f.k.GetMinReportedPerWindow(ctx) + + if miss { + ctx.EventManager().EmitEvent( + sdk.NewEvent( + oracletypes.EventTypeOracleLiveness, + sdk.NewAttribute(oracletypes.AttributeKeyValidatorKey, validator), + sdk.NewAttribute(oracletypes.AttributeKeyMissedRounds, fmt.Sprintf("%d", reportedInfo.MissedRoundsCounter)), + sdk.NewAttribute(oracletypes.AttributeKeyHeight, fmt.Sprintf("%d", height)), + ), + ) + + logger.Info( + "oracle_absent validator", + "height", ctx.BlockHeight(), + "validator", validator, + "missed", reportedInfo.MissedRoundsCounter, + "threshold", minReportedPerWindow, + ) + } + + minHeight := reportedInfo.StartHeight + reportedRoundsWindow + maxMissed := reportedRoundsWindow - minReportedPerWindow + // if we are past the minimum height and the validator has missed too many rounds reporting prices, punish them + if height > minHeight && reportedInfo.MissedRoundsCounter > maxMissed { + consAddr, err := sdk.ConsAddressFromBech32(validator) + if err != nil { + f.k.Logger(ctx).Error("when do orale_performance_review, got invalid consAddr string. This should never happen", "validatorStr", validator) + continue + } + operator := f.k.ValidatorByConsAddr(ctx, consAddr) + if operator != nil && !operator.IsJailed() { + // missing rounds confirmed: just jail the validator + f.k.Jail(ctx, consAddr) + jailUntil := ctx.BlockHeader().Time.Add(f.k.GetMissJailDuration(ctx)) + f.k.JailUntil(ctx, consAddr, jailUntil) + + // We need to reset the counter & array so that the validator won't be immediately slashed for miss report info upon rebonding. + reportedInfo.MissedRoundsCounter = 0 + reportedInfo.IndexOffset = 0 + f.k.ClearValidatorMissedRoundBitArray(ctx, validator) + + logger.Info( + "jailing validator due to oracle_liveness fault", + "height", height, + "validator", consAddr.String(), + "min_height", minHeight, + "threshold", minReportedPerWindow, + "jailed_until", jailUntil, + ) + } else { + // validator was (a) not found or (b) already jailed so we do not slash + logger.Info( + "validator would have been slashed for too many missed reporting price, but was either not found in store or already jailed", + "validator", validator, + ) + } + } + // Set the updated reportInfo + f.k.SetValidatorReportInfo(ctx, validator, reportedInfo) + } + r.closeQuotingWindow() + } + } +} + +func (f *FeederManager) setCommittableState(ctx sdk.Context) { + if f.forceSeal { + // safe to range map. update memory state only, the result would be the same in any order + for _, r := range f.rounds { + if r.status == roundStatusOpen { + r.status = roundStatusCommittable + } + } + } else { + height := ctx.BlockHeight() + // safe to range map. update memory state only, the result would be the same in any order + for _, r := range f.rounds { + if r.IsQuotingWindowEnd(height) && r.status == roundStatusOpen { + r.status = roundStatusCommittable + } + } + } +} + +func (f *FeederManager) updateRoundsParamsAndAddNewRounds(ctx sdk.Context) { + height := ctx.BlockHeight() + logger := f.k.Logger(ctx) + + if f.paramsUpdated { + params := &oracletypes.Params{} + f.cs.Read(params) + existsFeederIDs := make(map[int64]struct{}) + // safe to range map. update memory state only, the result would be the same in any order + for _, r := range f.rounds { + r.UpdateParams(params.TokenFeeders[r.feederID], int64(params.MaxNonce)) + existsFeederIDs[r.feederID] = struct{}{} + } + // add new rounds + for feederID, tokenFeeder := range params.TokenFeeders { + if feederID == 0 { + continue + } + feederID := int64(feederID) + // #nosec G115 + if _, ok := existsFeederIDs[feederID]; !ok && (tokenFeeder.EndBlock == 0 || tokenFeeder.EndBlock > uint64(height)) { + logger.Info("[mem] add new round", "feederID", feederID, "height", height) + f.sortedFeederIDs = append(f.sortedFeederIDs, feederID) + f.rounds[feederID] = newRound(feederID, tokenFeeder, int64(params.MaxNonce), f.cs, NewAggMedian()) + } + } + f.sortedFeederIDs.sort() + } +} + +func (f *FeederManager) removeExpiredRounds(ctx sdk.Context) { + height := ctx.BlockHeight() + expiredFeederIDs := make([]int64, 0) + // safe to range map, we generate the slice, the content of elements would be the same, order does not matter + for _, r := range f.rounds { + if r.endBlock > 0 && r.endBlock <= height { + expiredFeederIDs = append(expiredFeederIDs, r.feederID) + } + } + // the order does not matter when remove item from slice as RemoveNonceWithFeederIDForAll does + for _, feederID := range expiredFeederIDs { + if r := f.rounds[feederID]; r.status != roundStatusClosed { + r.closeQuotingWindow() + // #nosec G115 + f.k.RemoveNonceWithFeederIDForAll(ctx, uint64(r.feederID)) + } + delete(f.rounds, feederID) + f.sortedFeederIDs.remove(feederID) + } +} + +func (f *FeederManager) updateAndCommitRoundsInRecovery(ctx sdk.Context) { + f.setCommittableState(ctx) + f.commitRoundsInRecovery() + f.handleQuotingMisBehaviorInRecovery(ctx) + f.updateRoundsParamsAndAddNewRounds(ctx) + f.removeExpiredRounds(ctx) +} + +func (f *FeederManager) updateAndCommitRounds(ctx sdk.Context) { + f.setCommittableState(ctx) + f.commitRounds(ctx) + // behaviors review and close quotingWindow + f.handleQuotingMisBehavior(ctx) + f.updateRoundsParamsAndAddNewRounds(ctx) + f.removeExpiredRounds(ctx) +} + +func (f *FeederManager) ResetFlags() { + f.paramsUpdated = false + f.validatorsUpdated = false + f.forceSeal = false + f.resetSlashing = false +} + +func (f *FeederManager) SetParamsUpdated() { + f.paramsUpdated = true +} + +func (f *FeederManager) SetValidatorsUpdated() { + f.validatorsUpdated = true +} + +func (f *FeederManager) SetResetSlasing() { + f.resetSlashing = true +} + +func (f *FeederManager) SetForceSeal() { + f.forceSeal = true +} + +func (f *FeederManager) ValidateMsg(msg *oracletypes.MsgPriceFeed) error { + // nonce, feederID, creator has been verified by anteHandler + // baseBlock is going to be verified by its corresponding round + decimal, err := f.cs.GetDecimalFromFeederID(msg.FeederID) + if err != nil { + return err + } + for _, ps := range msg.Prices { + // #nosec G115 + deterministic, err := f.cs.IsDeterministic(int64(ps.SourceID)) + if err != nil { + return err + } + l := len(ps.Prices) + if deterministic { + if l == 0 { + return fmt.Errorf("source:id_%d has no valid price, empty list", ps.SourceID) + } + if l > int(f.cs.GetMaxNonce()) { + return fmt.Errorf("deterministic source:id_%d must provide no more than %d prices from different DetIDs, got:%d", ps.SourceID, f.cs.GetMaxNonce(), l) + } + seenDetIDs := make(map[string]struct{}) + for _, p := range ps.Prices { + if _, ok := seenDetIDs[p.DetID]; ok { + return errors.New("duplicated detIDs") + } + if len(p.Price) == 0 { + return errors.New("price must not be empty") + } + if len(p.DetID) == 0 { + return errors.New("detID of deteministic price must not be empty") + } + if p.Decimal != decimal { + return fmt.Errorf("decimal not match for feederID:%d, expect:%d, got:%d", msg.FeederID, decimal, p.Decimal) + } + seenDetIDs[p.DetID] = struct{}{} + } + } else { + // NOTE: v1 does not actually have this type of sources + if l != 1 { + return fmt.Errorf("non-deteministic sources should provide exactly one valid price, got:%d", len(ps.Prices)) + } + p := ps.Prices[0] + if len(p.Price) == 0 { + return errors.New("price must not be empty") + } + if p.Decimal != decimal { + return fmt.Errorf("decimal not match for feederID:%d, expect:%d, got:%d", msg.FeederID, decimal, p.Decimal) + } + if len(p.DetID) > 0 { + return errors.New("price from non-deterministic should not have detID") + } + if len(p.Timestamp) == 0 { + return errors.New("price from non-deterministic must have timestamp") + } + } + } + return nil +} + +func (f *FeederManager) ProcessQuote(ctx sdk.Context, msg *oracletypes.MsgPriceFeed, isCheckTx bool) (*oracletypes.PriceTimeRound, error) { + if isCheckTx { + f = f.getCheckTx() + } + if err := f.ValidateMsg(msg); err != nil { + return nil, oracletypes.ErrInvalidMsg.Wrap(err.Error()) + } + msgItem := getProtoMsgItemFromQuote(msg) + + // #nosec G115 // feederID is index of slice + r, ok := f.rounds[int64(msgItem.FeederID)] + if !ok { + // This should not happened since we do check the nonce in anthHandle + return nil, fmt.Errorf("round not exists for feederID:%d, proposer:%s", msgItem.FeederID, msgItem.Validator) + } + + // #nosec G115 // baseBlock is block height which is not negative + if valid := r.ValidQuotingBaseBlock(int64(msg.BasedBlock)); !valid { + return nil, fmt.Errorf("failed to process price-feed msg for feederID:%d, round is quoting:%t,quotingWindow is open:%t, expected baseBlock:%d, got baseBlock:%d", msgItem.FeederID, r.IsQuoting(), r.IsQuotingWindowOpen(), r.roundBaseBlock, msg.BasedBlock) + } + + // tally msgItem + finalPrice, validMsgItem, err := r.Tally(msgItem) + + // record msgItem in caches if needed + defer func() { + if !isCheckTx && + validMsgItem != nil && + (err == nil || sdkerrors.IsOf(err, oracletypes.ErrQuoteRecorded)) { + _ = f.cs.AddCache(validMsgItem) + } + }() + + if err != nil { + return nil, err + } + + if finalPrice == nil { + return nil, nil + } + return finalPrice.ProtoPriceTimeRound(r.roundID, ctx.BlockTime().Format(oracletypes.TimeLayout)), nil +} + +func (f *FeederManager) getCheckTx() *FeederManager { + fCheckTx := f.fCheckTx + ret := *fCheckTx + ret.fCheckTx = nil + + // rounds + rounds := make(map[int64]*round) + // safe to range map, map copy + for id, r := range fCheckTx.rounds { + rounds[id] = r.CopyForCheckTx() + } + ret.rounds = rounds + + return &ret +} + +func (f *FeederManager) updateCheckTx() { + // flgas are taken care of + // sortedFeederIDs will not be modified except in abci.EndBlock + // successFeedereIDs will not be modifed except in abci.EndBlock + // caches will not be modifed except in abci.EndBlock, abci.DeliverTx (in abci.Query_simulate, or abci.CheckTx the update in ProcessQuote is forbided) + // shallow copy is good enough for these fields + + ret := *f + ret.fCheckTx = nil + + rounds := make(map[int64]*round) + + // safe to range map, map copy + for id, r := range f.rounds { + rounds[id] = r.CopyForCheckTx() + } + ret.rounds = rounds + f.fCheckTx = &ret +} + +func (f *FeederManager) ProcessQuoteInRecovery(msgItems []*oracletypes.MsgItem) { + for _, msgItem := range msgItems { + // #nosec G115 // feederID is index of slice + r, ok := f.rounds[int64(msgItem.FeederID)] + if !ok { + continue + } + // error deos not need to be handled in recovery mode + //nolint:all + r.Tally(msgItem) + } +} + +// initCaches initializes the caches of the FeederManager with keeper, params, validatorPowers +func (f *FeederManager) initCaches(ctx sdk.Context) { + f.cs = newCaches() + params := f.k.GetParams(ctx) + validatorSet := f.k.GetAllExocoreValidators(ctx) + validatorPowers := make(map[string]*big.Int) + for _, v := range validatorSet { + validatorPowers[sdk.ConsAddress(v.Address).String()] = big.NewInt(v.Power) + } + f.cs.Init(f.k, ¶ms, validatorPowers) +} + +func (f *FeederManager) recovery(ctx sdk.Context) (bool, error) { + height := ctx.BlockHeight() + recentParamsList, prevRecentParams, latestRecentParams := f.k.GetRecentParamsWithinMaxNonce(ctx) + if latestRecentParams.Block == 0 { + return false, nil + } + validatorUpdateBlock, found := f.k.GetValidatorUpdateBlock(ctx) + if !found { + // on recovery mode, the validator update block must be found, otherwise we just panic to stop the node start + // it's safe to panic since this will only happen when the node is starting with something wrong in the store + return false, errors.New("validator update block not found in recovery mode for feeder manager") + } + // #nosec G115 // validatorUpdateBlock.Block represents blockheight + startHeight, replayRecentParamsList := getRecoveryStartPoint(height, recentParamsList, &prevRecentParams, &latestRecentParams, int64(validatorUpdateBlock.Block)) + + f.cs = newCaches() + params := replayRecentParamsList[0].Params + replayRecentParamsList = replayRecentParamsList[1:] + + validatorSet := f.k.GetAllExocoreValidators(ctx) + validatorPowers := make(map[string]*big.Int) + for _, v := range validatorSet { + validatorPowers[sdk.ConsAddress(v.Address).String()] = big.NewInt(v.Power) + } + + f.cs.Init(f.k, params, validatorPowers) + + replayHeight := startHeight - 1 + + ctxReplay := ctx.WithBlockHeight(replayHeight) + for tfID, tf := range params.TokenFeeders { + if tfID == 0 { + continue + } + // #nosec G115 // safe conversion + if tf.EndBlock > 0 && int64(tf.EndBlock) <= replayHeight { + continue + } + tfID := int64(tfID) + f.rounds[tfID] = newRound(tfID, tf, int64(params.MaxNonce), f.cs, NewAggMedian()) + f.sortedFeederIDs.add(tfID) + } + f.prepareRounds(ctxReplay) + + params = nil + recentMsgs := f.k.GetAllRecentMsg(ctxReplay) + for ; startHeight < height; startHeight++ { + ctxReplay = ctxReplay.WithBlockHeight(startHeight) + // only execute msgItems corresponding to rounds opened on or after replayHeight, since any rounds opened before replay height must be closed on or before height-1 + // which means no memory state need to be updated for thoes rounds + // and we don't need to take care of 'close quoting-window' since the size of replay window t most equals to maxNonce + // #nosec G115 // block is not negative + if len(recentMsgs) > 0 && int64(recentMsgs[0].Block) <= startHeight { + i := 0 + for idx, recentMsg := range recentMsgs { + // #nosec G115 // block height is defined as int64 in cosmossdk + if int64(recentMsg.Block) > startHeight { + break + } + i = idx + if int64(recentMsg.Block) == startHeight { + f.ProcessQuoteInRecovery(recentMsg.Msgs) + break + } + } + recentMsgs = recentMsgs[i+1:] + } + // #nosec G115 + if len(replayRecentParamsList) > 0 && int64(replayRecentParamsList[0].Block) == startHeight { + params = replayRecentParamsList[0].Params + replayRecentParamsList = replayRecentParamsList[1:] + } + f.EndBlockInRecovery(ctxReplay, params) + } + + f.cs.SkipCommit() + + return true, nil +} + +func (f *FeederManager) Equals(fm *FeederManager) bool { + if f == nil || fm == nil { + return f == fm + } + if f.fCheckTx == nil && fm.fCheckTx != nil { + return false + } + if f.fCheckTx != nil && fm.fCheckTx == nil { + return false + } + if !f.fCheckTx.Equals(fm.fCheckTx) { + return false + } + if f.paramsUpdated != fm.paramsUpdated || + f.validatorsUpdated != fm.validatorsUpdated || + f.resetSlashing != fm.resetSlashing || + f.forceSeal != fm.forceSeal { + return false + } + if !f.sortedFeederIDs.Equals(fm.sortedFeederIDs) { + return false + } + if !f.cs.Equals(fm.cs) { + return false + } + if len(f.rounds) != len(fm.rounds) { + return false + } + // safe to range map, compare map + for id, r := range f.rounds { + if r2, ok := fm.rounds[id]; !ok { + return false + } else if !r.Equals(r2) { + return false + } + } + return true +} + +// recoveryStartPoint returns the height to start the recovery process +func getRecoveryStartPoint(currentHeight int64, recentParamsList []*oracletypes.RecentParams, prevRecentParams, latestRecentParams *oracletypes.RecentParams, validatorUpdateHeight int64) (height int64, replayRecentParamsList []*oracletypes.RecentParams) { + if currentHeight > int64(latestRecentParams.Params.MaxNonce) { + height = currentHeight - int64(latestRecentParams.Params.MaxNonce) + } + // there is no params updated in the recentParamsList, we can start from the validator update block if it's not too old(out of the distance of maxNonce from current height) + if len(recentParamsList) == 0 { + if height < validatorUpdateHeight { + height = validatorUpdateHeight + } + // for empty recetParamsList, use latestrecentParams as the start point + replayRecentParamsList = append(replayRecentParamsList, latestRecentParams) + height++ + return height, replayRecentParamsList + } + + if prevRecentParams.Block > 0 && prevRecentParams.Params.IsForceSealingUpdate(recentParamsList[0].Params) { + // #nosec G115 + height = int64(recentParamsList[0].Block) + } + idx := 0 + for i := 1; i < len(recentParamsList); i++ { + if recentParamsList[i-1].Params.IsForceSealingUpdate(recentParamsList[i].Params) { + // #nosec G115 + height = int64(recentParamsList[i].Block) + idx = i + } + } + replayRecentParamsList = recentParamsList[idx:] + + if height < validatorUpdateHeight { + height = validatorUpdateHeight + } + height++ + return height, replayRecentParamsList +} + +func getProtoMsgItemFromQuote(msg *oracletypes.MsgPriceFeed) *oracletypes.MsgItem { + // address has been valid before + validator, _ := oracletypes.ConsAddrStrFromCreator(msg.Creator) + + return &oracletypes.MsgItem{ + FeederID: msg.FeederID, + // validator's consAddr + Validator: validator, + PSources: msg.Prices, + } +} diff --git a/x/oracle/keeper/feedermanagement/feedermanager_test.go b/x/oracle/keeper/feedermanagement/feedermanager_test.go new file mode 100644 index 000000000..32eb416a1 --- /dev/null +++ b/x/oracle/keeper/feedermanagement/feedermanager_test.go @@ -0,0 +1,101 @@ +package feedermanagement + +import ( + "math/big" + "testing" + + oracletypes "github.com/ExocoreNetwork/exocore/x/oracle/types" + gomock "go.uber.org/mock/gomock" + + . "github.com/smartystreets/goconvey/convey" +) + +//go:generate mockgen -destination mock_cachereader_test.go -package feedermanagement github.com/ExocoreNetwork/exocore/x/oracle/keeper/feedermanagement CacheReader + +func TestFeederManagement(t *testing.T) { + Convey("compare FeederManager", t, func() { + fm := NewFeederManager(nil) + ctrl := gomock.NewController(t) + c := NewMockCacheReader(ctrl) + c.EXPECT(). + GetThreshold(). + Return(&threshold{big.NewInt(4), big.NewInt(1), big.NewInt(3)}). + AnyTimes() + Convey("add a new round", func() { + ps1 := priceSource{deterministic: true, prices: []*PriceInfo{{Price: "123"}}} + ps2 := ps1 + fm2 := *fm + + fm.rounds[1] = newRound(1, oracletypes.DefaultParams().TokenFeeders[1], 3, c, defaultAggMedian) + fm.rounds[1].PrepareForNextBlock(20) + fm.sortedFeederIDs.add(1) + fm.rounds[1].a.ds.AddPriceSource(&ps1, big.NewInt(1), "v1") + + fm2.rounds = make(map[int64]*round) + fm2.sortedFeederIDs = make([]int64, 0) + fm2.rounds[1] = newRound(1, oracletypes.DefaultParams().TokenFeeders[1], 3, c, defaultAggMedian) + fm2.rounds[1].PrepareForNextBlock(20) + fm2.sortedFeederIDs.add(1) + fm2.rounds[1].a.ds.AddPriceSource(&ps2, big.NewInt(1), "v1") + + So(fm.Equals(&fm2), ShouldBeTrue) + }) + }) + Convey("check copy results", t, func() { + ctrl := gomock.NewController(t) + c := NewMockCacheReader(ctrl) + c.EXPECT(). + GetThreshold(). + Return(&threshold{big.NewInt(4), big.NewInt(1), big.NewInt(3)}). + AnyTimes() + + // feedermanager + Convey("copy of feedermanager", func() { + f := tData.NewFeederManager(c) + f.updateCheckTx() + fc := f.fCheckTx + f.fCheckTx = nil + So(f.Equals(fc), ShouldBeTrue) + }) + Convey("copy of round", func() { + r := tData.NewRound(c) + rc := r.CopyForCheckTx() + So(r.Equals(rc), ShouldBeTrue) + }) + Convey("copy of aggregagtor", func() { + a := tData.NewAggregator(true) + ac := a.CopyForCheckTx() + So(a.Equals(ac), ShouldBeTrue) + }) + Convey("copy of recordsValidators", func() { + v := tData.NewRecordsValidators(true) + vc := v.Cpy() + So(v.Equals(vc), ShouldBeTrue) + }) + Convey("copy of recordsDSs", func() { + dss := tData.NewRecordsDSs(true) + dssc := dss.Cpy() + So(dss.Equals(dssc), ShouldBeTrue) + }) + Convey("copy of recordsDS", func() { + ds := tData.NewRecordsDS(true) + dsc := ds.Cpy() + So(ds.Equals(dsc), ShouldBeTrue) + }) + Convey("copy of priceValidator", func() { + pv := tData.NewPriceValidator(true) + pvc := pv.Cpy() + So(pv.Equals(pvc), ShouldBeTrue) + }) + Convey("copy of priceSource", func() { + ps := tData.NewPriceSource(true, true) + psc := ps.Cpy() + So(ps.Equals(psc), ShouldBeTrue) + }) + Convey("copy of pricePower", func() { + pw := tData.NewPricePower() + pwc := pw.Cpy() + So(pw.Equals(pwc), ShouldBeTrue) + }) + }) +} diff --git a/x/oracle/keeper/feedermanagement/helper_test.go b/x/oracle/keeper/feedermanagement/helper_test.go new file mode 100644 index 000000000..a52b89ec9 --- /dev/null +++ b/x/oracle/keeper/feedermanagement/helper_test.go @@ -0,0 +1,116 @@ +package feedermanagement + +import ( + "math/big" + "math/rand" + + oracletypes "github.com/ExocoreNetwork/exocore/x/oracle/types" +) + +type Test struct { +} + +var ( + tData *Test + params = oracletypes.DefaultParams() + r = rand.New(rand.NewSource(1)) + timestamp = "2025-01-01 00:01:02" + decimal = int32(8) + big1 = big.NewInt(1) + big2 = big.NewInt(2) + big3 = big.NewInt(3) + big4 = big.NewInt(4) + th = &threshold{big4, big2, big3} +) + +func (t *Test) NewFeederManager(cs CacheReader) *FeederManager { + f := NewFeederManager(nil) + round := t.NewRound(cs) + f.rounds[round.feederID] = round + // prepare this Round + round.PrepareForNextBlock(int64(params.TokenFeeders[int(round.feederID)].StartBaseBlock)) + return f +} + +func (t *Test) NewPricePower() *PricePower { + return &PricePower{ + Price: t.NewPriceInfo("999", "1"), + Power: big1, + Validators: map[string]struct{}{"validator1": {}}, + } +} + +func (t *Test) NewPriceSource(deterministic bool, filled bool) *priceSource { + ret := newPriceSource(oracletypes.SourceChainlinkID, deterministic) + if filled { + price := t.NewPriceInfo("999", "1") + ret.prices = append(ret.prices, price) + } + return ret +} + +func (t *Test) NewPriceValidator(filled bool) *priceValidator { + ret := newPriceValidator("validator1", big1) + if filled { + ps := t.NewPriceSource(true, true) + ret.priceSources[oracletypes.SourceChainlinkID] = ps + } + return ret +} + +func (t *Test) NewRecordsDS(filled bool) *recordsDS { + ret := newRecordsDS() + if filled { + ret.validators["validtors"] = struct{}{} + ret.accumulatedPowers = big1 + ret.records = append(ret.records, t.NewPricePower()) + } + return ret +} + +func (t *Test) NewRecordsDSs(filled bool) *recordsDSs { + ret := newRecordsDSs(th) + if filled { + rds := t.NewRecordsDS(filled) + ret.dsMap[oracletypes.SourceChainlinkID] = rds + } + return nil +} + +func (t *Test) NewRecordsValidators(filled bool) *recordsValidators { + ret := newRecordsValidators() + if filled { + ret.accumulatedPower = big1 + ret.records["validtor1"] = t.NewPriceValidator(filled) + } + return nil +} + +func (t *Test) NewAggregator(filled bool) *aggregator { + ret := newAggregator(th, defaultAggMedian) + if filled { + ret.v = t.NewRecordsValidators(filled) + ret.ds = t.NewRecordsDSs(filled) + } + return ret +} + +func (t *Test) NewRound(cs CacheReader) *round { + feederID := r.Intn(len(params.TokenFeeders)-1) + 1 + round := newRound(int64(feederID), params.TokenFeeders[feederID], int64(params.MaxNonce), cs, defaultAggMedian) + return round +} + +func (t *Test) NewRoundWithFeederID(cs CacheReader, feederID int64) *round { + round := newRound(feederID, params.TokenFeeders[feederID], int64(params.MaxNonce), cs, defaultAggMedian) + return round +} + +func (f *Test) NewPriceInfo(price string, detID string) *PriceInfo { + return &PriceInfo{ + Price: price, + Decimal: decimal, + DetID: detID, + Timestamp: timestamp, + } +} diff --git a/x/oracle/keeper/feedermanagement/mock_cachereader_test.go b/x/oracle/keeper/feedermanagement/mock_cachereader_test.go new file mode 100644 index 000000000..0aeebca0b --- /dev/null +++ b/x/oracle/keeper/feedermanagement/mock_cachereader_test.go @@ -0,0 +1,127 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: github.com/ExocoreNetwork/exocore/x/oracle/keeper/feedermanagement (interfaces: CacheReader) +// +// Generated by this command: +// +// mockgen -destination mock_cachereader_test.go -package feedermanagement github.com/ExocoreNetwork/exocore/x/oracle/keeper/feedermanagement CacheReader +// + +// Package feedermanagement is a generated GoMock package. +package feedermanagement + +import ( + big "math/big" + reflect "reflect" + + gomock "go.uber.org/mock/gomock" +) + +// MockCacheReader is a mock of CacheReader interface. +type MockCacheReader struct { + ctrl *gomock.Controller + recorder *MockCacheReaderMockRecorder + isgomock struct{} +} + +// MockCacheReaderMockRecorder is the mock recorder for MockCacheReader. +type MockCacheReaderMockRecorder struct { + mock *MockCacheReader +} + +// NewMockCacheReader creates a new mock instance. +func NewMockCacheReader(ctrl *gomock.Controller) *MockCacheReader { + mock := &MockCacheReader{ctrl: ctrl} + mock.recorder = &MockCacheReaderMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockCacheReader) EXPECT() *MockCacheReaderMockRecorder { + return m.recorder +} + +// GetPowerForValidator mocks base method. +func (m *MockCacheReader) GetPowerForValidator(validator string) (*big.Int, bool) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetPowerForValidator", validator) + ret0, _ := ret[0].(*big.Int) + ret1, _ := ret[1].(bool) + return ret0, ret1 +} + +// GetPowerForValidator indicates an expected call of GetPowerForValidator. +func (mr *MockCacheReaderMockRecorder) GetPowerForValidator(validator any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPowerForValidator", reflect.TypeOf((*MockCacheReader)(nil).GetPowerForValidator), validator) +} + +// GetThreshold mocks base method. +func (m *MockCacheReader) GetThreshold() *threshold { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetThreshold") + ret0, _ := ret[0].(*threshold) + return ret0 +} + +// GetThreshold indicates an expected call of GetThreshold. +func (mr *MockCacheReaderMockRecorder) GetThreshold() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetThreshold", reflect.TypeOf((*MockCacheReader)(nil).GetThreshold)) +} + +// GetTotalPower mocks base method. +func (m *MockCacheReader) GetTotalPower() *big.Int { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetTotalPower") + ret0, _ := ret[0].(*big.Int) + return ret0 +} + +// GetTotalPower indicates an expected call of GetTotalPower. +func (mr *MockCacheReaderMockRecorder) GetTotalPower() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetTotalPower", reflect.TypeOf((*MockCacheReader)(nil).GetTotalPower)) +} + +// GetValidators mocks base method. +func (m *MockCacheReader) GetValidators() []string { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetValidators") + ret0, _ := ret[0].([]string) + return ret0 +} + +// GetValidators indicates an expected call of GetValidators. +func (mr *MockCacheReaderMockRecorder) GetValidators() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetValidators", reflect.TypeOf((*MockCacheReader)(nil).GetValidators)) +} + +// IsDeterministic mocks base method. +func (m *MockCacheReader) IsDeterministic(sournceID int64) (bool, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "IsDeterministic", sournceID) + ret0, _ := ret[0].(bool) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// IsDeterministic indicates an expected call of IsDeterministic. +func (mr *MockCacheReaderMockRecorder) IsDeterministic(sournceID any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsDeterministic", reflect.TypeOf((*MockCacheReader)(nil).IsDeterministic), sournceID) +} + +// IsRuleV1 mocks base method. +func (m *MockCacheReader) IsRuleV1(feederID int64) bool { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "IsRuleV1", feederID) + ret0, _ := ret[0].(bool) + return ret0 +} + +// IsRuleV1 indicates an expected call of IsRuleV1. +func (mr *MockCacheReaderMockRecorder) IsRuleV1(feederID any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsRuleV1", reflect.TypeOf((*MockCacheReader)(nil).IsRuleV1), feederID) +} diff --git a/x/oracle/keeper/feedermanagement/prices.go b/x/oracle/keeper/feedermanagement/prices.go new file mode 100644 index 000000000..6a3fd11f7 --- /dev/null +++ b/x/oracle/keeper/feedermanagement/prices.go @@ -0,0 +1,389 @@ +package feedermanagement + +import ( + "errors" + "fmt" + "math/big" + "reflect" + "slices" + "sort" + + oracletypes "github.com/ExocoreNetwork/exocore/x/oracle/types" +) + +func GetPriceInfoFromProtoPriceTimeDetID(p *oracletypes.PriceTimeDetID) *PriceInfo { + if p == nil { + return nil + } + return &PriceInfo{ + Price: p.Price, + Decimal: p.Decimal, + Timestamp: p.Timestamp, + DetID: p.DetID, + } +} + +func (p *PriceInfo) ProtoPriceTimeDetID() *oracletypes.PriceTimeDetID { + if p == nil { + return nil + } + return &oracletypes.PriceTimeDetID{ + Price: p.Price, + Decimal: p.Decimal, + Timestamp: p.Timestamp, + DetID: p.DetID, + } +} + +func (p *PriceInfo) EqualDS(p2 *PriceInfo) bool { + if p == nil || p2 == nil { + return p == p2 + } + return p.Price == p2.Price && p.DetID == p2.DetID && p.Decimal == p2.Decimal +} + +func (p *PriceInfo) PriceResult() *PriceResult { + if p == nil { + return nil + } + return &PriceResult{ + Price: p.Price, + Decimal: p.Decimal, + DetID: p.DetID, + Timestamp: p.Timestamp, + } +} + +func (p *PriceResult) PriceInfo() *PriceInfo { + if p == nil { + return nil + } + return &PriceInfo{ + Price: p.Price, + Decimal: p.Decimal, + DetID: p.DetID, + Timestamp: p.Timestamp, + } +} + +func (p *PriceResult) ProtoPriceTimeRound(roundID int64, timestamp string) *oracletypes.PriceTimeRound { + return &oracletypes.PriceTimeRound{ + Price: p.Price, + Decimal: p.Decimal, + Timestamp: timestamp, + // #nosec G115 + RoundID: uint64(roundID), + } +} + +func getPriceSourceFromProto(ps *oracletypes.PriceSource, checker sourceChecker) (*priceSource, error) { + prices := make([]*PriceInfo, 0, len(ps.Prices)) + // #nosec G115 + deterministic, err := checker.IsDeterministic(int64(ps.SourceID)) + if err != nil { + return nil, err + } + for _, p := range ps.Prices { + prices = append(prices, GetPriceInfoFromProtoPriceTimeDetID(p)) + } + return &priceSource{ + // #nosec G115 + deterministic: deterministic, + // #nosec G115 + sourceID: int64(ps.SourceID), + prices: prices, + }, nil +} + +func newPriceValidator(validator string, power *big.Int) *priceValidator { + return &priceValidator{ + finalPrice: nil, + validator: validator, + power: new(big.Int).Set(power), + priceSources: make(map[int64]*priceSource), + } +} + +func (pv *priceValidator) Cpy() *priceValidator { + if pv == nil { + return nil + } + var finalPrice *PriceResult + if pv.finalPrice != nil { + tmp := *pv.finalPrice + finalPrice = &tmp + } + priceSources := make(map[int64]*priceSource) + // safe to range map, map copy + for id, ps := range pv.priceSources { + priceSources[id] = ps.Cpy() + } + return &priceValidator{ + finalPrice: finalPrice, + validator: pv.validator, + power: new(big.Int).Set(pv.power), + priceSources: priceSources, + } +} + +func (pv *priceValidator) Equals(pv2 *priceValidator) bool { + if pv == nil || pv2 == nil { + return pv == pv2 + } + if pv.validator != pv2.validator || pv.power.Cmp(pv2.power) != 0 { + return false + } + if len(pv.priceSources) != len(pv2.priceSources) { + return false + } + // safe to range map, map compare + for id, ps := range pv.priceSources { + ps2, ok := pv2.priceSources[id] + if !ok || !ps.Equals(ps2) { + return false + } + } + return true +} + +func (pv *priceValidator) GetPSCopy(sourceID int64, deterministic bool) *priceSource { + if ps, ok := pv.priceSources[sourceID]; ok { + return ps.Cpy() + } + return newPriceSource(sourceID, deterministic) +} + +func (pv *priceValidator) TryAddPriceSources(pSs []*priceSource) (updated map[int64]*priceSource, added []*priceSource, err error) { + var es errorStr + updated = make(map[int64]*priceSource) + for _, psNew := range pSs { + ps, ok := updated[psNew.sourceID] + if !ok { + ps, ok = pv.priceSources[psNew.sourceID] + if !ok { + ps = newPriceSource(psNew.sourceID, psNew.deterministic) + } else { + ps = ps.Cpy() + } + } + psAdded, err := ps.Add(psNew) + if err != nil { + es.add(fmt.Sprintf("sourceID:%d, error:%s", psNew.sourceID, err.Error())) + } else { + updated[psNew.sourceID] = ps + added = append(added, psAdded) + } + } + if len(updated) > 0 { + return updated, added, nil + } + return nil, nil, fmt.Errorf("failed to add priceSource listi, error:%s", es) +} + +func (pv *priceValidator) ApplyAddedPriceSources(psMap map[int64]*priceSource) { + // safe to range map, set all k-v to antoher map + for id, ps := range psMap { + pv.priceSources[id] = ps + } +} + +// TODO: V2: check valdiator has provided all sources required by rules(defined in oracle.params) +func (pv *priceValidator) GetFinalPrice(algo AggAlgorithm) (*PriceResult, bool) { + if pv.finalPrice != nil { + return pv.finalPrice, true + } + if len(pv.priceSources) == 0 { + return nil, false + } + keySlice := make([]int64, 0, len(pv.priceSources)) + // safe to range map, the map is iteration to genrate sorted key slice + for sourceID := range pv.priceSources { + keySlice = append(keySlice, sourceID) + } + slices.Sort(keySlice) + algo.Reset() + for _, sourceID := range keySlice { + price := pv.priceSources[sourceID] + if price.finalPrice == nil { + algo.Reset() + return nil, false + } + if !algo.Add(price.finalPrice) { + algo.Reset() + return nil, false + } + } + if ret := algo.GetResult(); ret != nil { + pv.finalPrice = ret + return ret, true + } + return nil, false +} + +func (pv *priceValidator) UpdateFinalPriceForDS(sourceID int64, finalPrice *PriceResult) bool { + if finalPrice == nil { + return false + } + if price, ok := pv.priceSources[sourceID]; ok { + price.finalPrice = finalPrice + return true + } + return false +} + +func newPriceSource(sourceID int64, deterministic bool) *priceSource { + return &priceSource{ + deterministic: deterministic, + finalPrice: nil, + sourceID: sourceID, + detIDs: make(map[string]struct{}), + prices: make([]*PriceInfo, 0), + } +} + +func (ps *priceSource) Equals(ps2 *priceSource) bool { + if ps == nil || ps2 == nil { + return ps == ps2 + } + if ps.sourceID != ps2.sourceID || ps.deterministic != ps2.deterministic { + return false + } + if !reflect.DeepEqual(ps.detIDs, ps2.detIDs) { + return false + } + if !reflect.DeepEqual(ps.finalPrice, ps2.finalPrice) { + return false + } + if len(ps.prices) != len(ps2.prices) { + return false + } + if !reflect.DeepEqual(ps.prices, ps2.prices) { + return false + } + return true +} + +func (ps *priceSource) Cpy() *priceSource { + if ps == nil { + return nil + } + var finalPrice *PriceResult + if ps.finalPrice != nil { + tmp := *ps.finalPrice + finalPrice = &tmp + } + // deterministic, sourceID + detIDs := make(map[string]struct{}) + // safe to range map, map copy + for detID := range ps.detIDs { + detIDs[detID] = struct{}{} + } + prices := make([]*PriceInfo, 0, len(ps.prices)) + for _, p := range ps.prices { + pCpy := *p + prices = append(prices, &pCpy) + } + return &priceSource{ + deterministic: ps.deterministic, + finalPrice: finalPrice, + sourceID: ps.sourceID, + detIDs: detIDs, + prices: prices, + } +} + +// Add adds prices of a source from priceSource +// we don't verify the input is DS or NS, it's just handled under the rule restrict by p.deterministic +func (ps *priceSource) Add(psNew *priceSource) (*priceSource, error) { + if ps.sourceID != psNew.sourceID { + return nil, fmt.Errorf("failed to add priceSource, sourceID mismatch, expected:%d, got:%d", ps.sourceID, psNew.sourceID) + } + + if !ps.deterministic { + if len(psNew.prices) == 0 { + return nil, errors.New("failed to add ProtoPriceSource for NS, psNew.prices is empty") + } + // this is not ds, then just set the final price or overwrite if the input has a later timestamp + if ps.finalPrice == nil { + ps.finalPrice = psNew.prices[0].PriceResult() + ps.prices = append(ps.prices, psNew.prices[0]) + psNew.prices = psNew.prices[:1] + return psNew, nil + } + // equivalent to After, just overwrite the old value + if psNew.prices[0].Timestamp > ps.finalPrice.Timestamp { + ps.finalPrice = psNew.prices[0].PriceResult() + ps.prices = append(ps.prices, psNew.prices[0]) + psNew.prices = psNew.prices[:1] + return ps, nil + } + return nil, errors.New("failed to add ProtoPriceSource for NS, timestamp is old") + } + + var es errorStr + added := false + ret := &priceSource{ + deterministic: ps.deterministic, + sourceID: ps.sourceID, + prices: make([]*PriceInfo, 0), + } + for _, pNew := range psNew.prices { + if _, ok := ps.detIDs[pNew.DetID]; ok { + es.add(fmt.Sprintf("duplicated DetID:%s", pNew.DetID)) + continue + } + added = true + ps.detIDs[pNew.DetID] = struct{}{} + ps.prices = append(ps.prices, pNew) + ret.prices = append(ret.prices, pNew) + } + + if !added { + return nil, fmt.Errorf("failed to add ProtoPriceSource, sourceID:%d, errors:%s", ps.sourceID, es) + } + + sort.SliceStable(ps.prices, func(i, j int) bool { + return ps.prices[i].DetID < ps.prices[j].DetID + }) + return ret, nil +} + +func (p *PricePower) Equals(p2 *PricePower) bool { + if p == nil || p2 == nil { + return p == p2 + } + if !reflect.DeepEqual(p.Price, p2.Price) || p.Power.Cmp(p2.Power) != 0 { + return false + } + if len(p.Validators) != len(p2.Validators) { + return false + } + // safe to range map, map compare + for v := range p.Validators { + if _, ok := p2.Validators[v]; !ok { + return false + } + } + return true +} + +func (p *PricePower) Cpy() *PricePower { + price := *p.Price + validators := make(map[string]struct{}) + // safe to range map, map copy + for v := range p.Validators { + validators[v] = struct{}{} + } + return &PricePower{ + Price: &price, + Power: new(big.Int).Set(p.Power), + Validators: validators, + } +} + +type errorStr string + +func (e *errorStr) add(s string) { + es := string(*e) + *e = errorStr(fmt.Sprintf("%s[%s]", es, s)) +} diff --git a/x/oracle/keeper/feedermanagement/round.go b/x/oracle/keeper/feedermanagement/round.go new file mode 100644 index 000000000..05f98a577 --- /dev/null +++ b/x/oracle/keeper/feedermanagement/round.go @@ -0,0 +1,263 @@ +package feedermanagement + +import ( + "fmt" + + oracletypes "github.com/ExocoreNetwork/exocore/x/oracle/types" +) + +func newRound(feederID int64, tokenFeeder *oracletypes.TokenFeeder, quoteWindowSize int64, cache CacheReader, algo AggAlgorithm) *round { + return &round{ + // #nosec G115 + startBaseBlock: int64(tokenFeeder.StartBaseBlock), + // #nosec G115 + startRoundID: int64(tokenFeeder.StartRoundID), + // #nosec G115 + endBlock: int64(tokenFeeder.EndBlock), + // #nosec G115 + interval: int64(tokenFeeder.Interval), + quoteWindowSize: quoteWindowSize, + feederID: feederID, + // #nosec G115 + tokenID: int64(tokenFeeder.TokenID), + cache: cache, + + // default value + status: roundStatusClosed, + a: nil, + roundBaseBlock: 0, + roundID: 0, + algo: algo, + } +} + +func (r *round) Equals(r2 *round) bool { + if r == nil || r2 == nil { + return r == r2 + } + + if r.startBaseBlock != r2.startBaseBlock || + r.startRoundID != r2.startRoundID || + r.endBlock != r2.endBlock || + r.interval != r2.interval || + r.quoteWindowSize != r2.quoteWindowSize || + r.feederID != r2.feederID || + r.tokenID != r2.tokenID || + r.roundBaseBlock != r2.roundBaseBlock || + r.roundID != r2.roundID || + r.status != r2.status { + return false + } + if !r.a.Equals(r2.a) { + return false + } + + return true +} + +func (r *round) CopyForCheckTx() *round { + // flags has been taken care of + ret := *r + // cache does not need to be copied since it's a readonly interface, + // and there's no race condition since abci requests are not executing concurrntly + ret.a = ret.a.CopyForCheckTx() + return &ret +} + +func (r *round) getMsgItemFromProto(msg *oracletypes.MsgItem) (*MsgItem, error) { + power, found := r.cache.GetPowerForValidator(msg.Validator) + if !found { + return nil, fmt.Errorf("failed to get power for validator:%s", msg.Validator) + } + priceSources := make([]*priceSource, 0, len(msg.PSources)) + for _, ps := range msg.PSources { + psNew, err := getPriceSourceFromProto(ps, r.cache) + if err != nil { + return nil, err + } + priceSources = append(priceSources, psNew) + } + return &MsgItem{ + // #nosec G115 + FeederID: int64(msg.FeederID), + Validator: msg.Validator, + Power: power, + PriceSources: priceSources, + }, nil +} + +func (r *round) ValidQuotingBaseBlock(height int64) bool { + return r.IsQuotingWindowOpen() && r.roundBaseBlock == height +} + +// Tally process information to get the final price +// it does not verify if the msg is for the corresponding round(roundid/roundBaseBlock) +// TODO: use valid value instead of the original protoMsg in return +func (r *round) Tally(protoMsg *oracletypes.MsgItem) (*PriceResult, *oracletypes.MsgItem, error) { + if !r.IsQuotingWindowOpen() { + return nil, nil, fmt.Errorf("quoting window is not open, feederID:%d", r.feederID) + } + + msg, err := r.getMsgItemFromProto(protoMsg) + if err != nil { + return nil, nil, fmt.Errorf("failed to get msgItem from proto, error:%w", err) + } + if !r.IsQuoting() { + // record msg for 'handlQuotingMisBehavior' + err := r.a.RecordMsg(msg) + if err == nil { + return nil, protoMsg, oracletypes.ErrQuoteRecorded + } + return nil, nil, fmt.Errorf("failed to record quote for aggregated round, error:%w", err) + } + + err = r.a.AddMsg(msg) + if err != nil { + return nil, nil, fmt.Errorf("failed to add quote for aggregation of feederID:%d, roundID:%d, error:%w", r.feederID, r.roundID, err) + } + + finalPrice, ok := r.FinalPrice() + if ok { + r.status = roundStatusCommittable + // NOTE: for V1, we need return the DetID as well since chainlink is the only source + if r.cache.IsRuleV1(r.feederID) { + detID := r.getFinalDetIDForSourceID(oracletypes.SourceChainlinkID) + finalPrice.DetID = detID + } + return finalPrice, protoMsg, nil + } + + return nil, protoMsg, nil +} + +func (r *round) UpdateParams(tokenFeeder *oracletypes.TokenFeeder, quoteWindowSize int64) { + // #nosec G115 + r.startBaseBlock = int64(tokenFeeder.StartBaseBlock) + // #nosec G115 + r.endBlock = int64(tokenFeeder.EndBlock) + // #nosec G115 + r.interval = int64(tokenFeeder.Interval) + r.quoteWindowSize = quoteWindowSize +} + +// PrepareForNextBlock sets status to Open and create a new aggregator on the block before the first block of quoting +func (r *round) PrepareForNextBlock(currentHeight int64) (open bool) { + if currentHeight < r.roundBaseBlock && r.IsQuoting() { + r.closeQuotingWindow() + return open + } + // currentHeight euqls to baseBlock + if currentHeight == r.roundBaseBlock && !r.IsQuoting() { + r.openQuotingWindow() + open = true + return open + } + baseBlock, roundID, delta, expired := r.getPosition(currentHeight) + + if expired && r.IsQuoting() { + r.closeQuotingWindow() + return open + } + // open a new round + if baseBlock > r.roundBaseBlock { + // move to next round + r.roundBaseBlock = baseBlock + r.roundID = roundID + // the first block in the quoting window + if delta == 0 && !r.IsQuoting() { + r.openQuotingWindow() + open = true + } + } + return open +} + +func (r *round) openQuotingWindow() { + r.status = roundStatusOpen + r.a = newAggregator(r.cache.GetThreshold(), r.algo) +} + +// IsQuotingWindowOpen returns if the round is inside its current quoting window including status of {open, committable, close} +func (r *round) IsQuotingWindowOpen() bool { + // aggregator is set when quoting window open and removed when the window reaches the end or be force sealed + return r.a != nil +} + +func (r *round) IsQuotingWindowEnd(currentHeight int64) bool { + _, _, delta, _ := r.getPosition(currentHeight) + return delta == r.quoteWindowSize +} + +func (r *round) IsQuoting() bool { + return r.status == roundStatusOpen +} + +func (r *round) FinalPrice() (*PriceResult, bool) { + if r.a == nil { + return nil, false + } + return r.a.GetFinalPrice() +} + +// Close sets round status to roundStatusClosed and remove current aggregator +func (r *round) closeQuotingWindow() { + r.status = roundStatusClosed + r.a = nil +} + +func (r *round) PerformanceReview(validator string) (miss, malicious bool) { + finalPrice, ok := r.FinalPrice() + if !ok { + return + } + if !r.cache.IsRuleV1(r.feederID) { + // only rulev1 is supported for now + return + } + miss = true + detID := r.getFinalDetIDForSourceID(oracletypes.SourceChainlinkID) + price := finalPrice.PriceInfo() + price.DetID = detID + prices, ok := r.a.v.GetValidatorQuotePricesForSourceID(validator, oracletypes.SourceChainlinkID) + if !ok { + return + } + for _, p := range prices { + if p.EqualDS(price) { + miss = false + } else if p.DetID == price.DetID { + miss = false + malicious = true + } + } + return +} + +//nolint:unparam +func (r *round) getFinalDetIDForSourceID(sourceID int64) string { + return r.a.ds.GetFinalDetIDForSourceID(sourceID) +} + +func (r *round) Committable() bool { + return r.status == roundStatusCommittable +} + +func (r *round) getPosition(currentHeight int64) (baseBlock, roundID, delta int64, expired bool) { + // endBlock is included + if r.endBlock > 0 && currentHeight > r.endBlock { + expired = true + return + } + if currentHeight < r.startBaseBlock { + return + } + delta = currentHeight - r.startBaseBlock + if r.interval == 0 { + return + } + rounds := delta / r.interval + roundID = r.startRoundID + rounds + delta -= rounds * r.interval + baseBlock = currentHeight - delta + return +} diff --git a/x/oracle/keeper/feedermanagement/types.go b/x/oracle/keeper/feedermanagement/types.go new file mode 100644 index 000000000..ef1e919e7 --- /dev/null +++ b/x/oracle/keeper/feedermanagement/types.go @@ -0,0 +1,284 @@ +package feedermanagement + +import ( + "math/big" + "sort" + + "github.com/ExocoreNetwork/exocore/x/oracle/keeper/common" + oracletypes "github.com/ExocoreNetwork/exocore/x/oracle/types" + sdk "github.com/cosmos/cosmos-sdk/types" +) + +type Submitter interface { + SetValidatorUpdateForCache(sdk.Context, oracletypes.ValidatorUpdateBlock) + SetParamsForCache(sdk.Context, oracletypes.RecentParams) + SetMsgItemsForCache(sdk.Context, oracletypes.RecentMsg) +} + +type CacheReader interface { + GetPowerForValidator(validator string) (*big.Int, bool) + GetTotalPower() (totalPower *big.Int) + GetValidators() []string + IsRuleV1(feederID int64) bool + IsDeterministic(sourceID int64) (bool, error) + GetThreshold() *threshold +} + +// used to track validator change +type cacheValidator struct { + validators map[string]*big.Int + update bool +} + +// used to track params change +type cacheParams struct { + params *oracletypes.Params + update bool +} + +type cacheMsgs []*oracletypes.MsgItem + +type caches struct { + k Submitter + + msg *cacheMsgs + validators *cacheValidator + params *cacheParams +} + +type MsgItem struct { + FeederID int64 + Validator string + Power *big.Int + PriceSources []*priceSource +} + +// PriceInfo is the price information including price, decimal, time, and detID +// this is defined as a internal type as alias of oracletypes.PriceTimeDetID +type PriceInfo oracletypes.PriceTimeDetID + +// PricePower wraps PriceInfo with power and validators +// Validators indicates the validators who provided the price +// Power indicates the accumulated power of all validators who provided the price +type PricePower struct { + Price *PriceInfo + Power *big.Int + Validators map[string]struct{} +} + +// PriceResult is the final price information including price, decimal, time, and detID +// this is defined as a internal type as alias of PriceInfo +type PriceResult PriceInfo + +// PriceSource describes a specific source of related prices +// deteministic indicates whether the source is deterministic +// finalPrice indicates the final price of the source aggregated from all prices +// sourceID indicates the source ID +// detIDs indicates the detIDs of all prices, detID means the unique identifier of a price defined in the deterministic itself +// prices indicates all prices of the source, it's defined in slice instead of map to keep the order +type priceSource struct { + deterministic bool + finalPrice *PriceResult + sourceID int64 + detIDs map[string]struct{} + // ordered by detID + prices []*PriceInfo +} + +// priceValiadtor describes a specific validator of related priceSources(each source could has multiple prices) +// finalPrice indicates the final price of the validator aggregated from all prices of each source +// validator indicates the validator address +// power indicates the power of the validator +// priceSources indicates all priceSources of the validator, the map key is sourceID +type priceValidator struct { + finalPrice *PriceResult + validator string + power *big.Int + // each source will get a single final price independently, the order of sources does not matter, map is safe + priceSources map[int64]*priceSource +} + +// recordsValidators is the price records for all validators, the record item is priceValidator +// finalPrice indicates the final price of all validators aggregated from all prices of each validator +// finalPrices indicates the final price of each validator aggregated from all of its prices of each source +// accumulatedPower indicates the accumulated power of all validators +// records indicates all priceValidators, the map key is validator address +type recordsValidators struct { + finalPrice *PriceResult + finalPrices map[string]*PriceResult + // TODO: V2: accumulatedValidPower only includes validators who providing all sources required by rules(defined in oracle.Params) + // accumulatedValidVpower: map[string]*big.Int + accumulatedPower *big.Int + // each validator will get a single final price independently, the order of validators does not matter, map is safe + records map[string]*priceValidator +} + +// recordsDS is the price records of a deterministic source +// finalPrice indicates the final price of a specific detID chosen out of all prices with different detIDs which pass the threshold +// finalDetID indicates the final detID chosen out of all detIDs +// accumulatedPowers indicates the accumulated power of all validators who provided the price for this source +// valiators indicates all validators who provided the price for this source +// records indicates all PricePower of this source provided by different validators, the slice is ordered by detID +type recordsDS struct { + finalPrice *PriceResult + finalDetID string + accumulatedPowers *big.Int + validators map[string]struct{} + // ordered by detID + records []*PricePower +} + +// each source will get a final price independently, the order of sources does not matter, map is safe +// recordsDSs is the price records for all deterministic sources +// threshold indicates the threshold definition to detemin final price for each source +type recordsDSs struct { + t *threshold + dsMap map[int64]*recordsDS +} + +// threshold is defined as (thresholdA * thresholdB) / totalPower +// when do compare with power, it should be (thresholdB * power) > (thresholdA * totalPower) to avoid decimal calculation +type threshold struct { + totalPower *big.Int + thresholdA *big.Int + thresholdB *big.Int +} + +func (t *threshold) Equals(t2 *threshold) bool { + if t == nil || t2 == nil { + return t == t2 + } + return t.totalPower.Cmp(t2.totalPower) == 0 && t.thresholdA.Cmp(t2.thresholdA) == 0 && t.thresholdB.Cmp(t2.thresholdB) == 0 +} + +func (t *threshold) Cpy() *threshold { + return &threshold{ + totalPower: new(big.Int).Set(t.totalPower), + thresholdA: new(big.Int).Set(t.thresholdA), + thresholdB: new(big.Int).Set(t.thresholdB), + } +} + +func (t *threshold) Exceeds(power *big.Int) bool { + return new(big.Int).Mul(t.thresholdB, power).Cmp(new(big.Int).Mul(t.thresholdA, t.totalPower)) > 0 +} + +// aggregator is the price aggregator for a specific round +// t is the threshold definition for price consensus for the round +// finalPrice indicates the final price of the round +// v is the price records for all validators with prices they provided +// ds is the price records for all deterministic sources which could have prices with multiple detIDs provided by validators +// algo is the aggregation algorithm for the round, currently we use 'Median' as default +type aggregator struct { + t *threshold + finalPrice *PriceResult + v *recordsValidators + ds *recordsDSs + algo AggAlgorithm +} + +// roundStatus indicates the status of a round +type roundStatus int32 + +const ( + // define closed as default value 0 + // close: the round is closed for price submission and no valid price to commit + roundStatusClosed roundStatus = iota + // open: the round is open for price submission + roundStatusOpen + // committable: the round is closed for price submission and available for price to commit + roundStatusCommittable +) + +// round is the price round for a specific tokenFeeder corresponding to the price feed progress of a specific token +type round struct { + // startBaseBlock is the start block height of corresponding tokenFeeder + startBaseBlock int64 + // startRoundID is the round ID of corresponding tokenFeeder + startRoundID int64 + // endBlock is the end block height of the corresponding tokenFeeder + endBlock int64 + // interval is the interval of the corresponding tokenFeeder + interval int64 + // quoteWindowSize is the quote window size of the corresponding tokenFeeder + quoteWindowSize int64 + + // feederID is the feeder ID of the corresponding tokenFeeder + feederID int64 + // tokenID is the token ID of the corresponding tokenFeeder + tokenID int64 + + // roundBaseBlock is the round base block of current round + roundBaseBlock int64 + // roundID is the round ID of current round + roundID int64 + // status indicates the status of current round + status roundStatus + // aggregator is the price aggregator for current round + a *aggregator + // cache is the cache reader for current round to provid params, validators information + cache CacheReader + // algo is the aggregation algorithm for current round to get final price + algo AggAlgorithm +} + +type orderedSliceInt64 []int64 + +func (osi orderedSliceInt64) Equals(o2 orderedSliceInt64) bool { + if len(osi) == 0 || len(o2) == 0 { + return len(osi) == len(o2) + } + + for idx, v := range osi { + if v != (o2)[idx] { + return false + } + } + return true +} + +func (osi *orderedSliceInt64) add(i int64) { + result := append(*osi, i) + sort.Slice(result, func(i, j int) bool { + return result[i] < result[j] + }) + *osi = result +} + +func (osi *orderedSliceInt64) remove(i int64) { + for idx, v := range *osi { + if v == i { + *osi = append((*osi)[:idx], (*osi)[idx+1:]...) + return + } + } +} + +func (osi *orderedSliceInt64) sort() { + sort.Slice(*osi, func(i, j int) bool { + return (*osi)[i] < (*osi)[j] + }) +} + +// FeederManager is the manager for the price feed progress of all token feeders +type FeederManager struct { + // fCheckTx is a copy of FeederManager used for mode of checkTx to simulate transactions + fCheckTx *FeederManager + // k is the oracle keeper + k common.KeeperOracle + // sortedFeederIDs is the ordered feeder IDs corresponding to all the rounds included in FeederManager + sortedFeederIDs orderedSliceInt64 + // rounds is the map of all rounds included in FeederManager, the key is the feeder ID + rounds map[int64]*round + cs *caches + // paramsUpdated indicates whether the params are updated in current block + paramsUpdated bool + // validatorsUpdated indicates whether the validators are updated in current block + validatorsUpdated bool + // forceSeal indicates whether it's satisfied to force seal all the rounds + // when the validators are updated in current block or some related params are updated, this will be set to true. + forceSeal bool + // restSlashing indicates whether it's satisfied to reset slashing + // when the slashing params is changed in current block, this will be set to true. + resetSlashing bool +} diff --git a/x/oracle/keeper/keeper.go b/x/oracle/keeper/keeper.go index 6738b8b37..97ca41238 100644 --- a/x/oracle/keeper/keeper.go +++ b/x/oracle/keeper/keeper.go @@ -11,20 +11,12 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" paramtypes "github.com/cosmos/cosmos-sdk/x/params/types" - "github.com/ExocoreNetwork/exocore/x/oracle/keeper/aggregator" - "github.com/ExocoreNetwork/exocore/x/oracle/keeper/cache" "github.com/ExocoreNetwork/exocore/x/oracle/keeper/common" + "github.com/ExocoreNetwork/exocore/x/oracle/keeper/feedermanagement" "github.com/ExocoreNetwork/exocore/x/oracle/types" ) type ( - memoryStore struct { - cs *cache.Cache - agc *aggregator.AggregatorContext - agcCheckTx *aggregator.AggregatorContext - updatedFeederIDs []string - } - Keeper struct { cdc codec.BinaryCodec storeKey storetypes.StoreKey @@ -36,7 +28,8 @@ type ( assetsKeeper types.AssetsKeeper types.SlashingKeeper // wrap all four memory cache into one pointer to track them among cpoies of Keeper (msgServer, module) - memStore *memoryStore + // TODO: remove this + *feedermanagement.FeederManager } ) @@ -62,7 +55,7 @@ func NewKeeper( ps = ps.WithKeyTable(types.ParamKeyTable()) } - return Keeper{ + ret := Keeper{ cdc: cdc, storeKey: storeKey, memKey: memKey, @@ -72,8 +65,11 @@ func NewKeeper( assetsKeeper: assetsKeeper, authority: authority, SlashingKeeper: slashingKeeper, - memStore: new(memoryStore), + // fm: feedermanagement.NewFeederManager(nil), + FeederManager: feedermanagement.NewFeederManager(nil), } + ret.SetKeeper(ret) + return ret } func (k Keeper) Logger(ctx sdk.Context) log.Logger { diff --git a/x/oracle/keeper/keeper_suite_test.go b/x/oracle/keeper/keeper_suite_test.go index 3bc01c6b0..10023e65e 100644 --- a/x/oracle/keeper/keeper_suite_test.go +++ b/x/oracle/keeper/keeper_suite_test.go @@ -67,7 +67,6 @@ func TestKeeper(t *testing.T) { suite.Run(t, ks) - resetSingle(ks.App.OracleKeeper) RegisterFailHandler(Fail) RunSpecs(t, "Keeper Suite") } @@ -76,10 +75,10 @@ func (suite *KeeperSuite) Reset() { p4Test := types.DefaultParams() p4Test.TokenFeeders[1].StartBaseBlock = 1 suite.k.SetParams(suite.ctx, p4Test) + suite.k.FeederManager.SetNilCaches() + suite.k.FeederManager.BeginBlock(suite.ctx) suite.ctx = suite.ctx.WithBlockHeight(12) - suite.ctrl = gomock.NewController(suite.t) - resetSingle(suite.App.OracleKeeper) } func (suite *KeeperSuite) SetupTest() { @@ -101,7 +100,6 @@ func (suite *KeeperSuite) SetupTest() { validators := suite.ValSet.Validators suite.valAddr1, _ = sdk.ValAddressFromBech32(sdk.ValAddress(validators[0].Address).String()) suite.valAddr2, _ = sdk.ValAddressFromBech32(sdk.ValAddress(validators[1].Address).String()) - resetSingle(suite.App.OracleKeeper) suite.k = suite.App.OracleKeeper suite.ms = keeper.NewMsgServerImpl(suite.App.OracleKeeper) @@ -111,9 +109,5 @@ func (suite *KeeperSuite) SetupTest() { p4Test.TokenFeeders[1].StartBaseBlock = 1 suite.k.SetParams(suite.ctx, p4Test) suite.ctx = suite.ctx.WithBlockHeight(12) -} - -func resetSingle(k keeper.Keeper) { - k.ResetAggregatorContext() - k.ResetCache() + suite.k.FeederManager.BeginBlock(suite.ctx) } diff --git a/x/oracle/keeper/msg_server_create_price.go b/x/oracle/keeper/msg_server_create_price.go index a2c903abb..63ee68f19 100644 --- a/x/oracle/keeper/msg_server_create_price.go +++ b/x/oracle/keeper/msg_server_create_price.go @@ -2,21 +2,25 @@ package keeper import ( "context" - "errors" + "crypto/sha256" + "encoding/base64" "strconv" + "strings" "time" + sdkerrors "cosmossdk.io/errors" "github.com/ExocoreNetwork/exocore/x/oracle/types" sdk "github.com/cosmos/cosmos-sdk/types" ) const ( layout = "2006-01-02 15:04:05" - maxFutureOffset = 5 * time.Second + maxFutureOffset = 30 * time.Second + maxPriceLength = 32 ) -// CreatePrice proposes price for new round of specific tokenFeeder -func (ms msgServer) CreatePrice(goCtx context.Context, msg *types.MsgCreatePrice) (*types.MsgCreatePriceResponse, error) { +// PriceFeed proposes price for new round of specific tokenFeeder +func (ms msgServer) PriceFeed(goCtx context.Context, msg *types.MsgPriceFeed) (*types.MsgPriceFeedResponse, error) { ctx := sdk.UnwrapSDKContext(goCtx) gasMeter := ctx.GasMeter() @@ -24,78 +28,69 @@ func (ms msgServer) CreatePrice(goCtx context.Context, msg *types.MsgCreatePrice defer func() { ctx = ctx.WithGasMeter(gasMeter) }() - logger := ms.Keeper.Logger(ctx) + + logger := ms.Logger(ctx) + + validator, _ := types.ConsAddrStrFromCreator(msg.Creator) + logQuote := []interface{}{"feederID", msg.FeederID, "baseBlock", msg.BasedBlock, "proposer", validator, "msg-nonce", msg.Nonce, "height", ctx.BlockHeight()} + if err := checkTimestamp(ctx, msg); err != nil { - logger.Info("price proposal timestamp check failed", "error", err, "height", ctx.BlockHeight()) + logger.Error("quote has invalid timestamp", append(logQuote, "error", err)...) return nil, types.ErrPriceProposalFormatInvalid.Wrap(err.Error()) } - agc := ms.Keeper.GetAggregatorContext(ctx) - newItem, caches, err := agc.NewCreatePrice(ctx, msg) + if err := ms.ValidateMsg(msg); err != nil { + logger.Error("failed to validate msg", append(logQuote, "error", err)...) + return nil, err + } + // core logic and functionality of Price Aggregation + finalPrice, err := ms.ProcessQuote(ctx, msg, ctx.IsCheckTx()) if err != nil { - logger.Info("price proposal failed", "error", err, "height", ctx.BlockHeight(), "feederID", msg.FeederID) + if sdkerrors.IsOf(err, types.ErrQuoteRecorded) { + // quote is recorded only, this happens when a quoting-window is not availalbe before that window end due to final price aggregated successfully in advance + // we will still record this msg if it's valid + logger.Info("recorded quote for oracle-behavior evaluation", append(logQuote, "msg", msg)...) + return &types.MsgPriceFeedResponse{}, nil + } + logger.Error("failed to process quote", append(logQuote, "error", err)...) return nil, err } - logger.Info("add price proposal for aggregation", "feederID", msg.FeederID, "basedBlock", msg.BasedBlock, "proposer", msg.Creator, "height", ctx.BlockHeight()) - + logger.Info("added quote for aggregation", append(logQuote, "msg", msg)...) + // TODO: use another type ctx.EventManager().EmitEvent(sdk.NewEvent( - types.EventTypeCreatePrice, + types.EventTypePriceFeed, sdk.NewAttribute(types.AttributeKeyFeederID, strconv.FormatUint(msg.FeederID, 10)), sdk.NewAttribute(types.AttributeKeyBasedBlock, strconv.FormatUint(msg.BasedBlock, 10)), - sdk.NewAttribute(types.AttributeKeyProposer, msg.Creator), - ), - ) + sdk.NewAttribute(types.AttributeKeyProposer, validator), + )) - if caches == nil { - return &types.MsgCreatePriceResponse{}, nil - } - if newItem != nil { - if success := ms.AppendPriceTR(ctx, newItem.TokenID, newItem.PriceTR); !success { - // This case should not exist, keep this line to avoid consensus fail if this happens - prevPrice, nextRoundID := ms.GrowRoundID(ctx, newItem.TokenID) - logger.Error("append new price round fail for mismatch roundID, and will just grow roundID with previous price", "roundID from finalPrice", newItem.PriceTR.RoundID, "expect nextRoundID", nextRoundID, "prevPrice", prevPrice) - } else { - logger.Info("final price aggregation done", "feederID", msg.FeederID, "roundID", newItem.PriceTR.RoundID, "price", newItem.PriceTR.Price) + if finalPrice != nil { + logger.Info("final price successfully aggregated", "price", finalPrice, "feederID", msg.FeederID, "height", ctx.BlockHeight()) + decimalStr := strconv.FormatInt(int64(finalPrice.Decimal), 10) + // #nosec G115 + tokenID, _ := ms.GetTokenIDForFeederID(int64(msg.FeederID)) + tokenIDStr := strconv.FormatInt(tokenID, 10) + roundIDStr := strconv.FormatUint(finalPrice.RoundID, 10) + priceStr := finalPrice.Price + + // if price is too long, hash it + // this is to prevent the price from being too long and causing the event to be too long + // price is also used for 'nst' to describe the balance change, and it will be at least 32 bytes at that case + if len(priceStr) >= maxPriceLength { + hash := sha256.New() + hash.Write([]byte(priceStr)) + priceStr = base64.StdEncoding.EncodeToString(hash.Sum(nil)) } - decimalStr := strconv.FormatInt(int64(newItem.PriceTR.Decimal), 10) - tokenIDStr := strconv.FormatUint(newItem.TokenID, 10) - roundIDStr := strconv.FormatUint(newItem.PriceTR.RoundID, 10) + // emit event to tell price is updated for current round of corresponding feederID ctx.EventManager().EmitEvent(sdk.NewEvent( - types.EventTypeCreatePrice, + types.EventTypePriceFeed, sdk.NewAttribute(types.AttributeKeyRoundID, roundIDStr), - sdk.NewAttribute(types.AttributeKeyFinalPrice, tokenIDStr+"_"+roundIDStr+"_"+newItem.PriceTR.Price+"_"+decimalStr), + sdk.NewAttribute(types.AttributeKeyFinalPrice, strings.Join([]string{tokenIDStr, roundIDStr, priceStr, decimalStr}, "_")), sdk.NewAttribute(types.AttributeKeyPriceUpdated, types.AttributeValuePriceUpdatedSuccess)), ) - if !ctx.IsCheckTx() { - ms.Keeper.GetCaches().RemoveCache(caches) - ms.Keeper.AppendUpdatedFeederIDs(msg.FeederID) - } - } else if !ctx.IsCheckTx() { - ms.Keeper.GetCaches().AddCache(caches) } - return &types.MsgCreatePriceResponse{}, nil -} - -func checkTimestamp(goCtx context.Context, msg *types.MsgCreatePrice) error { - ctx := sdk.UnwrapSDKContext(goCtx) - now := ctx.BlockTime().UTC() - for _, ps := range msg.Prices { - for _, price := range ps.Prices { - ts := price.Timestamp - if len(ts) == 0 { - return errors.New("timestamp should not be empty") - } - t, err := time.ParseInLocation(layout, ts, time.UTC) - if err != nil { - return errors.New("timestamp format invalid") - } - if now.Add(maxFutureOffset).Before(t) { - return errors.New("timestamp is in the future") - } - } - } - return nil + return &types.MsgPriceFeedResponse{}, nil } diff --git a/x/oracle/keeper/msg_server_create_price_test.go b/x/oracle/keeper/msg_server_create_price_test.go deleted file mode 100644 index 3b5b3121f..000000000 --- a/x/oracle/keeper/msg_server_create_price_test.go +++ /dev/null @@ -1,124 +0,0 @@ -package keeper_test - -import ( - reflect "reflect" - - math "cosmossdk.io/math" - dogfoodkeeper "github.com/ExocoreNetwork/exocore/x/dogfood/keeper" - dogfoodtypes "github.com/ExocoreNetwork/exocore/x/dogfood/types" - "github.com/ExocoreNetwork/exocore/x/oracle/keeper/cache" - "github.com/ExocoreNetwork/exocore/x/oracle/keeper/testdata" - "github.com/ExocoreNetwork/exocore/x/oracle/types" - . "github.com/agiledragon/gomonkey/v2" - sdk "github.com/cosmos/cosmos-sdk/types" - - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" -) - -//go:generate mockgen -destination mock_validator_test.go -package keeper_test github.com/cosmos/cosmos-sdk/x/staking/types ValidatorI - -var _ = Describe("MsgCreatePrice", func() { - var c *cache.Cache - var p *Patches - BeforeEach(func() { - ks.Reset() - Expect(ks.ms).ToNot(BeNil()) - - // TODO: remove monkey patch for test - p = ApplyMethod(reflect.TypeOf(dogfoodkeeper.Keeper{}), "GetLastTotalPower", func(k dogfoodkeeper.Keeper, ctx sdk.Context) math.Int { return math.NewInt(3) }) - p.ApplyMethod(reflect.TypeOf(dogfoodkeeper.Keeper{}), "GetAllExocoreValidators", func(k dogfoodkeeper.Keeper, ctx sdk.Context) []dogfoodtypes.ExocoreValidator { - return []dogfoodtypes.ExocoreValidator{ - { - Address: ks.mockValAddr1, - Power: 1, - }, - { - Address: ks.mockValAddr2, - Power: 1, - }, - { - Address: ks.mockValAddr3, - Power: 1, - }, - } - }) - - Expect(ks.ctx.BlockHeight()).To(Equal(int64(12))) - }) - - AfterEach(func() { - ks.ctrl.Finish() - if p != nil { - p.Reset() - } - }) - - Context("3 validators with 1 voting power each", func() { - BeforeEach(func() { - ks.ms.CreatePrice(ks.ctx, &types.MsgCreatePrice{ - Creator: ks.mockConsAddr1.String(), - FeederID: 1, - Prices: testdata.PS1, - BasedBlock: 11, - Nonce: 1, - }) - - c = ks.App.OracleKeeper.GetCaches() - // c = ks.ms.Keeper.GetCaches() - var pRes cache.ItemP - c.GetCache(&pRes) - p4Test := types.DefaultParams() - p4Test.TokenFeeders[1].StartBaseBlock = 1 - Expect(pRes).Should(BeEquivalentTo(p4Test)) - }) - - It("success on 3rd message", func() { - iRes := make([]*cache.ItemM, 0) - c.GetCache(&iRes) - Expect(iRes[0].Validator).Should(Equal(ks.mockConsAddr1.String())) - - ks.ms.CreatePrice(ks.ctx, &types.MsgCreatePrice{ - Creator: ks.mockConsAddr2.String(), - FeederID: 1, - Prices: testdata.PS2, - BasedBlock: 11, - Nonce: 1, - }, - ) - ks.ms.CreatePrice(ks.ctx, &types.MsgCreatePrice{}) - c.GetCache(&iRes) - Expect(len(iRes)).Should(Equal(2)) - - ks.ms.CreatePrice(ks.ctx, &types.MsgCreatePrice{ - Creator: ks.mockConsAddr3.String(), - FeederID: 1, - Prices: testdata.PS4, - BasedBlock: 11, - Nonce: 1, - }, - ) - c.GetCache(&iRes) - Expect(len(iRes)).Should(Equal(0)) - prices := ks.k.GetAllPrices(sdk.UnwrapSDKContext(ks.ctx)) - Expect(prices[0]).Should(BeEquivalentTo(types.Prices{ - TokenID: 1, - NextRoundID: 3, - PriceList: []*types.PriceTimeRound{ - { - Price: "1", - Decimal: 0, - Timestamp: "", - RoundID: 1, - }, - { - Price: testdata.PTD2.Price, - Decimal: testdata.PTD2.Decimal, - Timestamp: prices[0].PriceList[1].Timestamp, - RoundID: 2, - }, - }, - })) - }) - }) -}) diff --git a/x/oracle/keeper/msg_server_test.go b/x/oracle/keeper/msg_server_test.go index d089e4b63..b3bc5e769 100644 --- a/x/oracle/keeper/msg_server_test.go +++ b/x/oracle/keeper/msg_server_test.go @@ -26,18 +26,18 @@ func TestMsgServer(t *testing.T) { } // TODO: re-enable these tests once fixed -// func (suite *KeeperSuite) TestCreatePriceSingleBlock() { +// func (suite *KeeperSuite) TestPriceFeedSingleBlock() { // router := suite.App.MsgServiceRouter() -// oServer := router.Handler(&types.MsgCreatePrice{}) +// oServer := router.Handler(&types.MsgPriceFeed{}) // require.EqualValues(suite.T(), 2, suite.Ctx.BlockHeight()) -// oServer(suite.Ctx, &types.MsgCreatePrice{ +// oServer(suite.Ctx, &types.MsgPriceFeed{ // Creator: suite.valAddr1.String(), // Nonce: 1, // FeederID: 1, // Prices: testdata.PS1, // BasedBlock: 1, // }) -// oServer(suite.Ctx, &types.MsgCreatePrice{ +// oServer(suite.Ctx, &types.MsgPriceFeed{ // Creator: suite.valAddr2.String(), // Nonce: 1, // FeederID: 1, @@ -67,7 +67,7 @@ func TestMsgServer(t *testing.T) { // // run the endblock to seal and prepare for next block // suite.NextBlock() // require.EqualValues(suite.T(), 3, suite.Ctx.BlockHeight()) -// _, err := oServer(suite.Ctx, &types.MsgCreatePrice{ +// _, err := oServer(suite.Ctx, &types.MsgPriceFeed{ // Creator: suite.valAddr1.String(), // Nonce: 1, // FeederID: 1, @@ -80,10 +80,10 @@ func TestMsgServer(t *testing.T) { // suite.Equal(log, err.Error()) // } -// func (suite *KeeperSuite) TestCreatePriceTwoBlock() { +// func (suite *KeeperSuite) TestPriceFeedTwoBlock() { // router := suite.App.MsgServiceRouter() -// oServer := router.Handler(&types.MsgCreatePrice{}) -// res, _ := oServer(suite.Ctx, &types.MsgCreatePrice{ +// oServer := router.Handler(&types.MsgPriceFeed{}) +// res, _ := oServer(suite.Ctx, &types.MsgPriceFeed{ // Creator: suite.valAddr1.String(), // Nonce: 1, // FeederID: 1, @@ -98,7 +98,7 @@ func TestMsgServer(t *testing.T) { // if suite.Equal(false, found) { // // run the endblock to seal and prepare for next block // suite.NextBlock() -// oServer(suite.Ctx, &types.MsgCreatePrice{ +// oServer(suite.Ctx, &types.MsgPriceFeed{ // Creator: suite.valAddr2.String(), // Nonce: 1, // FeederID: 1, diff --git a/x/oracle/keeper/msg_server_update_params.go b/x/oracle/keeper/msg_server_update_params.go index 87f35f2c4..3a8051303 100644 --- a/x/oracle/keeper/msg_server_update_params.go +++ b/x/oracle/keeper/msg_server_update_params.go @@ -4,7 +4,6 @@ import ( "context" utils "github.com/ExocoreNetwork/exocore/utils" - "github.com/ExocoreNetwork/exocore/x/oracle/keeper/cache" "github.com/ExocoreNetwork/exocore/x/oracle/types" sdk "github.com/cosmos/cosmos-sdk/types" govtypes "github.com/cosmos/cosmos-sdk/x/gov/types" @@ -66,7 +65,8 @@ func (ms msgServer) UpdateParams(goCtx context.Context, msg *types.MsgUpdatePara } // set updated new params ms.SetParams(ctx, p) - _ = ms.Keeper.GetAggregatorContext(ctx) - ms.Keeper.GetCaches().AddCache(cache.ItemP(p)) + if !ctx.IsCheckTx() { + ms.SetParamsUpdated() + } return &types.MsgUpdateParamsResponse{}, nil } diff --git a/x/oracle/keeper/native_token.go b/x/oracle/keeper/native_token.go index e67b9beb0..7c7033b61 100644 --- a/x/oracle/keeper/native_token.go +++ b/x/oracle/keeper/native_token.go @@ -3,6 +3,7 @@ package keeper import ( "errors" "fmt" + "strconv" "strings" sdkmath "cosmossdk.io/math" @@ -23,10 +24,15 @@ import ( type NSTAssetID string const ( - NSTETHAssetAddr = "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee" + // NSTETHAssetAddr = "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee" // TODO: we currently support NSTETH only which has capped effective balance for one validator // TODO: this is a bad practice, and for Lz, they have different version of endpoint with different chainID // Do the validation before invoke oracle related functions instead of check these hard code ids here. + ETHMainnetChainID = "0x7595" + ETHLocalnetChainID = "0x65" + ETHHoleskyChainID = "0x9d19" + ETHSepoliaChainID = "0x9ce1" + NSTETHAssetIDMainnet NSTAssetID = "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee_0x7595" NSTETHAssetIDLocalnet NSTAssetID = "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee_0x65" NSTETHAssetIDHolesky NSTAssetID = "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee_0x9d19" @@ -102,7 +108,9 @@ func (k Keeper) GetAllStakerInfosAssets(ctx sdk.Context) (ret []types.StakerInfo for ; iterator.Valid(); iterator.Next() { assetID, _ := types.ParseNativeTokenStakerKey(iterator.Key()) if l == 0 || ret[l-1].AssetId != assetID { + version := k.GetNSTVersion(ctx, assetID) ret = append(ret, types.StakerInfosAssets{ + NstVersion: version, AssetId: assetID, StakerInfos: make([]*types.StakerInfo, 0), }) @@ -147,9 +155,11 @@ func (k Keeper) GetAllStakerListAssets(ctx sdk.Context) (ret []types.StakerListA for ; iterator.Valid(); iterator.Next() { v := &types.StakerList{} k.cdc.MustUnmarshal(iterator.Value(), v) + version := k.GetNSTVersion(ctx, string(iterator.Key())) ret = append(ret, types.StakerListAssets{ AssetId: string(iterator.Key()), StakerList: v, + NstVersion: version, }) } return ret @@ -166,7 +176,7 @@ func (k Keeper) UpdateNSTValidatorListForStaker(ctx sdk.Context, assetID, staker amountInt64 := amount.Quo(decimalInt).Int64() // emit an event to tell that a staker's validator list has changed ctx.EventManager().EmitEvent(sdk.NewEvent( - types.EventTypeCreatePrice, + types.EventTypePriceFeed, sdk.NewAttribute(types.AttributeKeyNativeTokenUpdate, types.AttributeValueNativeTokenUpdate), )) store := ctx.KVStore(k.storeKey) @@ -190,6 +200,7 @@ func (k Keeper) UpdateNSTValidatorListForStaker(ctx sdk.Context, assetID, staker newBalance = *(stakerInfo.BalanceList[latestIndex]) newBalance.Index++ } + // #nosec G115 newBalance.Block = uint64(ctx.BlockHeight()) if amountInt64 > 0 { newBalance.Change = types.Action_ACTION_DEPOSIT @@ -247,27 +258,35 @@ func (k Keeper) UpdateNSTValidatorListForStaker(ctx sdk.Context, assetID, staker store.Set(key, bz) } + // valid veriosn start from 1 + version := k.IncreaseNSTVersion(ctx, assetID) // we use index to sync with client about status of stakerInfo.ValidatorPubkeyList - eventValue := fmt.Sprintf("%d_%s_%d", stakerInfo.StakerIndex, validatorPubkey, newBalance.Index) + eventValue := fmt.Sprintf("%d_%s_%d", stakerInfo.StakerIndex, validatorPubkey, version) if newBalance.Change == types.Action_ACTION_DEPOSIT { eventValue = fmt.Sprintf("%s_%s", types.AttributeValueNativeTokenDeposit, eventValue) } else { eventValue = fmt.Sprintf("%s_%s", types.AttributeValueNativeTokenWithdraw, eventValue) } - // emit an event to tell a new valdiator added/or a validator is removed for the staker + // emit an event to tell the details that a new valdiator added/or a validator is removed for the staker + // deposit_stakerID_validatorKey ctx.EventManager().EmitEvent(sdk.NewEvent( - types.EventTypeCreatePrice, + types.EventTypePriceFeed, sdk.NewAttribute(types.AttributeKeyNativeTokenChange, eventValue), )) + return nil } // UpdateNSTByBalanceChange updates balance info for staker under native-restaking asset of assetID when its balance changed by slash/refund on the source chain (beacon chain for eth) -func (k Keeper) UpdateNSTByBalanceChange(ctx sdk.Context, assetID string, rawData []byte, roundID uint64) error { +func (k Keeper) UpdateNSTByBalanceChange(ctx sdk.Context, assetID string, price types.PriceTimeRound, version int64) error { if !IsLimitedChangeNST(assetID) { return types.ErrNSTAssetNotSupported } + if version != k.GetNSTVersion(ctx, assetID) { + return errors.New("version not match") + } _, chainID, _ := assetstypes.ParseID(assetID) + rawData := []byte(price.Price) if len(rawData) < 32 { return errors.New("length of indicate maps for stakers should be exactly 32 bytes") } @@ -295,13 +314,10 @@ func (k Keeper) UpdateNSTByBalanceChange(ctx sdk.Context, assetID string, rawDat newBalance = *(stakerInfo.BalanceList[length-1]) } newBalance.Block = uint64(ctx.BlockHeight()) - if newBalance.RoundID == roundID { - newBalance.Index++ - } else { - newBalance.RoundID = roundID - newBalance.Index = 0 - } + // we set index as a global reference used through all rounds + newBalance.Index++ newBalance.Change = types.Action_ACTION_SLASH_REFUND + newBalance.RoundID = price.RoundID // balance update are based on initial/max effective balance: 32 maxBalance := maxEffectiveBalance(assetID) * (len(stakerInfo.ValidatorPubkeyList)) balance := maxBalance + change @@ -333,6 +349,47 @@ func (k Keeper) UpdateNSTByBalanceChange(ctx sdk.Context, assetID string, rawDat return nil } +// IncreaseNSTVersion increases the version of native token for assetID +func (k Keeper) IncreaseNSTVersion(ctx sdk.Context, assetID string) int64 { + store := ctx.KVStore(k.storeKey) + key := types.NativeTokenVersionKey(assetID) + value := store.Get(key) + if value == nil { + // set the first index of version to 1 + store.Set(key, k.cdc.MustMarshal(&types.NSTVersion{AssetId: assetID, Version: 1})) + return 1 + } + var nstVersion types.NSTVersion + k.cdc.MustUnmarshal(value, &nstVersion) + nstVersion.Version++ + store.Set(key, k.cdc.MustMarshal(&nstVersion)) + return nstVersion.Version +} + +// IncreaseNSTVersion increases the version of native token for assetID +func (k Keeper) SetNSTVersion(ctx sdk.Context, assetID string, version int64) int64 { + store := ctx.KVStore(k.storeKey) + key := types.NativeTokenVersionKey(assetID) + nstVersion := &types.NSTVersion{ + AssetId: assetID, + Version: version, + } + store.Set(key, k.cdc.MustMarshal(nstVersion)) + return nstVersion.Version +} + +func (k Keeper) GetNSTVersion(ctx sdk.Context, assetID string) int64 { + store := ctx.KVStore(k.storeKey) + key := types.NativeTokenVersionKey(assetID) + value := store.Get(key) + if value == nil { + return 0 + } + var nstVersion types.NSTVersion + k.cdc.MustUnmarshal(value, &nstVersion) + return nstVersion.Version +} + func (k Keeper) getDecimal(ctx sdk.Context, assetID string) (int, sdkmath.Int, error) { decimalMap, err := k.assetsKeeper.GetAssetsDecimal(ctx, map[string]interface{}{assetID: nil}) if err != nil { @@ -342,78 +399,6 @@ func (k Keeper) getDecimal(ctx sdk.Context, assetID string) (int, sdkmath.Int, e return int(decimal), sdkmath.NewIntWithDecimal(1, int(decimal)), nil } -// TODO: This conversion has limited length for balance change, it suites for beaconchain currently, If we extend to other changes, this method need to be upgrade -// for value that might be too big leading too long length of the change value, many related changes need to be done since the message size might be too big then -// parseBalanceChange parses rawData to details of amount change for all stakers relative to native restaking -func parseBalanceChangeCapped(rawData []byte, sl types.StakerList) (map[string]int, error) { - // eg. 0100-000011 - // first part 0100 tells that the effective-balance of staker corresponding to index 2 in StakerList - // the left part 000011. we use the first 4 bits to tell the length of this number, and it shows as 1 here, the 5th bit is used to tell symbol of the number, 1 means negative, then we can get the abs number indicate by the length. It's -1 here, means effective-balane is 32-1 on beacon chain for now - // the first 32 bytes are information to indicates effective-balance of which staker has changed, 1 means changed, 0 means not. 32 bytes can represents changes for at most 256 stakers - indexes := rawData[:32] - // bytes after first 32 are details of effective-balance change for each staker which has been marked with 1 in the first 32 bytes, for those who are marked with 0 will just be ignored - // For each staker we support at most 256 validators to join, so the biggest effective-balance change we would have is 256*32, then we need 13 bits to represents the number for each staker. And for compression we use 4 bits to tell the length of bits without leading 0 this number has. - // Then with the symbol we need at most 18 bits for each staker's effective-balance change: 0000.0.0000-0000-0000 (the leading 0 will be ignored for the last 13 bits) - changes := rawData[32:] - index := -1 - byteIndex := 0 - bitOffset := 0 - lengthBits := 5 - stakerChanges := make(map[string]int) - for _, b := range indexes { - for i := 7; i >= 0; i-- { - index++ - if (b>>i)&1 == 1 { - lenValue := changes[byteIndex] << bitOffset - bitsLeft := 8 - bitOffset - lenValue >>= (8 - lengthBits) - if bitsLeft < lengthBits { - byteIndex++ - lenValue |= changes[byteIndex] >> (8 - lengthBits + bitsLeft) - bitOffset = lengthBits - bitsLeft - } else { - if bitOffset += lengthBits; bitOffset == 8 { - bitOffset = 0 - } - if bitsLeft == lengthBits { - byteIndex++ - } - } - - symbol := lenValue & 1 - lenValue >>= 1 - if lenValue <= 0 { - // the range of length we accept is 1-15(the max we will use is actually 13) - return stakerChanges, errors.New("length of change value must be at least 1 bit") - } - - bitsExtracted := 0 - stakerChange := 0 - for bitsExtracted < int(lenValue) { - bitsLeft := 8 - bitOffset - byteValue := changes[byteIndex] << bitOffset - if (int(lenValue) - bitsExtracted) < bitsLeft { - bitsLeft = int(lenValue) - bitsExtracted - bitOffset += bitsLeft - } else { - byteIndex++ - bitOffset = 0 - } - byteValue >>= (8 - bitsLeft) - stakerChange = (stakerChange << bitsLeft) | int(byteValue) - bitsExtracted += bitsLeft - } - stakerChange++ - if symbol == 1 { - stakerChange *= -1 - } - stakerChanges[sl.StakerAddrs[index]] = stakerChange - } - } - } - return stakerChanges, nil -} - // TODO use []byte and assetstypes.GetStakerIDAndAssetID for stakerAddr representation func getStakerID(stakerAddr string, chainID uint64) string { return strings.Join([]string{strings.ToLower(stakerAddr), hexutil.EncodeUint64(chainID)}, utils.DelimiterForID) @@ -427,3 +412,15 @@ func IsLimitedChangeNST(assetID string) bool { func maxEffectiveBalance(assetID string) int { return maxEffectiveBalances[NSTAssetID(assetID)] } + +func getNSTVersionFromDetID(detID string) (int64, error) { + parsedDetID := strings.Split(detID, "_") + if len(parsedDetID) != 2 { + return 0, fmt.Errorf("invalid detID for nst, should be in format of detID_version, got:%s", detID) + } + nstVersion, err := strconv.ParseInt(parsedDetID[1], 10, 64) + if err != nil { + return 0, fmt.Errorf("failed to parse version from:%s, error:%w", parsedDetID[1], err) + } + return nstVersion, nil +} diff --git a/x/oracle/keeper/native_token_parser.go b/x/oracle/keeper/native_token_parser.go new file mode 100644 index 000000000..96f3d9404 --- /dev/null +++ b/x/oracle/keeper/native_token_parser.go @@ -0,0 +1,79 @@ +package keeper + +import ( + "errors" + + "github.com/ExocoreNetwork/exocore/x/oracle/types" +) + +// TODO: This conversion has limited length for balance change, it suites for beaconchain currently, If we extend to other changes, this method need to be upgrade +// for value that might be too big leading too long length of the change value, many related changes need to be done since the message size might be too big then +// parseBalanceChange parses rawData to details of amount change for all stakers relative to native restaking +func parseBalanceChangeCapped(rawData []byte, sl types.StakerList) (map[string]int, error) { + // eg. 0100-000011 + // first part 0100 tells that the effective-balance of staker corresponding to index 2 in StakerList + // the left part 000011. we use the first 4 bits to tell the length of this number, and it shows as 1 here, the 5th bit is used to tell symbol of the number, 1 means negative, then we can get the abs number indicate by the length. It's -1 here, means effective-balane is 32-1 on beacon chain for now + // the first 32 bytes are information to indicates effective-balance of which staker has changed, 1 means changed, 0 means not. 32 bytes can represents changes for at most 256 stakers + indexes := rawData[:32] + // bytes after first 32 are details of effective-balance change for each staker which has been marked with 1 in the first 32 bytes, for those who are marked with 0 will just be ignored + // For each staker we support at most 256 validators to join, so the biggest effective-balance change we would have is 256*32, then we need 13 bits to represents the number for each staker. And for compression we use 4 bits to tell the length of bits without leading 0 this number has. + // Then with the symbol we need at most 18 bits for each staker's effective-balance change: 0000.0.0000-0000-0000 (the leading 0 will be ignored for the last 13 bits) + changes := rawData[32:] + index := -1 + byteIndex := 0 + bitOffset := 0 + lengthBits := 5 + stakerChanges := make(map[string]int) + for _, b := range indexes { + for i := 7; i >= 0; i-- { + index++ + if (b>>i)&1 == 1 { + lenValue := changes[byteIndex] << bitOffset + bitsLeft := 8 - bitOffset + lenValue >>= (8 - lengthBits) + if bitsLeft < lengthBits { + byteIndex++ + lenValue |= changes[byteIndex] >> (8 - lengthBits + bitsLeft) + bitOffset = lengthBits - bitsLeft + } else { + if bitOffset += lengthBits; bitOffset == 8 { + bitOffset = 0 + } + if bitsLeft == lengthBits { + byteIndex++ + } + } + + symbol := lenValue & 1 + lenValue >>= 1 + if lenValue <= 0 { + // the range of length we accept is 1-15(the max we will use is actually 13) + return stakerChanges, errors.New("length of change value must be at least 1 bit") + } + + bitsExtracted := 0 + stakerChange := 0 + for bitsExtracted < int(lenValue) { + bitsLeft := 8 - bitOffset + byteValue := changes[byteIndex] << bitOffset + if (int(lenValue) - bitsExtracted) < bitsLeft { + bitsLeft = int(lenValue) - bitsExtracted + bitOffset += bitsLeft + } else { + byteIndex++ + bitOffset = 0 + } + byteValue >>= (8 - bitsLeft) + stakerChange = (stakerChange << bitsLeft) | int(byteValue) + bitsExtracted += bitsLeft + } + stakerChange++ + if symbol == 1 { + stakerChange *= -1 + } + stakerChanges[sl.StakerAddrs[index]] = stakerChange + } + } + } + return stakerChanges, nil +} diff --git a/x/oracle/keeper/native_token_test.go b/x/oracle/keeper/native_token_test.go index ec02bb94c..cb3492eee 100644 --- a/x/oracle/keeper/native_token_test.go +++ b/x/oracle/keeper/native_token_test.go @@ -78,11 +78,13 @@ func (ks *KeeperSuite) TestNSTLifeCycleOneStaker() { {0, -10}, } rawData := convertBalanceChangeToBytes(stakerChanges) - ks.App.OracleKeeper.UpdateNSTByBalanceChange(ks.Ctx, assetID, rawData, 9) + // ks.App.OracleKeeper.UpdateNSTByBalanceChange(ks.Ctx, assetID, rawData, 9) + ks.App.OracleKeeper.UpdateNSTByBalanceChange(ks.Ctx, assetID, types.PriceTimeRound{Price: string(rawData), RoundID: 9}, 1) // - 2.1 check stakerInfo stakerInfo = ks.App.OracleKeeper.GetStakerInfo(ks.Ctx, assetID, stakerStr) ks.Equal(types.BalanceInfo{ Block: 1, + Index: 1, RoundID: 9, Change: types.Action_ACTION_SLASH_REFUND, // this is expected to be 32-10=22, not 100-10 @@ -121,7 +123,7 @@ func (ks *KeeperSuite) TestNSTLifeCycleOneStaker() { ks.Equal(types.BalanceInfo{ Block: 1, RoundID: 9, - Index: 1, + Index: 2, Change: types.Action_ACTION_DEPOSIT, Balance: 54, }, *stakerInfo.BalanceList[2]) @@ -134,14 +136,14 @@ func (ks *KeeperSuite) TestNSTLifeCycleOneStaker() { {0, -5}, } rawData = convertBalanceChangeToBytes(stakerChanges) - ks.App.OracleKeeper.UpdateNSTByBalanceChange(ks.Ctx, assetID, rawData, 11) + ks.App.OracleKeeper.UpdateNSTByBalanceChange(ks.Ctx, assetID, types.PriceTimeRound{Price: string(rawData), RoundID: 11}, 2) // - 4.1 check stakerInfo stakerInfo = ks.App.OracleKeeper.GetStakerInfo(ks.Ctx, assetID, stakerStr) ks.Equal(types.BalanceInfo{ Balance: 59, Block: 1, RoundID: 11, - Index: 0, + Index: 3, Change: types.Action_ACTION_SLASH_REFUND, }, *stakerInfo.BalanceList[3]) // check stakerAssetInfo is updated correctly in assets module, this should be triggered in assets module by oracle module's UpdateNSTByBalanceChange @@ -162,7 +164,7 @@ func (ks *KeeperSuite) TestNSTLifeCycleOneStaker() { Balance: 29, Block: 1, RoundID: 11, - Index: 1, + Index: 4, Change: types.Action_ACTION_WITHDRAW, }, *stakerInfo.BalanceList[4]) // withdraw will remove this validator diff --git a/x/oracle/keeper/nonce.go b/x/oracle/keeper/nonce.go index 447d8133b..819173c38 100644 --- a/x/oracle/keeper/nonce.go +++ b/x/oracle/keeper/nonce.go @@ -3,7 +3,6 @@ package keeper import ( "fmt" - "github.com/ExocoreNetwork/exocore/x/oracle/keeper/common" "github.com/ExocoreNetwork/exocore/x/oracle/types" "github.com/cosmos/cosmos-sdk/store/prefix" sdk "github.com/cosmos/cosmos-sdk/types" @@ -110,8 +109,10 @@ func (k Keeper) RemoveNonceWithFeederIDForAll(ctx sdk.Context, feederID uint64) // CheckAndIncreaseNonce check and increase the nonce for a specific validator and feederID func (k Keeper) CheckAndIncreaseNonce(ctx sdk.Context, validator string, feederID uint64, nonce uint32) (prevNonce uint32, err error) { - if nonce > uint32(common.MaxNonce) { - return 0, fmt.Errorf("nonce_check_failed: max_exceeded: limit=%d received=%d", common.MaxNonce, nonce) + maxNonce := k.GetMaxNonceFromCache() + // #nosec G115 // safe conversion + if nonce > uint32(maxNonce) { + return 0, fmt.Errorf("nonce_check_failed: max_exceeded: limit=%d received=%d", maxNonce, nonce) } store := prefix.NewStore(ctx.KVStore(k.storeKey), types.KeyPrefix(types.NonceKeyPrefix)) if n, found := k.getNonce(store, validator); found { @@ -127,7 +128,7 @@ func (k Keeper) CheckAndIncreaseNonce(ctx sdk.Context, validator string, feederI } return 0, fmt.Errorf("nonce_check_failed: feeder_not_found: validator=%s feeder_id=%d", validator, feederID) } - return 0, fmt.Errorf("nonce_check_failed: validator_not_active: validator=%s tx_type=create-price", validator) + return 0, fmt.Errorf("nonce_check_failed: validator_not_active: validator=%s tx_type=price-feed", validator) } // internal usage for avoiding duplicated 'NewStore' diff --git a/x/oracle/keeper/params.go b/x/oracle/keeper/params.go index 9f7ccff5a..6a189b9db 100644 --- a/x/oracle/keeper/params.go +++ b/x/oracle/keeper/params.go @@ -5,7 +5,6 @@ import ( "strconv" "strings" - "github.com/ExocoreNetwork/exocore/x/oracle/keeper/cache" "github.com/ExocoreNetwork/exocore/x/oracle/types" sdk "github.com/cosmos/cosmos-sdk/types" ) @@ -40,6 +39,7 @@ func (k Keeper) RegisterNewTokenAndSetTokenFeeder(ctx sdk.Context, oInfo *types. chainID := uint64(0) for id, c := range p.Chains { if c.Name == oInfo.Chain.Name { + // #nosec G115 chainID = uint64(id) break } @@ -50,6 +50,7 @@ func (k Keeper) RegisterNewTokenAndSetTokenFeeder(ctx sdk.Context, oInfo *types. Name: oInfo.Chain.Name, Desc: oInfo.Chain.Desc, }) + // #nosec G115 chainID = uint64(len(p.Chains) - 1) } decimalInt, err := strconv.ParseInt(oInfo.Token.Decimal, 10, 32) @@ -67,16 +68,18 @@ func (k Keeper) RegisterNewTokenAndSetTokenFeeder(ctx sdk.Context, oInfo *types. intervalInt = defaultInterval } + defer func() { + if !ctx.IsCheckTx() { + k.SetParamsUpdated() + } + }() + for _, t := range p.Tokens { // token exists, bind assetID for this token // it's possible for one price bonded with multiple assetID, like ETHUSDT from sepolia/mainnet if t.Name == oInfo.Token.Name && t.ChainID == chainID { t.AssetID = strings.Join([]string{t.AssetID, oInfo.AssetID}, ",") k.SetParams(ctx, p) - if !ctx.IsCheckTx() { - _ = k.GetAggregatorContext(ctx) - k.GetCaches().AddCache(cache.ItemP(p)) - } // there should have been existing tokenFeeder running(currently we register tokens from assets-module and with infinite endBlock) return nil } @@ -94,10 +97,12 @@ func (k Keeper) RegisterNewTokenAndSetTokenFeeder(ctx sdk.Context, oInfo *types. // set a tokenFeeder for the new token p.TokenFeeders = append(p.TokenFeeders, &types.TokenFeeder{ + // #nosec G115 // len(p.Tokens) must be positive since we just append an element for it TokenID: uint64(len(p.Tokens) - 1), // we only support rule_1 for v1 - RuleID: 1, - StartRoundID: 1, + RuleID: 1, + StartRoundID: 1, + // #nosec G115 StartBaseBlock: uint64(ctx.BlockHeight() + startAfterBlocks), Interval: intervalInt, // we don't end feeders for v1 @@ -105,11 +110,5 @@ func (k Keeper) RegisterNewTokenAndSetTokenFeeder(ctx sdk.Context, oInfo *types. }) k.SetParams(ctx, p) - // skip cache update if this is not deliverTx - // for normal cosmostx, checkTx will skip actual message exucution and do anteHandler only, but from ethc.callContract the message will be executed without anteHandler check as checkTx mode. - if !ctx.IsCheckTx() { - _ = k.GetAggregatorContext(ctx) - k.GetCaches().AddCache(cache.ItemP(p)) - } return nil } diff --git a/x/oracle/keeper/params_test.go b/x/oracle/keeper/params_test.go index 592a962bc..8ffa02517 100644 --- a/x/oracle/keeper/params_test.go +++ b/x/oracle/keeper/params_test.go @@ -269,7 +269,7 @@ func TestTokenFeederValidate(t *testing.T) { }, { name: "valid case with two feeders", - prevEndBlock: 1000015, + prevEndBlock: 35, feeder: &types.TokenFeeder{ TokenID: 1, RuleID: 1, diff --git a/x/oracle/keeper/prices.go b/x/oracle/keeper/prices.go index 3a3862c67..721c27e20 100644 --- a/x/oracle/keeper/prices.go +++ b/x/oracle/keeper/prices.go @@ -2,12 +2,9 @@ package keeper import ( "encoding/binary" - "fmt" - "strings" sdkmath "cosmossdk.io/math" assetstypes "github.com/ExocoreNetwork/exocore/x/assets/types" - "github.com/ExocoreNetwork/exocore/x/oracle/keeper/common" "github.com/ExocoreNetwork/exocore/x/oracle/types" "github.com/cosmos/cosmos-sdk/store/prefix" sdk "github.com/cosmos/cosmos-sdk/types" @@ -34,12 +31,15 @@ func (k Keeper) GetPrices( val.TokenID = tokenID val.NextRoundID = nextRoundID var i uint64 - if nextRoundID <= uint64(common.MaxSizePrices) { + maxSizePrices := k.FeederManager.GetMaxSizePricesFromCache() + // #nosec G115 + if nextRoundID <= uint64(maxSizePrices) { i = 1 val.PriceList = make([]*types.PriceTimeRound, 0, nextRoundID) } else { - i = nextRoundID - uint64(common.MaxSizePrices) - val.PriceList = make([]*types.PriceTimeRound, 0, common.MaxSizePrices) + // #nosec G11 + i = nextRoundID - uint64(maxSizePrices) + val.PriceList = make([]*types.PriceTimeRound, 0, maxSizePrices) } for ; i < nextRoundID; i++ { b := store.Get(types.PricesRoundKey(i)) @@ -63,17 +63,13 @@ func (k Keeper) GetSpecifiedAssetsPrice(ctx sdk.Context, assetID string) (types. }, nil } - var p types.Params // get params from cache if exists - if k.memStore.agc != nil { - p = k.memStore.agc.GetParams() - } else { - p = k.GetParams(ctx) - } + p := k.GetParamsFromCache() tokenID := p.GetTokenIDFromAssetID(assetID) if tokenID == 0 { return types.Price{}, types.ErrGetPriceAssetNotFound.Wrapf("assetID does not exist in oracle %s", assetID) } + // #nosec G115 price, found := k.GetPriceTRLatest(ctx, uint64(tokenID)) if !found { return types.Price{ @@ -97,13 +93,8 @@ func (k Keeper) GetSpecifiedAssetsPrice(ctx sdk.Context, assetID string) (types. // return latest price for assets func (k Keeper) GetMultipleAssetsPrices(ctx sdk.Context, assets map[string]interface{}) (prices map[string]types.Price, err error) { - var p types.Params // get params from cache if exists - if k.memStore.agc != nil { - p = k.memStore.agc.GetParams() - } else { - p = k.GetParams(ctx) - } + p := k.GetParamsFromCache() // ret := make(map[string]types.Price) prices = make(map[string]types.Price) info := "" @@ -122,6 +113,7 @@ func (k Keeper) GetMultipleAssetsPrices(ctx sdk.Context, assets map[string]inter prices = nil break } + // #nosec G115 price, found := k.GetPriceTRLatest(ctx, uint64(tokenID)) if !found { info = info + assetID + " " @@ -200,56 +192,60 @@ func (k Keeper) GetAllPrices(ctx sdk.Context) (list []types.Prices) { } // AppenPriceTR append a new round of price for specific token, return false if the roundID not match -func (k Keeper) AppendPriceTR(ctx sdk.Context, tokenID uint64, priceTR types.PriceTimeRound) bool { +func (k Keeper) AppendPriceTR(ctx sdk.Context, tokenID uint64, priceTR types.PriceTimeRound, detID string) bool { nextRoundID := k.GetNextRoundID(ctx, tokenID) + logger := k.Logger(ctx) // This should not happen if nextRoundID != priceTR.RoundID { + logger.Error("roundID not match", "nextRoundID", nextRoundID, "priceTR.RoundID", priceTR.RoundID) return false } store := k.getPriceTRStore(ctx, tokenID) b := k.cdc.MustMarshal(&priceTR) store.Set(types.PricesRoundKey(nextRoundID), b) - if expiredRoundID := nextRoundID - k.memStore.agc.GetParamsMaxSizePrices(); expiredRoundID > 0 { + + p := *k.GetParamsFromCache() + // #nosec G115 // maxSizePrices is not negative + if expiredRoundID := nextRoundID - uint64(p.MaxSizePrices); expiredRoundID > 0 { store.Delete(types.PricesRoundKey(expiredRoundID)) } - roundID := k.IncreaseNextRoundID(ctx, tokenID) + k.IncreaseNextRoundID(ctx, tokenID) - // update for native tokens - // TODO: set hooks as a genral approach - var p types.Params - // get params from cache if exists - if k.memStore.agc != nil { - p = k.memStore.agc.GetParams() - } else { - p = k.GetParams(ctx) + if len(priceTR.Price) == 0 { + return true } - assetIDs := p.GetAssetIDsFromTokenID(tokenID) - for _, assetID := range assetIDs { - if nstChain, ok := strings.CutPrefix(assetID, types.NSTIDPrefix); ok { - if err := k.UpdateNSTByBalanceChange(ctx, fmt.Sprintf("%s%s", NSTETHAssetAddr, nstChain), []byte(priceTR.Price), roundID); err != nil { - // we just report this error in log to notify validators - k.Logger(ctx).Error(types.ErrUpdateNativeTokenVirtualPriceFail.Error(), "error", err) - } + if nstAssetID := p.GetAssetIDForNSTFromTokenID(tokenID); len(nstAssetID) > 0 { + nstVersion, err := getNSTVersionFromDetID(detID) + if err != nil || nstVersion == 0 { + logger.Error(types.ErrUpdateNativeTokenVirtualPriceFail.Error(), "error", err, "nstVersion", nstVersion, "tokenID", tokenID, "roundID", nextRoundID) + return true + } + err = k.UpdateNSTByBalanceChange(ctx, nstAssetID, priceTR, nstVersion) + if err != nil { + // we just report this error in log to notify validators + logger.Error(types.ErrUpdateNativeTokenVirtualPriceFail.Error(), "error", err) + } else { + logger.Info("updated balance change for NST") } } - return true } // GrowRoundID Increases roundID with the previous price +// func (k Keeper) GrowRoundID(ctx sdk.Context, tokenID uint64) (price *types.PriceTimeRound, roundID uint64) { func (k Keeper) GrowRoundID(ctx sdk.Context, tokenID uint64) (price string, roundID uint64) { if pTR, ok := k.GetPriceTRLatest(ctx, tokenID); ok { pTR.RoundID++ - k.AppendPriceTR(ctx, tokenID, pTR) + k.AppendPriceTR(ctx, tokenID, pTR, "") price = pTR.Price roundID = pTR.RoundID } else { nextRoundID := k.GetNextRoundID(ctx, tokenID) k.AppendPriceTR(ctx, tokenID, types.PriceTimeRound{ RoundID: nextRoundID, - }) - price = "" + }, "") roundID = nextRoundID + price = "" } return } diff --git a/x/oracle/keeper/prices_test.go b/x/oracle/keeper/prices_test.go index cad34ee6b..4d84f4e7a 100644 --- a/x/oracle/keeper/prices_test.go +++ b/x/oracle/keeper/prices_test.go @@ -48,6 +48,8 @@ func TestPricesGet(t *testing.T) { func TestPricesGetMultiAssets(t *testing.T) { keeper, ctx := keepertest.OracleKeeper(t) + keeper.FeederManager.SetNilCaches() + keeper.FeederManager.BeginBlock(ctx) keeper.SetPrices(ctx, testdata.P1) assets := make(map[string]interface{}) assets["0x0b34c4d876cd569129cf56bafabb3f9e97a4ff42_0x9ce1"] = new(interface{}) diff --git a/x/oracle/keeper/query_native_token.go b/x/oracle/keeper/query_native_token.go index 94034974b..2130cdf90 100644 --- a/x/oracle/keeper/query_native_token.go +++ b/x/oracle/keeper/query_native_token.go @@ -25,7 +25,8 @@ func (k Keeper) StakerInfos(goCtx context.Context, req *types.QueryStakerInfosRe } ctx := sdk.UnwrapSDKContext(goCtx) stakerInfos := k.GetStakerInfos(ctx, req.AssetId) - return &types.QueryStakerInfosResponse{StakerInfos: stakerInfos}, nil + version := k.GetNSTVersion(ctx, req.AssetId) + return &types.QueryStakerInfosResponse{Version: version, StakerInfos: stakerInfos}, nil } func (k Keeper) StakerInfo(goCtx context.Context, req *types.QueryStakerInfoRequest) (*types.QueryStakerInfoResponse, error) { @@ -37,7 +38,8 @@ func (k Keeper) StakerInfo(goCtx context.Context, req *types.QueryStakerInfoRequ } ctx := sdk.UnwrapSDKContext(goCtx) stakerInfo := k.GetStakerInfo(ctx, req.AssetId, req.StakerAddr) - return &types.QueryStakerInfoResponse{StakerInfo: &stakerInfo}, nil + version := k.GetNSTVersion(ctx, req.AssetId) + return &types.QueryStakerInfoResponse{Version: version, StakerInfo: &stakerInfo}, nil } func (k Keeper) StakerList(goCtx context.Context, req *types.QueryStakerListRequest) (*types.QueryStakerListResponse, error) { @@ -49,5 +51,6 @@ func (k Keeper) StakerList(goCtx context.Context, req *types.QueryStakerListRequ } ctx := sdk.UnwrapSDKContext(goCtx) stakerList := k.GetStakerList(ctx, req.AssetId) - return &types.QueryStakerListResponse{StakerList: &stakerList}, nil + version := k.GetNSTVersion(ctx, req.AssetId) + return &types.QueryStakerListResponse{Version: version, StakerList: &stakerList}, nil } diff --git a/x/oracle/keeper/recent_msg.go b/x/oracle/keeper/recent_msg.go index bf7f25983..d6094fbe4 100644 --- a/x/oracle/keeper/recent_msg.go +++ b/x/oracle/keeper/recent_msg.go @@ -6,6 +6,29 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" ) +// SetMsgItemsForCache set a specific recentMsg with its height as index in the store +func (k Keeper) SetMsgItemsForCache(ctx sdk.Context, recentMsg types.RecentMsg) { + index, found := k.GetIndexRecentMsg(ctx) + block := uint64(ctx.BlockHeight()) + if found { + i := 0 + maxNonce := k.GetParams(ctx).MaxNonce + for ; i < len(index.Index); i++ { + b := index.Index[i] + // #nosec G115 // maxNonce is not negative + if block < uint64(maxNonce) || b > block-uint64(maxNonce) { + break + } + // remove old recentMsg + k.RemoveRecentMsg(ctx, b) + } + index.Index = index.Index[i:] + } + index.Index = append(index.Index, block) + k.SetIndexRecentMsg(ctx, index) + k.SetRecentMsg(ctx, recentMsg) +} + // SetRecentMsg set a specific recentMsg in the store from its index func (k Keeper) SetRecentMsg(ctx sdk.Context, recentMsg types.RecentMsg) { store := prefix.NewStore(ctx.KVStore(k.storeKey), types.KeyPrefix(types.RecentMsgKeyPrefix)) diff --git a/x/oracle/keeper/recent_params.go b/x/oracle/keeper/recent_params.go index efd4af316..e43cf9d27 100644 --- a/x/oracle/keeper/recent_params.go +++ b/x/oracle/keeper/recent_params.go @@ -6,6 +6,38 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" ) +func (k Keeper) SetParamsForCache(ctx sdk.Context, params types.RecentParams) { + block := uint64(ctx.BlockHeight()) + index, found := k.GetIndexRecentParams(ctx) + if found { + // if the maxNonce is changed in this block, all rounds would be force sealed, so it's ok to use either the old or new maxNonce + maxNonce := k.GetParams(ctx).MaxNonce + l := len(index.Index) + if l > 0 { + // keep at least one history params before appending current new params + prev := index.Index[0] + idx := 0 + // #nosec G115 + if prev <= block-uint64(maxNonce) && l > 1 { + for i := 1; i < l; i++ { + k.RemoveRecentParams(ctx, prev) + b := index.Index[i] + // #nosec G115 + if b > block-uint64(maxNonce) { + break + } + prev = b + idx = i + } + } + index.Index = index.Index[idx:] + } + } + index.Index = append(index.Index, block) + k.SetIndexRecentParams(ctx, index) + k.SetRecentParams(ctx, params) +} + // SetRecentParams set a specific recentParams in the store from its index func (k Keeper) SetRecentParams(ctx sdk.Context, recentParams types.RecentParams) { store := prefix.NewStore(ctx.KVStore(k.storeKey), types.KeyPrefix(types.RecentParamsKeyPrefix)) @@ -77,3 +109,40 @@ func (k Keeper) GetAllRecentParamsAsMap(ctx sdk.Context) (result map[int64]*type return } + +// GetRecentParamsWithinMaxNonce returns all recentParams within the maxNonce and the latest recentParams separately +func (k Keeper) GetRecentParamsWithinMaxNonce(ctx sdk.Context) (recentParamsList []*types.RecentParams, prev, latest types.RecentParams) { + maxNonce := k.GetParams(ctx).MaxNonce + var startHeight uint64 + if uint64(ctx.BlockHeight()) > uint64(maxNonce) { + startHeight = uint64(ctx.BlockHeight()) - uint64(maxNonce) + } + store := prefix.NewStore(ctx.KVStore(k.storeKey), types.KeyPrefix(types.RecentParamsKeyPrefix)) + iterator := sdk.KVStorePrefixIterator(store, []byte{}) + + defer iterator.Close() + + recentParamsList = make([]*types.RecentParams, 0, maxNonce) + notFound := true + for ; iterator.Valid(); iterator.Next() { + var val types.RecentParams + k.cdc.MustUnmarshal(iterator.Value(), &val) + latest = val + if val.Block >= startHeight { + if notFound { + notFound = false + } + recentParamsList = append(recentParamsList, &val) + } + if notFound { + prev = val + } + + } + if len(recentParamsList) > 0 { + if prev.Block == recentParamsList[0].Block { + prev = types.RecentParams{} + } + } + return recentParamsList, prev, latest +} diff --git a/x/oracle/keeper/single.go b/x/oracle/keeper/single.go deleted file mode 100644 index b40668333..000000000 --- a/x/oracle/keeper/single.go +++ /dev/null @@ -1,225 +0,0 @@ -package keeper - -import ( - "math/big" - "strconv" - - "github.com/ExocoreNetwork/exocore/x/oracle/keeper/aggregator" - "github.com/ExocoreNetwork/exocore/x/oracle/keeper/cache" - "github.com/ExocoreNetwork/exocore/x/oracle/keeper/common" - "github.com/ExocoreNetwork/exocore/x/oracle/types" - sdk "github.com/cosmos/cosmos-sdk/types" -) - -func (k *Keeper) GetCaches() *cache.Cache { - if k.memStore.cs != nil { - return k.memStore.cs - } - k.memStore.cs = cache.NewCache() - return k.memStore.cs -} - -// GetAggregatorContext returns singleton aggregatorContext used to calculate final price for each round of each tokenFeeder -func (k *Keeper) GetAggregatorContext(ctx sdk.Context) *aggregator.AggregatorContext { - if ctx.IsCheckTx() { - if k.memStore.agcCheckTx != nil { - return k.memStore.agcCheckTx - } - if k.memStore.agc == nil { - c := k.GetCaches() - c.ResetCaches() - k.memStore.agcCheckTx = aggregator.NewAggregatorContext() - if ok := k.recacheAggregatorContext(ctx, k.memStore.agcCheckTx, c); !ok { - // this is the very first time oracle has been started, fill relalted info as initialization - initAggregatorContext(ctx, k.memStore.agcCheckTx, k, c) - } - return k.memStore.agcCheckTx - } - k.memStore.agcCheckTx = k.memStore.agc.Copy4CheckTx() - return k.memStore.agcCheckTx - } - - if k.memStore.agc != nil { - return k.memStore.agc - } - - c := k.GetCaches() - c.ResetCaches() - k.memStore.agc = aggregator.NewAggregatorContext() - if ok := k.recacheAggregatorContext(ctx, k.memStore.agc, c); !ok { - // this is the very first time oracle has been started, fill relalted info as initialization - initAggregatorContext(ctx, k.memStore.agc, k, c) - } else { - // this is when a node restart and use the persistent state to refill cache, we don't need to commit these data again - c.SkipCommit() - } - return k.memStore.agc -} - -func (k Keeper) recacheAggregatorContext(ctx sdk.Context, agc *aggregator.AggregatorContext, c *cache.Cache) bool { - logger := k.Logger(ctx) - oracleParams := k.GetParams(ctx) - from := ctx.BlockHeight() - int64(oracleParams.MaxNonce) + 1 - to := ctx.BlockHeight() - - h, ok := k.GetValidatorUpdateBlock(ctx) - recentParamsMap := k.GetAllRecentParamsAsMap(ctx) - if !ok || len(recentParamsMap) == 0 { - logger.Info("recacheAggregatorContext: no validatorUpdateBlock found, go to initial process", "height", ctx.BlockHeight()) - // no cache, this is the very first running, so go to initial process instead - return false - } - - forceSealHeight := h.Block - // #nosec G115 - if int64(forceSealHeight) >= from { - from = int64(h.Block) + 1 - logger.Info("recacheAggregatorContext: with validatorSet updated recently", "latestValidatorUpdateBlock", h.Block, "currentHeight", ctx.BlockHeight()) - } - - logger.Info("recacheAggregatorContext", "from", from, "to", to, "height", ctx.BlockHeight()) - totalPower := big.NewInt(0) - validatorPowers := make(map[string]*big.Int) - validatorSet := k.GetAllExocoreValidators(ctx) - for _, v := range validatorSet { - validatorPowers[sdk.ConsAddress(v.Address).String()] = big.NewInt(v.Power) - totalPower = new(big.Int).Add(totalPower, big.NewInt(v.Power)) - } - agc.SetValidatorPowers(validatorPowers) - - // reset validators - c.AddCache(cache.ItemV(validatorPowers)) - - recentMsgs := k.GetAllRecentMsgAsMap(ctx) - var p *types.Params - var b int64 - if from >= to { - // backwards compatible for that the validatorUpdateBlock updated every block - prev := int64(0) - for b = range recentParamsMap { - if b > prev { - prev = b - } - } - p = recentParamsMap[prev] - agc.SetParams(p) - setCommonParams(p) - } else { - prev := int64(0) - for ; from < to; from++ { - // fill params - for b, p = range recentParamsMap { - // find the params which is the latest one before the replayed block height since prepareRoundEndBlock will use it and it should be the latest one before current block - if b < from && b > prev { - agc.SetParams(p) - prev = b - setCommonParams(p) - delete(recentParamsMap, b) - } - } - - logger.Info("recacheAggregatorContext: prepareRoundEndBlock", "baseBlock", from-1, "forceSealHeight", forceSealHeight) - agc.PrepareRoundEndBlock(ctx, from-1, forceSealHeight) - - if msgs := recentMsgs[from]; msgs != nil { - for _, msg := range msgs { - // these messages are retreived for recache, just skip the validation check and fill the memory cache - //nolint - agc.FillPrice(&types.MsgCreatePrice{ - Creator: msg.Validator, - FeederID: msg.FeederID, - Prices: msg.PSources, - }) - } - } - ctxReplay := ctx.WithBlockHeight(from) - logger.Info("recacheAggregatorContext: sealRound", "blockEnd", from) - agc.SealRound(ctxReplay, false) - } - - for b, p = range recentParamsMap { - // use the latest params before the current block height - if b < to && b > prev { - agc.SetParams(p) - prev = b - setCommonParams(p) - } - } - } - logger.Info("recacheAggregatorContext: PrepareRoundEndBlock", "baseBlock", to-1) - agc.PrepareRoundEndBlock(ctx, to-1, forceSealHeight) - - var pRet cache.ItemP - if updated := c.GetCache(&pRet); !updated { - c.AddCache(cache.ItemP(*p)) - } - // TODO: these 4 lines are mainly used for hot fix - // since the latest params stored in KV for recache should be the same with the latest params, so these lines are just duplicated actions if everything is fine. - *p = k.GetParams(ctx) - agc.SetParams(p) - setCommonParams(p) - c.AddCache(cache.ItemP(*p)) - - return true -} - -func initAggregatorContext(ctx sdk.Context, agc *aggregator.AggregatorContext, k *Keeper, c *cache.Cache) { - ctx.Logger().Info("initAggregatorContext", "height", ctx.BlockHeight()) - // set params - p := k.GetParams(ctx) - agc.SetParams(&p) - // set params cache - c.AddCache(cache.ItemP(p)) - setCommonParams(&p) - - totalPower := big.NewInt(0) - validatorPowers := make(map[string]*big.Int) - validatorSet := k.GetAllExocoreValidators(ctx) - for _, v := range validatorSet { - validatorPowers[sdk.ConsAddress(v.Address).String()] = big.NewInt(v.Power) - totalPower = new(big.Int).Add(totalPower, big.NewInt(v.Power)) - } - - agc.SetValidatorPowers(validatorPowers) - // set validatorPower cache - c.AddCache(cache.ItemV(validatorPowers)) - - agc.PrepareRoundEndBlock(ctx, ctx.BlockHeight()-1, 0) -} - -func (k *Keeper) ResetAggregatorContext() { - k.memStore.agc = nil -} - -func (k *Keeper) ResetCache() { - k.memStore.cs = nil -} - -func (k *Keeper) ResetAggregatorContextCheckTx() { - k.memStore.agcCheckTx = nil -} - -// setCommonParams save static fields in params in memory cache since these fields will not change during node running -// TODO: further when params is abled to be updated through tx/gov, this cache should be taken care if any is available to be changed -func setCommonParams(p *types.Params) { - common.MaxNonce = p.MaxNonce - common.ThresholdA = p.ThresholdA - common.ThresholdB = p.ThresholdB - common.MaxDetID = p.MaxDetId - common.Mode = p.Mode - common.MaxSizePrices = int(p.MaxSizePrices) -} - -func (k *Keeper) ResetUpdatedFeederIDs() { - if k.memStore.updatedFeederIDs != nil { - k.memStore.updatedFeederIDs = nil - } -} - -func (k Keeper) GetUpdatedFeederIDs() []string { - return k.memStore.updatedFeederIDs -} - -func (k *Keeper) AppendUpdatedFeederIDs(id uint64) { - k.memStore.updatedFeederIDs = append(k.memStore.updatedFeederIDs, strconv.FormatUint(id, 10)) -} diff --git a/x/oracle/keeper/slashing.go b/x/oracle/keeper/slashing.go index b05c761e8..83abee9ad 100644 --- a/x/oracle/keeper/slashing.go +++ b/x/oracle/keeper/slashing.go @@ -26,6 +26,16 @@ func (k Keeper) InitValidatorReportInfo(ctx sdk.Context, validator string, heigh } } +func (k Keeper) ClearAllValidatorReportInfo(ctx sdk.Context) { + // k.ClearAllValidatorMissedRoundBitArray(ctx) + store := prefix.NewStore(ctx.KVStore(k.storeKey), types.ValidatorReportInfoPrefix) + iterator := sdk.KVStorePrefixIterator(store, []byte{}) + for ; iterator.Valid(); iterator.Next() { + store.Delete(iterator.Key()) + } + iterator.Close() +} + // SetValidatorReportInfo sets the validator reporting info for a validator func (k Keeper) SetValidatorReportInfo(ctx sdk.Context, validator string, info types.ValidatorReportInfo) { store := ctx.KVStore(k.storeKey) @@ -70,11 +80,6 @@ func (k Keeper) GetReportedRoundsWindow(ctx sdk.Context) int64 { return k.GetParams(ctx).Slashing.ReportedRoundsWindow } -// GetSlashFractionMiss fraction of power slashed for missed rounds -func (k Keeper) GetSlashFractionMiss(ctx sdk.Context) (res sdk.Dec) { - return k.GetParams(ctx).Slashing.SlashFractionMiss -} - // GetSlashFractionMalicious fraction returns the fraction of power slashed for malicious behavior func (k Keeper) GetSlashFractionMalicious(ctx sdk.Context) (res sdk.Dec) { return k.GetParams(ctx).Slashing.SlashFractionMalicious @@ -118,7 +123,7 @@ func (k Keeper) IterateValidatorReportInfos(ctx sdk.Context, handler func(addres // IterateValidatorMissedRoundBitArrray iterates all missed rounds in one performance window of rounds func (k Keeper) IterateValidatorMissedRoundBitArray(ctx sdk.Context, validator string, handler func(index uint64, missed bool) (stop bool)) { - store := prefix.NewStore(ctx.KVStore(k.storeKey), types.SlashingMissedBitArrayPrefix(validator)) + store := prefix.NewStore(ctx.KVStore(k.storeKey), types.SlashingMissedBitArrayValidatorPrefix(validator)) iterator := sdk.KVStorePrefixIterator(store, []byte{}) defer iterator.Close() for ; iterator.Valid(); iterator.Next() { @@ -145,7 +150,17 @@ func (k Keeper) GetValidatorMissedRounds(ctx sdk.Context, address string) []*typ // ClearValidatorMissedBlockBitArray deletes every instance of ValidatorMissedBlockBitArray in the store func (k Keeper) ClearValidatorMissedRoundBitArray(ctx sdk.Context, validator string) { store := ctx.KVStore(k.storeKey) - iterator := sdk.KVStorePrefixIterator(store, types.SlashingMissedBitArrayPrefix(validator)) + iterator := sdk.KVStorePrefixIterator(store, types.SlashingMissedBitArrayValidatorPrefix(validator)) + defer iterator.Close() + for ; iterator.Valid(); iterator.Next() { + store.Delete(iterator.Key()) + } +} + +// ClearAllValidatorMissedRoundBitArray clear all instances of ValidatorMissedBlockBitArray in the store +func (k Keeper) ClearAllValidatorMissedRoundBitArray(ctx sdk.Context) { + store := ctx.KVStore(k.storeKey) + iterator := sdk.KVStorePrefixIterator(store, types.MissedBitArrayPrefix) defer iterator.Close() for ; iterator.Valid(); iterator.Next() { store.Delete(iterator.Key()) diff --git a/x/oracle/keeper/tokens.go b/x/oracle/keeper/tokens.go index 6b97e8733..3221af141 100644 --- a/x/oracle/keeper/tokens.go +++ b/x/oracle/keeper/tokens.go @@ -12,6 +12,7 @@ func (k Keeper) GetTokens(ctx sdk.Context) []*types.TokenIndex { for idx, token := range params.Tokens { ret = append(ret, &types.TokenIndex{ Token: token.Name, + // #nosec G115 Index: uint64(idx), }) } diff --git a/x/oracle/keeper/validate_timestamp.go b/x/oracle/keeper/validate_timestamp.go new file mode 100644 index 000000000..35918d4e4 --- /dev/null +++ b/x/oracle/keeper/validate_timestamp.go @@ -0,0 +1,31 @@ +package keeper + +import ( + "context" + "fmt" + "time" + + "github.com/ExocoreNetwork/exocore/x/oracle/types" + sdk "github.com/cosmos/cosmos-sdk/types" +) + +func checkTimestamp(goCtx context.Context, msg *types.MsgPriceFeed) error { + ctx := sdk.UnwrapSDKContext(goCtx) + now := ctx.BlockTime().UTC() + for _, ps := range msg.Prices { + for _, price := range ps.Prices { + ts := price.Timestamp + if len(ts) == 0 { + return fmt.Errorf("timestamp should not be empty, blockTime:%s, got:%s", now.Format(layout), ts) + } + t, err := time.ParseInLocation(layout, ts, time.UTC) + if err != nil { + return fmt.Errorf("timestamp format invalid, blockTime:%s, got:%s", now.Format(layout), ts) + } + if now.Add(maxFutureOffset).Before(t) { + return fmt.Errorf("timestamp is in the future, blockTime:%s, got:%s", now.Format(layout), ts) + } + } + } + return nil +} diff --git a/x/oracle/keeper/validator_update_block.go b/x/oracle/keeper/validator_update_block.go index e5e6ea96d..9b5cfa97d 100644 --- a/x/oracle/keeper/validator_update_block.go +++ b/x/oracle/keeper/validator_update_block.go @@ -6,8 +6,7 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" ) -// SetValidatorUpdateBlock set validatorUpdateBlock in the store -func (k Keeper) SetValidatorUpdateBlock(ctx sdk.Context, validatorUpdateBlock types.ValidatorUpdateBlock) { +func (k Keeper) SetValidatorUpdateForCache(ctx sdk.Context, validatorUpdateBlock types.ValidatorUpdateBlock) { store := prefix.NewStore(ctx.KVStore(k.storeKey), types.KeyPrefix(types.ValidatorUpdateBlockKey)) b := k.cdc.MustMarshal(&validatorUpdateBlock) store.Set(types.BlockKey, b) diff --git a/x/oracle/keeper/validator_update_block_test.go b/x/oracle/keeper/validator_update_block_test.go index 56bd66356..b128ed325 100644 --- a/x/oracle/keeper/validator_update_block_test.go +++ b/x/oracle/keeper/validator_update_block_test.go @@ -14,7 +14,7 @@ import ( func createTestValidatorUpdateBlock(keeper *keeper.Keeper, ctx sdk.Context) types.ValidatorUpdateBlock { item := types.ValidatorUpdateBlock{} - keeper.SetValidatorUpdateBlock(ctx, item) + keeper.SetValidatorUpdateForCache(ctx, item) return item } diff --git a/x/oracle/module.go b/x/oracle/module.go index 097376d29..b2745b2ae 100644 --- a/x/oracle/module.go +++ b/x/oracle/module.go @@ -4,9 +4,6 @@ import ( "context" "encoding/json" "fmt" - "math/big" - "sort" - "strings" // this line is used by starport scaffolding # 1 @@ -15,16 +12,13 @@ import ( "github.com/ExocoreNetwork/exocore/x/oracle/client/cli" "github.com/ExocoreNetwork/exocore/x/oracle/keeper" - "github.com/ExocoreNetwork/exocore/x/oracle/keeper/cache" "github.com/ExocoreNetwork/exocore/x/oracle/types" abci "github.com/cometbft/cometbft/abci/types" "github.com/cosmos/cosmos-sdk/client" "github.com/cosmos/cosmos-sdk/codec" cdctypes "github.com/cosmos/cosmos-sdk/codec/types" - cryptocodec "github.com/cosmos/cosmos-sdk/crypto/codec" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/types/module" - stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" ) var ( @@ -151,244 +145,8 @@ func (am AppModule) ExportGenesis(ctx sdk.Context, cdc codec.JSONCodec) json.Raw // ConsensusVersion is a sequence number for state-breaking change of the module. It should be incremented on each consensus-breaking change introduced by the module. To avoid wrong/empty versions, the initial version should be set to 1 func (AppModule) ConsensusVersion() uint64 { return 1 } -// BeginBlock contains the logic that is automatically triggered at the beginning of each block -func (am AppModule) BeginBlock(ctx sdk.Context, _ abci.RequestBeginBlock) { - // init caches and aggregatorContext for node restart - // TODO: try better way to init caches and aggregatorContext than beginBlock - _ = am.keeper.GetCaches() - agc := am.keeper.GetAggregatorContext(ctx) - validatorPowers := agc.GetValidatorPowers() - // set validatorReportInfo to track performance - for validator := range validatorPowers { - am.keeper.InitValidatorReportInfo(ctx, validator, ctx.BlockHeight()) - } -} - // EndBlock contains the logic that is automatically triggered at the end of each block func (am AppModule) EndBlock(ctx sdk.Context, _ abci.RequestEndBlock) []abci.ValidatorUpdate { - cs := am.keeper.GetCaches() - validatorUpdates := am.keeper.GetValidatorUpdates(ctx) - forceSeal := false - agc := am.keeper.GetAggregatorContext(ctx) - - logger := am.keeper.Logger(ctx) - height := ctx.BlockHeight() - if len(validatorUpdates) > 0 { - validatorList := make(map[string]*big.Int) - for _, vu := range validatorUpdates { - pubKey, _ := cryptocodec.FromTmProtoPublicKey(vu.PubKey) - validatorStr := sdk.ConsAddress(pubKey.Address()).String() - validatorList[validatorStr] = big.NewInt(vu.Power) - // add possible new added validator info for slashing tracking - if vu.Power > 0 { - am.keeper.InitValidatorReportInfo(ctx, validatorStr, height) - } - } - // update validator set information in cache - cs.AddCache(cache.ItemV(validatorList)) - validatorPowers := make(map[string]*big.Int) - cs.GetCache(cache.ItemV(validatorPowers)) - // update validatorPowerList in aggregatorContext - agc.SetValidatorPowers(validatorPowers) - // TODO: seal all alive round since validatorSet changed here - forceSeal = true - logger.Info("validator set changed, force seal all active rounds", "height", height) - } - - // TODO: for v1 use mode==1, just check the failed feeders - _, failed, _, windowClosed := agc.SealRound(ctx, forceSeal) - defer func() { - logger.Debug("remove aggregators(workers) on window closed", "feederIDs", windowClosed) - for _, feederID := range windowClosed { - agc.RemoveWorker(feederID) - am.keeper.RemoveNonceWithFeederIDForValidators(ctx, feederID, agc.GetValidators()) - } - }() - // update&check slashing info - validatorPowers := agc.GetValidatorPowers() - validators := make([]string, 0, len(validatorPowers)) - for validator := range validatorPowers { - validators = append(validators, validator) - } - sort.Strings(validators) - for _, validator := range validators { - power := validatorPowers[validator] - reportedInfo, found := am.keeper.GetValidatorReportInfo(ctx, validator) - if !found { - logger.Error(fmt.Sprintf("Expected report info for validator %s but not found", validator)) - continue - } - // TODO: for the round calculation, now only sourceID=1 is used so {feederID, sourceID} have only one value for each feederID which corresponding to one round. - // But when we came to multiple sources, we should consider the round corresponding to feedeerID instead of {feederID, sourceID} - for _, finalPrice := range agc.GetFinalPriceListForFeederIDs(windowClosed) { - exist, matched := agc.PerformanceReview(ctx, finalPrice, validator) - if exist && !matched { - // TODO: malicious price, just slash&jail immediately - logger.Info( - "confirmed malicious price", - "validator", validator, - "infraction_height", height, - "infraction_time", ctx.BlockTime(), - "feederID", finalPrice.FeederID, - "detID", finalPrice.DetID, - "sourceID", finalPrice.SourceID, - "finalPrice", finalPrice.Price, - ) - consAddr, err := sdk.ConsAddressFromBech32(validator) - if err != nil { - panic("invalid consAddr string") - } - - operator := am.keeper.ValidatorByConsAddr(ctx, consAddr) - if operator != nil && !operator.IsJailed() { - coinsBurned := am.keeper.SlashWithInfractionReason(ctx, consAddr, height, power.Int64(), am.keeper.GetSlashFractionMalicious(ctx), stakingtypes.Infraction_INFRACTION_UNSPECIFIED) - ctx.EventManager().EmitEvent( - sdk.NewEvent( - types.EventTypeOracleSlash, - sdk.NewAttribute(types.AttributeKeyValidatorKey, validator), - sdk.NewAttribute(types.AttributeKeyPower, fmt.Sprintf("%d", power)), - sdk.NewAttribute(types.AttributeKeyReason, types.AttributeValueMaliciousReportPrice), - sdk.NewAttribute(types.AttributeKeyJailed, validator), - sdk.NewAttribute(types.AttributeKeyBurnedCoins, coinsBurned.String()), - ), - ) - am.keeper.Jail(ctx, consAddr) - jailUntil := ctx.BlockHeader().Time.Add(am.keeper.GetMaliciousJailDuration(ctx)) - am.keeper.JailUntil(ctx, consAddr, jailUntil) - reportedInfo.MissedRoundsCounter = 0 - reportedInfo.IndexOffset = 0 - am.keeper.ClearValidatorMissedRoundBitArray(ctx, validator) - } - continue - } - - reportedRoundsWindow := am.keeper.GetReportedRoundsWindow(ctx) - index := uint64(reportedInfo.IndexOffset % reportedRoundsWindow) - reportedInfo.IndexOffset++ - // Update reported round bit array & counter - // This counter just tracks the sum of the bit array - // That way we avoid needing to read/write the whole array each time - previous := am.keeper.GetValidatorMissedRoundBitArray(ctx, validator, index) - missed := !exist - switch { - case !previous && missed: - // Array value has changed from not missed to missed, increment counter - am.keeper.SetValidatorMissedRoundBitArray(ctx, validator, index, true) - reportedInfo.MissedRoundsCounter++ - case previous && !missed: - // Array value has changed from missed to not missed, decrement counter - am.keeper.SetValidatorMissedRoundBitArray(ctx, validator, index, false) - reportedInfo.MissedRoundsCounter-- - default: - // Array value at this index has not changed, no need to update counter - } - - minReportedPerWindow := am.keeper.GetMinReportedPerWindow(ctx) - - if missed { - ctx.EventManager().EmitEvent( - sdk.NewEvent( - types.EventTypeOracleLiveness, - sdk.NewAttribute(types.AttributeKeyValidatorKey, validator), - sdk.NewAttribute(types.AttributeKeyMissedRounds, fmt.Sprintf("%d", reportedInfo.MissedRoundsCounter)), - sdk.NewAttribute(types.AttributeKeyHeight, fmt.Sprintf("%d", height)), - ), - ) - - logger.Debug( - "absent validator", - "height", ctx.BlockHeight(), - "validator", validator, - "missed", reportedInfo.MissedRoundsCounter, - "threshold", minReportedPerWindow, - ) - } - - minHeight := reportedInfo.StartHeight + reportedRoundsWindow - maxMissed := reportedRoundsWindow - minReportedPerWindow - // if we are past the minimum height and the validator has missed too many rounds reporting prices, punish them - if height > minHeight && reportedInfo.MissedRoundsCounter > maxMissed { - consAddr, err := sdk.ConsAddressFromBech32(validator) - if err != nil { - panic("invalid consAddr string") - } - operator := am.keeper.ValidatorByConsAddr(ctx, consAddr) - if operator != nil && !operator.IsJailed() { - // missing rounds confirmed: slash and jail the validator - coinsBurned := am.keeper.SlashWithInfractionReason(ctx, consAddr, height, power.Int64(), am.keeper.GetSlashFractionMiss(ctx), stakingtypes.Infraction_INFRACTION_UNSPECIFIED) - ctx.EventManager().EmitEvent( - sdk.NewEvent( - types.EventTypeOracleSlash, - sdk.NewAttribute(types.AttributeKeyValidatorKey, validator), - sdk.NewAttribute(types.AttributeKeyPower, fmt.Sprintf("%d", power)), - sdk.NewAttribute(types.AttributeKeyReason, types.AttributeValueMissingReportPrice), - sdk.NewAttribute(types.AttributeKeyJailed, validator), - sdk.NewAttribute(types.AttributeKeyBurnedCoins, coinsBurned.String()), - ), - ) - am.keeper.Jail(ctx, consAddr) - jailUntil := ctx.BlockHeader().Time.Add(am.keeper.GetMissJailDuration(ctx)) - am.keeper.JailUntil(ctx, consAddr, jailUntil) - - // We need to reset the counter & array so that the validator won't be immediately slashed for miss report info upon rebonding. - reportedInfo.MissedRoundsCounter = 0 - reportedInfo.IndexOffset = 0 - am.keeper.ClearValidatorMissedRoundBitArray(ctx, validator) - - logger.Info( - "slashing and jailing validator due to liveness fault", - "height", height, - "validator", consAddr.String(), - "min_height", minHeight, - "threshold", minReportedPerWindow, - "slashed", am.keeper.GetSlashFractionMiss(ctx).String(), - "jailed_until", jailUntil, - ) - } else { - // validator was (a) not found or (b) already jailed so we do not slash - logger.Info( - "validator would have been slashed for too many missed repoerting price, but was either not found in store or already jailed", - "validator", validator, - ) - } - } - // Set the updated reportInfo - am.keeper.SetValidatorReportInfo(ctx, validator, reportedInfo) - } - } - - // append new round with previous price for fail-sealed token - for _, tokenID := range failed { - prevPrice, nextRoundID := am.keeper.GrowRoundID(ctx, tokenID) - logger.Info("add new round with previous price under fail aggregation", "tokenID", tokenID, "roundID", nextRoundID, "price", prevPrice) - } - - am.keeper.ResetAggregatorContextCheckTx() - - if _, _, paramsUpdated := cs.CommitCache(ctx, false, am.keeper); paramsUpdated { - var p cache.ItemP - cs.GetCache(&p) - params := types.Params(p) - agc.SetParams(¶ms) - ctx.EventManager().EmitEvent(sdk.NewEvent( - types.EventTypeCreatePrice, - sdk.NewAttribute(types.AttributeKeyParamsUpdated, types.AttributeValueParamsUpdatedSuccess), - )) - } - - if feederIDs := am.keeper.GetUpdatedFeederIDs(); len(feederIDs) > 0 { - feederIDsStr := strings.Join(feederIDs, "_") - ctx.EventManager().EmitEvent(sdk.NewEvent( - types.EventTypeCreatePrice, - sdk.NewAttribute(types.AttributeKeyPriceUpdated, types.AttributeValuePriceUpdatedSuccess), - sdk.NewAttribute(types.AttributeKeyFeederIDs, feederIDsStr), - )) - am.keeper.ResetUpdatedFeederIDs() - } - - newRoundFeederIDs := agc.PrepareRoundEndBlock(ctx, ctx.BlockHeight(), 0) - for _, feederID := range newRoundFeederIDs { - am.keeper.AddZeroNonceItemWithFeederIDForValidators(ctx, feederID, agc.GetValidators()) - } + am.keeper.EndBlock(ctx) return []abci.ValidatorUpdate{} } diff --git a/x/oracle/module_beginblock.go b/x/oracle/module_beginblock.go new file mode 100644 index 000000000..c010b6669 --- /dev/null +++ b/x/oracle/module_beginblock.go @@ -0,0 +1,13 @@ +//go:build !devmode + +package oracle + +import ( + abci "github.com/cometbft/cometbft/abci/types" + sdk "github.com/cosmos/cosmos-sdk/types" +) + +// BeginBlock contains the logic that is automatically triggered at the beginning of each block +func (am AppModule) BeginBlock(ctx sdk.Context, _ abci.RequestBeginBlock) { + am.keeper.BeginBlock(ctx) +} diff --git a/x/oracle/module_beginblock_devmode.go b/x/oracle/module_beginblock_devmode.go new file mode 100644 index 000000000..55cda7e7e --- /dev/null +++ b/x/oracle/module_beginblock_devmode.go @@ -0,0 +1,34 @@ +//go:build devmode + +package oracle + +import ( + "fmt" + + "github.com/ExocoreNetwork/exocore/x/oracle/keeper" + "github.com/ExocoreNetwork/exocore/x/oracle/keeper/feedermanagement" + abci "github.com/cometbft/cometbft/abci/types" + sdk "github.com/cosmos/cosmos-sdk/types" +) + +// BeginBlock contains the logic that is automatically triggered at the beginning of each block +func (am AppModule) BeginBlock(ctx sdk.Context, _ abci.RequestBeginBlock) { + logger := am.keeper.Logger(ctx) + am.keeper.BeginBlock(ctx) + + logger.Info("start simulating recovery in BeginBlock", "height", ctx.BlockHeight()) + // check the result of recovery + f := recoveryFeederManagerOnNextBlock(ctx, am.keeper) + if ok := am.keeper.FeederManager.Equals(f); !ok { + panic(fmt.Sprintf("there's something wrong in the recovery logic of feedermanager, block:%d", ctx.BlockHeight())) + } +} + +func recoveryFeederManagerOnNextBlock(ctx sdk.Context, k keeper.Keeper) *feedermanagement.FeederManager { + f := feedermanagement.NewFeederManager(k) + recovered := f.BeginBlock(ctx) + if ctx.BlockHeight() > 1 && !recovered { + panic(fmt.Sprintf("failed to do recovery for feedermanager, block:%d", ctx.BlockHeight())) + } + return f +} diff --git a/x/oracle/types/codec.go b/x/oracle/types/codec.go index d8e9b828d..4b208755b 100644 --- a/x/oracle/types/codec.go +++ b/x/oracle/types/codec.go @@ -8,14 +8,14 @@ import ( ) func RegisterCodec(cdc *codec.LegacyAmino) { - cdc.RegisterConcrete(&MsgCreatePrice{}, "oracle/CreatePrice", nil) + cdc.RegisterConcrete(&MsgPriceFeed{}, "oracle/PriceFeed", nil) cdc.RegisterConcrete(Params{}, "exocore/x/oracle/Params", nil) // this line is used by starport scaffolding # 2 } func RegisterInterfaces(registry cdctypes.InterfaceRegistry) { registry.RegisterImplementations((*sdk.Msg)(nil), - &MsgCreatePrice{}, + &MsgPriceFeed{}, ) // this line is used by starport scaffolding # 3 diff --git a/x/oracle/types/errors.go b/x/oracle/types/errors.go index 79598e6aa..68dc2c6c1 100644 --- a/x/oracle/types/errors.go +++ b/x/oracle/types/errors.go @@ -15,16 +15,20 @@ const ( getPriceFailedRoundNotFound updateNativeTokenVirtualPriceFail nstAssetNotSurpported + failedInAggregation + quoteRecorded ) // x/oracle module sentinel errors var ( - ErrInvalidMsg = sdkerrors.Register(ModuleName, invalidMsg, "invalid input create price") - ErrPriceProposalIgnored = sdkerrors.Register(ModuleName, priceProposalIgnored, "price proposal ignored") + ErrInvalidMsg = sdkerrors.Register(ModuleName, invalidMsg, "invalid input price-feed") + ErrPriceProposalIgnored = sdkerrors.Register(ModuleName, priceProposalIgnored, "quote is 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") ErrUpdateNativeTokenVirtualPriceFail = sdkerrors.Register(ModuleName, updateNativeTokenVirtualPriceFail, "update native token balance change failed") ErrNSTAssetNotSupported = sdkerrors.Register(ModuleName, nstAssetNotSurpported, "nstAsset not supported") + ErrFailedInAggregation = sdkerrors.Register(ModuleName, failedInAggregation, "failed in aggregation") + ErrQuoteRecorded = sdkerrors.Register(ModuleName, quoteRecorded, "quote recorded") ) diff --git a/x/oracle/types/events.go b/x/oracle/types/events.go index 3a0d42c2b..1a03a3b89 100644 --- a/x/oracle/types/events.go +++ b/x/oracle/types/events.go @@ -1,7 +1,7 @@ package types const ( - EventTypeCreatePrice = "create_price" + EventTypePriceFeed = "create_price" EventTypeOracleLiveness = "oracle_liveness" EventTypeOracleSlash = "oracle_slash" diff --git a/x/oracle/types/genesis.pb.go b/x/oracle/types/genesis.pb.go index 5b2b80079..d1bccc0ac 100644 --- a/x/oracle/types/genesis.pb.go +++ b/x/oracle/types/genesis.pb.go @@ -169,6 +169,8 @@ type StakerInfosAssets struct { AssetId string `protobuf:"bytes,1,opt,name=asset_id,json=assetId,proto3" json:"asset_id,omitempty"` // stakerInfos StakerInfos []*StakerInfo `protobuf:"bytes,2,rep,name=staker_infos,json=stakerInfos,proto3" json:"staker_infos,omitempty"` + // nst_version is the version of nst to track validator list changes + NstVersion int64 `protobuf:"varint,3,opt,name=nst_version,json=nstVersion,proto3" json:"nst_version,omitempty"` } func (m *StakerInfosAssets) Reset() { *m = StakerInfosAssets{} } @@ -218,12 +220,21 @@ func (m *StakerInfosAssets) GetStakerInfos() []*StakerInfo { return nil } +func (m *StakerInfosAssets) GetNstVersion() int64 { + if m != nil { + return m.NstVersion + } + return 0 +} + // stakerListAssets bond stakerList to their related assets id type StakerListAssets struct { // asset_id tells the assetid which the stakerList belong to AssetId string `protobuf:"bytes,1,opt,name=asset_id,json=assetId,proto3" json:"asset_id,omitempty"` // stakerList StakerList *StakerList `protobuf:"bytes,2,opt,name=staker_list,json=stakerList,proto3" json:"staker_list,omitempty"` + // nst_version is the version of nst to track validator list changes + NstVersion int64 `protobuf:"varint,3,opt,name=nst_version,json=nstVersion,proto3" json:"nst_version,omitempty"` } func (m *StakerListAssets) Reset() { *m = StakerListAssets{} } @@ -273,6 +284,13 @@ func (m *StakerListAssets) GetStakerList() *StakerList { return nil } +func (m *StakerListAssets) GetNstVersion() int64 { + if m != nil { + return m.NstVersion + } + return 0 +} + // ValidatorMissedRounds record missed rounds indexes for a validator which consAddr corresponding to the address type ValidatorMissedRounds struct { // address of validator @@ -394,51 +412,52 @@ func init() { func init() { proto.RegisterFile("exocore/oracle/v1/genesis.proto", fileDescriptor_6b68ac5b0c7f4305) } var fileDescriptor_6b68ac5b0c7f4305 = []byte{ - // 691 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x84, 0x94, 0xdd, 0x6e, 0xd3, 0x30, - 0x14, 0xc7, 0xdb, 0x7d, 0xb4, 0xdb, 0xe9, 0x06, 0xad, 0xf7, 0x41, 0x36, 0xb1, 0x6c, 0x94, 0x01, - 0x93, 0x90, 0x52, 0x06, 0x17, 0x5c, 0x20, 0xa1, 0x31, 0x84, 0xd0, 0x06, 0x43, 0x28, 0xe3, 0x43, - 0x9a, 0x84, 0xa2, 0xb4, 0xf1, 0x3a, 0xab, 0x6d, 0x5c, 0xd9, 0x5e, 0x18, 0x6f, 0xc1, 0x8b, 0xf0, - 0x1e, 0xbb, 0xdc, 0x25, 0x57, 0x08, 0xad, 0x2f, 0x82, 0x72, 0x9c, 0xac, 0x49, 0x93, 0x96, 0xbb, - 0xd8, 0xe7, 0x7f, 0x7e, 0xe7, 0xf8, 0x7f, 0xec, 0xc0, 0x26, 0xbd, 0xe0, 0x2d, 0x2e, 0x68, 0x83, - 0x0b, 0xb7, 0xd5, 0xa5, 0x8d, 0x60, 0xb7, 0xd1, 0xa6, 0x3e, 0x95, 0x4c, 0x5a, 0x7d, 0xc1, 0x15, - 0x27, 0xb5, 0x48, 0x60, 0x69, 0x81, 0x15, 0xec, 0xae, 0xef, 0x64, 0x73, 0x98, 0xef, 0xd1, 0x0b, - 0x47, 0xd0, 0x16, 0xf5, 0x95, 0xd3, 0x93, 0x6d, 0x9d, 0xbc, 0xfe, 0xf8, 0x3f, 0xca, 0xbe, 0x2b, - 0xdc, 0x5e, 0x54, 0x69, 0x7d, 0x3b, 0x2b, 0xf6, 0x5d, 0xc5, 0x02, 0xea, 0x28, 0xde, 0xa1, 0x7e, - 0xa4, 0x32, 0xb3, 0xaa, 0x14, 0x25, 0x2f, 0x2e, 0x58, 0x8b, 0xc6, 0xf1, 0x7a, 0x36, 0x9e, 0x69, - 0xfb, 0xc1, 0x58, 0x4d, 0xaa, 0xd4, 0x56, 0x56, 0x26, 0xbb, 0xae, 0x3c, 0x63, 0x7e, 0x0c, 0xb2, - 0xb2, 0x8a, 0xc0, 0xed, 0x32, 0xcf, 0x55, 0x5c, 0x38, 0xe7, 0x7d, 0xcf, 0x55, 0xd4, 0x69, 0x76, - 0x79, 0xab, 0x13, 0xe9, 0x97, 0xdb, 0xbc, 0xcd, 0xf1, 0xb3, 0x11, 0x7e, 0xe9, 0xdd, 0xfa, 0xaf, - 0x32, 0x2c, 0xbc, 0xd5, 0x43, 0x39, 0x56, 0xae, 0xa2, 0xe4, 0x39, 0x94, 0x74, 0x23, 0x46, 0x71, - 0xab, 0xb8, 0x53, 0x79, 0xba, 0x66, 0x65, 0x86, 0x64, 0x7d, 0x44, 0xc1, 0xfe, 0xcc, 0xe5, 0x9f, - 0xcd, 0x82, 0x1d, 0xc9, 0xc9, 0x1e, 0x54, 0xb4, 0x19, 0x4e, 0x97, 0x49, 0x65, 0x4c, 0x6d, 0x4d, - 0x8f, 0xcb, 0x46, 0x55, 0x94, 0x0d, 0x3a, 0xe7, 0x3d, 0x93, 0x8a, 0x7c, 0x83, 0xd5, 0xfc, 0x13, - 0x18, 0xd3, 0xd8, 0xca, 0xa3, 0x1c, 0xd8, 0x97, 0x38, 0xe1, 0x33, 0xea, 0xf7, 0x43, 0xb9, 0xbd, - 0x1c, 0xe4, 0xec, 0x92, 0x4f, 0xb0, 0x94, 0x73, 0x41, 0x8c, 0x19, 0x64, 0x6f, 0xe7, 0xb0, 0x0f, - 0x42, 0xb5, 0x8d, 0x62, 0x7d, 0x62, 0xbb, 0xc6, 0x46, 0xb7, 0xc8, 0x3b, 0xa8, 0x8e, 0x5e, 0x50, - 0x63, 0x16, 0x91, 0xf7, 0x26, 0x23, 0x8f, 0x64, 0xdb, 0xbe, 0xc5, 0x52, 0x6b, 0x72, 0x08, 0xb7, - 0x87, 0x18, 0xed, 0x63, 0x09, 0x7d, 0xbc, 0x9b, 0xc3, 0xba, 0x49, 0x8b, 0xac, 0x5c, 0x14, 0xf1, - 0x06, 0xba, 0x79, 0x0c, 0x24, 0x75, 0x50, 0x8d, 0x2b, 0x23, 0x6e, 0x73, 0x2c, 0x2e, 0x35, 0xda, - 0xaa, 0x48, 0xec, 0x21, 0xf4, 0x04, 0x96, 0xa4, 0x72, 0x3b, 0x54, 0x38, 0xcc, 0x3f, 0xe5, 0xd2, - 0x71, 0xa5, 0xa4, 0x4a, 0x1a, 0x73, 0x48, 0xcd, 0xf3, 0xf0, 0x18, 0xd5, 0x07, 0xa1, 0xf8, 0x15, - 0x6a, 0x23, 0x74, 0x4d, 0x8e, 0x06, 0xc8, 0x57, 0x20, 0x11, 0x3b, 0xec, 0x34, 0x46, 0xcf, 0x23, - 0xfa, 0xfe, 0x58, 0x74, 0xd8, 0x56, 0x8a, 0x5c, 0x95, 0x23, 0xfb, 0xa4, 0x99, 0xbc, 0x57, 0x82, - 0xf6, 0xb9, 0x50, 0xba, 0x7d, 0x03, 0x10, 0xfe, 0x70, 0xd2, 0xbd, 0xb2, 0x51, 0x1f, 0xf6, 0x19, - 0xf1, 0x87, 0x97, 0x6b, 0x18, 0x92, 0xe4, 0x14, 0xee, 0x0c, 0x6b, 0xf4, 0x98, 0x94, 0xd4, 0x73, - 0x04, 0x3f, 0xf7, 0x3d, 0x69, 0x54, 0xb0, 0xc8, 0xce, 0xa4, 0x22, 0x47, 0x98, 0x60, 0xa3, 0x3e, - 0x2a, 0xb3, 0x12, 0xe4, 0x05, 0xeb, 0x7d, 0xa8, 0x65, 0x2c, 0x25, 0x6b, 0x30, 0x87, 0x6e, 0x39, - 0xcc, 0xc3, 0x57, 0x3b, 0x6f, 0x97, 0x71, 0x7d, 0xe0, 0x91, 0x3d, 0x58, 0x48, 0x0e, 0x2c, 0x7a, - 0x96, 0x1b, 0x13, 0x27, 0x65, 0x57, 0x12, 0xc3, 0xa9, 0xf7, 0xa0, 0x3a, 0xea, 0xf4, 0xa4, 0x82, - 0x2f, 0xa1, 0x92, 0x98, 0xa2, 0x31, 0x85, 0x4f, 0x61, 0x63, 0xe2, 0xf8, 0x6c, 0x18, 0x8e, 0xac, - 0x1e, 0xc0, 0x4a, 0xae, 0x2d, 0xc4, 0x80, 0xb2, 0xeb, 0x79, 0x82, 0x4a, 0x79, 0x53, 0x52, 0x2f, - 0xc9, 0x6b, 0x58, 0x4c, 0x3b, 0xae, 0x0f, 0x69, 0xe6, 0x14, 0x4d, 0x10, 0xed, 0x85, 0x5e, 0xd2, - 0xd8, 0x17, 0x50, 0x49, 0x04, 0xc9, 0x32, 0xcc, 0xe2, 0xdb, 0xc4, 0x5a, 0x33, 0xb6, 0x5e, 0x90, - 0x55, 0x28, 0xe9, 0x24, 0x3c, 0xd7, 0x9c, 0x1d, 0xad, 0xf6, 0x0f, 0x2f, 0xaf, 0xcd, 0xe2, 0xd5, - 0xb5, 0x59, 0xfc, 0x7b, 0x6d, 0x16, 0x7f, 0x0e, 0xcc, 0xc2, 0xd5, 0xc0, 0x2c, 0xfc, 0x1e, 0x98, - 0x85, 0x93, 0x27, 0x6d, 0xa6, 0xce, 0xce, 0x9b, 0x56, 0x8b, 0xf7, 0x1a, 0x6f, 0x74, 0x3b, 0x1f, - 0xa8, 0xfa, 0xce, 0x45, 0xa7, 0x11, 0xff, 0xbf, 0x2f, 0xe2, 0x3f, 0xb8, 0xfa, 0xd1, 0xa7, 0xb2, - 0x59, 0xc2, 0x1f, 0xf3, 0xb3, 0x7f, 0x01, 0x00, 0x00, 0xff, 0xff, 0x3f, 0xd5, 0x91, 0x78, 0x3e, - 0x07, 0x00, 0x00, + // 715 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x84, 0x55, 0xdd, 0x4e, 0x13, 0x41, + 0x14, 0x6e, 0xf9, 0x69, 0xe1, 0x14, 0xb4, 0x1d, 0x7e, 0x5c, 0x88, 0x6c, 0xb1, 0xa2, 0x92, 0x98, + 0xb4, 0xa2, 0x17, 0x5e, 0x98, 0x18, 0xc4, 0x18, 0x03, 0x8a, 0x31, 0x83, 0x62, 0x42, 0x62, 0x36, + 0xdb, 0xee, 0x50, 0x26, 0x6d, 0x77, 0x9a, 0x99, 0x61, 0xc5, 0x97, 0x30, 0xfa, 0x20, 0xbe, 0x07, + 0x97, 0x5c, 0x7a, 0x65, 0x0c, 0xbc, 0x88, 0xe9, 0x99, 0x2d, 0xdd, 0xed, 0x6e, 0xcb, 0x5d, 0xe7, + 0x9c, 0xef, 0x7c, 0xe7, 0x9b, 0xf3, 0xcd, 0xe9, 0x42, 0x99, 0x9d, 0x89, 0x86, 0x90, 0xac, 0x26, + 0xa4, 0xdb, 0x68, 0xb3, 0x5a, 0xb0, 0x55, 0x6b, 0x32, 0x9f, 0x29, 0xae, 0xaa, 0x5d, 0x29, 0xb4, + 0x20, 0xa5, 0x10, 0x50, 0x35, 0x80, 0x6a, 0xb0, 0xb5, 0xba, 0x99, 0xac, 0xe1, 0xbe, 0xc7, 0xce, + 0x1c, 0xc9, 0x1a, 0xcc, 0xd7, 0x4e, 0x47, 0x35, 0x4d, 0xf1, 0xea, 0xe3, 0x1b, 0x90, 0x5d, 0x57, + 0xba, 0x9d, 0xb0, 0xd3, 0xea, 0x46, 0x12, 0xec, 0xbb, 0x9a, 0x07, 0xcc, 0xd1, 0xa2, 0xc5, 0xfc, + 0x10, 0x65, 0x27, 0x51, 0x31, 0x96, 0xb4, 0xbc, 0xe4, 0x0d, 0xd6, 0xcf, 0x57, 0x92, 0xf9, 0x84, + 0xec, 0x07, 0x23, 0x31, 0xb1, 0x56, 0xeb, 0x49, 0x98, 0x6a, 0xbb, 0xea, 0x84, 0xfb, 0x7d, 0xa2, + 0x6a, 0x12, 0x11, 0xb8, 0x6d, 0xee, 0xb9, 0x5a, 0x48, 0xe7, 0xb4, 0xeb, 0xb9, 0x9a, 0x39, 0xf5, + 0xb6, 0x68, 0xb4, 0x42, 0xfc, 0x62, 0x53, 0x34, 0x05, 0xfe, 0xac, 0xf5, 0x7e, 0x99, 0x68, 0xe5, + 0x77, 0x1e, 0xe6, 0xde, 0x1a, 0x53, 0x0e, 0xb4, 0xab, 0x19, 0x79, 0x0e, 0x39, 0x23, 0xc4, 0xca, + 0xae, 0x67, 0x37, 0x0b, 0x4f, 0x57, 0xaa, 0x09, 0x93, 0xaa, 0x1f, 0x11, 0xb0, 0x33, 0x75, 0xfe, + 0xb7, 0x9c, 0xa1, 0x21, 0x9c, 0x6c, 0x43, 0xc1, 0x0c, 0xc3, 0x69, 0x73, 0xa5, 0xad, 0x89, 0xf5, + 0xc9, 0x51, 0xd5, 0x88, 0x0a, 0xab, 0xc1, 0xd4, 0xbc, 0xe7, 0x4a, 0x93, 0xaf, 0xb0, 0x9c, 0x7e, + 0x03, 0x6b, 0x12, 0xa5, 0x3c, 0x4a, 0x21, 0x3b, 0xec, 0x17, 0x7c, 0x46, 0xfc, 0x4e, 0x0f, 0x4e, + 0x17, 0x83, 0x94, 0x28, 0xf9, 0x04, 0x0b, 0x29, 0x0f, 0xc4, 0x9a, 0x42, 0xee, 0x8d, 0x14, 0xee, + 0xdd, 0x1e, 0x9a, 0x22, 0xd8, 0xdc, 0x98, 0x96, 0xf8, 0x70, 0x88, 0xbc, 0x83, 0xe2, 0xf0, 0x03, + 0xb5, 0xa6, 0x91, 0xf2, 0xde, 0x78, 0xca, 0x7d, 0xd5, 0xa4, 0xb7, 0x78, 0xec, 0x4c, 0xf6, 0xe0, + 0xf6, 0x80, 0xc6, 0xcc, 0x31, 0x87, 0x73, 0xbc, 0x9b, 0xc2, 0x75, 0x5d, 0x16, 0x8e, 0x72, 0x5e, + 0xf6, 0x03, 0x38, 0xcd, 0x03, 0x20, 0xb1, 0x8b, 0x1a, 0xba, 0x3c, 0xd2, 0x95, 0x47, 0xd2, 0xc5, + 0xac, 0x2d, 0xca, 0x48, 0x0c, 0x49, 0x8f, 0x60, 0x41, 0x69, 0xb7, 0xc5, 0xa4, 0xc3, 0xfd, 0x63, + 0xa1, 0x1c, 0x57, 0x29, 0xa6, 0x95, 0x35, 0x83, 0xac, 0x69, 0x33, 0x3c, 0x40, 0xf4, 0x6e, 0x0f, + 0xfc, 0x0a, 0xb1, 0x21, 0x75, 0x49, 0x0d, 0x27, 0xc8, 0x17, 0x20, 0x21, 0x77, 0x4f, 0x69, 0x9f, + 0x7a, 0x16, 0xa9, 0xef, 0x8f, 0xa4, 0xee, 0xc9, 0x8a, 0x31, 0x17, 0xd5, 0x50, 0x9c, 0xd4, 0xa3, + 0xef, 0x4a, 0xb2, 0xae, 0x90, 0xda, 0xc8, 0xb7, 0x00, 0xc9, 0x1f, 0x8e, 0x7b, 0x57, 0x14, 0xf1, + 0x3d, 0x9d, 0x21, 0xff, 0xe0, 0x71, 0x0d, 0x52, 0x8a, 0x1c, 0xc3, 0x9d, 0x41, 0x8f, 0x0e, 0x57, + 0x8a, 0x79, 0x8e, 0x14, 0xa7, 0xbe, 0xa7, 0xac, 0x02, 0x36, 0xd9, 0x1c, 0xd7, 0x64, 0x1f, 0x0b, + 0x28, 0xe2, 0xc3, 0x36, 0x4b, 0x41, 0x5a, 0xb2, 0xf2, 0x2b, 0x0b, 0xa5, 0xc4, 0x4c, 0xc9, 0x0a, + 0xcc, 0xe0, 0xb8, 0x1c, 0xee, 0xe1, 0xda, 0xce, 0xd2, 0x3c, 0x9e, 0x77, 0x3d, 0xb2, 0x0d, 0x73, + 0x51, 0xc7, 0xc2, 0xbd, 0x5c, 0x1b, 0x6b, 0x15, 0x2d, 0x44, 0xdc, 0x21, 0x65, 0x28, 0xf8, 0x4a, + 0x3b, 0x01, 0x93, 0x8a, 0x0b, 0x1f, 0x77, 0x71, 0x92, 0x82, 0xaf, 0xf4, 0xa1, 0x89, 0x54, 0x7e, + 0x64, 0xa1, 0x38, 0x6c, 0xc6, 0x38, 0x49, 0x2f, 0xa1, 0x10, 0x31, 0xda, 0x9a, 0xc0, 0x6d, 0x59, + 0x1b, 0xeb, 0x30, 0x85, 0x81, 0xab, 0x37, 0x0b, 0x0a, 0x60, 0x29, 0x75, 0xb4, 0xc4, 0x82, 0xbc, + 0xeb, 0x79, 0x92, 0x29, 0x75, 0xad, 0xc9, 0x1c, 0xc9, 0x6b, 0x98, 0x8f, 0xbb, 0x66, 0xe6, 0x64, + 0xa7, 0xa8, 0x8a, 0x30, 0xd2, 0xb9, 0x4e, 0xd4, 0x9c, 0x17, 0x50, 0x88, 0x24, 0xc9, 0x22, 0x4c, + 0xe3, 0x7e, 0x63, 0xaf, 0x29, 0x6a, 0x0e, 0x64, 0x19, 0x72, 0xa6, 0x08, 0x2f, 0x3e, 0x43, 0xc3, + 0xd3, 0xce, 0xde, 0xf9, 0xa5, 0x9d, 0xbd, 0xb8, 0xb4, 0xb3, 0xff, 0x2e, 0xed, 0xec, 0xcf, 0x2b, + 0x3b, 0x73, 0x71, 0x65, 0x67, 0xfe, 0x5c, 0xd9, 0x99, 0xa3, 0x27, 0x4d, 0xae, 0x4f, 0x4e, 0xeb, + 0xd5, 0x86, 0xe8, 0xd4, 0xde, 0x18, 0x39, 0x1f, 0x98, 0xfe, 0x26, 0x64, 0xab, 0xd6, 0xff, 0x06, + 0x9c, 0xf5, 0xbf, 0x02, 0xfa, 0x7b, 0x97, 0xa9, 0x7a, 0x0e, 0xff, 0xdc, 0x9f, 0xfd, 0x0f, 0x00, + 0x00, 0xff, 0xff, 0x41, 0x47, 0xc5, 0x22, 0x82, 0x07, 0x00, 0x00, } func (m *GenesisState) Marshal() (dAtA []byte, err error) { @@ -628,6 +647,11 @@ func (m *StakerInfosAssets) MarshalToSizedBuffer(dAtA []byte) (int, error) { _ = i var l int _ = l + if m.NstVersion != 0 { + i = encodeVarintGenesis(dAtA, i, uint64(m.NstVersion)) + i-- + dAtA[i] = 0x18 + } if len(m.StakerInfos) > 0 { for iNdEx := len(m.StakerInfos) - 1; iNdEx >= 0; iNdEx-- { { @@ -672,6 +696,11 @@ func (m *StakerListAssets) MarshalToSizedBuffer(dAtA []byte) (int, error) { _ = i var l int _ = l + if m.NstVersion != 0 { + i = encodeVarintGenesis(dAtA, i, uint64(m.NstVersion)) + i-- + dAtA[i] = 0x18 + } if m.StakerList != nil { { size, err := m.StakerList.MarshalToSizedBuffer(dAtA[:i]) @@ -868,6 +897,9 @@ func (m *StakerInfosAssets) Size() (n int) { n += 1 + l + sovGenesis(uint64(l)) } } + if m.NstVersion != 0 { + n += 1 + sovGenesis(uint64(m.NstVersion)) + } return n } @@ -885,6 +917,9 @@ func (m *StakerListAssets) Size() (n int) { l = m.StakerList.Size() n += 1 + l + sovGenesis(uint64(l)) } + if m.NstVersion != 0 { + n += 1 + sovGenesis(uint64(m.NstVersion)) + } return n } @@ -1452,6 +1487,25 @@ func (m *StakerInfosAssets) Unmarshal(dAtA []byte) error { return err } iNdEx = postIndex + case 3: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field NstVersion", wireType) + } + m.NstVersion = 0 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowGenesis + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + m.NstVersion |= int64(b&0x7F) << shift + if b < 0x80 { + break + } + } default: iNdEx = preIndex skippy, err := skipGenesis(dAtA[iNdEx:]) @@ -1570,6 +1624,25 @@ func (m *StakerListAssets) Unmarshal(dAtA []byte) error { return err } iNdEx = postIndex + case 3: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field NstVersion", wireType) + } + m.NstVersion = 0 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowGenesis + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + m.NstVersion |= int64(b&0x7F) << shift + if b < 0x80 { + break + } + } default: iNdEx = preIndex skippy, err := skipGenesis(dAtA[iNdEx:]) diff --git a/x/oracle/types/genesis_test.go b/x/oracle/types/genesis_test.go index bf73106b9..c0f551a7c 100644 --- a/x/oracle/types/genesis_test.go +++ b/x/oracle/types/genesis_test.go @@ -62,7 +62,6 @@ func TestGenesisState_Validate(t *testing.T) { MinReportedPerWindow: sdkmath.LegacyNewDec(1).Quo(sdkmath.LegacyNewDec(2)), OracleMissJailDuration: 600 * time.Second, OracleMaliciousJailDuration: 30 * 24 * time.Hour, - SlashFractionMiss: sdkmath.LegacyNewDec(1).Quo(sdkmath.LegacyNewDec(20)), SlashFractionMalicious: sdkmath.LegacyNewDec(1).Quo(sdkmath.LegacyNewDec(10)), }, }, diff --git a/x/oracle/types/key_native_token.go b/x/oracle/types/key_native_token.go index 9c7797cf0..0d444f741 100644 --- a/x/oracle/types/key_native_token.go +++ b/x/oracle/types/key_native_token.go @@ -8,6 +8,7 @@ const ( NativeTokenPriceKeyPrefix = NativeTokenKeyPrefix + "price/value/" NativeTokenStakerInfoKeyPrefix = NativeTokenKeyPrefix + "stakerInfo/value/" NativeTokenStakerListKeyPrefix = NativeTokenKeyPrefix + "stakerList/value/" + NativeTokenVersionKeyPrefix = NativeTokenKeyPrefix + "version/" ) // NativeTokenStakerKeyPrefix returns the prefix for stakerInfo key @@ -41,3 +42,7 @@ func ParseNativeTokenStakerKey(key []byte) (assetID, stakerAddr string) { } return parsed[0], parsed[1] } + +func NativeTokenVersionKey(assetID string) []byte { + return append([]byte(NativeTokenVersionKeyPrefix), []byte(assetID)...) +} diff --git a/x/oracle/types/key_slashing.go b/x/oracle/types/key_slashing.go index ec5f13ee2..893a83ccb 100644 --- a/x/oracle/types/key_slashing.go +++ b/x/oracle/types/key_slashing.go @@ -10,11 +10,15 @@ func SlashingValidatorReportInfoKey(validator string) []byte { return append(ValidatorReportInfoPrefix, []byte(validator)...) } -func SlashingMissedBitArrayPrefix(validator string) []byte { +func SlashingMissedBitArrayPrefix() []byte { + return MissedBitArrayPrefix +} + +func SlashingMissedBitArrayValidatorPrefix(validator string) []byte { key := append([]byte(validator), DelimiterForCombinedKey) return append(MissedBitArrayPrefix, key...) } func SlashingMissedBitArrayKey(validator string, index uint64) []byte { - return append(SlashingMissedBitArrayPrefix(validator), Uint64Bytes(index)...) + return append(SlashingMissedBitArrayValidatorPrefix(validator), Uint64Bytes(index)...) } diff --git a/x/oracle/types/message_create_price.go b/x/oracle/types/message_create_price.go index 9a98da606..ebd2b591e 100644 --- a/x/oracle/types/message_create_price.go +++ b/x/oracle/types/message_create_price.go @@ -6,12 +6,12 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" ) -const TypeMsgCreatePrice = "create_price" +const TypeMsgPriceFeed = "create_price" -var _ sdk.Msg = &MsgCreatePrice{} +var _ sdk.Msg = &MsgPriceFeed{} -func NewMsgCreatePrice(creator string, feederID uint64, prices []*PriceSource, basedBlock uint64, nonce int32) *MsgCreatePrice { - return &MsgCreatePrice{ +func NewMsgPriceFeed(creator string, feederID uint64, prices []*PriceSource, basedBlock uint64, nonce int32) *MsgPriceFeed { + return &MsgPriceFeed{ Creator: creator, FeederID: feederID, Prices: prices, @@ -20,15 +20,15 @@ func NewMsgCreatePrice(creator string, feederID uint64, prices []*PriceSource, b } } -func (msg *MsgCreatePrice) Route() string { +func (msg *MsgPriceFeed) Route() string { return RouterKey } -func (msg *MsgCreatePrice) Type() string { - return TypeMsgCreatePrice +func (msg *MsgPriceFeed) Type() string { + return TypeMsgPriceFeed } -func (msg *MsgCreatePrice) GetSigners() []sdk.AccAddress { +func (msg *MsgPriceFeed) GetSigners() []sdk.AccAddress { creator, err := sdk.AccAddressFromBech32(msg.Creator) if err != nil { panic(err) @@ -36,12 +36,12 @@ func (msg *MsgCreatePrice) GetSigners() []sdk.AccAddress { return []sdk.AccAddress{creator} } -func (msg *MsgCreatePrice) GetSignBytes() []byte { +func (msg *MsgPriceFeed) GetSignBytes() []byte { bz := ModuleCdc.MustMarshalJSON(msg) return sdk.MustSortJSON(bz) } -func (msg *MsgCreatePrice) ValidateBasic() error { +func (msg *MsgPriceFeed) ValidateBasic() error { _, err := sdk.AccAddressFromBech32(msg.Creator) if err != nil { return sdkerrors.ErrInvalidAddress.Wrapf("invalid creator address (%s)", err) diff --git a/x/oracle/types/message_create_price_test.go b/x/oracle/types/message_create_price_test.go index 8b53e6aeb..b386ae959 100644 --- a/x/oracle/types/message_create_price_test.go +++ b/x/oracle/types/message_create_price_test.go @@ -10,21 +10,21 @@ import ( "github.com/stretchr/testify/require" ) -func TestMsgCreatePrice_ValidateBasic(t *testing.T) { +func TestMsgPriceFeed_ValidateBasic(t *testing.T) { tests := []struct { name string - msg MsgCreatePrice + msg MsgPriceFeed err error }{ { name: "invalid address", - msg: MsgCreatePrice{ + msg: MsgPriceFeed{ Creator: "invalid_address", }, err: sdkerrors.ErrInvalidAddress, }, { name: "valid address", - msg: MsgCreatePrice{ + msg: MsgPriceFeed{ Creator: sample.AccAddress(), }, }, diff --git a/x/oracle/types/native_token.pb.go b/x/oracle/types/native_token.pb.go index 7d7f8e05f..43e2fe110 100644 --- a/x/oracle/types/native_token.pb.go +++ b/x/oracle/types/native_token.pb.go @@ -261,11 +261,67 @@ func (m *StakerList) GetStakerAddrs() []string { return nil } +// NSTVersion used to track the validator list changes for each NST +type NSTVersion struct { + // asset_id of the NST + AssetId string `protobuf:"bytes,1,opt,name=asset_id,json=assetId,proto3" json:"asset_id,omitempty"` + // version of the validator list + Version int64 `protobuf:"varint,2,opt,name=version,proto3" json:"version,omitempty"` +} + +func (m *NSTVersion) Reset() { *m = NSTVersion{} } +func (m *NSTVersion) String() string { return proto.CompactTextString(m) } +func (*NSTVersion) ProtoMessage() {} +func (*NSTVersion) Descriptor() ([]byte, []int) { + return fileDescriptor_ed348d855b8fd9a8, []int{3} +} +func (m *NSTVersion) XXX_Unmarshal(b []byte) error { + return m.Unmarshal(b) +} +func (m *NSTVersion) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + if deterministic { + return xxx_messageInfo_NSTVersion.Marshal(b, m, deterministic) + } else { + b = b[:cap(b)] + n, err := m.MarshalToSizedBuffer(b) + if err != nil { + return nil, err + } + return b[:n], nil + } +} +func (m *NSTVersion) XXX_Merge(src proto.Message) { + xxx_messageInfo_NSTVersion.Merge(m, src) +} +func (m *NSTVersion) XXX_Size() int { + return m.Size() +} +func (m *NSTVersion) XXX_DiscardUnknown() { + xxx_messageInfo_NSTVersion.DiscardUnknown(m) +} + +var xxx_messageInfo_NSTVersion proto.InternalMessageInfo + +func (m *NSTVersion) GetAssetId() string { + if m != nil { + return m.AssetId + } + return "" +} + +func (m *NSTVersion) GetVersion() int64 { + if m != nil { + return m.Version + } + return 0 +} + func init() { proto.RegisterEnum("exocore.oracle.v1.Action", Action_name, Action_value) proto.RegisterType((*BalanceInfo)(nil), "exocore.oracle.v1.BalanceInfo") proto.RegisterType((*StakerInfo)(nil), "exocore.oracle.v1.StakerInfo") proto.RegisterType((*StakerList)(nil), "exocore.oracle.v1.StakerList") + proto.RegisterType((*NSTVersion)(nil), "exocore.oracle.v1.NSTVersion") } func init() { @@ -273,37 +329,40 @@ func init() { } var fileDescriptor_ed348d855b8fd9a8 = []byte{ - // 479 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x6c, 0x52, 0xdd, 0x6e, 0xd3, 0x30, - 0x18, 0xad, 0x9b, 0xae, 0x65, 0xce, 0x34, 0x8a, 0x3b, 0x44, 0x40, 0x28, 0x2b, 0x15, 0x42, 0x11, - 0x17, 0x09, 0x2d, 0x4f, 0x90, 0x2e, 0x99, 0x16, 0x34, 0xa5, 0x95, 0xd3, 0x6a, 0x12, 0x37, 0x51, - 0x7e, 0x4c, 0x1b, 0x25, 0xc4, 0x55, 0xe2, 0x96, 0xee, 0x2d, 0x78, 0x12, 0x5e, 0x03, 0x2e, 0x77, - 0xc9, 0x15, 0x42, 0xed, 0x8b, 0xa0, 0xd8, 0x29, 0x43, 0x62, 0x77, 0x3e, 0x3f, 0xb6, 0xcf, 0xf9, - 0xf4, 0xc1, 0xd7, 0x64, 0x4b, 0x23, 0x5a, 0x10, 0x83, 0x16, 0x41, 0x94, 0x11, 0x63, 0x33, 0x34, - 0xf2, 0x80, 0x25, 0x1b, 0xe2, 0x33, 0x9a, 0x92, 0x5c, 0x5f, 0x15, 0x94, 0x51, 0xf4, 0xa4, 0x76, - 0xe9, 0xc2, 0xa5, 0x6f, 0x86, 0x2f, 0xce, 0x16, 0x74, 0x41, 0xb9, 0x6a, 0x54, 0x27, 0x61, 0x1c, - 0x7c, 0x03, 0x50, 0x1e, 0x07, 0x59, 0x90, 0x47, 0xc4, 0xc9, 0x3f, 0x51, 0xf4, 0x06, 0x3e, 0x2a, - 0xe8, 0x3a, 0x8f, 0xfd, 0x24, 0x56, 0x40, 0x1f, 0x68, 0xad, 0xb1, 0xbc, 0xfb, 0x75, 0xde, 0xc1, - 0x15, 0xe7, 0x58, 0xb8, 0xc3, 0x45, 0x27, 0x46, 0x67, 0xf0, 0x28, 0xcc, 0x68, 0x94, 0x2a, 0xcd, - 0xca, 0x84, 0x05, 0xa8, 0xd8, 0x24, 0x8f, 0xc9, 0x56, 0x91, 0x04, 0xcb, 0x01, 0x52, 0x60, 0x27, - 0x14, 0x5f, 0x28, 0xad, 0x3e, 0xd0, 0x24, 0x7c, 0x80, 0x68, 0x08, 0xdb, 0xd1, 0x32, 0xc8, 0x17, - 0x44, 0x39, 0xea, 0x03, 0xed, 0x74, 0xf4, 0x5c, 0xff, 0x2f, 0xb7, 0x6e, 0x46, 0x2c, 0xa1, 0x39, - 0xae, 0x8d, 0x83, 0xef, 0x00, 0x42, 0x8f, 0x05, 0x29, 0x29, 0x78, 0xde, 0x73, 0x28, 0x97, 0x1c, - 0xf9, 0x41, 0x1c, 0x17, 0x3c, 0xf2, 0x31, 0x86, 0x82, 0x32, 0xe3, 0xb8, 0x40, 0xaf, 0xe0, 0x49, - 0x6d, 0x10, 0xc9, 0x9a, 0x3c, 0x41, 0x7d, 0xc9, 0xe1, 0xf9, 0x46, 0xf0, 0xe9, 0x26, 0xc8, 0x92, - 0x38, 0x60, 0xb4, 0xf0, 0x57, 0xeb, 0x30, 0x25, 0xb7, 0x7e, 0x96, 0x94, 0x4c, 0x91, 0xfa, 0x92, - 0x76, 0x8c, 0x7b, 0x7f, 0xc5, 0x29, 0xd7, 0xae, 0x93, 0x92, 0x21, 0x13, 0x9e, 0xd4, 0x25, 0x84, - 0xb5, 0xd5, 0x97, 0x34, 0x79, 0xa4, 0x3e, 0x90, 0xff, 0x9f, 0xe9, 0x62, 0xb9, 0xbe, 0x53, 0x3d, - 0x31, 0x30, 0x0e, 0x45, 0xf8, 0x83, 0xf7, 0x39, 0xab, 0x22, 0xa5, 0x02, 0xf8, 0xdf, 0xf2, 0x7d, - 0x93, 0xf2, 0xed, 0x12, 0xb6, 0xc5, 0x30, 0xd0, 0x4b, 0xa8, 0x98, 0x17, 0x33, 0x67, 0xe2, 0xfa, - 0x78, 0x32, 0x77, 0x2d, 0x7f, 0xee, 0x7a, 0x53, 0xfb, 0xc2, 0xb9, 0x74, 0x6c, 0xab, 0xdb, 0x40, - 0x08, 0x9e, 0xd6, 0xaa, 0x65, 0x4f, 0x27, 0x9e, 0x33, 0xeb, 0x02, 0xd4, 0x83, 0x8f, 0x6b, 0xee, - 0xc6, 0x99, 0x5d, 0x59, 0xd8, 0xbc, 0xe9, 0x36, 0xd1, 0x33, 0xd8, 0xab, 0x49, 0xef, 0xda, 0xf4, - 0xae, 0x7c, 0x6c, 0x5f, 0xce, 0x5d, 0xab, 0x2b, 0x8d, 0x3f, 0xfc, 0xd8, 0xa9, 0xe0, 0x6e, 0xa7, - 0x82, 0xdf, 0x3b, 0x15, 0x7c, 0xdd, 0xab, 0x8d, 0xbb, 0xbd, 0xda, 0xf8, 0xb9, 0x57, 0x1b, 0x1f, - 0xdf, 0x2d, 0x12, 0xb6, 0x5c, 0x87, 0x7a, 0x44, 0x3f, 0x1b, 0xb6, 0xe8, 0xea, 0x12, 0xf6, 0x85, - 0x16, 0xa9, 0x71, 0x58, 0xcc, 0xed, 0x61, 0x35, 0xd9, 0xed, 0x8a, 0x94, 0x61, 0x9b, 0x2f, 0xda, - 0xfb, 0x3f, 0x01, 0x00, 0x00, 0xff, 0xff, 0xa9, 0xb0, 0x3a, 0xf3, 0xb9, 0x02, 0x00, 0x00, + // 513 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x6c, 0x52, 0xdd, 0x8e, 0xd2, 0x40, + 0x14, 0x66, 0xb6, 0xbb, 0xb0, 0x9c, 0x6e, 0x56, 0x1c, 0xd6, 0xd8, 0x35, 0xa6, 0x8b, 0xc4, 0x18, + 0xe2, 0x45, 0x2b, 0xf8, 0x04, 0x65, 0xcb, 0x66, 0x6b, 0x36, 0x85, 0x4c, 0xc1, 0x4d, 0xbc, 0x69, + 0x4a, 0x3b, 0x42, 0x03, 0x76, 0x48, 0x3b, 0x20, 0xfb, 0x16, 0x3e, 0x89, 0xaf, 0xa1, 0x97, 0x7b, + 0xe9, 0x95, 0x31, 0xf0, 0x22, 0xa6, 0x33, 0xad, 0x6b, 0xa2, 0x77, 0xfd, 0x7e, 0x66, 0xe6, 0xfb, + 0x4e, 0x0f, 0xbc, 0xa4, 0x5b, 0x16, 0xb2, 0x94, 0x9a, 0x2c, 0x0d, 0xc2, 0x25, 0x35, 0x37, 0x5d, + 0x33, 0x09, 0x78, 0xbc, 0xa1, 0x3e, 0x67, 0x0b, 0x9a, 0x18, 0xab, 0x94, 0x71, 0x86, 0x1f, 0x17, + 0x2e, 0x43, 0xba, 0x8c, 0x4d, 0xf7, 0xd9, 0xd9, 0x8c, 0xcd, 0x98, 0x50, 0xcd, 0xfc, 0x4b, 0x1a, + 0xdb, 0x5f, 0x11, 0xa8, 0xfd, 0x60, 0x19, 0x24, 0x21, 0x75, 0x92, 0x8f, 0x0c, 0xbf, 0x82, 0xe3, + 0x94, 0xad, 0x93, 0xc8, 0x8f, 0x23, 0x0d, 0xb5, 0x50, 0xe7, 0xb0, 0xaf, 0xee, 0x7e, 0x5e, 0xd4, + 0x48, 0xce, 0x39, 0x36, 0xa9, 0x09, 0xd1, 0x89, 0xf0, 0x19, 0x1c, 0x4d, 0x97, 0x2c, 0x5c, 0x68, + 0x07, 0xb9, 0x89, 0x48, 0x90, 0xb3, 0x71, 0x12, 0xd1, 0xad, 0xa6, 0x48, 0x56, 0x00, 0xac, 0x41, + 0x6d, 0x2a, 0x9f, 0xd0, 0x0e, 0x5b, 0xa8, 0xa3, 0x90, 0x12, 0xe2, 0x2e, 0x54, 0xc3, 0x79, 0x90, + 0xcc, 0xa8, 0x76, 0xd4, 0x42, 0x9d, 0xd3, 0xde, 0xb9, 0xf1, 0x4f, 0x6e, 0xc3, 0x0a, 0x79, 0xcc, + 0x12, 0x52, 0x18, 0xdb, 0xdf, 0x10, 0x80, 0xc7, 0x83, 0x05, 0x4d, 0x45, 0xde, 0x0b, 0x50, 0x33, + 0x81, 0xfc, 0x20, 0x8a, 0x52, 0x11, 0xb9, 0x4e, 0x40, 0x52, 0x56, 0x14, 0xa5, 0xf8, 0x05, 0x9c, + 0x14, 0x06, 0x99, 0xec, 0x40, 0x24, 0x28, 0x0e, 0x39, 0x22, 0x5f, 0x0f, 0x9e, 0x6c, 0x82, 0x65, + 0x1c, 0x05, 0x9c, 0xa5, 0xfe, 0x6a, 0x3d, 0x5d, 0xd0, 0x3b, 0x7f, 0x19, 0x67, 0x5c, 0x53, 0x5a, + 0x4a, 0xa7, 0x4e, 0x9a, 0x7f, 0xc4, 0x91, 0xd0, 0x6e, 0xe2, 0x8c, 0x63, 0x0b, 0x4e, 0x8a, 0x12, + 0xd2, 0x7a, 0xd8, 0x52, 0x3a, 0x6a, 0x4f, 0xff, 0x4f, 0xfe, 0xbf, 0xa6, 0x4b, 0xd4, 0xe2, 0x4c, + 0x7e, 0x45, 0xdb, 0x2c, 0x8b, 0x88, 0x0b, 0x1f, 0x72, 0xe6, 0x45, 0x32, 0x0d, 0x89, 0xb7, 0xd5, + 0x87, 0x26, 0x59, 0xdb, 0x02, 0x70, 0xbd, 0xf1, 0x7b, 0x9a, 0x66, 0x31, 0x4b, 0xf0, 0x39, 0x1c, + 0x07, 0x59, 0x46, 0x79, 0xf9, 0xa7, 0xea, 0xa4, 0x26, 0xb0, 0x13, 0xe5, 0x03, 0xdf, 0x48, 0x57, + 0x51, 0xb7, 0x84, 0xaf, 0xe7, 0x50, 0x95, 0xf3, 0xc4, 0xcf, 0x41, 0xb3, 0x2e, 0xc7, 0xce, 0xd0, + 0xf5, 0xc9, 0x70, 0xe2, 0xda, 0xfe, 0xc4, 0xf5, 0x46, 0x83, 0x4b, 0xe7, 0xca, 0x19, 0xd8, 0x8d, + 0x0a, 0xc6, 0x70, 0x5a, 0xa8, 0xf6, 0x60, 0x34, 0xf4, 0x9c, 0x71, 0x03, 0xe1, 0x26, 0x3c, 0x2a, + 0xb8, 0x5b, 0x67, 0x7c, 0x6d, 0x13, 0xeb, 0xb6, 0x71, 0x80, 0x9f, 0x42, 0xb3, 0x20, 0xbd, 0x1b, + 0xcb, 0xbb, 0xf6, 0xc9, 0xe0, 0x6a, 0xe2, 0xda, 0x0d, 0xa5, 0xff, 0xee, 0xfb, 0x4e, 0x47, 0xf7, + 0x3b, 0x1d, 0xfd, 0xda, 0xe9, 0xe8, 0xcb, 0x5e, 0xaf, 0xdc, 0xef, 0xf5, 0xca, 0x8f, 0xbd, 0x5e, + 0xf9, 0xf0, 0x66, 0x16, 0xf3, 0xf9, 0x7a, 0x6a, 0x84, 0xec, 0x93, 0x39, 0x90, 0xe3, 0x72, 0x29, + 0xff, 0xcc, 0xd2, 0x85, 0x59, 0xee, 0xf6, 0xb6, 0xdc, 0x6e, 0x7e, 0xb7, 0xa2, 0xd9, 0xb4, 0x2a, + 0x76, 0xf5, 0xed, 0xef, 0x00, 0x00, 0x00, 0xff, 0xff, 0x1f, 0x96, 0x3b, 0x5f, 0xfc, 0x02, 0x00, + 0x00, } func (m *BalanceInfo) Marshal() (dAtA []byte, err error) { @@ -444,6 +503,41 @@ func (m *StakerList) MarshalToSizedBuffer(dAtA []byte) (int, error) { return len(dAtA) - i, nil } +func (m *NSTVersion) Marshal() (dAtA []byte, err error) { + size := m.Size() + dAtA = make([]byte, size) + n, err := m.MarshalToSizedBuffer(dAtA[:size]) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *NSTVersion) MarshalTo(dAtA []byte) (int, error) { + size := m.Size() + return m.MarshalToSizedBuffer(dAtA[:size]) +} + +func (m *NSTVersion) MarshalToSizedBuffer(dAtA []byte) (int, error) { + i := len(dAtA) + _ = i + var l int + _ = l + if m.Version != 0 { + i = encodeVarintNativeToken(dAtA, i, uint64(m.Version)) + i-- + dAtA[i] = 0x10 + } + if len(m.AssetId) > 0 { + i -= len(m.AssetId) + copy(dAtA[i:], m.AssetId) + i = encodeVarintNativeToken(dAtA, i, uint64(len(m.AssetId))) + i-- + dAtA[i] = 0xa + } + return len(dAtA) - i, nil +} + func encodeVarintNativeToken(dAtA []byte, offset int, v uint64) int { offset -= sovNativeToken(v) base := offset @@ -522,6 +616,22 @@ func (m *StakerList) Size() (n int) { return n } +func (m *NSTVersion) Size() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + l = len(m.AssetId) + if l > 0 { + n += 1 + l + sovNativeToken(uint64(l)) + } + if m.Version != 0 { + n += 1 + sovNativeToken(uint64(m.Version)) + } + return n +} + func sovNativeToken(x uint64) (n int) { return (math_bits.Len64(x|1) + 6) / 7 } @@ -922,6 +1032,107 @@ func (m *StakerList) Unmarshal(dAtA []byte) error { } return nil } +func (m *NSTVersion) Unmarshal(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowNativeToken + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: NSTVersion: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: NSTVersion: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field AssetId", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowNativeToken + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return ErrInvalidLengthNativeToken + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return ErrInvalidLengthNativeToken + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.AssetId = string(dAtA[iNdEx:postIndex]) + iNdEx = postIndex + case 2: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field Version", wireType) + } + m.Version = 0 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowNativeToken + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + m.Version |= int64(b&0x7F) << shift + if b < 0x80 { + break + } + } + default: + iNdEx = preIndex + skippy, err := skipNativeToken(dAtA[iNdEx:]) + if err != nil { + return err + } + if (skippy < 0) || (iNdEx+skippy) < 0 { + return ErrInvalidLengthNativeToken + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} func skipNativeToken(dAtA []byte) (n int, err error) { l := len(dAtA) iNdEx := 0 diff --git a/x/oracle/types/params.go b/x/oracle/types/params.go index 1e72de69e..282070a14 100644 --- a/x/oracle/types/params.go +++ b/x/oracle/types/params.go @@ -2,6 +2,7 @@ package types import ( "errors" + "fmt" "strings" "time" @@ -60,7 +61,7 @@ func DefaultParams() Params { Name: "ETH", ChainID: 1, ContractAddress: "0x", - Decimal: 18, + Decimal: 8, Active: true, AssetID: "0x0b34c4d876cd569129cf56bafabb3f9e97a4ff42_0x9ce1", }, @@ -71,7 +72,7 @@ func DefaultParams() Params { Name: "0 position is reserved", }, { - Name: "Chainlink", + Name: SourceChainlinkName, Entry: &Endpoint{ Offchain: map[uint64]string{0: ""}, }, @@ -95,7 +96,7 @@ func DefaultParams() Params { TokenID: 1, RuleID: 1, StartRoundID: 1, - StartBaseBlock: 1000000, + StartBaseBlock: 20, Interval: 10, }, }, @@ -111,7 +112,6 @@ func DefaultParams() Params { MinReportedPerWindow: sdkmath.LegacyNewDec(1).Quo(sdkmath.LegacyNewDec(2)), OracleMissJailDuration: 600 * time.Second, OracleMaliciousJailDuration: 30 * 24 * time.Hour, - SlashFractionMiss: sdkmath.LegacyNewDec(1).Quo(sdkmath.LegacyNewDec(20)), SlashFractionMalicious: sdkmath.LegacyNewDec(1).Quo(sdkmath.LegacyNewDec(10)), }, } @@ -152,9 +152,6 @@ func (p Params) Validate() error { if slashing.MinReportedPerWindow.GT(oneDec) || !slashing.MinReportedPerWindow.IsPositive() { return ErrInvalidParams.Wrapf("MinReportedPerWindow must be in (0, 1], got %v", slashing.MinReportedPerWindow) } - if slashing.SlashFractionMiss.GT(oneDec) || !slashing.SlashFractionMiss.IsPositive() { - return ErrInvalidParams.Wrapf("SlashFractionMiss must be in (0, 1], got %v", slashing.SlashFractionMiss) - } if slashing.SlashFractionMalicious.GT(oneDec) || !slashing.SlashFractionMalicious.IsPositive() { return ErrInvalidParams.Wrapf("SlashFractionMalicious must be in (0, 1], got %v", slashing.SlashFractionMalicious) } @@ -322,6 +319,8 @@ func (p Params) UpdateTokens(currentHeight uint64, tokens ...*Token) (Params, er if len(t.AssetID) > 0 { token.AssetID = t.AssetID } + // tokenID is actually uint since it's index of array + // #nosec G115 if !p.TokenStarted(uint64(tokenID), currentHeight) { // contractAddres is mainly used as a description information if len(t.ContractAddress) > 0 { @@ -532,6 +531,18 @@ func (p Params) GetTokenIDFromAssetID(assetID string) int { return 0 } +func (p Params) GetAssetIDForNSTFromTokenID(tokenID uint64) string { + assetIDs := p.GetAssetIDsFromTokenID(tokenID) + for _, assetID := range assetIDs { + if nstChain, ok := strings.CutPrefix(strings.ToLower(assetID), NSTIDPrefix); ok { + if NSTChain, ok := NSTChainsInverted[nstChain]; ok { + return fmt.Sprintf("%s_%s", NSTAssetAddr[NSTChain], nstChain) + } + } + } + return "" +} + func (p Params) GetAssetIDsFromTokenID(tokenID uint64) []string { if tokenID >= uint64(len(p.Tokens)) { return nil @@ -553,7 +564,8 @@ func (p Params) IsValidSource(sourceID uint64) bool { func (p Params) GetTokenFeeder(feederID uint64) *TokenFeeder { for k, v := range p.TokenFeeders { - if uint64(k) == feederID { + // #nosec G115 // index of array is uint + if k >= 0 && uint64(k) == feederID { return v } } @@ -562,6 +574,7 @@ func (p Params) GetTokenFeeder(feederID uint64) *TokenFeeder { func (p Params) GetTokenInfo(feederID uint64) *Token { for k, v := range p.TokenFeeders { + // #nosec G115 // index of arry is uint if uint64(k) == feederID { return p.Tokens[v.TokenID] } @@ -587,6 +600,7 @@ func (p Params) CheckRules(feederID uint64, prices []*PriceSource) (bool, error) if source.Valid { notFound = true for _, p := range prices { + // #nosec G115 // index of array is uint if p.SourceID == uint64(sID) { notFound = false break @@ -622,3 +636,22 @@ func (p Params) CheckDecimal(feederID uint64, decimal int32) bool { token := p.Tokens[feeder.TokenID] return token.Decimal == decimal } + +func (p Params) IsForceSealingUpdate(params *Params) bool { + if p.MaxNonce != params.MaxNonce || + p.MaxDetId != params.MaxDetId || + p.ThresholdA != params.ThresholdA || + p.ThresholdB != params.ThresholdB || + p.Mode != params.Mode { + return true + } + return false +} + +func (p Params) IsSlashingResetUpdate(params *Params) bool { + if p.Slashing.ReportedRoundsWindow != params.Slashing.ReportedRoundsWindow || + p.Slashing.MinReportedPerWindow != params.Slashing.MinReportedPerWindow { + return true + } + return false +} diff --git a/x/oracle/types/params.pb.go b/x/oracle/types/params.pb.go index bde782f63..369003a8c 100644 --- a/x/oracle/types/params.pb.go +++ b/x/oracle/types/params.pb.go @@ -212,10 +212,8 @@ type SlashingParams struct { OracleMissJailDuration time.Duration `protobuf:"bytes,3,opt,name=oracle_miss_jail_duration,json=oracleMissJailDuration,proto3,stdduration" json:"oracle_miss_jail_duration"` // oracle_malicious_jail_duration defines the duratin one validator should be jailed for malicious behavior OracleMaliciousJailDuration time.Duration `protobuf:"bytes,4,opt,name=oracle_malicious_jail_duration,json=oracleMaliciousJailDuration,proto3,stdduration" json:"oracle_malicious_jail_duration"` - // slash_fraction_miss defines the fraction one validator should be punished for msissing reporting price - SlashFractionMiss github_com_cosmos_cosmos_sdk_types.Dec `protobuf:"bytes,5,opt,name=slash_fraction_miss,json=slashFractionMiss,proto3,customtype=github.com/cosmos/cosmos-sdk/types.Dec" json:"slash_fraction_miss"` // slash_fraction_malicious defines the fraction one validator should be punished for malicious behavior - SlashFractionMalicious github_com_cosmos_cosmos_sdk_types.Dec `protobuf:"bytes,6,opt,name=slash_fraction_malicious,json=slashFractionMalicious,proto3,customtype=github.com/cosmos/cosmos-sdk/types.Dec" json:"slash_fraction_malicious"` + SlashFractionMalicious github_com_cosmos_cosmos_sdk_types.Dec `protobuf:"bytes,5,opt,name=slash_fraction_malicious,json=slashFractionMalicious,proto3,customtype=github.com/cosmos/cosmos-sdk/types.Dec" json:"slash_fraction_malicious"` } func (m *SlashingParams) Reset() { *m = SlashingParams{} } @@ -281,57 +279,56 @@ func init() { func init() { proto.RegisterFile("exocore/oracle/v1/params.proto", fileDescriptor_72f39bba4594b794) } var fileDescriptor_72f39bba4594b794 = []byte{ - // 791 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x9c, 0x54, 0x4d, 0x4f, 0xeb, 0x46, - 0x14, 0x8d, 0x5f, 0x3e, 0x1e, 0x6f, 0x42, 0x5e, 0x5f, 0x06, 0x4a, 0x87, 0x40, 0x1d, 0x17, 0x55, - 0x28, 0x42, 0xaa, 0x0d, 0x84, 0x55, 0xd5, 0x2e, 0xc8, 0x07, 0x12, 0x48, 0x84, 0xc8, 0x29, 0xaa, - 0xd4, 0x8d, 0xe5, 0xd8, 0x93, 0x64, 0xc0, 0xf6, 0x44, 0x33, 0x36, 0xa4, 0x6c, 0xbb, 0xa9, 0x58, - 0x75, 0xc9, 0x06, 0xa9, 0x52, 0x37, 0x5d, 0x76, 0xdf, 0x3f, 0xc0, 0x92, 0x65, 0xc5, 0x82, 0x56, - 0xb0, 0xe8, 0xdf, 0xa8, 0x3c, 0xb6, 0x53, 0x12, 0xc2, 0xe2, 0xb1, 0x49, 0x3c, 0xf7, 0x9c, 0x73, - 0xcf, 0xbd, 0x77, 0x7c, 0x0d, 0x64, 0x3c, 0xa2, 0x16, 0x65, 0x58, 0xa3, 0xcc, 0xb4, 0x1c, 0xac, - 0x9d, 0x6d, 0x69, 0x43, 0x93, 0x99, 0x2e, 0x57, 0x87, 0x8c, 0xfa, 0x14, 0x16, 0x63, 0x5c, 0x8d, - 0x70, 0xf5, 0x6c, 0xab, 0x54, 0x34, 0x5d, 0xe2, 0x51, 0x4d, 0xfc, 0x46, 0xac, 0xd2, 0xea, 0xf3, - 0x2c, 0xc4, 0xeb, 0x25, 0xe8, 0x97, 0xcf, 0x51, 0x9f, 0x9e, 0x62, 0xcf, 0xe8, 0x61, 0x6c, 0x63, - 0x16, 0xb3, 0x16, 0xfb, 0xb4, 0x4f, 0xc5, 0xa3, 0x16, 0x3e, 0xc5, 0x51, 0xb9, 0x4f, 0x69, 0xdf, - 0xc1, 0x9a, 0x38, 0x75, 0x83, 0x9e, 0x66, 0x07, 0xcc, 0xf4, 0x09, 0xf5, 0x22, 0x7c, 0xed, 0xcf, - 0x0c, 0xc8, 0xb5, 0x45, 0xc1, 0x70, 0x13, 0xe4, 0xac, 0x81, 0x49, 0x3c, 0x8e, 0x24, 0x25, 0x5d, - 0xc9, 0x6f, 0x23, 0xf5, 0x59, 0xed, 0x6a, 0x3d, 0x24, 0xe8, 0x31, 0x2f, 0x54, 0x88, 0x42, 0x38, - 0x7a, 0xf3, 0xa2, 0xe2, 0xbb, 0x90, 0xa0, 0xc7, 0x3c, 0x58, 0x05, 0x6f, 0x39, 0x0d, 0x98, 0x85, - 0x39, 0x4a, 0x0b, 0xc9, 0xf2, 0x0c, 0x49, 0x47, 0x30, 0xf4, 0x84, 0x09, 0xab, 0x20, 0xcb, 0x02, - 0x07, 0x73, 0x94, 0x11, 0x92, 0xcf, 0x67, 0x48, 0xf4, 0xc0, 0xc1, 0xb1, 0x2c, 0xe2, 0xc2, 0x3a, - 0x28, 0x3c, 0x1d, 0x12, 0x47, 0x59, 0x21, 0x96, 0x5f, 0x2a, 0x71, 0x4f, 0xd0, 0xf4, 0x79, 0xff, - 0xff, 0x03, 0x87, 0x2b, 0xe0, 0x9d, 0x6b, 0x8e, 0x0c, 0x8f, 0x7a, 0x16, 0x46, 0x39, 0x45, 0xaa, - 0x64, 0xf5, 0x39, 0xd7, 0x1c, 0xb5, 0xc2, 0x33, 0x2c, 0x83, 0xbc, 0x3f, 0x60, 0x98, 0x0f, 0xa8, - 0x63, 0x1b, 0x26, 0x7a, 0x2b, 0x60, 0x30, 0x0e, 0xed, 0x4e, 0x12, 0xba, 0x68, 0x6e, 0x8a, 0x50, - 0x83, 0x3b, 0x20, 0xe3, 0x52, 0x1b, 0xa3, 0x77, 0x8a, 0x54, 0x79, 0xbf, 0xad, 0xcc, 0x9a, 0x37, - 0xf5, 0x38, 0xf6, 0x78, 0xc0, 0x0f, 0xa9, 0x8d, 0x75, 0xc1, 0x86, 0xab, 0x00, 0x84, 0x45, 0xd9, - 0xd8, 0x37, 0x88, 0x8d, 0xc0, 0xb8, 0xaa, 0x06, 0xf6, 0xf7, 0x6d, 0xb8, 0x0e, 0x3e, 0x09, 0x51, - 0x4e, 0x2e, 0xb0, 0x31, 0x64, 0x24, 0x9c, 0x74, 0x5e, 0x50, 0x0a, 0xae, 0x39, 0xea, 0x90, 0x0b, - 0xdc, 0x16, 0x41, 0xf8, 0x2d, 0x98, 0xe3, 0x8e, 0xc9, 0x07, 0xc4, 0xeb, 0xa3, 0x79, 0x45, 0xaa, - 0xe4, 0xb7, 0xbf, 0x98, 0x75, 0x15, 0x31, 0x25, 0x7a, 0x45, 0xf4, 0xb1, 0xe4, 0xeb, 0xcc, 0xd5, - 0xaf, 0xe5, 0xd4, 0xda, 0x5d, 0x06, 0xbc, 0x9f, 0xa4, 0xc0, 0x1d, 0xb0, 0xc4, 0xf0, 0x90, 0x32, - 0x1f, 0xdb, 0x06, 0xa3, 0x81, 0x67, 0x73, 0xe3, 0x9c, 0x78, 0x36, 0x3d, 0x47, 0x92, 0x22, 0x55, - 0xd2, 0xfa, 0x62, 0x82, 0xea, 0x02, 0xfc, 0x5e, 0x60, 0xf0, 0x04, 0x7c, 0xe6, 0x12, 0xcf, 0x18, - 0x2b, 0x87, 0x98, 0x25, 0xb2, 0x37, 0x8a, 0x54, 0x99, 0xaf, 0x55, 0x6f, 0xee, 0xcb, 0xa9, 0xbb, - 0xfb, 0xf2, 0x7a, 0x9f, 0xf8, 0x83, 0xa0, 0xab, 0x5a, 0xd4, 0xd5, 0x2c, 0xca, 0x5d, 0xca, 0xe3, - 0xbf, 0xaf, 0xb8, 0x7d, 0xaa, 0xf9, 0x3f, 0x0e, 0x31, 0x57, 0x1b, 0xd8, 0xfa, 0xfd, 0xdf, 0x3f, - 0x36, 0x24, 0x7d, 0xd1, 0x25, 0x9e, 0x1e, 0xa7, 0x6c, 0x63, 0x16, 0x7b, 0x59, 0x60, 0x39, 0x6a, - 0xd0, 0x70, 0x09, 0xe7, 0xc6, 0x89, 0x49, 0x1c, 0x23, 0xd9, 0x0a, 0x94, 0x16, 0xa3, 0x58, 0x56, - 0xa3, 0xb5, 0x51, 0x93, 0xb5, 0x51, 0x1b, 0x31, 0xa1, 0x56, 0x08, 0x0b, 0xb9, 0xfa, 0xbb, 0x2c, - 0x45, 0x16, 0x4b, 0x51, 0xaa, 0x43, 0xc2, 0xf9, 0x81, 0x49, 0x9c, 0x84, 0x06, 0x5d, 0x20, 0x27, - 0x26, 0xa6, 0x43, 0x2c, 0x42, 0x83, 0x69, 0xa7, 0xcc, 0x47, 0x3a, 0xad, 0xc4, 0x4e, 0x49, 0xba, - 0x09, 0x3b, 0x0b, 0x2c, 0x88, 0xab, 0x31, 0x7a, 0xcc, 0xb4, 0xc2, 0x88, 0xe8, 0x0d, 0x65, 0x5f, - 0x3f, 0xbb, 0xa2, 0xc8, 0xb7, 0x17, 0xa7, 0x0b, 0xfb, 0x83, 0x2e, 0x40, 0xd3, 0x26, 0x49, 0x31, - 0x62, 0x39, 0x5e, 0xe9, 0xb4, 0x34, 0xe9, 0x94, 0xa4, 0xdc, 0xf8, 0x49, 0x02, 0x85, 0x89, 0xf7, - 0x1f, 0x7e, 0x03, 0x4a, 0xf5, 0xa3, 0x56, 0xa7, 0xd9, 0xea, 0x1c, 0x77, 0x8c, 0xc3, 0xa3, 0x46, - 0xd3, 0x38, 0x6e, 0x75, 0xda, 0xcd, 0xfa, 0xfe, 0xde, 0x7e, 0xb3, 0xf1, 0x21, 0x55, 0x5a, 0xbd, - 0xbc, 0x56, 0xd0, 0x84, 0xe4, 0xd8, 0xe3, 0x43, 0x6c, 0x91, 0x1e, 0xc1, 0x36, 0x54, 0xc1, 0xc2, - 0x94, 0x7a, 0xb7, 0xb3, 0xdb, 0xfe, 0x20, 0x95, 0x3e, 0xbd, 0xbc, 0x56, 0x8a, 0x13, 0xb2, 0x10, - 0x28, 0x65, 0x7e, 0xfe, 0x4d, 0x4e, 0xd5, 0x0e, 0x6e, 0x1e, 0x64, 0xe9, 0xf6, 0x41, 0x96, 0xfe, - 0x79, 0x90, 0xa5, 0x5f, 0x1e, 0xe5, 0xd4, 0xed, 0xa3, 0x9c, 0xfa, 0xeb, 0x51, 0x4e, 0xfd, 0xb0, - 0xf9, 0xa4, 0xc9, 0x66, 0xb4, 0x39, 0x2d, 0xec, 0x9f, 0x53, 0x76, 0xaa, 0x25, 0x1f, 0xec, 0x51, - 0xf2, 0xc9, 0x16, 0x2d, 0x77, 0x73, 0xe2, 0x92, 0xab, 0xff, 0x05, 0x00, 0x00, 0xff, 0xff, 0x58, - 0x98, 0x95, 0x8e, 0x35, 0x06, 0x00, 0x00, + // 772 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x9c, 0x94, 0xbf, 0x4f, 0xc3, 0x46, + 0x14, 0xc7, 0x63, 0xf2, 0x03, 0xb8, 0x10, 0x0a, 0x57, 0x4a, 0x8f, 0x40, 0x1d, 0x17, 0x55, 0x28, + 0x42, 0xaa, 0x0d, 0x84, 0xa9, 0x6a, 0x07, 0xf2, 0x03, 0x09, 0x24, 0x42, 0xe4, 0x14, 0x55, 0xea, + 0x62, 0x39, 0xf6, 0x25, 0x39, 0xb0, 0x7d, 0xd1, 0x9d, 0x0d, 0x29, 0x6b, 0x97, 0x8a, 0xa9, 0x23, + 0x0b, 0x52, 0xa5, 0x2e, 0x1d, 0xbb, 0xf7, 0x1f, 0x60, 0x64, 0xac, 0x3a, 0xd0, 0x0a, 0x86, 0xfe, + 0x05, 0xdd, 0x2b, 0x9f, 0xed, 0x40, 0x42, 0x18, 0xda, 0x25, 0xf1, 0xbd, 0xef, 0xe7, 0xfb, 0xde, + 0xbb, 0xe7, 0x3b, 0x03, 0x19, 0x0f, 0xa9, 0x45, 0x19, 0xd6, 0x28, 0x33, 0x2d, 0x07, 0x6b, 0x97, + 0xbb, 0xda, 0xc0, 0x64, 0xa6, 0xcb, 0xd5, 0x01, 0xa3, 0x3e, 0x85, 0xcb, 0xb1, 0xae, 0x46, 0xba, + 0x7a, 0xb9, 0x5b, 0x5c, 0x36, 0x5d, 0xe2, 0x51, 0x4d, 0xfc, 0x46, 0x54, 0x71, 0xe3, 0x6d, 0x16, + 0xe2, 0x75, 0x13, 0xf5, 0xb3, 0xb7, 0xaa, 0x4f, 0x2f, 0xb0, 0x67, 0x74, 0x31, 0xb6, 0x31, 0x8b, + 0xa9, 0x95, 0x1e, 0xed, 0x51, 0xf1, 0xa8, 0x85, 0x4f, 0x71, 0x54, 0xee, 0x51, 0xda, 0x73, 0xb0, + 0x26, 0x56, 0x9d, 0xa0, 0xab, 0xd9, 0x01, 0x33, 0x7d, 0x42, 0xbd, 0x48, 0xdf, 0xfc, 0x2d, 0x03, + 0x72, 0x2d, 0xd1, 0x30, 0xdc, 0x01, 0x39, 0xab, 0x6f, 0x12, 0x8f, 0x23, 0x49, 0x49, 0x97, 0xf3, + 0x7b, 0x48, 0x7d, 0xd3, 0xbb, 0x5a, 0x0b, 0x01, 0x3d, 0xe6, 0x42, 0x87, 0x68, 0x84, 0xa3, 0x99, + 0x77, 0x1d, 0x5f, 0x87, 0x80, 0x1e, 0x73, 0xb0, 0x02, 0x66, 0x39, 0x0d, 0x98, 0x85, 0x39, 0x4a, + 0x0b, 0xcb, 0xda, 0x14, 0x4b, 0x5b, 0x10, 0x7a, 0x42, 0xc2, 0x0a, 0xc8, 0xb2, 0xc0, 0xc1, 0x1c, + 0x65, 0x84, 0xe5, 0x93, 0x29, 0x16, 0x3d, 0x70, 0x70, 0x6c, 0x8b, 0x58, 0x58, 0x03, 0x85, 0xd7, + 0x43, 0xe2, 0x28, 0x2b, 0xcc, 0xf2, 0x7b, 0x2d, 0x1e, 0x0a, 0x4c, 0x5f, 0xf0, 0x5f, 0x16, 0x1c, + 0xae, 0x83, 0x79, 0xd7, 0x1c, 0x1a, 0x1e, 0xf5, 0x2c, 0x8c, 0x72, 0x8a, 0x54, 0xce, 0xea, 0x73, + 0xae, 0x39, 0x6c, 0x86, 0x6b, 0x58, 0x02, 0x79, 0xbf, 0xcf, 0x30, 0xef, 0x53, 0xc7, 0x36, 0x4c, + 0x34, 0x2b, 0x64, 0x30, 0x0a, 0x1d, 0x8c, 0x03, 0x1d, 0x34, 0x37, 0x01, 0x54, 0xe1, 0x3e, 0xc8, + 0xb8, 0xd4, 0xc6, 0x68, 0x5e, 0x91, 0xca, 0x8b, 0x7b, 0xca, 0xb4, 0x79, 0x53, 0x8f, 0x63, 0x8f, + 0x07, 0xfc, 0x84, 0xda, 0x58, 0x17, 0x34, 0xdc, 0x00, 0x20, 0x6c, 0xca, 0xc6, 0xbe, 0x41, 0x6c, + 0x04, 0x46, 0x5d, 0xd5, 0xb1, 0x7f, 0x64, 0xc3, 0x2d, 0xf0, 0x41, 0xa8, 0x72, 0x72, 0x8d, 0x8d, + 0x01, 0x23, 0xe1, 0xa4, 0xf3, 0x02, 0x29, 0xb8, 0xe6, 0xb0, 0x4d, 0xae, 0x71, 0x4b, 0x04, 0xe1, + 0x57, 0x60, 0x8e, 0x3b, 0x26, 0xef, 0x13, 0xaf, 0x87, 0x16, 0x14, 0xa9, 0x9c, 0xdf, 0xfb, 0x74, + 0xda, 0xab, 0x88, 0x91, 0xe8, 0x88, 0xe8, 0x23, 0xcb, 0x17, 0x99, 0xdb, 0x9f, 0x4a, 0xa9, 0xcd, + 0x7f, 0xd2, 0x60, 0x71, 0x1c, 0x81, 0xfb, 0x60, 0x95, 0xe1, 0x01, 0x65, 0x3e, 0xb6, 0x0d, 0x46, + 0x03, 0xcf, 0xe6, 0xc6, 0x15, 0xf1, 0x6c, 0x7a, 0x85, 0x24, 0x45, 0x2a, 0xa7, 0xf5, 0x95, 0x44, + 0xd5, 0x85, 0xf8, 0x8d, 0xd0, 0xe0, 0x39, 0xf8, 0xd8, 0x25, 0x9e, 0x31, 0x72, 0x0e, 0x30, 0x4b, + 0x6c, 0x33, 0x8a, 0x54, 0x5e, 0xa8, 0x56, 0xee, 0x1f, 0x4b, 0xa9, 0x3f, 0x1e, 0x4b, 0x5b, 0x3d, + 0xe2, 0xf7, 0x83, 0x8e, 0x6a, 0x51, 0x57, 0xb3, 0x28, 0x77, 0x29, 0x8f, 0xff, 0x3e, 0xe7, 0xf6, + 0x85, 0xe6, 0x7f, 0x37, 0xc0, 0x5c, 0xad, 0x63, 0xeb, 0x97, 0xbf, 0x7f, 0xdd, 0x96, 0xf4, 0x15, + 0x97, 0x78, 0x7a, 0x9c, 0xb2, 0x85, 0x59, 0x5c, 0xcb, 0x02, 0x6b, 0xd1, 0x06, 0x0d, 0x97, 0x70, + 0x6e, 0x9c, 0x9b, 0xc4, 0x31, 0x92, 0x5b, 0x81, 0xd2, 0x62, 0x14, 0x6b, 0x6a, 0x74, 0x6d, 0xd4, + 0xe4, 0xda, 0xa8, 0xf5, 0x18, 0xa8, 0x16, 0xc2, 0x46, 0x6e, 0xff, 0x2c, 0x49, 0x51, 0x89, 0xd5, + 0x28, 0xd5, 0x09, 0xe1, 0xfc, 0xd8, 0x24, 0x4e, 0x82, 0x41, 0x17, 0xc8, 0x49, 0x11, 0xd3, 0x21, + 0x16, 0xa1, 0xc1, 0x64, 0xa5, 0xcc, 0x7f, 0xac, 0xb4, 0x1e, 0x57, 0x4a, 0xd2, 0x4d, 0x94, 0x43, + 0xe2, 0xd5, 0x18, 0x5d, 0x66, 0x5a, 0x61, 0xe4, 0xa5, 0x2c, 0xca, 0xfe, 0xff, 0x01, 0xae, 0x8a, + 0xa4, 0x87, 0x71, 0xce, 0x51, 0xe9, 0xed, 0xef, 0x25, 0x50, 0x18, 0x3b, 0x9a, 0xf0, 0x4b, 0x50, + 0xac, 0x9d, 0x36, 0xdb, 0x8d, 0x66, 0xfb, 0xac, 0x6d, 0x9c, 0x9c, 0xd6, 0x1b, 0xc6, 0x59, 0xb3, + 0xdd, 0x6a, 0xd4, 0x8e, 0x0e, 0x8f, 0x1a, 0xf5, 0xa5, 0x54, 0x71, 0xe3, 0xe6, 0x4e, 0x41, 0x63, + 0x96, 0x33, 0x8f, 0x0f, 0xb0, 0x45, 0xba, 0x04, 0xdb, 0x50, 0x05, 0x1f, 0x4e, 0xb8, 0x0f, 0xda, + 0x07, 0xad, 0x25, 0xa9, 0xf8, 0xd1, 0xcd, 0x9d, 0xb2, 0x3c, 0x66, 0x0b, 0x85, 0x62, 0xe6, 0x87, + 0x9f, 0xe5, 0x54, 0xf5, 0xf8, 0xfe, 0x49, 0x96, 0x1e, 0x9e, 0x64, 0xe9, 0xaf, 0x27, 0x59, 0xfa, + 0xf1, 0x59, 0x4e, 0x3d, 0x3c, 0xcb, 0xa9, 0xdf, 0x9f, 0xe5, 0xd4, 0xb7, 0x3b, 0xaf, 0x36, 0xd9, + 0x88, 0x0e, 0x75, 0x13, 0xfb, 0x57, 0x94, 0x5d, 0x68, 0xc9, 0xb7, 0x74, 0x98, 0x7c, 0x4d, 0xc5, + 0x96, 0x3b, 0x39, 0x31, 0xff, 0xca, 0xbf, 0x01, 0x00, 0x00, 0xff, 0xff, 0x24, 0x82, 0xa5, 0x31, + 0xd0, 0x05, 0x00, 0x00, } func (m *Params) Marshal() (dAtA []byte, err error) { @@ -498,16 +495,6 @@ func (m *SlashingParams) MarshalToSizedBuffer(dAtA []byte) (int, error) { i = encodeVarintParams(dAtA, i, uint64(size)) } i-- - dAtA[i] = 0x32 - { - size := m.SlashFractionMiss.Size() - i -= size - if _, err := m.SlashFractionMiss.MarshalTo(dAtA[i:]); err != nil { - return 0, err - } - i = encodeVarintParams(dAtA, i, uint64(size)) - } - i-- dAtA[i] = 0x2a n2, err2 := github_com_cosmos_gogoproto_types.StdDurationMarshalTo(m.OracleMaliciousJailDuration, dAtA[i-github_com_cosmos_gogoproto_types.SizeOfStdDuration(m.OracleMaliciousJailDuration):]) if err2 != nil { @@ -630,8 +617,6 @@ func (m *SlashingParams) Size() (n int) { n += 1 + l + sovParams(uint64(l)) l = github_com_cosmos_gogoproto_types.SizeOfStdDuration(m.OracleMaliciousJailDuration) n += 1 + l + sovParams(uint64(l)) - l = m.SlashFractionMiss.Size() - n += 1 + l + sovParams(uint64(l)) l = m.SlashFractionMalicious.Size() n += 1 + l + sovParams(uint64(l)) return n @@ -1161,39 +1146,6 @@ func (m *SlashingParams) Unmarshal(dAtA []byte) error { } iNdEx = postIndex case 5: - if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field SlashFractionMiss", wireType) - } - var byteLen int - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflowParams - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - byteLen |= int(b&0x7F) << shift - if b < 0x80 { - break - } - } - if byteLen < 0 { - return ErrInvalidLengthParams - } - postIndex := iNdEx + byteLen - if postIndex < 0 { - return ErrInvalidLengthParams - } - if postIndex > l { - return io.ErrUnexpectedEOF - } - if err := m.SlashFractionMiss.Unmarshal(dAtA[iNdEx:postIndex]); err != nil { - return err - } - iNdEx = postIndex - case 6: if wireType != 2 { return fmt.Errorf("proto: wrong wireType = %d for field SlashFractionMalicious", wireType) } diff --git a/x/oracle/types/query.pb.go b/x/oracle/types/query.pb.go index 1fb2ce014..f58aefbb3 100644 --- a/x/oracle/types/query.pb.go +++ b/x/oracle/types/query.pb.go @@ -216,8 +216,10 @@ func (m *QueryStakerListRequest) GetAssetId() string { // QueryStakerListResponse is response type for Query/StakerList RPC method type QueryStakerListResponse struct { + // version of the staker validator list changes + Version int64 `protobuf:"varint,1,opt,name=version,proto3" json:"version,omitempty"` // staker list including all stakers of request asset - StakerList *StakerList `protobuf:"bytes,1,opt,name=staker_list,json=stakerList,proto3" json:"staker_list,omitempty"` + StakerList *StakerList `protobuf:"bytes,2,opt,name=staker_list,json=stakerList,proto3" json:"staker_list,omitempty"` } func (m *QueryStakerListResponse) Reset() { *m = QueryStakerListResponse{} } @@ -253,6 +255,13 @@ func (m *QueryStakerListResponse) XXX_DiscardUnknown() { var xxx_messageInfo_QueryStakerListResponse proto.InternalMessageInfo +func (m *QueryStakerListResponse) GetVersion() int64 { + if m != nil { + return m.Version + } + return 0 +} + func (m *QueryStakerListResponse) GetStakerList() *StakerList { if m != nil { return m.StakerList @@ -317,8 +326,10 @@ func (m *QueryStakerInfoRequest) GetStakerAddr() string { // QueryStakerInfoResponse is response type for Query/StakerInfo RCP method type QueryStakerInfoResponse struct { + // version of the staker validator list changes + Version int64 `protobuf:"varint,1,opt,name=version,proto3" json:"version,omitempty"` // all staker infos under the specified asset - StakerInfo *StakerInfo `protobuf:"bytes,1,opt,name=staker_info,json=stakerInfo,proto3" json:"staker_info,omitempty"` + StakerInfo *StakerInfo `protobuf:"bytes,2,opt,name=staker_info,json=stakerInfo,proto3" json:"staker_info,omitempty"` } func (m *QueryStakerInfoResponse) Reset() { *m = QueryStakerInfoResponse{} } @@ -354,6 +365,13 @@ func (m *QueryStakerInfoResponse) XXX_DiscardUnknown() { var xxx_messageInfo_QueryStakerInfoResponse proto.InternalMessageInfo +func (m *QueryStakerInfoResponse) GetVersion() int64 { + if m != nil { + return m.Version + } + return 0 +} + func (m *QueryStakerInfoResponse) GetStakerInfo() *StakerInfo { if m != nil { return m.StakerInfo @@ -409,8 +427,10 @@ func (m *QueryStakerInfosRequest) GetAssetId() string { // QueryStakerInfosResponse is response type for Query/StakerInfo RCP method type QueryStakerInfosResponse struct { + // version of the staker validator list changes + Version int64 `protobuf:"varint,1,opt,name=version,proto3" json:"version,omitempty"` // all staker infos under the specified asset - StakerInfos []*StakerInfo `protobuf:"bytes,1,rep,name=staker_infos,json=stakerInfos,proto3" json:"staker_infos,omitempty"` + StakerInfos []*StakerInfo `protobuf:"bytes,2,rep,name=staker_infos,json=stakerInfos,proto3" json:"staker_infos,omitempty"` } func (m *QueryStakerInfosResponse) Reset() { *m = QueryStakerInfosResponse{} } @@ -446,6 +466,13 @@ func (m *QueryStakerInfosResponse) XXX_DiscardUnknown() { var xxx_messageInfo_QueryStakerInfosResponse proto.InternalMessageInfo +func (m *QueryStakerInfosResponse) GetVersion() int64 { + if m != nil { + return m.Version + } + return 0 +} + func (m *QueryStakerInfosResponse) GetStakerInfos() []*StakerInfo { if m != nil { return m.StakerInfos @@ -1493,93 +1520,94 @@ func init() { func init() { proto.RegisterFile("exocore/oracle/v1/query.proto", fileDescriptor_b8cba1249806967d) } var fileDescriptor_b8cba1249806967d = []byte{ - // 1368 bytes of a gzipped FileDescriptorProto + // 1386 bytes of a gzipped FileDescriptorProto 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xac, 0x98, 0xdf, 0x6f, 0xdb, 0x54, - 0x14, 0xc7, 0xeb, 0xf5, 0xc7, 0xe8, 0x69, 0x37, 0xe8, 0x5d, 0x19, 0xad, 0xd7, 0xa6, 0xa9, 0xd7, - 0xb4, 0x69, 0xd3, 0xf9, 0x36, 0xed, 0x44, 0xe1, 0x81, 0x1f, 0xad, 0x04, 0x55, 0xa7, 0x31, 0xb5, - 0xa1, 0x20, 0x31, 0x4d, 0x44, 0x4e, 0x72, 0x17, 0x4c, 0xdd, 0x38, 0xb3, 0xdd, 0xd0, 0x51, 0x55, - 0x48, 0xbc, 0xee, 0x65, 0xd2, 0xc4, 0xcb, 0x04, 0x12, 0x20, 0x24, 0xde, 0x78, 0xe1, 0x8d, 0xbf, - 0x60, 0x8f, 0x93, 0x78, 0xe1, 0x09, 0xa1, 0x96, 0x3f, 0x04, 0xf9, 0xfa, 0x38, 0xb1, 0xe3, 0x6b, - 0xc7, 0x45, 0x7d, 0x8b, 0x7d, 0xcf, 0x8f, 0xcf, 0x39, 0xf7, 0xf8, 0xfa, 0x1b, 0xc3, 0x34, 0x3b, - 0x32, 0xab, 0xa6, 0xc5, 0xa8, 0x69, 0x69, 0x55, 0x83, 0xd1, 0x56, 0x91, 0x3e, 0x3a, 0x64, 0xd6, - 0x63, 0xb5, 0x69, 0x99, 0x8e, 0x49, 0xc6, 0x70, 0x59, 0xf5, 0x96, 0xd5, 0x56, 0x51, 0x5e, 0xaa, - 0x9a, 0xf6, 0x81, 0x69, 0xd3, 0x8a, 0x66, 0x33, 0xcf, 0x96, 0xb6, 0x8a, 0x15, 0xe6, 0x68, 0x45, - 0xda, 0xd4, 0xea, 0x7a, 0x43, 0x73, 0x74, 0xb3, 0xe1, 0xb9, 0xcb, 0xf9, 0x68, 0x74, 0xbd, 0x51, - 0x63, 0x47, 0x65, 0x8b, 0x55, 0x59, 0xc3, 0x29, 0x1f, 0xd8, 0x75, 0xb4, 0x2c, 0xf4, 0xb0, 0x6c, - 0x6a, 0x96, 0x76, 0x60, 0xa3, 0xf1, 0x5c, 0xd4, 0xd8, 0x4d, 0xdb, 0x62, 0x65, 0xc7, 0xdc, 0x67, - 0x7e, 0xf2, 0x4c, 0xd4, 0x2a, 0x14, 0x45, 0x50, 0x7a, 0xd3, 0xd2, 0xab, 0x2c, 0xc1, 0xdd, 0x5d, - 0xf6, 0xdd, 0x95, 0xe8, 0x7a, 0xa4, 0xaa, 0x5c, 0xac, 0x4d, 0x88, 0x44, 0x8d, 0x9a, 0xb5, 0x34, - 0x43, 0xaf, 0x69, 0x8e, 0x69, 0x95, 0x0f, 0x9b, 0x35, 0xcd, 0x61, 0xe5, 0x8a, 0x61, 0x56, 0xf7, - 0xd1, 0x7e, 0xbc, 0x6e, 0xd6, 0x4d, 0xfe, 0x93, 0xba, 0xbf, 0xf0, 0xee, 0x54, 0xdd, 0x34, 0xeb, - 0x06, 0xa3, 0x5a, 0x53, 0xa7, 0x5a, 0xa3, 0x61, 0x3a, 0x7c, 0x27, 0x30, 0x87, 0xf2, 0x16, 0xc0, - 0x9e, 0xdb, 0x9c, 0x6d, 0xb7, 0xab, 0x64, 0x1c, 0x06, 0x79, 0xab, 0x26, 0xa4, 0xac, 0x94, 0x1f, - 0x2e, 0x79, 0x17, 0xee, 0x5d, 0xde, 0xf4, 0x89, 0x4b, 0x59, 0x29, 0x3f, 0x50, 0xf2, 0x2e, 0x14, - 0x19, 0x26, 0x76, 0xdd, 0x6d, 0xee, 0xb8, 0x33, 0xbb, 0xc4, 0x1e, 0x1d, 0x32, 0xdb, 0x51, 0xca, - 0x30, 0x29, 0x58, 0xb3, 0x9b, 0x66, 0xc3, 0x66, 0x64, 0x13, 0xae, 0xf0, 0xb8, 0x65, 0xdd, 0x5b, - 0x98, 0x90, 0xb2, 0xfd, 0xf9, 0x91, 0xd5, 0x69, 0x35, 0x32, 0x54, 0x6a, 0xc7, 0xbf, 0x34, 0xea, - 0x04, 0x62, 0x29, 0x6b, 0x70, 0x9d, 0x27, 0xf8, 0xd8, 0xd1, 0xf6, 0x99, 0x75, 0x57, 0xb7, 0x1d, - 0x4c, 0x4d, 0x26, 0xe1, 0x15, 0xcd, 0xb6, 0x99, 0x53, 0xd6, 0x6b, 0x58, 0xc5, 0x65, 0x7e, 0xbd, - 0x5d, 0x53, 0x3e, 0x83, 0x37, 0x22, 0x4e, 0xc8, 0xf4, 0x2e, 0x8c, 0xd8, 0xfc, 0x6e, 0xd9, 0xd0, - 0x6d, 0x87, 0x3b, 0x8a, 0x89, 0x02, 0xbe, 0x60, 0xb7, 0x7f, 0x2b, 0x7b, 0x21, 0x9e, 0xed, 0xc6, - 0x43, 0xb3, 0x37, 0x0f, 0x99, 0x69, 0x27, 0xd5, 0x6a, 0x35, 0x8b, 0x77, 0x77, 0xd8, 0x8f, 0xba, - 0x51, 0xab, 0x59, 0x5d, 0xc0, 0x5e, 0xd4, 0x08, 0xb0, 0xde, 0x78, 0x68, 0xf6, 0x04, 0xe6, 0xbe, - 0x18, 0xda, 0xfd, 0xad, 0xdc, 0x8e, 0x84, 0xb6, 0x53, 0x74, 0xf0, 0x01, 0xee, 0x79, 0xc8, 0x0b, - 0x89, 0xde, 0x87, 0xd1, 0x00, 0x51, 0xd2, 0xae, 0x06, 0x90, 0x46, 0x3a, 0x48, 0xb6, 0x32, 0x0e, - 0x84, 0x47, 0xdf, 0xe1, 0x0f, 0x81, 0x3f, 0x4b, 0xf7, 0xe0, 0x5a, 0xe8, 0x2e, 0xa6, 0x5b, 0x87, - 0x21, 0xef, 0x61, 0xc1, 0xda, 0x27, 0x05, 0x89, 0x3c, 0x97, 0xcd, 0x81, 0x17, 0x7f, 0xcf, 0xf4, - 0x95, 0xd0, 0x5c, 0x59, 0x85, 0xd7, 0x79, 0xbc, 0x2d, 0xe6, 0xec, 0xf0, 0x07, 0x37, 0x50, 0x37, - 0xce, 0xa5, 0x57, 0xf7, 0x40, 0xe9, 0xb2, 0x37, 0x73, 0x35, 0x65, 0x1d, 0x64, 0xdf, 0xe7, 0xae, - 0xe6, 0x30, 0xdb, 0xf3, 0x4c, 0xe1, 0xb8, 0x8b, 0x73, 0x11, 0x48, 0x16, 0xe0, 0xe7, 0x77, 0x92, - 0xf8, 0xb9, 0x41, 0x9b, 0x9f, 0x5f, 0x29, 0x0f, 0xe0, 0x86, 0x90, 0x05, 0xe3, 0xbe, 0x03, 0x83, - 0xdc, 0x10, 0xc3, 0xce, 0xc6, 0x85, 0xdd, 0xd3, 0x0f, 0x58, 0xc9, 0x3c, 0x6c, 0xd4, 0x30, 0xbc, - 0xe7, 0xa5, 0x94, 0xb1, 0x3b, 0x1b, 0x86, 0x11, 0xee, 0xce, 0x87, 0x00, 0x9d, 0x73, 0x1c, 0x83, - 0xcf, 0xab, 0xde, 0xa1, 0xaf, 0xba, 0x87, 0xbe, 0xea, 0xbd, 0x20, 0xf0, 0xd0, 0x57, 0x77, 0xb4, - 0xba, 0xdf, 0xa0, 0x52, 0xc0, 0x53, 0x79, 0x2e, 0x61, 0x4b, 0x02, 0x19, 0x04, 0x2d, 0xe9, 0x3f, - 0x47, 0x4b, 0xc8, 0x56, 0x88, 0xed, 0x12, 0x67, 0x5b, 0xe8, 0xc9, 0xe6, 0x65, 0x0d, 0xc1, 0xe5, - 0xe0, 0xa6, 0xdf, 0xdb, 0x4f, 0xfd, 0x93, 0xf6, 0x13, 0x7e, 0xd0, 0x6e, 0xba, 0xe7, 0xac, 0x3f, - 0x92, 0x4f, 0x24, 0x98, 0x4b, 0xb6, 0xc3, 0x8a, 0xaa, 0x70, 0x5d, 0x7c, 0x62, 0x63, 0x03, 0x17, - 0x04, 0x15, 0x8a, 0x02, 0x62, 0xbd, 0xe3, 0x2d, 0xc1, 0x9a, 0xa2, 0x40, 0xd6, 0x87, 0xf1, 0x8e, - 0x4a, 0xfe, 0x2a, 0x09, 0x3f, 0x44, 0xdf, 0xc0, 0x6c, 0x82, 0x0d, 0xd2, 0xde, 0x87, 0x6b, 0x82, - 0x97, 0x2b, 0xa2, 0xce, 0x09, 0x50, 0x23, 0xa1, 0x90, 0x73, 0x4c, 0xef, 0x5e, 0x50, 0x66, 0x60, - 0x5a, 0x00, 0xf0, 0x91, 0x5d, 0xf7, 0x09, 0x6d, 0xc8, 0xc4, 0x19, 0x20, 0xde, 0x2e, 0xbc, 0xd6, - 0xad, 0x12, 0x12, 0x86, 0x3c, 0x1c, 0x04, 0xc1, 0xae, 0xea, 0xa1, 0xbb, 0xca, 0x0a, 0x9e, 0x67, - 0x5b, 0xcc, 0xe9, 0x06, 0x72, 0xdf, 0x7a, 0x9d, 0xad, 0x1a, 0x28, 0x79, 0x17, 0xca, 0xe7, 0xf8, - 0x66, 0x0b, 0x7b, 0x20, 0xe1, 0x06, 0x40, 0x84, 0x6d, 0x4a, 0xc0, 0xd6, 0x8d, 0x35, 0x6c, 0xb5, - 0x89, 0x2a, 0x48, 0xb4, 0x61, 0x18, 0x11, 0xa2, 0x8b, 0x7a, 0x04, 0x7f, 0x95, 0xb0, 0x88, 0x70, - 0x92, 0x98, 0x22, 0xfa, 0xcf, 0x5d, 0xc4, 0xc5, 0x3d, 0x8f, 0x6b, 0x9d, 0xb3, 0x4e, 0x30, 0xd5, - 0x31, 0x5b, 0xf4, 0x25, 0x4c, 0x89, 0x9d, 0xb0, 0xc0, 0x3b, 0x70, 0x45, 0x34, 0xe0, 0x33, 0xb1, - 0x35, 0x86, 0x66, 0x7b, 0xd4, 0x0a, 0x8e, 0x35, 0x43, 0xc0, 0x76, 0x27, 0xc3, 0x80, 0x17, 0xb5, - 0x63, 0xbf, 0x4b, 0x58, 0x53, 0x24, 0x4f, 0x7c, 0x4d, 0xfd, 0xff, 0xb3, 0xa6, 0x0b, 0xdb, 0xbd, - 0xd5, 0x1f, 0xc6, 0x60, 0x90, 0x53, 0x93, 0x67, 0x12, 0x8c, 0x06, 0xb5, 0x20, 0x29, 0x08, 0xc0, - 0xe2, 0xd4, 0xa4, 0xbc, 0x9c, 0xce, 0xd8, 0x23, 0x50, 0xf2, 0xdf, 0xfe, 0xf9, 0xef, 0xb3, 0x4b, - 0x0a, 0xc9, 0xd2, 0xa8, 0x7c, 0x0e, 0xe9, 0x4e, 0xf2, 0x44, 0x02, 0xe8, 0xe8, 0x39, 0xb2, 0x18, - 0x97, 0x26, 0x22, 0x32, 0xe5, 0xa5, 0x34, 0xa6, 0xc8, 0x33, 0xcf, 0x79, 0xb2, 0x24, 0x23, 0xe0, - 0x09, 0x68, 0x4e, 0xf2, 0x5c, 0x82, 0x91, 0x80, 0xae, 0x22, 0x3d, 0x72, 0x04, 0x25, 0x9b, 0x5c, - 0x48, 0x65, 0x8b, 0x40, 0xab, 0x1c, 0x68, 0x99, 0x2c, 0xc5, 0x03, 0x71, 0x05, 0x47, 0x8f, 0x7d, - 0x19, 0x78, 0x42, 0x7e, 0x69, 0xb7, 0xca, 0x8d, 0xd5, 0xab, 0x55, 0x01, 0xfd, 0x2b, 0x2f, 0xa5, - 0x31, 0x45, 0xb2, 0xf7, 0x38, 0xd9, 0xdb, 0x64, 0x3d, 0x99, 0x2c, 0x00, 0x46, 0x8f, 0x03, 0x0a, - 0xfa, 0x84, 0x7c, 0x0d, 0x43, 0x38, 0xc4, 0xb9, 0xb8, 0xb4, 0xa1, 0x07, 0x54, 0x9e, 0xef, 0x65, - 0x86, 0x64, 0xb3, 0x9c, 0xec, 0x06, 0x99, 0xa4, 0x71, 0xff, 0x1e, 0xdd, 0x69, 0x1a, 0xf2, 0xd4, - 0x09, 0xc9, 0xc7, 0x45, 0xed, 0xd6, 0x9c, 0xf2, 0x62, 0x0a, 0x4b, 0x44, 0x58, 0xe6, 0x08, 0xf3, - 0x64, 0x8e, 0xc6, 0xfd, 0x03, 0xa5, 0xc7, 0xbe, 0x0c, 0x3d, 0x21, 0x3f, 0x4a, 0x30, 0x12, 0x90, - 0x87, 0xe4, 0x56, 0x42, 0xa2, 0xa8, 0xa4, 0x95, 0xd5, 0xb4, 0xe6, 0x29, 0x66, 0xca, 0xe0, 0xf6, - 0x65, 0xce, 0x18, 0x44, 0xfc, 0x43, 0x82, 0x71, 0x91, 0xd8, 0x21, 0x6f, 0x26, 0x24, 0x4f, 0x90, - 0x65, 0xf2, 0xfa, 0xb9, 0xfd, 0x90, 0xbe, 0xc8, 0xe9, 0x0b, 0x64, 0x91, 0xa6, 0xfd, 0xc7, 0x4d, - 0x7e, 0x93, 0x60, 0x2c, 0x22, 0x7f, 0xc8, 0x5a, 0x02, 0x41, 0x9c, 0x36, 0x93, 0x6f, 0x9f, 0xcf, - 0x09, 0x99, 0x55, 0xce, 0x9c, 0x27, 0xf3, 0x34, 0xd5, 0x27, 0x12, 0xf2, 0xb3, 0x04, 0x57, 0xc3, - 0x9a, 0x88, 0xac, 0xa4, 0x4b, 0xdc, 0x51, 0x20, 0x72, 0xf1, 0x1c, 0x1e, 0xc8, 0x59, 0xe0, 0x9c, - 0x39, 0x72, 0x93, 0xf6, 0xfe, 0xe8, 0x43, 0xbe, 0x93, 0x60, 0xb8, 0xc3, 0x57, 0x48, 0xc8, 0x16, - 0x41, 0x5b, 0x4e, 0x67, 0x8c, 0x54, 0xb7, 0x38, 0xd5, 0x02, 0xc9, 0xd1, 0xa4, 0xcf, 0x35, 0xf4, - 0x98, 0x6f, 0xf6, 0x09, 0x79, 0x2a, 0xc1, 0x68, 0x3b, 0xc8, 0x86, 0x61, 0xc4, 0xa3, 0x09, 0x74, - 0x5b, 0x3c, 0x9a, 0x48, 0x7f, 0x29, 0x39, 0x8e, 0x36, 0x43, 0xa6, 0x13, 0xd1, 0xc8, 0x4f, 0x6d, - 0x24, 0x9c, 0x3d, 0xb5, 0x67, 0x03, 0xc2, 0x63, 0x47, 0x53, 0xdb, 0x23, 0xd8, 0x0a, 0x07, 0x5b, - 0x22, 0x79, 0xda, 0xe3, 0xf3, 0x55, 0xbb, 0x6d, 0xdf, 0x4b, 0xf0, 0x6a, 0x30, 0x94, 0xdb, 0x39, - 0xb5, 0x67, 0x33, 0x52, 0x62, 0xc6, 0x48, 0xa1, 0xc4, 0xf7, 0x7f, 0x08, 0x73, 0xf3, 0xce, 0x8b, - 0xd3, 0x8c, 0xf4, 0xf2, 0x34, 0x23, 0xfd, 0x73, 0x9a, 0x91, 0x9e, 0x9e, 0x65, 0xfa, 0x5e, 0x9e, - 0x65, 0xfa, 0xfe, 0x3a, 0xcb, 0xf4, 0xdd, 0x5f, 0xa9, 0xeb, 0xce, 0x17, 0x87, 0x15, 0xb5, 0x6a, - 0x1e, 0xd0, 0x0f, 0xbc, 0x28, 0xf7, 0x98, 0xf3, 0x95, 0x69, 0xed, 0xb7, 0x83, 0x1e, 0xf9, 0x61, - 0x9d, 0xc7, 0x4d, 0x66, 0x57, 0x86, 0xf8, 0xe7, 0xb4, 0xb5, 0xff, 0x02, 0x00, 0x00, 0xff, 0xff, - 0x94, 0xbe, 0xb7, 0xd4, 0x39, 0x15, 0x00, 0x00, + 0x14, 0xc7, 0xeb, 0xb5, 0xeb, 0xe8, 0x49, 0x37, 0xe8, 0x5d, 0x19, 0xa9, 0xd7, 0xa6, 0xa9, 0xd7, + 0xb4, 0x69, 0xd3, 0xd9, 0x4d, 0x3b, 0x51, 0x78, 0xe0, 0x47, 0x2b, 0x41, 0xd5, 0x69, 0x4c, 0x6d, + 0x28, 0x3c, 0x4c, 0x88, 0xc8, 0x49, 0xee, 0x82, 0xa9, 0x1b, 0x67, 0xbe, 0x6e, 0xe8, 0xa8, 0x2a, + 0x24, 0x5e, 0xf7, 0x32, 0x69, 0xe2, 0x65, 0x02, 0x09, 0x10, 0x12, 0x6f, 0xbc, 0xf0, 0xc6, 0x5f, + 0xb0, 0xc7, 0x49, 0xbc, 0xf0, 0x84, 0x50, 0xcb, 0x1f, 0x82, 0x7c, 0x7d, 0x9c, 0xd8, 0xf1, 0xcf, + 0xa0, 0xbe, 0xc5, 0xbe, 0xe7, 0xc7, 0xe7, 0x1c, 0x1f, 0xdf, 0xfb, 0x8d, 0x61, 0x86, 0x1e, 0x1b, + 0x75, 0xc3, 0xa4, 0x8a, 0x61, 0xaa, 0x75, 0x9d, 0x2a, 0x9d, 0xb2, 0xf2, 0xe8, 0x88, 0x9a, 0x8f, + 0xe5, 0xb6, 0x69, 0x58, 0x06, 0x99, 0xc0, 0x65, 0xd9, 0x59, 0x96, 0x3b, 0x65, 0x71, 0xb9, 0x6e, + 0xb0, 0x43, 0x83, 0x29, 0x35, 0x95, 0x51, 0xc7, 0x56, 0xe9, 0x94, 0x6b, 0xd4, 0x52, 0xcb, 0x4a, + 0x5b, 0x6d, 0x6a, 0x2d, 0xd5, 0xd2, 0x8c, 0x96, 0xe3, 0x2e, 0x16, 0x83, 0xd1, 0xb5, 0x56, 0x83, + 0x1e, 0x57, 0x4d, 0x5a, 0xa7, 0x2d, 0xab, 0x7a, 0xc8, 0x9a, 0x68, 0x59, 0x4a, 0xb0, 0x6c, 0xab, + 0xa6, 0x7a, 0xc8, 0xd0, 0x78, 0x3e, 0x68, 0x6c, 0xa7, 0xed, 0xd0, 0xaa, 0x65, 0x1c, 0x50, 0x37, + 0x79, 0x2e, 0x68, 0xe5, 0x8b, 0x12, 0x52, 0x7a, 0xdb, 0xd4, 0xea, 0x34, 0xc6, 0xdd, 0x5e, 0x76, + 0xdd, 0xa5, 0xe0, 0x7a, 0xa0, 0xaa, 0x42, 0xa4, 0x8d, 0x8f, 0x44, 0x0e, 0x9a, 0x75, 0x54, 0x5d, + 0x6b, 0xa8, 0x96, 0x61, 0x56, 0x8f, 0xda, 0x0d, 0xd5, 0xa2, 0xd5, 0x9a, 0x6e, 0xd4, 0x0f, 0xd0, + 0x7e, 0xb2, 0x69, 0x34, 0x0d, 0xfe, 0x53, 0xb1, 0x7f, 0xe1, 0xdd, 0xe9, 0xa6, 0x61, 0x34, 0x75, + 0xaa, 0xa8, 0x6d, 0x4d, 0x51, 0x5b, 0x2d, 0xc3, 0xe2, 0x4f, 0x02, 0x73, 0x48, 0x6f, 0x01, 0xec, + 0xdb, 0xcd, 0xd9, 0xb1, 0xbb, 0x4a, 0x26, 0xe1, 0x32, 0x6f, 0x55, 0x56, 0xc8, 0x0b, 0xc5, 0xb1, + 0x8a, 0x73, 0x61, 0xdf, 0xe5, 0x4d, 0xcf, 0x5e, 0xca, 0x0b, 0xc5, 0x91, 0x8a, 0x73, 0x21, 0x89, + 0x90, 0xdd, 0xb3, 0x1f, 0x73, 0xcf, 0x9d, 0xb2, 0x0a, 0x7d, 0x74, 0x44, 0x99, 0x25, 0x55, 0x61, + 0x2a, 0x64, 0x8d, 0xb5, 0x8d, 0x16, 0xa3, 0x64, 0x0b, 0xae, 0xf2, 0xb8, 0x55, 0xcd, 0x59, 0xc8, + 0x0a, 0xf9, 0xe1, 0x62, 0x66, 0x6d, 0x46, 0x0e, 0x0c, 0x95, 0xdc, 0xf3, 0xaf, 0x8c, 0x5b, 0x9e, + 0x58, 0xd2, 0x3a, 0xdc, 0xe0, 0x09, 0x3e, 0xb6, 0xd4, 0x03, 0x6a, 0xde, 0xd3, 0x98, 0x85, 0xa9, + 0xc9, 0x14, 0xbc, 0xa2, 0x32, 0x46, 0xad, 0xaa, 0xd6, 0xc0, 0x2a, 0xae, 0xf0, 0xeb, 0x9d, 0x86, + 0xc4, 0xe0, 0x8d, 0x80, 0x13, 0x32, 0x65, 0xe1, 0x4a, 0x87, 0x9a, 0x4c, 0x33, 0x9c, 0xd2, 0x87, + 0x2b, 0xee, 0x25, 0x79, 0x17, 0x32, 0x8c, 0xdb, 0x57, 0x75, 0x8d, 0x59, 0xbc, 0x05, 0xe1, 0xac, + 0x9e, 0xa8, 0xc0, 0xba, 0xbf, 0xa5, 0x7d, 0x1f, 0xe9, 0x4e, 0xeb, 0xa1, 0x91, 0x4c, 0x4a, 0x66, + 0xbb, 0x49, 0xd5, 0x46, 0xc3, 0xe4, 0x49, 0xc7, 0xdc, 0xa8, 0x9b, 0x8d, 0x86, 0xd9, 0x57, 0x8a, + 0x13, 0x75, 0x80, 0x52, 0xb4, 0xd6, 0x43, 0x23, 0xb1, 0x14, 0x1e, 0x15, 0x93, 0xda, 0xbf, 0xa5, + 0x3b, 0x81, 0xa4, 0x2c, 0x45, 0xd7, 0x3b, 0x38, 0x27, 0x3e, 0xaf, 0x44, 0xd6, 0xf7, 0x61, 0xdc, + 0xc3, 0xca, 0xb2, 0x97, 0x22, 0x67, 0xc4, 0x03, 0x9b, 0xe9, 0xc1, 0x32, 0x69, 0x12, 0x08, 0xcf, + 0xbb, 0xcb, 0x5f, 0x29, 0x77, 0x32, 0xef, 0xc3, 0x75, 0xdf, 0x5d, 0x04, 0xd9, 0x80, 0x51, 0xe7, + 0xd5, 0xe3, 0x1c, 0x99, 0xb5, 0xa9, 0x90, 0x44, 0x8e, 0xcb, 0xd6, 0xc8, 0x8b, 0xbf, 0x67, 0x87, + 0x2a, 0x68, 0x2e, 0xad, 0xc1, 0xeb, 0x3c, 0xde, 0x36, 0xb5, 0x76, 0xf9, 0x36, 0xe0, 0xe9, 0x08, + 0x4e, 0xb9, 0xd3, 0x91, 0x91, 0xca, 0x15, 0x67, 0x82, 0x1b, 0xd2, 0x06, 0x88, 0xae, 0xcf, 0x3d, + 0xd5, 0xa2, 0xcc, 0xf1, 0x4c, 0xe1, 0xb8, 0x87, 0xb3, 0xe4, 0x49, 0xe6, 0xe1, 0xe7, 0x77, 0xe2, + 0xf8, 0xb9, 0x41, 0x97, 0x9f, 0x5f, 0x49, 0x9f, 0xc1, 0xcd, 0x50, 0x16, 0x8c, 0xfb, 0x0e, 0x5c, + 0xe6, 0x86, 0x18, 0x76, 0x2e, 0x2a, 0xec, 0xbe, 0x76, 0x48, 0x2b, 0xc6, 0x51, 0xab, 0x81, 0xe1, + 0x1d, 0x2f, 0xa9, 0x8a, 0xdd, 0xd9, 0xd4, 0x75, 0x7f, 0x77, 0x3e, 0x04, 0xe8, 0x9d, 0x0a, 0x18, + 0x7c, 0x41, 0x76, 0x8e, 0x10, 0xd9, 0x3e, 0x42, 0x64, 0xe7, 0xb8, 0xc1, 0x23, 0x44, 0xde, 0x55, + 0x9b, 0x6e, 0x83, 0x2a, 0x1e, 0x4f, 0xe9, 0xb9, 0x80, 0x2d, 0xf1, 0x64, 0x08, 0x69, 0xc9, 0xf0, + 0x00, 0x2d, 0x21, 0xdb, 0x3e, 0x36, 0xe7, 0x2d, 0x59, 0x4c, 0x64, 0x73, 0xb2, 0xfa, 0xe0, 0x0a, + 0x70, 0xcb, 0xed, 0xed, 0xa7, 0xee, 0xbe, 0xfd, 0x09, 0xdf, 0xb6, 0xb7, 0xec, 0x5d, 0xdb, 0x1d, + 0xc9, 0x27, 0x02, 0xcc, 0xc7, 0xdb, 0x61, 0x45, 0x75, 0xb8, 0x11, 0xbe, 0xff, 0x63, 0x03, 0x17, + 0x43, 0x2a, 0x0c, 0x0b, 0x88, 0xf5, 0x4e, 0x76, 0x42, 0xd6, 0x24, 0x09, 0xf2, 0x2e, 0x8c, 0xb3, + 0xf1, 0xf2, 0x83, 0xc9, 0xff, 0x12, 0x7d, 0x03, 0x73, 0x31, 0x36, 0x48, 0xfb, 0x00, 0xae, 0x87, + 0x1c, 0xd5, 0x88, 0x3a, 0x1f, 0x82, 0x1a, 0x08, 0x85, 0x9c, 0x13, 0x5a, 0xff, 0x82, 0x34, 0x0b, + 0x33, 0x21, 0x00, 0x1f, 0xb1, 0xa6, 0x4b, 0xc8, 0x20, 0x17, 0x65, 0x80, 0x78, 0x7b, 0xf0, 0x5a, + 0xbf, 0xe6, 0x88, 0x19, 0x72, 0x7f, 0x10, 0x04, 0xbb, 0xa6, 0xf9, 0xee, 0x4a, 0xab, 0xb8, 0xd3, + 0x6d, 0x53, 0xab, 0x1f, 0xc8, 0x3e, 0x43, 0x7b, 0x8f, 0x6a, 0xa4, 0xe2, 0x5c, 0x48, 0x9f, 0xe3, + 0x39, 0xe9, 0xf7, 0x40, 0xc2, 0x4d, 0x80, 0x00, 0xdb, 0x74, 0x08, 0x5b, 0x3f, 0xd6, 0x98, 0xd9, + 0x25, 0xaa, 0x21, 0xd1, 0xa6, 0xae, 0x07, 0x88, 0x2e, 0xea, 0x15, 0xfc, 0x55, 0xc0, 0x22, 0xfc, + 0x49, 0x22, 0x8a, 0x18, 0x1e, 0xb8, 0x88, 0x8b, 0x7b, 0x1f, 0xd7, 0x7b, 0x7b, 0x5d, 0xc8, 0x54, + 0x47, 0x3c, 0xa2, 0x2f, 0x61, 0x3a, 0xdc, 0x09, 0x0b, 0xbc, 0x0b, 0x57, 0xc3, 0x06, 0x7c, 0x36, + 0xb2, 0x46, 0xdf, 0x6c, 0x8f, 0x9b, 0xde, 0xb1, 0xa6, 0x08, 0xd8, 0xed, 0xa4, 0x1f, 0xf0, 0xa2, + 0x9e, 0xd8, 0xef, 0x02, 0xd6, 0x14, 0xc8, 0x13, 0x5d, 0xd3, 0xf0, 0xff, 0xac, 0xe9, 0xc2, 0x9e, + 0xde, 0xda, 0x0f, 0x13, 0x70, 0x99, 0x53, 0x93, 0x67, 0x02, 0x8c, 0x7b, 0x95, 0x25, 0x29, 0x85, + 0x80, 0x45, 0x69, 0x53, 0x71, 0x25, 0x9d, 0xb1, 0x43, 0x20, 0x15, 0xbf, 0xfd, 0xf3, 0xdf, 0x67, + 0x97, 0x24, 0x92, 0x57, 0x82, 0x62, 0xdc, 0xa7, 0x62, 0xc9, 0x13, 0x01, 0xa0, 0xa7, 0x01, 0xc9, + 0x52, 0x54, 0x9a, 0x80, 0x64, 0x15, 0x97, 0xd3, 0x98, 0x22, 0xcf, 0x02, 0xe7, 0xc9, 0x93, 0x5c, + 0x08, 0x8f, 0x47, 0xa7, 0x92, 0xe7, 0x02, 0x64, 0x3c, 0x8a, 0x8b, 0x24, 0xe4, 0xf0, 0x8a, 0x39, + 0xb1, 0x94, 0xca, 0x16, 0x81, 0xd6, 0x38, 0xd0, 0x0a, 0x59, 0x8e, 0x06, 0xe2, 0x0a, 0x4e, 0x39, + 0x71, 0x05, 0xe2, 0x29, 0xf9, 0xa5, 0xdb, 0x2a, 0x3b, 0x56, 0x52, 0xab, 0x3c, 0x9a, 0x59, 0x5c, + 0x4e, 0x63, 0x8a, 0x64, 0xef, 0x71, 0xb2, 0xb7, 0xc9, 0x46, 0x3c, 0x99, 0x07, 0x4c, 0x39, 0xf1, + 0xa8, 0xee, 0x53, 0xf2, 0x35, 0x8c, 0xe2, 0x10, 0x17, 0xa2, 0xd2, 0xfa, 0x5e, 0x50, 0x71, 0x21, + 0xc9, 0x0c, 0xc9, 0xe6, 0x38, 0xd9, 0x4d, 0x32, 0xa5, 0x44, 0xfd, 0x17, 0xb5, 0xa7, 0x69, 0xd4, + 0x51, 0x27, 0xa4, 0x18, 0x15, 0xb5, 0x5f, 0x73, 0x8a, 0x4b, 0x29, 0x2c, 0x11, 0x61, 0x85, 0x23, + 0x2c, 0x90, 0x79, 0x25, 0xea, 0xff, 0xac, 0x72, 0xe2, 0xca, 0xd0, 0x53, 0xf2, 0xa3, 0x00, 0x19, + 0x8f, 0x3c, 0x24, 0xb7, 0x63, 0x12, 0x05, 0x25, 0xad, 0x28, 0xa7, 0x35, 0x4f, 0x31, 0x53, 0x3a, + 0xb7, 0xaf, 0x72, 0x46, 0x2f, 0xe2, 0x1f, 0x02, 0x4c, 0x86, 0x89, 0x1d, 0xf2, 0x66, 0x4c, 0xf2, + 0x18, 0x59, 0x26, 0x6e, 0x0c, 0xec, 0x87, 0xf4, 0x65, 0x4e, 0x5f, 0x22, 0x4b, 0x4a, 0xda, 0xff, + 0xef, 0xe4, 0x37, 0x01, 0x26, 0x02, 0xf2, 0x87, 0xac, 0xc7, 0x10, 0x44, 0x69, 0x33, 0xf1, 0xce, + 0x60, 0x4e, 0xc8, 0x2c, 0x73, 0xe6, 0x22, 0x59, 0x50, 0x52, 0x7d, 0x70, 0x21, 0x3f, 0x0b, 0x70, + 0xcd, 0xaf, 0x89, 0xc8, 0x6a, 0xba, 0xc4, 0x3d, 0x05, 0x22, 0x96, 0x07, 0xf0, 0x40, 0xce, 0x12, + 0xe7, 0x2c, 0x90, 0x5b, 0x4a, 0xf2, 0x27, 0x24, 0xf2, 0x9d, 0x00, 0x63, 0x3d, 0xbe, 0x52, 0x4c, + 0xb6, 0x00, 0xda, 0x4a, 0x3a, 0x63, 0xa4, 0xba, 0xcd, 0xa9, 0x16, 0x49, 0x41, 0x89, 0xfb, 0xf8, + 0xa3, 0x9c, 0xf0, 0x87, 0x7d, 0x4a, 0x9e, 0x0a, 0x30, 0xde, 0x0d, 0xb2, 0xa9, 0xeb, 0xd1, 0x68, + 0x21, 0xba, 0x2d, 0x1a, 0x2d, 0x4c, 0x7f, 0x49, 0x05, 0x8e, 0x36, 0x4b, 0x66, 0x62, 0xd1, 0xc8, + 0x4f, 0x5d, 0x24, 0x9c, 0x3d, 0x39, 0xb1, 0x01, 0xfe, 0xb1, 0x53, 0x52, 0xdb, 0x23, 0xd8, 0x2a, + 0x07, 0x5b, 0x26, 0x45, 0x25, 0xe1, 0x63, 0x58, 0xb7, 0x6d, 0xdf, 0x0b, 0xf0, 0xaa, 0x37, 0x94, + 0xdd, 0x39, 0x39, 0xb1, 0x19, 0x29, 0x31, 0x23, 0xa4, 0x50, 0xec, 0xf9, 0xef, 0xc3, 0xdc, 0xba, + 0xfb, 0xe2, 0x2c, 0x27, 0xbc, 0x3c, 0xcb, 0x09, 0xff, 0x9c, 0xe5, 0x84, 0xa7, 0xe7, 0xb9, 0xa1, + 0x97, 0xe7, 0xb9, 0xa1, 0xbf, 0xce, 0x73, 0x43, 0x0f, 0x56, 0x9b, 0x9a, 0xf5, 0xc5, 0x51, 0x4d, + 0xae, 0x1b, 0x87, 0xca, 0x07, 0x4e, 0x94, 0xfb, 0xd4, 0xfa, 0xca, 0x30, 0x0f, 0xba, 0x41, 0x8f, + 0xdd, 0xb0, 0xd6, 0xe3, 0x36, 0x65, 0xb5, 0x51, 0xfe, 0x71, 0x6e, 0xfd, 0xbf, 0x00, 0x00, 0x00, + 0xff, 0xff, 0x90, 0x8a, 0xfc, 0xe9, 0x87, 0x15, 0x00, 0x00, } // Reference imports to suppress errors if they are not otherwise used. @@ -2313,7 +2341,12 @@ func (m *QueryStakerListResponse) MarshalToSizedBuffer(dAtA []byte) (int, error) i = encodeVarintQuery(dAtA, i, uint64(size)) } i-- - dAtA[i] = 0xa + dAtA[i] = 0x12 + } + if m.Version != 0 { + i = encodeVarintQuery(dAtA, i, uint64(m.Version)) + i-- + dAtA[i] = 0x8 } return len(dAtA) - i, nil } @@ -2385,7 +2418,12 @@ func (m *QueryStakerInfoResponse) MarshalToSizedBuffer(dAtA []byte) (int, error) i = encodeVarintQuery(dAtA, i, uint64(size)) } i-- - dAtA[i] = 0xa + dAtA[i] = 0x12 + } + if m.Version != 0 { + i = encodeVarintQuery(dAtA, i, uint64(m.Version)) + i-- + dAtA[i] = 0x8 } return len(dAtA) - i, nil } @@ -2451,9 +2489,14 @@ func (m *QueryStakerInfosResponse) MarshalToSizedBuffer(dAtA []byte) (int, error i = encodeVarintQuery(dAtA, i, uint64(size)) } i-- - dAtA[i] = 0xa + dAtA[i] = 0x12 } } + if m.Version != 0 { + i = encodeVarintQuery(dAtA, i, uint64(m.Version)) + i-- + dAtA[i] = 0x8 + } return len(dAtA) - i, nil } @@ -3247,6 +3290,9 @@ func (m *QueryStakerListResponse) Size() (n int) { } var l int _ = l + if m.Version != 0 { + n += 1 + sovQuery(uint64(m.Version)) + } if m.StakerList != nil { l = m.StakerList.Size() n += 1 + l + sovQuery(uint64(l)) @@ -3277,6 +3323,9 @@ func (m *QueryStakerInfoResponse) Size() (n int) { } var l int _ = l + if m.Version != 0 { + n += 1 + sovQuery(uint64(m.Version)) + } if m.StakerInfo != nil { l = m.StakerInfo.Size() n += 1 + l + sovQuery(uint64(l)) @@ -3303,6 +3352,9 @@ func (m *QueryStakerInfosResponse) Size() (n int) { } var l int _ = l + if m.Version != 0 { + n += 1 + sovQuery(uint64(m.Version)) + } if len(m.StakerInfos) > 0 { for _, e := range m.StakerInfos { l = e.Size() @@ -3933,6 +3985,25 @@ func (m *QueryStakerListResponse) Unmarshal(dAtA []byte) error { } switch fieldNum { case 1: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field Version", wireType) + } + m.Version = 0 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowQuery + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + m.Version |= int64(b&0x7F) << shift + if b < 0x80 { + break + } + } + case 2: if wireType != 2 { return fmt.Errorf("proto: wrong wireType = %d for field StakerList", wireType) } @@ -4133,6 +4204,25 @@ func (m *QueryStakerInfoResponse) Unmarshal(dAtA []byte) error { } switch fieldNum { case 1: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field Version", wireType) + } + m.Version = 0 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowQuery + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + m.Version |= int64(b&0x7F) << shift + if b < 0x80 { + break + } + } + case 2: if wireType != 2 { return fmt.Errorf("proto: wrong wireType = %d for field StakerInfo", wireType) } @@ -4301,6 +4391,25 @@ func (m *QueryStakerInfosResponse) Unmarshal(dAtA []byte) error { } switch fieldNum { case 1: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field Version", wireType) + } + m.Version = 0 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowQuery + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + m.Version |= int64(b&0x7F) << shift + if b < 0x80 { + break + } + } + case 2: if wireType != 2 { return fmt.Errorf("proto: wrong wireType = %d for field StakerInfos", wireType) } diff --git a/x/oracle/types/recent_msg.pb.go b/x/oracle/types/recent_msg.pb.go index c32f0a096..3326ed4ce 100644 --- a/x/oracle/types/recent_msg.pb.go +++ b/x/oracle/types/recent_msg.pb.go @@ -78,13 +78,13 @@ func (m *RecentMsg) GetMsgs() []*MsgItem { return nil } -// MsgItem represents the message info of createPrice +// MsgItem represents the message info of priceFeed type MsgItem struct { // feeder_id tells of wich feeder this price if corresponding to FeederID uint64 `protobuf:"varint,2,opt,name=feeder_id,json=feederId,proto3" json:"feeder_id,omitempty"` // p_source price with its source info PSources []*PriceSource `protobuf:"bytes,3,rep,name=p_sources,json=pSources,proto3" json:"p_sources,omitempty"` - // validator tells which validator create this price + // validator tells which validator provide this price Validator string `protobuf:"bytes,4,opt,name=validator,proto3" json:"validator,omitempty"` } diff --git a/x/oracle/types/tx.pb.go b/x/oracle/types/tx.pb.go index 5cc6f4680..eddb6fdb5 100644 --- a/x/oracle/types/tx.pb.go +++ b/x/oracle/types/tx.pb.go @@ -31,8 +31,8 @@ var _ = math.Inf // proto package needs to be updated. const _ = proto.GoGoProtoPackageIsVersion3 // please upgrade the proto package -// MsgCreatePrice provide the price updating message -type MsgCreatePrice struct { +// MsgPriceFeed provide the price updating message +type MsgPriceFeed struct { // creator tells which is the message sender and should sign this message Creator string `protobuf:"bytes,1,opt,name=creator,proto3" json:"creator,omitempty"` // refer to id from Params.TokenFeeders, 0 is reserved, invalid to use @@ -45,18 +45,18 @@ type MsgCreatePrice struct { Nonce int32 `protobuf:"varint,5,opt,name=nonce,proto3" json:"nonce,omitempty"` } -func (m *MsgCreatePrice) Reset() { *m = MsgCreatePrice{} } -func (m *MsgCreatePrice) String() string { return proto.CompactTextString(m) } -func (*MsgCreatePrice) ProtoMessage() {} -func (*MsgCreatePrice) Descriptor() ([]byte, []int) { +func (m *MsgPriceFeed) Reset() { *m = MsgPriceFeed{} } +func (m *MsgPriceFeed) String() string { return proto.CompactTextString(m) } +func (*MsgPriceFeed) ProtoMessage() {} +func (*MsgPriceFeed) Descriptor() ([]byte, []int) { return fileDescriptor_0d4ae88d3a4a094d, []int{0} } -func (m *MsgCreatePrice) XXX_Unmarshal(b []byte) error { +func (m *MsgPriceFeed) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) } -func (m *MsgCreatePrice) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { +func (m *MsgPriceFeed) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { if deterministic { - return xxx_messageInfo_MsgCreatePrice.Marshal(b, m, deterministic) + return xxx_messageInfo_MsgPriceFeed.Marshal(b, m, deterministic) } else { b = b[:cap(b)] n, err := m.MarshalToSizedBuffer(b) @@ -66,69 +66,69 @@ func (m *MsgCreatePrice) XXX_Marshal(b []byte, deterministic bool) ([]byte, erro return b[:n], nil } } -func (m *MsgCreatePrice) XXX_Merge(src proto.Message) { - xxx_messageInfo_MsgCreatePrice.Merge(m, src) +func (m *MsgPriceFeed) XXX_Merge(src proto.Message) { + xxx_messageInfo_MsgPriceFeed.Merge(m, src) } -func (m *MsgCreatePrice) XXX_Size() int { +func (m *MsgPriceFeed) XXX_Size() int { return m.Size() } -func (m *MsgCreatePrice) XXX_DiscardUnknown() { - xxx_messageInfo_MsgCreatePrice.DiscardUnknown(m) +func (m *MsgPriceFeed) XXX_DiscardUnknown() { + xxx_messageInfo_MsgPriceFeed.DiscardUnknown(m) } -var xxx_messageInfo_MsgCreatePrice proto.InternalMessageInfo +var xxx_messageInfo_MsgPriceFeed proto.InternalMessageInfo -func (m *MsgCreatePrice) GetCreator() string { +func (m *MsgPriceFeed) GetCreator() string { if m != nil { return m.Creator } return "" } -func (m *MsgCreatePrice) GetFeederID() uint64 { +func (m *MsgPriceFeed) GetFeederID() uint64 { if m != nil { return m.FeederID } return 0 } -func (m *MsgCreatePrice) GetPrices() []*PriceSource { +func (m *MsgPriceFeed) GetPrices() []*PriceSource { if m != nil { return m.Prices } return nil } -func (m *MsgCreatePrice) GetBasedBlock() uint64 { +func (m *MsgPriceFeed) GetBasedBlock() uint64 { if m != nil { return m.BasedBlock } return 0 } -func (m *MsgCreatePrice) GetNonce() int32 { +func (m *MsgPriceFeed) GetNonce() int32 { if m != nil { return m.Nonce } return 0 } -// MsgCreatePriceResponse -type MsgCreatePriceResponse struct { +// MsgPriceFeedResponse +type MsgPriceFeedResponse struct { } -func (m *MsgCreatePriceResponse) Reset() { *m = MsgCreatePriceResponse{} } -func (m *MsgCreatePriceResponse) String() string { return proto.CompactTextString(m) } -func (*MsgCreatePriceResponse) ProtoMessage() {} -func (*MsgCreatePriceResponse) Descriptor() ([]byte, []int) { +func (m *MsgPriceFeedResponse) Reset() { *m = MsgPriceFeedResponse{} } +func (m *MsgPriceFeedResponse) String() string { return proto.CompactTextString(m) } +func (*MsgPriceFeedResponse) ProtoMessage() {} +func (*MsgPriceFeedResponse) Descriptor() ([]byte, []int) { return fileDescriptor_0d4ae88d3a4a094d, []int{1} } -func (m *MsgCreatePriceResponse) XXX_Unmarshal(b []byte) error { +func (m *MsgPriceFeedResponse) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) } -func (m *MsgCreatePriceResponse) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { +func (m *MsgPriceFeedResponse) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { if deterministic { - return xxx_messageInfo_MsgCreatePriceResponse.Marshal(b, m, deterministic) + return xxx_messageInfo_MsgPriceFeedResponse.Marshal(b, m, deterministic) } else { b = b[:cap(b)] n, err := m.MarshalToSizedBuffer(b) @@ -138,17 +138,17 @@ func (m *MsgCreatePriceResponse) XXX_Marshal(b []byte, deterministic bool) ([]by return b[:n], nil } } -func (m *MsgCreatePriceResponse) XXX_Merge(src proto.Message) { - xxx_messageInfo_MsgCreatePriceResponse.Merge(m, src) +func (m *MsgPriceFeedResponse) XXX_Merge(src proto.Message) { + xxx_messageInfo_MsgPriceFeedResponse.Merge(m, src) } -func (m *MsgCreatePriceResponse) XXX_Size() int { +func (m *MsgPriceFeedResponse) XXX_Size() int { return m.Size() } -func (m *MsgCreatePriceResponse) XXX_DiscardUnknown() { - xxx_messageInfo_MsgCreatePriceResponse.DiscardUnknown(m) +func (m *MsgPriceFeedResponse) XXX_DiscardUnknown() { + xxx_messageInfo_MsgPriceFeedResponse.DiscardUnknown(m) } -var xxx_messageInfo_MsgCreatePriceResponse proto.InternalMessageInfo +var xxx_messageInfo_MsgPriceFeedResponse proto.InternalMessageInfo // MsgUpdateParms type MsgUpdateParams struct { @@ -245,8 +245,8 @@ func (m *MsgUpdateParamsResponse) XXX_DiscardUnknown() { var xxx_messageInfo_MsgUpdateParamsResponse proto.InternalMessageInfo func init() { - proto.RegisterType((*MsgCreatePrice)(nil), "exocore.oracle.v1.MsgCreatePrice") - proto.RegisterType((*MsgCreatePriceResponse)(nil), "exocore.oracle.v1.MsgCreatePriceResponse") + proto.RegisterType((*MsgPriceFeed)(nil), "exocore.oracle.v1.MsgPriceFeed") + proto.RegisterType((*MsgPriceFeedResponse)(nil), "exocore.oracle.v1.MsgPriceFeedResponse") proto.RegisterType((*MsgUpdateParams)(nil), "exocore.oracle.v1.MsgUpdateParams") proto.RegisterType((*MsgUpdateParamsResponse)(nil), "exocore.oracle.v1.MsgUpdateParamsResponse") } @@ -254,40 +254,41 @@ func init() { func init() { proto.RegisterFile("exocore/oracle/v1/tx.proto", fileDescriptor_0d4ae88d3a4a094d) } var fileDescriptor_0d4ae88d3a4a094d = []byte{ - // 528 bytes of a gzipped FileDescriptorProto + // 529 bytes of a gzipped FileDescriptorProto 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x84, 0x53, 0xbf, 0x6f, 0xd3, 0x40, - 0x14, 0xce, 0x91, 0x26, 0x34, 0x97, 0x0a, 0xd4, 0x53, 0x44, 0x9d, 0x48, 0x75, 0x52, 0xb3, 0xa4, - 0x91, 0x62, 0xd3, 0x20, 0x65, 0x00, 0x16, 0xcc, 0x0f, 0xa9, 0x48, 0x41, 0xc8, 0x15, 0x0c, 0x20, - 0x11, 0x39, 0xf6, 0xe1, 0x5a, 0x89, 0xfd, 0xac, 0x3b, 0xa7, 0xa4, 0x2b, 0x23, 0x13, 0x7f, 0x06, - 0x63, 0x86, 0xee, 0x48, 0x4c, 0x1d, 0xab, 0x4e, 0x4c, 0x15, 0x4a, 0x86, 0xfc, 0x1b, 0xc8, 0x77, - 0x36, 0x6d, 0xda, 0x48, 0x5d, 0x2c, 0xbf, 0xf7, 0x7d, 0xef, 0xc7, 0xf7, 0x3d, 0x1b, 0xd7, 0xe8, - 0x04, 0x1c, 0x60, 0xd4, 0x00, 0x66, 0x3b, 0x23, 0x6a, 0x1c, 0xed, 0x19, 0xf1, 0x44, 0x8f, 0x18, - 0xc4, 0x40, 0x36, 0x53, 0x4c, 0x97, 0x98, 0x7e, 0xb4, 0x57, 0xdb, 0xb4, 0x03, 0x3f, 0x04, 0x43, - 0x3c, 0x25, 0xab, 0xb6, 0xe5, 0x00, 0x0f, 0x80, 0x1b, 0x01, 0xf7, 0x92, 0xea, 0x80, 0x7b, 0x29, - 0x50, 0x95, 0x40, 0x5f, 0x44, 0x86, 0x0c, 0x52, 0x48, 0xbd, 0x39, 0x35, 0xb2, 0x99, 0x1d, 0x64, - 0xf8, 0xf6, 0x0a, 0x9c, 0xf9, 0x0e, 0x4d, 0xe1, 0x8a, 0x07, 0x1e, 0xc8, 0xb6, 0xc9, 0x9b, 0xcc, - 0x6a, 0x0b, 0x84, 0xef, 0xf5, 0xb8, 0xf7, 0x82, 0x51, 0x3b, 0xa6, 0xef, 0x12, 0x3a, 0x79, 0x8a, - 0xef, 0x3a, 0x49, 0x08, 0x4c, 0x41, 0x0d, 0xd4, 0x2c, 0x99, 0x3b, 0xe7, 0x27, 0xed, 0xed, 0x74, - 0x95, 0x0f, 0xf6, 0xc8, 0x77, 0x13, 0xec, 0xb9, 0xeb, 0x32, 0xca, 0xf9, 0x41, 0xcc, 0xfc, 0xd0, - 0xb3, 0xb2, 0x0a, 0xb2, 0x8b, 0x4b, 0x5f, 0x28, 0x75, 0x29, 0xeb, 0xfb, 0xae, 0x72, 0xa7, 0x81, - 0x9a, 0x6b, 0xe6, 0xc6, 0xec, 0xa2, 0xbe, 0xfe, 0x5a, 0x24, 0xf7, 0x5f, 0x5a, 0xeb, 0x12, 0xde, - 0x77, 0x49, 0x17, 0x17, 0xc5, 0x7e, 0x5c, 0xc9, 0x37, 0xf2, 0xcd, 0x72, 0x47, 0xd5, 0x6f, 0x58, - 0xa7, 0x8b, 0x8d, 0x0e, 0x60, 0xcc, 0x1c, 0x6a, 0xa5, 0x6c, 0x52, 0xc7, 0xe5, 0x81, 0xcd, 0xa9, - 0xdb, 0x1f, 0x8c, 0xc0, 0x19, 0x2a, 0x6b, 0xc9, 0x10, 0x0b, 0x8b, 0x94, 0x99, 0x64, 0x48, 0x05, - 0x17, 0x42, 0x08, 0x1d, 0xaa, 0x14, 0x1a, 0xa8, 0x59, 0xb0, 0x64, 0xa0, 0x29, 0xf8, 0xc1, 0xb2, - 0x50, 0x8b, 0xf2, 0x08, 0x42, 0x4e, 0xb5, 0x5f, 0x08, 0xdf, 0xef, 0x71, 0xef, 0x7d, 0xe4, 0x26, - 0x90, 0xb0, 0x94, 0x74, 0x71, 0xc9, 0x1e, 0xc7, 0x87, 0xc0, 0xfc, 0xf8, 0x38, 0xb5, 0x41, 0x39, - 0x3f, 0x69, 0x57, 0x52, 0x1b, 0x96, 0xd5, 0x5f, 0x52, 0xc9, 0x33, 0x5c, 0x94, 0x47, 0x11, 0xe2, - 0xcb, 0x9d, 0xea, 0x2a, 0x51, 0x82, 0x60, 0x96, 0x4e, 0x2f, 0xea, 0xb9, 0x9f, 0x8b, 0x69, 0x0b, - 0x59, 0x69, 0xcd, 0x93, 0xee, 0xb7, 0xc5, 0xb4, 0x75, 0xd9, 0xed, 0xfb, 0x62, 0xda, 0x7a, 0x28, - 0x27, 0xb6, 0xb9, 0x3b, 0x34, 0x26, 0xd9, 0x69, 0xaf, 0x6d, 0xab, 0x55, 0xf1, 0xd6, 0xb5, 0x54, - 0x26, 0xae, 0xf3, 0x1b, 0xe1, 0x7c, 0x8f, 0x7b, 0xe4, 0x13, 0x2e, 0x5f, 0x3d, 0xf2, 0xce, 0x8a, - 0xbd, 0x96, 0xed, 0xa9, 0xed, 0xde, 0x4a, 0xc9, 0x86, 0x90, 0xcf, 0x78, 0x63, 0xc9, 0x3d, 0x6d, - 0x75, 0xe9, 0x55, 0x4e, 0xad, 0x75, 0x3b, 0x27, 0xeb, 0x6f, 0xbe, 0x39, 0x9d, 0xa9, 0xe8, 0x6c, - 0xa6, 0xa2, 0xbf, 0x33, 0x15, 0xfd, 0x98, 0xab, 0xb9, 0xb3, 0xb9, 0x9a, 0xfb, 0x33, 0x57, 0x73, - 0x1f, 0x1f, 0x79, 0x7e, 0x7c, 0x38, 0x1e, 0xe8, 0x0e, 0x04, 0xc6, 0x2b, 0xd9, 0xef, 0x2d, 0x8d, - 0xbf, 0x02, 0x1b, 0x1a, 0xd9, 0xef, 0xf0, 0xdf, 0xb5, 0xf8, 0x38, 0xa2, 0x7c, 0x50, 0x14, 0x1f, - 0xfe, 0xe3, 0x7f, 0x01, 0x00, 0x00, 0xff, 0xff, 0xc5, 0xc0, 0xcc, 0xd8, 0xc5, 0x03, 0x00, 0x00, + 0x14, 0xce, 0x91, 0x26, 0x34, 0x97, 0x48, 0xa8, 0xa7, 0x88, 0x3a, 0x91, 0xea, 0x84, 0x30, 0x10, + 0x22, 0xc5, 0xa6, 0x41, 0xca, 0x00, 0x2c, 0x58, 0x50, 0xa9, 0x48, 0x41, 0x95, 0xab, 0x32, 0x30, + 0x10, 0x39, 0xbe, 0xc3, 0xb5, 0x12, 0xfb, 0xac, 0xbb, 0x4b, 0x49, 0x57, 0x46, 0x26, 0xfe, 0x0c, + 0xc6, 0x0c, 0x1d, 0x91, 0x58, 0x3b, 0x56, 0x9d, 0x98, 0x2a, 0x94, 0x08, 0xe5, 0xdf, 0x40, 0xbe, + 0xb3, 0x49, 0xda, 0x46, 0xea, 0x62, 0xf9, 0xbd, 0xef, 0x7b, 0x3f, 0xbe, 0xef, 0xd9, 0xb0, 0x4a, + 0x26, 0xd4, 0xa5, 0x8c, 0x98, 0x94, 0x39, 0xee, 0x88, 0x98, 0x27, 0xbb, 0xa6, 0x98, 0x18, 0x11, + 0xa3, 0x82, 0xa2, 0xad, 0x04, 0x33, 0x14, 0x66, 0x9c, 0xec, 0x56, 0xb7, 0x9c, 0xc0, 0x0f, 0xa9, + 0x29, 0x9f, 0x8a, 0x55, 0xdd, 0x76, 0x29, 0x0f, 0x28, 0x37, 0x03, 0xee, 0xc5, 0xd5, 0x01, 0xf7, + 0x12, 0xa0, 0xa2, 0x80, 0xbe, 0x8c, 0x4c, 0x15, 0x24, 0x90, 0x7e, 0x7b, 0x6a, 0xe4, 0x30, 0x27, + 0x48, 0xf1, 0x9d, 0x35, 0x38, 0xf3, 0x5d, 0x92, 0xc0, 0x65, 0x8f, 0x7a, 0x54, 0xb5, 0x8d, 0xdf, + 0x54, 0xb6, 0xf1, 0x17, 0xc0, 0x52, 0x8f, 0x7b, 0x07, 0x31, 0x71, 0x8f, 0x10, 0x8c, 0x5e, 0xc2, + 0xfb, 0x2e, 0x23, 0x8e, 0xa0, 0x4c, 0x03, 0x75, 0xd0, 0x2c, 0x58, 0x8f, 0x2e, 0xcf, 0xda, 0x3b, + 0xc9, 0x22, 0x1f, 0x9c, 0x91, 0x8f, 0x63, 0xec, 0x35, 0xc6, 0x8c, 0x70, 0x7e, 0x28, 0x98, 0x1f, + 0x7a, 0x76, 0x5a, 0x81, 0x9e, 0xc2, 0xc2, 0x67, 0x42, 0x30, 0x61, 0x7d, 0x1f, 0x6b, 0xf7, 0xea, + 0xa0, 0xb9, 0x61, 0x95, 0x66, 0x57, 0xb5, 0xcd, 0x3d, 0x99, 0xdc, 0x7f, 0x63, 0x6f, 0x2a, 0x78, + 0x1f, 0xa3, 0x2e, 0xcc, 0xcb, 0xed, 0xb8, 0x96, 0xad, 0x67, 0x9b, 0xc5, 0x8e, 0x6e, 0xdc, 0x32, + 0xce, 0x90, 0x5b, 0x1d, 0xd2, 0x31, 0x73, 0x89, 0x9d, 0xb0, 0x51, 0x0d, 0x16, 0x07, 0x0e, 0x27, + 0xb8, 0x3f, 0x18, 0x51, 0x77, 0xa8, 0x6d, 0xc4, 0x43, 0x6c, 0x28, 0x53, 0x56, 0x9c, 0x41, 0x65, + 0x98, 0x0b, 0x69, 0xe8, 0x12, 0x2d, 0x57, 0x07, 0xcd, 0x9c, 0xad, 0x82, 0xc6, 0x43, 0x58, 0x5e, + 0x95, 0x69, 0x13, 0x1e, 0xd1, 0x90, 0x93, 0xc6, 0x2f, 0x00, 0x1f, 0xf4, 0xb8, 0x77, 0x14, 0x61, + 0x47, 0x90, 0x03, 0x69, 0x27, 0xea, 0xc2, 0x82, 0x33, 0x16, 0xc7, 0x94, 0xf9, 0xe2, 0x34, 0x31, + 0x41, 0xbb, 0x3c, 0x6b, 0x97, 0x13, 0x13, 0xae, 0x6b, 0x5f, 0x52, 0xd1, 0x2b, 0x98, 0x57, 0x07, + 0x91, 0xd2, 0x8b, 0x9d, 0xca, 0x3a, 0x49, 0x92, 0x60, 0x15, 0xce, 0xaf, 0x6a, 0x99, 0x1f, 0x8b, + 0x69, 0x0b, 0xd8, 0x49, 0xcd, 0x8b, 0xee, 0xd7, 0xc5, 0xb4, 0xb5, 0xec, 0xf6, 0x6d, 0x31, 0x6d, + 0x3d, 0x56, 0x13, 0xdb, 0x1c, 0x0f, 0xcd, 0x49, 0x7a, 0xd6, 0x1b, 0xdb, 0x36, 0x2a, 0x70, 0xfb, + 0x46, 0x2a, 0x15, 0xd7, 0xf9, 0x09, 0x60, 0xb6, 0xc7, 0x3d, 0x74, 0x04, 0x0b, 0xcb, 0x03, 0xd7, + 0xd6, 0x6c, 0xb5, 0x6a, 0x4d, 0xf5, 0xc9, 0x1d, 0x84, 0xb4, 0x3d, 0xfa, 0x04, 0x4b, 0xd7, 0x7c, + 0x6b, 0xac, 0x2f, 0x5c, 0xe5, 0x54, 0x5b, 0x77, 0x73, 0xd2, 0xfe, 0xd6, 0xbb, 0xf3, 0x99, 0x0e, + 0x2e, 0x66, 0x3a, 0xf8, 0x33, 0xd3, 0xc1, 0xf7, 0xb9, 0x9e, 0xb9, 0x98, 0xeb, 0x99, 0xdf, 0x73, + 0x3d, 0xf3, 0xf1, 0x99, 0xe7, 0x8b, 0xe3, 0xf1, 0xc0, 0x70, 0x69, 0x60, 0xbe, 0x55, 0xfd, 0xde, + 0x13, 0xf1, 0x85, 0xb2, 0xa1, 0x99, 0xfe, 0x04, 0xff, 0xfd, 0x12, 0xa7, 0x11, 0xe1, 0x83, 0xbc, + 0xfc, 0xdc, 0x9f, 0xff, 0x0b, 0x00, 0x00, 0xff, 0xff, 0xc9, 0xd0, 0x15, 0xef, 0xbb, 0x03, 0x00, + 0x00, } // Reference imports to suppress errors if they are not otherwise used. @@ -302,8 +303,8 @@ const _ = grpc.SupportPackageIsVersion4 // // For semantics around ctx use and closing/ending streaming RPCs, please refer to https://godoc.org/google.golang.org/grpc#ClientConn.NewStream. type MsgClient interface { - // CreatePrice creates price for a new oracle round - CreatePrice(ctx context.Context, in *MsgCreatePrice, opts ...grpc.CallOption) (*MsgCreatePriceResponse, error) + // PriceFeed creates price for a new oracle round + PriceFeed(ctx context.Context, in *MsgPriceFeed, opts ...grpc.CallOption) (*MsgPriceFeedResponse, error) // UpdateParams update params value UpdateParams(ctx context.Context, in *MsgUpdateParams, opts ...grpc.CallOption) (*MsgUpdateParamsResponse, error) } @@ -316,9 +317,9 @@ func NewMsgClient(cc grpc1.ClientConn) MsgClient { return &msgClient{cc} } -func (c *msgClient) CreatePrice(ctx context.Context, in *MsgCreatePrice, opts ...grpc.CallOption) (*MsgCreatePriceResponse, error) { - out := new(MsgCreatePriceResponse) - err := c.cc.Invoke(ctx, "/exocore.oracle.v1.Msg/CreatePrice", in, out, opts...) +func (c *msgClient) PriceFeed(ctx context.Context, in *MsgPriceFeed, opts ...grpc.CallOption) (*MsgPriceFeedResponse, error) { + out := new(MsgPriceFeedResponse) + err := c.cc.Invoke(ctx, "/exocore.oracle.v1.Msg/PriceFeed", in, out, opts...) if err != nil { return nil, err } @@ -336,8 +337,8 @@ func (c *msgClient) UpdateParams(ctx context.Context, in *MsgUpdateParams, opts // MsgServer is the server API for Msg service. type MsgServer interface { - // CreatePrice creates price for a new oracle round - CreatePrice(context.Context, *MsgCreatePrice) (*MsgCreatePriceResponse, error) + // PriceFeed creates price for a new oracle round + PriceFeed(context.Context, *MsgPriceFeed) (*MsgPriceFeedResponse, error) // UpdateParams update params value UpdateParams(context.Context, *MsgUpdateParams) (*MsgUpdateParamsResponse, error) } @@ -346,8 +347,8 @@ type MsgServer interface { type UnimplementedMsgServer struct { } -func (*UnimplementedMsgServer) CreatePrice(ctx context.Context, req *MsgCreatePrice) (*MsgCreatePriceResponse, error) { - return nil, status.Errorf(codes.Unimplemented, "method CreatePrice not implemented") +func (*UnimplementedMsgServer) PriceFeed(ctx context.Context, req *MsgPriceFeed) (*MsgPriceFeedResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method PriceFeed not implemented") } func (*UnimplementedMsgServer) UpdateParams(ctx context.Context, req *MsgUpdateParams) (*MsgUpdateParamsResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method UpdateParams not implemented") @@ -357,20 +358,20 @@ func RegisterMsgServer(s grpc1.Server, srv MsgServer) { s.RegisterService(&_Msg_serviceDesc, srv) } -func _Msg_CreatePrice_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { - in := new(MsgCreatePrice) +func _Msg_PriceFeed_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(MsgPriceFeed) if err := dec(in); err != nil { return nil, err } if interceptor == nil { - return srv.(MsgServer).CreatePrice(ctx, in) + return srv.(MsgServer).PriceFeed(ctx, in) } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: "/exocore.oracle.v1.Msg/CreatePrice", + FullMethod: "/exocore.oracle.v1.Msg/PriceFeed", } handler := func(ctx context.Context, req interface{}) (interface{}, error) { - return srv.(MsgServer).CreatePrice(ctx, req.(*MsgCreatePrice)) + return srv.(MsgServer).PriceFeed(ctx, req.(*MsgPriceFeed)) } return interceptor(ctx, in, info, handler) } @@ -398,8 +399,8 @@ var _Msg_serviceDesc = grpc.ServiceDesc{ HandlerType: (*MsgServer)(nil), Methods: []grpc.MethodDesc{ { - MethodName: "CreatePrice", - Handler: _Msg_CreatePrice_Handler, + MethodName: "PriceFeed", + Handler: _Msg_PriceFeed_Handler, }, { MethodName: "UpdateParams", @@ -410,7 +411,7 @@ var _Msg_serviceDesc = grpc.ServiceDesc{ Metadata: "exocore/oracle/v1/tx.proto", } -func (m *MsgCreatePrice) Marshal() (dAtA []byte, err error) { +func (m *MsgPriceFeed) Marshal() (dAtA []byte, err error) { size := m.Size() dAtA = make([]byte, size) n, err := m.MarshalToSizedBuffer(dAtA[:size]) @@ -420,12 +421,12 @@ func (m *MsgCreatePrice) Marshal() (dAtA []byte, err error) { return dAtA[:n], nil } -func (m *MsgCreatePrice) MarshalTo(dAtA []byte) (int, error) { +func (m *MsgPriceFeed) MarshalTo(dAtA []byte) (int, error) { size := m.Size() return m.MarshalToSizedBuffer(dAtA[:size]) } -func (m *MsgCreatePrice) MarshalToSizedBuffer(dAtA []byte) (int, error) { +func (m *MsgPriceFeed) MarshalToSizedBuffer(dAtA []byte) (int, error) { i := len(dAtA) _ = i var l int @@ -469,7 +470,7 @@ func (m *MsgCreatePrice) MarshalToSizedBuffer(dAtA []byte) (int, error) { return len(dAtA) - i, nil } -func (m *MsgCreatePriceResponse) Marshal() (dAtA []byte, err error) { +func (m *MsgPriceFeedResponse) Marshal() (dAtA []byte, err error) { size := m.Size() dAtA = make([]byte, size) n, err := m.MarshalToSizedBuffer(dAtA[:size]) @@ -479,12 +480,12 @@ func (m *MsgCreatePriceResponse) Marshal() (dAtA []byte, err error) { return dAtA[:n], nil } -func (m *MsgCreatePriceResponse) MarshalTo(dAtA []byte) (int, error) { +func (m *MsgPriceFeedResponse) MarshalTo(dAtA []byte) (int, error) { size := m.Size() return m.MarshalToSizedBuffer(dAtA[:size]) } -func (m *MsgCreatePriceResponse) MarshalToSizedBuffer(dAtA []byte) (int, error) { +func (m *MsgPriceFeedResponse) MarshalToSizedBuffer(dAtA []byte) (int, error) { i := len(dAtA) _ = i var l int @@ -566,7 +567,7 @@ func encodeVarintTx(dAtA []byte, offset int, v uint64) int { dAtA[offset] = uint8(v) return base } -func (m *MsgCreatePrice) Size() (n int) { +func (m *MsgPriceFeed) Size() (n int) { if m == nil { return 0 } @@ -594,7 +595,7 @@ func (m *MsgCreatePrice) Size() (n int) { return n } -func (m *MsgCreatePriceResponse) Size() (n int) { +func (m *MsgPriceFeedResponse) Size() (n int) { if m == nil { return 0 } @@ -633,7 +634,7 @@ func sovTx(x uint64) (n int) { func sozTx(x uint64) (n int) { return sovTx(uint64((x << 1) ^ uint64((int64(x) >> 63)))) } -func (m *MsgCreatePrice) Unmarshal(dAtA []byte) error { +func (m *MsgPriceFeed) Unmarshal(dAtA []byte) error { l := len(dAtA) iNdEx := 0 for iNdEx < l { @@ -656,10 +657,10 @@ func (m *MsgCreatePrice) Unmarshal(dAtA []byte) error { fieldNum := int32(wire >> 3) wireType := int(wire & 0x7) if wireType == 4 { - return fmt.Errorf("proto: MsgCreatePrice: wiretype end group for non-group") + return fmt.Errorf("proto: MsgPriceFeed: wiretype end group for non-group") } if fieldNum <= 0 { - return fmt.Errorf("proto: MsgCreatePrice: illegal tag %d (wire type %d)", fieldNum, wire) + return fmt.Errorf("proto: MsgPriceFeed: illegal tag %d (wire type %d)", fieldNum, wire) } switch fieldNum { case 1: @@ -806,7 +807,7 @@ func (m *MsgCreatePrice) Unmarshal(dAtA []byte) error { } return nil } -func (m *MsgCreatePriceResponse) Unmarshal(dAtA []byte) error { +func (m *MsgPriceFeedResponse) Unmarshal(dAtA []byte) error { l := len(dAtA) iNdEx := 0 for iNdEx < l { @@ -829,10 +830,10 @@ func (m *MsgCreatePriceResponse) Unmarshal(dAtA []byte) error { fieldNum := int32(wire >> 3) wireType := int(wire & 0x7) if wireType == 4 { - return fmt.Errorf("proto: MsgCreatePriceResponse: wiretype end group for non-group") + return fmt.Errorf("proto: MsgPriceFeedResponse: wiretype end group for non-group") } if fieldNum <= 0 { - return fmt.Errorf("proto: MsgCreatePriceResponse: illegal tag %d (wire type %d)", fieldNum, wire) + return fmt.Errorf("proto: MsgPriceFeedResponse: illegal tag %d (wire type %d)", fieldNum, wire) } switch fieldNum { default: diff --git a/x/oracle/types/types.go b/x/oracle/types/types.go index b1b6c2bc1..bfaf5aba8 100644 --- a/x/oracle/types/types.go +++ b/x/oracle/types/types.go @@ -4,6 +4,7 @@ import ( "encoding/binary" sdkmath "cosmossdk.io/math" + sdk "github.com/cosmos/cosmos-sdk/types" ) type OracleInfo struct { @@ -43,17 +44,54 @@ type AggFinalPrice struct { Price string } +type NSTType string + const ( + NSTIDPrefix = "nst_" + ETHChain NSTType = "eth" + SOLANAChain NSTType = "solana" + + ETHMainnetChainID = "0x7595" + ETHLocalnetChainID = "0x65" + ETHHoleskyChainID = "0x9d19" + ETHSepoliaChainID = "0x9ce1" + NSTETHAssetAddr = "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee" + DefaultPriceValue = 1 DefaultPriceDecimal = 0 - NSTIDPrefix = "NST" + SourceChainlinkName = "Chainlink" + SourceChainlinkID = 1 + TimeLayout = "2006-01-02 15:04:05" + + DelimiterForCombinedKey = byte('/') ) -var DelimiterForCombinedKey = byte('/') +var ( + NSTChains = map[NSTType][]string{ + ETHChain: {ETHMainnetChainID, ETHLocalnetChainID, ETHHoleskyChainID, ETHSepoliaChainID}, + } + NSTChainsInverted = map[string]NSTType{ + ETHMainnetChainID: ETHChain, + ETHLocalnetChainID: ETHChain, + ETHHoleskyChainID: ETHChain, + ETHSepoliaChainID: ETHChain, + } + NSTAssetAddr = map[NSTType]string{ + ETHChain: NSTETHAssetAddr, + } +) func Uint64Bytes(value uint64) []byte { valueBytes := make([]byte, 8) binary.BigEndian.PutUint64(valueBytes, value) return valueBytes } + +func ConsAddrStrFromCreator(creator string) (string, error) { + accAddress, err := sdk.AccAddressFromBech32(creator) + if err != nil { + return "", err + } + return sdk.ConsAddress(accAddress).String(), nil +}