diff --git a/Makefile b/Makefile index 5ceb6d7..dc6b125 100644 --- a/Makefile +++ b/Makefile @@ -35,6 +35,9 @@ test-unit-contract: compile-contract: cosmwasm/scripts/build_artifacts.sh +compile-contract-arm: + cosmwasm/scripts/build_artifacts_arm.sh + start-relayer: cd cw-relayer && ${MAKE} start @@ -45,4 +48,11 @@ test-e2e: cd cw-relayer && ${MAKE} test-e2e rm cw-relayer/tests/e2e/config/std_reference.wasm -.PHONY: test-e2e \ No newline at end of file +test-e2e-arm: + @echo "Running e2e tests" + ${MAKE} compile-contract-arm + cp -f cosmwasm/artifacts/std_reference-aarch64.wasm cw-relayer/tests/e2e/config/std_reference.wasm + cd cw-relayer && ${MAKE} test-e2e + rm cw-relayer/tests/e2e/config/std_reference.wasm + +.PHONY: test-e2e test-e2e-arm compile-contract compile-contract-arm \ No newline at end of file diff --git a/cosmwasm/Cargo.lock b/cosmwasm/Cargo.lock index bbbb675..e0c40cf 100644 --- a/cosmwasm/Cargo.lock +++ b/cosmwasm/Cargo.lock @@ -735,4 +735,4 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" name = "zeroize" version = "1.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c394b5bd0c6f669e7275d9c20aa90ae064cb22e75a1cad54e1b34088034b149f" +checksum = "c394b5bd0c6f669e7275d9c20aa90ae064cb22e75a1cad54e1b34088034b149f" \ No newline at end of file diff --git a/cosmwasm/scripts/build_artifacts.sh b/cosmwasm/scripts/build_artifacts.sh index 0fba766..b72195f 100755 --- a/cosmwasm/scripts/build_artifacts.sh +++ b/cosmwasm/scripts/build_artifacts.sh @@ -1,4 +1,4 @@ docker run --rm -v "$(pwd)/cosmwasm":/code \ --mount type=volume,source="$(basename "$(pwd)")_cache",target=/code/target \ --mount type=volume,source=registry_cache,target=/usr/local/cargo/registry \ - cosmwasm/workspace-optimizer:0.12.7 + cosmwasm/workspace-optimizer:0.12.7 \ No newline at end of file diff --git a/cosmwasm/scripts/build_artifacts_arm.sh b/cosmwasm/scripts/build_artifacts_arm.sh new file mode 100755 index 0000000..e419cb3 --- /dev/null +++ b/cosmwasm/scripts/build_artifacts_arm.sh @@ -0,0 +1,4 @@ +docker run --rm -v "$(pwd)/cosmwasm":/code \ +--mount type=volume,source="$(basename "$(pwd)")_cache",target=/code/target \ +--mount type=volume,source=registry_cache,target=/usr/local/cargo/registry \ +cosmwasm/workspace-optimizer-arm64:0.12.7 diff --git a/cw-relayer/cmd/cw-relayer.go b/cw-relayer/cmd/cw-relayer.go index c6cadb2..59dbb91 100644 --- a/cw-relayer/cmd/cw-relayer.go +++ b/cw-relayer/cmd/cw-relayer.go @@ -171,6 +171,9 @@ func cwRelayerCmdHandler(cmd *cobra.Command, args []string) error { cfg.MissedThreshold, cfg.MaxRetries, cfg.MedianDuration, + cfg.DeviationDuration, + cfg.SkipNumEvents, + cfg.IgnoreMedianErrors, resolveDuration, queryTimeout, cfg.RequestID, @@ -181,10 +184,12 @@ func cwRelayerCmdHandler(cmd *cobra.Command, args []string) error { cfg.QueryRPCS, ) - g.Go(func() error { - // start the process that queries the prices on Ojo & submits them on Wasmd - return startPriceRelayer(ctx, logger, newRelayer) - }) + g.Go( + func() error { + // start the process that queries the prices on Ojo & submits them on Wasmd + return startPriceRelayer(ctx, logger, newRelayer) + }, + ) // Block main process until all spawned goroutines have gracefully exited and // signal has been captured in the main process or if an error occurs. diff --git a/cw-relayer/config.toml b/cw-relayer/config.toml index e31d2d1..6223226 100644 --- a/cw-relayer/config.toml +++ b/cw-relayer/config.toml @@ -21,8 +21,13 @@ max_retries = 1 gas_adjustment = 1.5 timeout_height = 10 gas_prices = "0.2stake" + # set median duration to 0 to disable posting medians median_duration = 1 + +# set deviation duration to 0 to disable posting deviations +deviation_duration=1 + # resolve duration is the estimated delay between price updates on the contract resolve_duration = "1000ms" missed_threshold = 2 diff --git a/cw-relayer/config/config.go b/cw-relayer/config/config.go index 31cf204..4617334 100644 --- a/cw-relayer/config/config.go +++ b/cw-relayer/config/config.go @@ -47,9 +47,16 @@ type ( DeviationRequestID uint64 `mapstructure:"deviation_request_id"` // force relay prices and reset epoch time in contracts if err in broadcasting tx - MissedThreshold int64 `mapstructure:"missed_threshold"` - MedianDuration int64 `mapstructure:"median_duration"` - ResolveDuration string `mapstructure:"resolve_duration"` + MissedThreshold int64 `mapstructure:"missed_threshold"` + MedianDuration int64 `mapstructure:"median_duration"` + DeviationDuration int64 `mapstructure:"deviation_duration"` + ResolveDuration string `mapstructure:"resolve_duration"` + + // skip price update events + SkipNumEvents int64 `mapstructure:"skip_num_events"` + + // if true, would ignore any errors when querying median or deviations + IgnoreMedianErrors bool `mapstructure:"ignore_median_errors"` GasAdjustment float64 `mapstructure:"gas_adjustment" validate:"required"` GasPrices string `mapstructure:"gas_prices" validate:"required"` diff --git a/cw-relayer/relayer/client/chain_subscribe.go b/cw-relayer/relayer/client/chain_subscribe.go index 28916cc..5d4dd8c 100644 --- a/cw-relayer/relayer/client/chain_subscribe.go +++ b/cw-relayer/relayer/client/chain_subscribe.go @@ -37,8 +37,9 @@ func NewBlockHeightSubscription( maxRetries int64, ) (*EventSubscribe, error) { newEvent := &EventSubscribe{ - logger: logger.With().Str("event", tickEventType).Logger(), - Tick: make(chan struct{}), + logger: logger.With().Str("event", tickEventType).Logger(), + // assuming 15-second price update + Tick: make(chan struct{}, 4), timeout: timeout, maxTickTimeout: maxTickTimeout, rpcAddress: rpcAddress, diff --git a/cw-relayer/relayer/relayer.go b/cw-relayer/relayer/relayer.go index c893ff4..aba7a7c 100644 --- a/cw-relayer/relayer/relayer.go +++ b/cw-relayer/relayer/relayer.go @@ -23,7 +23,10 @@ import ( var ( // RateFactor is used to convert ojo prices to contract-compatible values. - RateFactor = types.NewDec(10).Power(9) + RateFactor = types.NewDec(10).Power(9) + noRates = fmt.Errorf("no rates found") + noMedians = fmt.Errorf("median deviations empty") + noDeviations = fmt.Errorf("deviation deviations empty") ) // Relayer defines a structure that queries prices from ojo and publishes prices to wasm contract. @@ -45,13 +48,17 @@ type Relayer struct { queryTimeout time.Duration // if missedCounter >= missedThreshold, force relay prices (bypasses timing restrictions) - missedCounter int64 - missedThreshold int64 - timeoutHeight int64 - medianDuration int64 - maxQueryRetries int64 - queryRetries int64 - index int + missedCounter int64 + missedThreshold int64 + timeoutHeight int64 + medianDuration int64 + deviationDuration int64 + maxQueryRetries int64 + queryRetries int64 + skipNumEvents int64 + index int + + ignoreMedianErrors bool event chan struct{} config AutoRestartConfig @@ -72,6 +79,9 @@ func New( missedThreshold int64, maxQueryRetries int64, medianDuration int64, + deviationDuration int64, + skipNumEvents int64, + ignoreMedianErrors bool, resolveDuration time.Duration, queryTimeout time.Duration, requestID uint64, @@ -90,11 +100,14 @@ func New( timeoutHeight: timeoutHeight, queryTimeout: queryTimeout, medianDuration: medianDuration, + deviationDuration: deviationDuration, + ignoreMedianErrors: ignoreMedianErrors, resolveDuration: resolveDuration, requestID: requestID, medianRequestID: medianRequestID, deviationRequestID: deviationRequestID, maxQueryRetries: maxQueryRetries, + skipNumEvents: skipNumEvents, closer: psync.NewCloser(), event: event, config: config, @@ -120,12 +133,23 @@ func (r *Relayer) Start(ctx context.Context) error { Uint64("deviation request id", r.deviationRequestID).Msg("relayer state startup successful") } + epoch := int64(-1) + skipEvents := r.skipNumEvents > 0 + r.skipNumEvents++ for { select { case <-ctx.Done(): r.closer.Close() case <-r.event: + epoch++ + if skipEvents { + if epoch%r.skipNumEvents != 0 { + r.logger.Debug().Int64("epoch", epoch).Msg("skipping events") + continue + } + } + r.logger.Debug().Msg("relayer tick") startTime := time.Now() if err := r.tick(ctx); err != nil { @@ -193,10 +217,10 @@ func (r *Relayer) restart(ctx context.Context) error { return nil } -func (r *Relayer) setDenomPrices(ctx context.Context, postMedian bool) error { +func (r *Relayer) setDenomPrices(ctx context.Context, postMedian, postDeviation bool) error { if r.queryRetries > r.maxQueryRetries { r.queryRetries = 0 - return fmt.Errorf("retry threshold exceeded") + return noRates } grpcConn, err := grpc.Dial( @@ -209,7 +233,7 @@ func (r *Relayer) setDenomPrices(ctx context.Context, postMedian bool) error { // retry or switch rpc if err != nil { r.increment() - return r.setDenomPrices(ctx, postMedian) + return r.setDenomPrices(ctx, postMedian, postDeviation) } defer grpcConn.Close() @@ -224,7 +248,7 @@ func (r *Relayer) setDenomPrices(ctx context.Context, postMedian bool) error { if err != nil || queryResponse.ExchangeRates.Empty() { r.logger.Debug().Msg("error querying exchange rates") r.increment() - return r.setDenomPrices(ctx, postMedian) + return r.setDenomPrices(ctx, postMedian, postDeviation) } r.exchangeRates = queryResponse.ExchangeRates @@ -232,50 +256,56 @@ func (r *Relayer) setDenomPrices(ctx context.Context, postMedian bool) error { var mu sync.Mutex g, _ := errgroup.WithContext(ctx) - if postMedian { - g.Go(func() error { - deviationsQueryResponse, err := queryClient.MedianDeviations(ctx, &oracletypes.QueryMedianDeviations{}) - if err != nil { - return err - } + if postDeviation { + g.Go( + func() error { + deviationsQueryResponse, err := queryClient.MedianDeviations(ctx, &oracletypes.QueryMedianDeviations{}) + if err != nil { + return err + } - if len(deviationsQueryResponse.MedianDeviations) == 0 { - return fmt.Errorf("median deviations empty") - } + if len(deviationsQueryResponse.MedianDeviations) == 0 { + return noDeviations + } - deviations := make([]types.DecCoin, len(deviationsQueryResponse.MedianDeviations)) - for i, priceStamp := range deviationsQueryResponse.MedianDeviations { - deviations[i] = *priceStamp.ExchangeRate - } - - mu.Lock() - r.historicalDeviations = deviations - mu.Unlock() - - return nil - }) - - g.Go(func() error { - medianQueryResponse, err := queryClient.Medians(ctx, &oracletypes.QueryMedians{}) - if err != nil { - return err - } + deviations := make([]types.DecCoin, len(deviationsQueryResponse.MedianDeviations)) + for i, priceStamp := range deviationsQueryResponse.MedianDeviations { + deviations[i] = *priceStamp.ExchangeRate + } - if len(medianQueryResponse.Medians) == 0 { - return fmt.Errorf("median rates empty") - } + mu.Lock() + r.historicalDeviations = deviations + mu.Unlock() - medians := make([]types.DecCoin, len(medianQueryResponse.Medians)) - for i, priceStamp := range medianQueryResponse.Medians { - medians[i] = *priceStamp.ExchangeRate - } + return nil + }, + ) + } - mu.Lock() - r.historicalMedians = medians - mu.Unlock() + if postMedian { + g.Go( + func() error { + medianQueryResponse, err := queryClient.Medians(ctx, &oracletypes.QueryMedians{}) + if err != nil { + return err + } + + if len(medianQueryResponse.Medians) == 0 { + return noMedians + } + + medians := make([]types.DecCoin, len(medianQueryResponse.Medians)) + for i, priceStamp := range medianQueryResponse.Medians { + medians[i] = *priceStamp.ExchangeRate + } + + mu.Lock() + r.historicalMedians = medians + mu.Unlock() - return nil - }) + return nil + }, + ) } return g.Wait() @@ -307,12 +337,28 @@ func (r *Relayer) tick(ctx context.Context) error { postMedian = r.requestID%uint64(r.medianDuration) == 0 } - if err := r.setDenomPrices(ctx, postMedian); err != nil { + var postDeviation bool + if r.deviationDuration > 0 { + postDeviation = r.requestID%uint64(r.deviationDuration) == 0 + } + + err = r.setDenomPrices(ctx, postMedian, postDeviation) + switch err { + case nil: + break + case noMedians, noDeviations: + if !r.ignoreMedianErrors { + return err + } + + // as median and deviation are not properly set, do not push prices to contract + postMedian = false + postDeviation = false + default: return err } nextBlockHeight := blockHeight + 1 - forceRelay := r.missedCounter >= r.missedThreshold // set the next resolve time for price feeds on wasm contract @@ -322,39 +368,54 @@ func (r *Relayer) tick(ctx context.Context) error { return err } + logs := r.logger.Info() + logs.Str("contract address", r.contractAddress). + Str("relayer address", r.relayerClient.RelayerAddrString). + Str("block timestamp", blockTimestamp.String()). + Bool("median posted", postMedian). + Bool("deviation posted", postDeviation). + Uint64("request id", r.requestID) + var msgs []types.Msg msgs = append(msgs, r.genWasmMsg(exchangeMsg)) - if postMedian { - deviationMsg, err := genRateMsgData(forceRelay, RelayHistoricalDeviation, r.deviationRequestID, nextBlockTime, r.historicalDeviations) + if postDeviation { + resolveTime := time.Duration(r.resolveDuration.Nanoseconds() * r.deviationDuration) + nextDeviationBlockTime := blockTimestamp.Add(resolveTime).Unix() + deviationMsg, err := genRateMsgData( + forceRelay, + RelayHistoricalDeviation, + r.deviationRequestID, + nextDeviationBlockTime, + r.historicalDeviations, + ) if err != nil { return err } + msgs = append(msgs, r.genWasmMsg(deviationMsg)) + logs.Uint64("deviation request id", r.deviationRequestID) + } + + if postMedian { resolveTime := time.Duration(r.resolveDuration.Nanoseconds() * r.medianDuration) nextMedianBlockTime := blockTimestamp.Add(resolveTime).Unix() - medianMsg, err := genRateMsgData(forceRelay, RelayHistoricalMedian, r.medianRequestID, nextMedianBlockTime, r.historicalMedians) + medianMsg, err := genRateMsgData( + forceRelay, + RelayHistoricalMedian, + r.medianRequestID, + nextMedianBlockTime, + r.historicalMedians, + ) if err != nil { return err } - msgs = append(msgs, r.genWasmMsg(medianMsg), r.genWasmMsg(deviationMsg)) - } - - logs := r.logger.Info() - logs.Str("contract address", r.contractAddress). - Str("relayer address", r.relayerClient.RelayerAddrString). - Str("block timestamp", blockTimestamp.String()). - Bool("median posted", postMedian). - Uint64("request id", r.requestID) - - if postMedian { - logs.Uint64("median request id", r.medianRequestID). - Uint64("deviation request id", r.deviationRequestID) + msgs = append(msgs, r.genWasmMsg(medianMsg)) + logs.Uint64("median request id", r.medianRequestID) } logs.Msg("broadcasting execute to contract") - if err := r.relayerClient.BroadcastTx(nextBlockHeight, r.timeoutHeight, msgs...); err != nil { r.missedCounter += 1 return err @@ -368,9 +429,12 @@ func (r *Relayer) tick(ctx context.Context) error { // increment request id to be stored in contracts r.requestID += 1 if postMedian { - r.deviationRequestID += 1 r.medianRequestID += 1 } + if postDeviation { + r.deviationRequestID += 1 + } + return nil } diff --git a/cw-relayer/relayer/relayer_test.go b/cw-relayer/relayer/relayer_test.go index 18bbc15..1e2ea66 100644 --- a/cw-relayer/relayer/relayer_test.go +++ b/cw-relayer/relayer/relayer_test.go @@ -23,11 +23,26 @@ type RelayerTestSuite struct { func (rts *RelayerTestSuite) SetupSuite() { rts.relayer = New( zerolog.Nop(), - client.RelayerClient{}, "", 100, 5, 10, 0, 1*time.Second, 1*time.Second, 0, 0, 0, AutoRestartConfig{ + client.RelayerClient{}, + "", + 100, + 5, + 10, + 0, + 0, + 2, + true, + 1*time.Second, + 1*time.Second, + 0, + 0, + 0, + AutoRestartConfig{ AutoRestart: false, Denom: "", SkipError: false, - }, nil, []string{""}) + }, nil, []string{""}, + ) } func TestServiceTestSuite(t *testing.T) { @@ -80,28 +95,30 @@ func (rts *RelayerTestSuite) Test_generateRelayMsg() { } for _, tc := range testCases { - rts.Run(tc.tc, func() { - msg, err := genRateMsgData(tc.forceRelay, tc.msgType, 0, 0, exchangeRates) - rts.Require().NoError(err) - - var expectedMsg map[string]Msg - err = json.Unmarshal(msg, &expectedMsg) - rts.Require().NoError(err) - - var msgKey string - if tc.forceRelay { - msgKey = fmt.Sprintf("force_%s", tc.msgType.String()) - } else { - msgKey = tc.msgType.String() - } - - rates := expectedMsg[msgKey].SymbolRates - rts.Require().NotZero(len(rates)) - for i, rate := range rates { - rts.Require().Equal(rate[0], exchangeRates[i].Denom) - rts.Require().Equal(rate[1], exchangeRates[i].Amount.Mul(RateFactor).TruncateInt().String()) - } - }) + rts.Run( + tc.tc, func() { + msg, err := genRateMsgData(tc.forceRelay, tc.msgType, 0, 0, exchangeRates) + rts.Require().NoError(err) + + var expectedMsg map[string]Msg + err = json.Unmarshal(msg, &expectedMsg) + rts.Require().NoError(err) + + var msgKey string + if tc.forceRelay { + msgKey = fmt.Sprintf("force_%s", tc.msgType.String()) + } else { + msgKey = tc.msgType.String() + } + + rates := expectedMsg[msgKey].SymbolRates + rts.Require().NotZero(len(rates)) + for i, rate := range rates { + rts.Require().Equal(rate[0], exchangeRates[i].Denom) + rts.Require().Equal(rate[1], exchangeRates[i].Amount.Mul(RateFactor).TruncateInt().String()) + } + }, + ) } } diff --git a/cw-relayer/tests/e2e/config/relayer-config.toml b/cw-relayer/tests/e2e/config/relayer-config.toml index ccfe9e6..b45422b 100644 --- a/cw-relayer/tests/e2e/config/relayer-config.toml +++ b/cw-relayer/tests/e2e/config/relayer-config.toml @@ -12,8 +12,12 @@ gas_adjustment = 1.5 timeout_height = 10 gas_prices = "0.2stake" median_duration = 1 +deviation_duration = 1 +ignore_median_errors = false + missed_threshold = 2 event_type = "ojo.oracle.v1.EventSetFxRate" +skip_num_events= 2 [restart] auto_id = true diff --git a/scripts/deploy_contract.sh b/scripts/deploy_contract.sh index f6b4ceb..9ebd802 100755 --- a/scripts/deploy_contract.sh +++ b/scripts/deploy_contract.sh @@ -5,18 +5,16 @@ DEMO_MNEMONIC_1="pony glide frown crisp unfold lawn cup loan trial govern usual RPC="http://0.0.0.0:26657" NODE="--node $RPC" -TXFLAG="$NODE --chain-id $CHAINID_1 --gas-prices 0.25stake --keyring-backend test --gas auto --gas-adjustment 1.3" +TXFLAG="$NODE --chain-id $CHAINID_1 --gas-prices 0.25stake --keyring-backend test --gas auto --gas-adjustment 1.3 --broadcast-mode=block -y" # network check export DEMOWALLET=$($BINARY keys show demowallet1 -a --keyring-backend test --home ./data/$CHAINID_1) && echo $DEMOWALLET; #$BINARY query wasm list-code $NODE # deploy smart contract -$BINARY tx wasm store $CONTRACT_PATH --from $DEMOWALLET --home ./data/$CHAINID_1 $TXFLAG -y -sleep 5 +$BINARY tx wasm store $CONTRACT_PATH --from $DEMOWALLET --home ./data/$CHAINID_1 $TXFLAG #instantiate contract -$BINARY tx wasm instantiate 1 '{}' --label test --admin $DEMOWALLET --from $DEMOWALLET --home ./data/$CHAINID_1 $TXFLAG -y -sleep 5 +$BINARY tx wasm instantiate 1 '{}' --label test --admin $DEMOWALLET --from $DEMOWALLET --home ./data/$CHAINID_1 $TXFLAG # query contract address CONTRACT=$($BINARY query wasm list-contract-by-code "1" $NODE --output json | jq -r '.contracts[-1]') @@ -24,12 +22,10 @@ echo $CONTRACT #sample transactions ADD_RELAYERS='{"add_relayers": {"relayers": ["wasm1usr9g5a4s2qrwl63sdjtrs2qd4a7huh6qksawp"]}}' -$BINARY tx wasm execute $CONTRACT "$ADD_RELAYERS" --home ./data/$CHAINID_1 --from $DEMOWALLET $TXFLAG -y -sleep 5 +$BINARY tx wasm execute $CONTRACT "$ADD_RELAYERS" --home ./data/$CHAINID_1 --from $DEMOWALLET $TXFLAG RELAY='{"force_relay": {"symbol_rates": [["stake","30"]], "resolve_time":"10", "request_id":"1"}}' -$BINARY tx wasm execute $CONTRACT "$RELAY" --home ./data/$CHAINID_1 --from $DEMOWALLET $TXFLAG -y -sleep 5 +$BINARY tx wasm execute $CONTRACT "$RELAY" --home ./data/$CHAINID_1 --from $DEMOWALLET $TXFLAG QUERY='{"get_ref": {"symbol": "stake"}}' $BINARY query wasm contract-state smart $CONTRACT "$QUERY" $NODE --output json \ No newline at end of file