From 0c2733b0ee233105ec56da8ec884b9c53ff8d4a2 Mon Sep 17 00:00:00 2001 From: izyak Date: Thu, 30 May 2024 17:02:12 +0545 Subject: [PATCH 1/3] feat: cli command to call recv packet and ack packet --- cmd/tx.go | 118 ++++++++++++++++ relayer/chains/icon/query.go | 40 +++++- relayer/chains/icon/tx.go | 44 ++++++ relayer/client.go | 264 +++++++++++++++++++++++++++++++---- relayer/relayMsgs.go | 21 +++ 5 files changed, 454 insertions(+), 33 deletions(-) diff --git a/cmd/tx.go b/cmd/tx.go index 49fdf09e9..40b4d5e6a 100644 --- a/cmd/tx.go +++ b/cmd/tx.go @@ -55,6 +55,8 @@ Most of these commands take a [path] argument. Make sure: registerCounterpartyCmd(a), lineBreakCommand(), claimFeesCmd(a), + recvPacket(a), + ackPacket(a), ) return cmd @@ -933,6 +935,122 @@ $ %s tx flush demo-path channel-0`, return cmd } +func recvPacket(a *appState) *cobra.Command { + cmd := &cobra.Command{ + Use: "recv-packet src_chain_name dst_chain_name path_name [txn-hash]", + Short: "update block [height] of src_chain on dst_chain", + Args: withUsage(cobra.RangeArgs(4, 5)), + Example: strings.TrimSpace( + fmt.Sprintf(` + $ %s transact recv-packet icon archway icon-archway [tx_hash] [trusted_height OR {empty} OR {skip-update}] + $ %s transact recv-packet icon archway icon-archway [tx_hash] [trusted_height] + $ %s transact recv-packet icon archway icon-archway [tx_hash] + $ %s transact recv-packet icon archway icon-archway [tx_hash] [skip-update] + `, appName, appName, appName, appName), + ), + RunE: func(cmd *cobra.Command, args []string) error { + src, ok := a.config.Chains[args[0]] + if !ok { + return errChainNotFound(args[0]) + } + dst, ok := a.config.Chains[args[1]] + if !ok { + return errChainNotFound(args[1]) + } + _, _, _, err := a.config.ChainsFromPath(args[2]) + if err != nil { + return err + } + + // ensure that keys exist + if exists := src.ChainProvider.KeyExists(src.ChainProvider.Key()); !exists { + return fmt.Errorf("key %s not found on src chain %s", src.ChainProvider.Key(), src.ChainID()) + } + if exists := dst.ChainProvider.KeyExists(dst.ChainProvider.Key()); !exists { + return fmt.Errorf("key %s not found on dst chain %s", dst.ChainProvider.Key(), dst.ChainID()) + } + + // get transaction hash + txnHash := args[3] + + // get trusted height + var trustedHeight int + var skipUpdate bool + skipUpdate = false + if len(args) == 5 { + var err error + trustedHeight, err = strconv.Atoi(args[4]) + if err != nil && args[4] == "skip-update" { + skipUpdate = true + } else { + return err + } + } + + return relayer.UpdateClientAndRecvMessage(cmd.Context(), src, dst, a.config.memo(cmd), txnHash, int64(trustedHeight), skipUpdate) + + }, + } + return cmd +} + +func ackPacket(a *appState) *cobra.Command { + cmd := &cobra.Command{ + Use: "ack-packet src_chain_name dst_chain_name path_name [txn-hash]", + Short: "update block [height] of src_chain on dst_chain, then acknowlede packet based on the txn-hash", + Args: withUsage(cobra.RangeArgs(4, 5)), + Example: strings.TrimSpace( + fmt.Sprintf(` + $ %s transact ack-packet icon archway icon-archway [tx_hash] [trusted_height OR {empty} OR {skip-update}] + $ %s transact ack-packet icon archway icon-archway [tx_hash] [trusted_height] + $ %s transact ack-packet icon archway icon-archway [tx_hash] + $ %s transact ack-packet icon archway icon-archway [tx_hash] [skip-update] + `, appName, appName, appName, appName), + ), + RunE: func(cmd *cobra.Command, args []string) error { + src, ok := a.config.Chains[args[0]] + if !ok { + return errChainNotFound(args[0]) + } + dst, ok := a.config.Chains[args[1]] + if !ok { + return errChainNotFound(args[1]) + } + _, _, _, err := a.config.ChainsFromPath(args[2]) + if err != nil { + return err + } + + // ensure that keys exist + if exists := src.ChainProvider.KeyExists(src.ChainProvider.Key()); !exists { + return fmt.Errorf("key %s not found on src chain %s", src.ChainProvider.Key(), src.ChainID()) + } + if exists := dst.ChainProvider.KeyExists(dst.ChainProvider.Key()); !exists { + return fmt.Errorf("key %s not found on dst chain %s", dst.ChainProvider.Key(), dst.ChainID()) + } + // get transaction detail + txnHash := args[3] + + // get trusted height + var trustedHeight int + var skipUpdate bool + skipUpdate = false + if len(args) == 5 { + var err error + trustedHeight, err = strconv.Atoi(args[4]) + if err != nil && args[4] == "skip-update" { + skipUpdate = true + } else { + return err + } + } + + return relayer.UpdateClientAndAckMessage(cmd.Context(), src, dst, a.config.memo(cmd), txnHash, int64(trustedHeight), skipUpdate) + }, + } + return cmd +} + func relayMsgsCmd(a *appState) *cobra.Command { cmd := &cobra.Command{ Use: "relay-packets path_name src_channel_id", diff --git a/relayer/chains/icon/query.go b/relayer/chains/icon/query.go index 1804ac8ce..f5f282814 100644 --- a/relayer/chains/icon/query.go +++ b/relayer/chains/icon/query.go @@ -80,9 +80,45 @@ func (icp *IconProvider) BlockTime(ctx context.Context, height int64) (time.Time return time.Unix(header.Timestamp, 0), nil } -// required for cosmos only +// WARN: Handles events only for write ack and send packet +// WARN: Used to call recv packet and ack packet via cli func (icp *IconProvider) QueryTx(ctx context.Context, hashHex string) (*provider.RelayerTxResponse, error) { - panic(fmt.Sprintf("%s%s", icp.ChainName(), NOT_IMPLEMENTED)) + txRes, err := icp.client.GetTransactionResult(&types.TransactionHashParam{ + Hash: types.HexBytes(hashHex), + }) + if err != nil { + return nil, err + } + + ht, err := txRes.BlockHeight.Value() + if err != nil { + return nil, err + } + + status, _ := txRes.Status.Int() + if status != 1 { + return &provider.RelayerTxResponse{}, fmt.Errorf("transaction failed: %v", err) + } + var eventLogs []provider.RelayerEvent + events := txRes.EventLogs + + for _, event := range events { + if event.Indexed[0] == EventTypeSendPacket || event.Indexed[0] == EventTypeWriteAcknowledgement { + if event.Addr == types.Address(icp.PCfg.IbcHandlerAddress) { + evt := icp.parseSendPacketAndWriteAckEvent(event) + eventLogs = append(eventLogs, evt) + } + } + } + + response := provider.RelayerTxResponse{ + Height: ht, + TxHash: hashHex, + Code: uint32(status), + Data: string(txRes.SCOREAddress), + Events: eventLogs, + } + return &response, nil } // required for cosmos only diff --git a/relayer/chains/icon/tx.go b/relayer/chains/icon/tx.go index 6a7f11776..2891cbbd2 100644 --- a/relayer/chains/icon/tx.go +++ b/relayer/chains/icon/tx.go @@ -3,6 +3,7 @@ package icon import ( "context" "encoding/hex" + "encoding/json" "fmt" "strings" "sync" @@ -606,6 +607,47 @@ func (icp *IconProvider) SendMessage(ctx context.Context, msg provider.RelayerMe return rlyResp, true, callbackErr } +func (icp *IconProvider) parseSendPacketAndWriteAckEvent(event types.EventLogStr) provider.RelayerEvent { + eventName := event.Indexed[0] + switch eventName { + case EventTypeSendPacket, EventTypeWriteAcknowledgement: + protoPacket, err := hex.DecodeString(strings.TrimPrefix(event.Indexed[1], "0x")) + if err != nil { + icp.log.Error("Error decoding packet data ", zap.String("packet_hex", event.Data[0])) + break + } + + var packetInfo icon.Packet + err = proto.Unmarshal(protoPacket, &packetInfo) + if err != nil { + icp.log.Error("error marshaling packet", zap.String("packet_data", string(protoPacket))) + break + } + + relayerEvent := provider.RelayerEvent{ + EventType: IconCosmosEventMap[eventName], + Attributes: map[string]string{ + chantypes.AttributeKeySequence: fmt.Sprintf("%d", packetInfo.Sequence), + chantypes.AttributeKeyDstChannel: packetInfo.DestinationChannel, + chantypes.AttributeKeyDstPort: packetInfo.DestinationPort, + chantypes.AttributeKeySrcChannel: packetInfo.SourceChannel, + chantypes.AttributeKeySrcPort: packetInfo.SourcePort, + chantypes.AttributeKeyDataHex: fmt.Sprintf("%x", packetInfo.Data), + chantypes.AttributeKeyTimeoutHeight: fmt.Sprintf("%d-%d", packetInfo.TimeoutHeight.RevisionNumber, packetInfo.TimeoutHeight.RevisionHeight), + chantypes.AttributeKeyTimeoutTimestamp: fmt.Sprintf("%d", packetInfo.TimeoutTimestamp), + }, + } + var ackData string + if eventName == EventTypeWriteAcknowledgement { + ackData = strings.TrimPrefix(event.Data[0], "0x") + relayerEvent.Attributes[chantypes.AttributeKeyAckHex] = ackData + } + + return relayerEvent + } + return provider.RelayerEvent{} +} + func (icp *IconProvider) parseConfirmedEventLogStr(event types.EventLogStr) provider.RelayerEvent { eventName := event.Indexed[0] @@ -793,6 +835,8 @@ func (icp *IconProvider) SendIconTransaction( step, err := icp.client.EstimateStep(txParamEst) if err != nil { + estimate_txn_bytes, _ := json.Marshal(txParamEst) + icp.log.Warn("Transaction data during estimate step", zap.ByteString("txn_data", estimate_txn_bytes)) return fmt.Errorf("failed estimating step: %w", err) } stepVal, err := step.Int() diff --git a/relayer/client.go b/relayer/client.go index dcd85079f..74b76c536 100644 --- a/relayer/client.go +++ b/relayer/client.go @@ -2,12 +2,17 @@ package relayer import ( "context" + "encoding/hex" "fmt" + "log" + "strconv" + "strings" "time" "github.com/avast/retry-go/v4" codectypes "github.com/cosmos/cosmos-sdk/codec/types" clienttypes "github.com/cosmos/ibc-go/v7/modules/core/02-client/types" + chantypes "github.com/cosmos/ibc-go/v7/modules/core/04-channel/types" ibcexported "github.com/cosmos/ibc-go/v7/modules/core/exported" tmclient "github.com/cosmos/ibc-go/v7/modules/light-clients/07-tendermint" "github.com/cosmos/relayer/v2/relayer/chains/icon" @@ -357,7 +362,7 @@ func MsgUpdateClient( return dst.ChainProvider.MsgUpdateClient(dst.ClientID(), updateHeader) } -func msgUpdateClientOneWay(ctx context.Context, src, dst *Chain, height int64) (provider.RelayerMessage, error) { +func msgUpdateClientOneWay(ctx context.Context, src, dst *Chain, latestHeight int64, trustedHeight int64) (provider.RelayerMessage, error) { var updateHeader ibcexported.ClientMessage if err := retry.Do(func() error { @@ -368,17 +373,27 @@ func msgUpdateClientOneWay(ctx context.Context, src, dst *Chain, height int64) ( return err } - dstClientState, err := dst.ChainProvider.QueryClientState(ctx, dstHeight, dst.ClientID()) - if err != nil { - return err - } + var trustedHdr provider.IBCHeader - trustedHdr, err := src.ChainProvider.QueryIBCHeader(ctx, int64(dstClientState.GetLatestHeight().GetRevisionHeight())) - if err != nil { - return err + if trustedHeight == 0 { + var err error + dstClientState, err := dst.ChainProvider.QueryClientState(ctx, dstHeight, dst.ClientID()) + if err != nil { + return err + } + + trustedHdr, err = src.ChainProvider.QueryIBCHeader(ctx, int64(dstClientState.GetLatestHeight().GetRevisionHeight())) + if err != nil { + return err + } + } else { + trustedHdr, err = src.ChainProvider.QueryIBCHeader(ctx, trustedHeight) + if err != nil { + return err + } } - latestHdr, err := src.ChainProvider.QueryIBCHeader(ctx, height) + latestHdr, err := src.ChainProvider.QueryIBCHeader(ctx, latestHeight) if err != nil { return err } @@ -405,14 +420,157 @@ func msgUpdateClientOneWay(ctx context.Context, src, dst *Chain, height int64) ( return dst.ChainProvider.MsgUpdateClient(dst.ClientID(), updateHeader) } -func UpdateClient(ctx context.Context, src, dst *Chain, memo string, heights []int64) error { +func UpdateClientAndRecvMessage(ctx context.Context, src, dst *Chain, memo string, txHash string, trustedHt int64, skipUpdate bool) error { + var txres *provider.RelayerTxResponse + err := retry.Do(func() error { + var err error + txres, err = src.ChainProvider.QueryTx(ctx, txHash) + return err + }, retry.Attempts(5)) + + if err != nil { + return err + } + + // for next height: required both in wasm and icon + updatedHeight := txres.Height + 1 + packetInfoHeight := txres.Height + if src.ChainProvider.Type() == common.IconModule { + packetInfoHeight = txres.Height + 1 + + } + + if !skipUpdate { + err = UpdateClientAgainstTrustedHeader(ctx, src, dst, "", updatedHeight, trustedHt) + if err != nil { + return fmt.Errorf("failed updating header: %v", err) + } + } + + var recvMessages []provider.RelayerMessage + packets := parsePacketInfoFromEvent(txres.Events, uint64(packetInfoHeight)) + for _, packet := range packets { + proof, err := src.ChainProvider.PacketCommitment(ctx, packet, uint64(updatedHeight)) + if err != nil { + return fmt.Errorf("failed getting proof for packet of sequence: %d err: %v", packet.Sequence, err) + } + + recvMessage, err := dst.ChainProvider.MsgRecvPacket(packet, proof) + if err != nil { + return fmt.Errorf("failed constructing recv message for packet sn %d err: %v", packet.Sequence, err) + } + recvMessages = append(recvMessages, recvMessage) + } + + clients := &RelayMsgs{ + Src: []provider.RelayerMessage{}, + Dst: recvMessages, + } + + clients.SendMessageToDest(ctx, src.log, AsRelayMsgSender(dst), memo) + + return nil +} + +func parsePacketInfoFromEvent(events []provider.RelayerEvent, packetHeight uint64) []provider.PacketInfo { + var infos []provider.PacketInfo + + for _, evt := range events { + // TrimPrefix returns s without the provided leading prefix string. If s doesn't start with prefix, s is returned unchanged. + if strings.TrimPrefix(evt.EventType, "wasm-") == chantypes.EventTypeSendPacket || + strings.TrimPrefix(evt.EventType, "wasm-") == chantypes.EventTypeWriteAck { + seq, err := strconv.Atoi(evt.Attributes[chantypes.AttributeKeySequence]) + if err != nil { + return nil + } + srcPort := evt.Attributes[chantypes.AttributeKeySrcPort] + srcChannel := evt.Attributes[chantypes.AttributeKeySrcChannel] + dstPort := evt.Attributes[chantypes.AttributeKeyDstPort] + dstChannel := evt.Attributes[chantypes.AttributeKeyDstChannel] + data, _ := hex.DecodeString(evt.Attributes[chantypes.AttributeKeyDataHex]) + ack, _ := hex.DecodeString(evt.Attributes[chantypes.AttributeKeyAckHex]) + timeoutHeight := evt.Attributes[chantypes.AttributeKeyTimeoutHeight] + timeoutTimestamp, _ := strconv.Atoi(evt.Attributes[chantypes.AttributeKeyTimeoutTimestamp]) + + timeoutSplit := strings.Split(timeoutHeight, "-") + if len(timeoutSplit) != 2 { + log.Fatalf("failed to parse timeout height: %s", timeoutHeight) + } + revisionNumber, err := strconv.ParseUint(timeoutSplit[0], 10, 64) + if err != nil { + log.Fatalf("Error parsing packet timeout height revision number: %s", timeoutHeight) + } + revisionHeight, err := strconv.ParseUint(timeoutSplit[1], 10, 64) + if err != nil { + log.Fatalf("Error parsing packet timeout height revision number: %s", timeoutHeight) + } + + info := provider.PacketInfo{ + Height: packetHeight, + Sequence: uint64(seq), + SourcePort: srcPort, + SourceChannel: srcChannel, + DestPort: dstPort, + DestChannel: dstChannel, + Data: data, + TimeoutHeight: clienttypes.Height{ + RevisionHeight: revisionHeight, + RevisionNumber: revisionNumber, + }, + TimeoutTimestamp: uint64(timeoutTimestamp), + Ack: ack, + } + + infos = append(infos, info) + } + + } + return infos +} + +func UpdateClientAgainstTrustedHeader(ctx context.Context, src, dst *Chain, memo string, latestHeight int64, trustedHeight int64) error { eg, egCtx := errgroup.WithContext(ctx) + + var dstMsgUpdateClient provider.RelayerMessage + eg.Go(func() error { + var err error + dstMsgUpdateClient, err = msgUpdateClientOneWay(egCtx, src, dst, latestHeight, trustedHeight) + return err + }) + + if err := eg.Wait(); err != nil { + return err + } + + clients := &RelayMsgs{ + Src: []provider.RelayerMessage{}, + Dst: []provider.RelayerMessage{dstMsgUpdateClient}, + } + + err := clients.SendMessageToDest(ctx, dst.log, AsRelayMsgSender(dst), memo) + + if err == nil { + src.log.Info( + "Client updated", + zap.String("src_chain_id", src.ChainID()), + zap.String("src_client", src.PathEnd.ClientID), + + zap.String("dst_chain_id", dst.ChainID()), + zap.String("dst_client", dst.PathEnd.ClientID), + ) + } + + return err +} + +func UpdateClient(ctx context.Context, src, dst *Chain, memo string, heights []int64) error { + eg, _ := errgroup.WithContext(ctx) for _, height := range heights { var dstMsgUpdateClient provider.RelayerMessage eg.Go(func() error { var err error - dstMsgUpdateClient, err = msgUpdateClientOneWay(egCtx, src, dst, height) + dstMsgUpdateClient, err = msgUpdateClientOneWay(ctx, src, dst, height, 0) return err }) @@ -425,28 +583,17 @@ func UpdateClient(ctx context.Context, src, dst *Chain, memo string, heights []i Dst: []provider.RelayerMessage{dstMsgUpdateClient}, } - result := clients.Send(ctx, src.log, AsRelayMsgSender(src), AsRelayMsgSender(dst), memo) + err := clients.SendMessageToDest(ctx, dst.log, AsRelayMsgSender(dst), memo) + if err != nil { + src.log.Info( + "Client updated", + zap.String("src_chain_id", src.ChainID()), + zap.String("src_client", src.PathEnd.ClientID), - if err := result.Error(); err != nil { - if result.PartiallySent() { - src.log.Info( - "Partial success when updating clients", - zap.String("src_chain_id", src.ChainID()), - zap.String("dst_chain_id", dst.ChainID()), - zap.Object("send_result", result), - ) - } - return err + zap.String("dst_chain_id", dst.ChainID()), + zap.String("dst_client", dst.PathEnd.ClientID), + ) } - - src.log.Info( - "Client updated", - zap.String("src_chain_id", src.ChainID()), - zap.String("src_client", src.PathEnd.ClientID), - - zap.String("dst_chain_id", dst.ChainID()), - zap.String("dst_client", dst.PathEnd.ClientID), - ) } return nil @@ -670,3 +817,58 @@ func ClientInfoFromClientState(clientState *codectypes.Any) (ClientStateInfo, er return ClientStateInfo{}, fmt.Errorf("unhandled client state type: (%T)", clientState) } } + +func UpdateClientAndAckMessage(ctx context.Context, src, dst *Chain, memo string, txHash string, trustedHt int64, skipUpdate bool) error { + var txres *provider.RelayerTxResponse + err := retry.Do(func() error { + var err error + txres, err = src.ChainProvider.QueryTx(ctx, txHash) + return err + }, retry.Attempts(5)) + + if err != nil { + return err + } + + // for next height: required both in wasm and icon + updatedHeight := txres.Height + 1 + packetInfoHeight := txres.Height + if src.ChainProvider.Type() == common.IconModule { + packetInfoHeight = txres.Height + 1 + + } + + if !skipUpdate { + err = UpdateClientAgainstTrustedHeader(ctx, src, dst, "", updatedHeight, trustedHt) + if err != nil { + return fmt.Errorf("failed updating header: %v", err) + } + } + + var ackMessages []provider.RelayerMessage + packets := parsePacketInfoFromEvent(txres.Events, uint64(packetInfoHeight)) + for _, packet := range packets { + if packet.Ack != nil { + return fmt.Errorf("ack canot be nil") + } + proof, err := src.ChainProvider.PacketAcknowledgement(ctx, packet, uint64(updatedHeight)) + if err != nil { + return fmt.Errorf("failed getting proof for packet of sequence: %d err: %v", packet.Sequence, err) + } + + recvMessage, err := dst.ChainProvider.MsgAcknowledgement(packet, proof) + if err != nil { + return fmt.Errorf("failed constructing recv message for packet sn %d err: %v", packet.Sequence, err) + } + ackMessages = append(ackMessages, recvMessage) + } + + clients := &RelayMsgs{ + Src: []provider.RelayerMessage{}, + Dst: ackMessages, + } + + clients.SendMessageToDest(ctx, src.log, AsRelayMsgSender(dst), memo) + + return nil +} diff --git a/relayer/relayMsgs.go b/relayer/relayMsgs.go index 698e8b28a..aff194abf 100644 --- a/relayer/relayMsgs.go +++ b/relayer/relayMsgs.go @@ -145,6 +145,27 @@ func (r SendMsgsResult) MarshalLogObject(enc zapcore.ObjectEncoder) error { return nil } +// Send regular non concurrent messages +func (r *RelayMsgs) SendMessageToDest(ctx context.Context, log *zap.Logger, dst RelayMsgSender, memo string) error { + + done := make(chan struct{}) + + go func() { + resp, success, err := dst.SendMessages(ctx, r.Dst, memo) + if err != nil { + log.Error("Sending Transaction Failed", zap.Error(err)) + } + if !success { + log.Error("Failed Transaction ", zap.String("tx_hash", resp.TxHash)) + } + // Signal that the goroutine is done + done <- struct{}{} + }() + <-done + + return nil +} + // Send concurrently sends out r's messages to the corresponding RelayMsgSenders. func (r *RelayMsgs) Send(ctx context.Context, log *zap.Logger, src, dst RelayMsgSender, memo string) SendMsgsResult { var ( From fb532e25fb59b901a54b24a0cf1a617f5491405c Mon Sep 17 00:00:00 2001 From: izyak <76203436+izyak@users.noreply.github.com> Date: Tue, 9 Jul 2024 12:46:13 +0545 Subject: [PATCH 2/3] fix: handle trusted height --- cmd/tx.go | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/cmd/tx.go b/cmd/tx.go index 40b4d5e6a..c6d340fb3 100644 --- a/cmd/tx.go +++ b/cmd/tx.go @@ -1037,11 +1037,13 @@ func ackPacket(a *appState) *cobra.Command { skipUpdate = false if len(args) == 5 { var err error - trustedHeight, err = strconv.Atoi(args[4]) - if err != nil && args[4] == "skip-update" { + if args[4] == "skip-update" { skipUpdate = true } else { - return err + trustedHeight, err = strconv.Atoi(args[4]) + if err != nil { + return fmt.Errorf("error: %w, arg: %s", err, args[4]) + } } } From 8f3fd2a5f20a176849076c590c605370db64759a Mon Sep 17 00:00:00 2001 From: izyak <76203436+izyak@users.noreply.github.com> Date: Tue, 9 Jul 2024 13:29:53 +0545 Subject: [PATCH 3/3] fix: trusted height handle for recv --- cmd/tx.go | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/cmd/tx.go b/cmd/tx.go index c6d340fb3..a9b7e1df7 100644 --- a/cmd/tx.go +++ b/cmd/tx.go @@ -979,11 +979,13 @@ func recvPacket(a *appState) *cobra.Command { skipUpdate = false if len(args) == 5 { var err error - trustedHeight, err = strconv.Atoi(args[4]) - if err != nil && args[4] == "skip-update" { + if args[4] == "skip-update" { skipUpdate = true } else { - return err + trustedHeight, err = strconv.Atoi(args[4]) + if err != nil { + return fmt.Errorf("error: %w, arg: %s", err, args[4]) + } } }