Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

auto update clients to prevent expiry #412

Merged
merged 10 commits into from
Feb 12, 2021
9 changes: 9 additions & 0 deletions cmd/flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ var (
flagTimeoutHeightOffset = "timeout-height-offset"
flagTimeoutTimeOffset = "timeout-time-offset"
flagMaxRetries = "max-retries"
flagThresholdTime = "time-threshold"
)

func ibcDenomFlags(cmd *cobra.Command) *cobra.Command {
Expand Down Expand Up @@ -259,3 +260,11 @@ func retryFlag(cmd *cobra.Command) *cobra.Command {
}
return cmd
}

func updateTimeFlags(cmd *cobra.Command) *cobra.Command {
cmd.Flags().Duration(flagThresholdTime, 6*time.Hour, "time before to expiry time to update client")
if err := viper.BindPFlag(flagThresholdTime, cmd.Flags().Lookup(flagThresholdTime)); err != nil {
panic(err)
}
return cmd
}
60 changes: 59 additions & 1 deletion cmd/start.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,13 @@ import (
"os/signal"
"strings"
"syscall"
"time"

retry "github.com/avast/retry-go"
"github.com/cosmos/relayer/relayer"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"golang.org/x/sync/errgroup"
)

// startCmd represents the start command
Expand Down Expand Up @@ -70,11 +74,33 @@ $ %s start demo-path2 --max-tx-size 10`, appName, appName)),
return err
}

thresholdTime := viper.GetDuration(flagThresholdTime)

eg := new(errgroup.Group)
eg.Go(func() error {
colin-axner marked this conversation as resolved.
Show resolved Hide resolved
for {
var timeToExpiry time.Duration
if err := retry.Do(func() error {
timeToExpiry, err = UpdateClientsFromChains(c[src], c[dst], thresholdTime)
if err != nil {
return err
}
return nil
}, retry.Attempts(5), retry.Delay(time.Millisecond*500), retry.LastErrorOnly(true)); err != nil {
return err
}
time.Sleep(timeToExpiry - thresholdTime)
}
})
if err = eg.Wait(); err != nil {
return err
}

trapSignal(done)
return nil
},
}
return strategyFlag(cmd)
return strategyFlag(updateTimeFlags(cmd))
}

// trap signal waits for a SIGINT or SIGTERM and then sends down the done channel
Expand All @@ -91,3 +117,35 @@ func trapSignal(done func()) {
// call the cleanup func
done()
}

// UpdateClientsFromChains takes src, dst chains, threshold time and update clients based on expiry time
func UpdateClientsFromChains(src, dst *relayer.Chain, thresholdTime time.Duration) (time.Duration, error) {
var (
srcTimeExpiry, dstTimeExpiry time.Duration
err error
)

eg := new(errgroup.Group)
eg.Go(func() error {
srcTimeExpiry, err = relayer.GetClientAndUpdate(src, dst, thresholdTime)
return err
})
eg.Go(func() error {
dstTimeExpiry, err = relayer.GetClientAndUpdate(dst, src, thresholdTime)
return err
})
if err := eg.Wait(); err != nil {
return 0, err
}

if srcTimeExpiry <= 0 || dstTimeExpiry <= 0 {
return 0, fmt.Errorf("one of the clients (%s,%s) trusting period is 0 or expired",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it'd be useful to separate these checks and tell the user which client on which chain is expired. Also I believe it is not possible for the trusting period to be 0

src.PathEnd.ClientID, dst.PathEnd.ClientID)
}

if srcTimeExpiry <= dstTimeExpiry {
colin-axner marked this conversation as resolved.
Show resolved Hide resolved
return srcTimeExpiry, nil
}

return dstTimeExpiry, nil
}
91 changes: 91 additions & 0 deletions relayer/client-tx.go
Original file line number Diff line number Diff line change
Expand Up @@ -349,3 +349,94 @@ func IsMatchingClient(clientStateA, clientStateB ibctmtypes.ClientState) bool {
func IsMatchingConsensusState(consensusStateA, consensusStateB *ibctmtypes.ConsensusState) bool {
return reflect.DeepEqual(*consensusStateA, *consensusStateB)
}

// GetClientAndUpdate update clients to prevent expiry
func GetClientAndUpdate(src, dst *Chain, thresholdTime time.Duration) (time.Duration, error) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this name is misleading in some way, this doesn't return the client but the trusting period.

height, err := src.QueryLatestHeight()
if err != nil {
return 0, err
}

clientStateRes, err := src.QueryClientState(height)
if err != nil {
return 0, err
}

// unpack any into ibc tendermint client state
clientStateExported, err := clienttypes.UnpackClientState(clientStateRes.ClientState)
if err != nil {
return 0, err
}

// cast from interface to concrete type
clientState, ok := clientStateExported.(*ibctmtypes.ClientState)
if !ok {
return 0, fmt.Errorf("error when casting exported clientstate with clientID %s on chain: %s",
src.PathEnd.ClientID, src.PathEnd.ChainID)
}

if clientState.TrustingPeriod <= thresholdTime {
return 0, fmt.Errorf("client (%s) trusting period time is less than or equal to threshold time",
src.PathEnd.ClientID)
}

// query the latest consensus state of the potential matching client
consensusStateResp, err := clientutils.QueryConsensusStateABCI(src.CLIContext(0),
src.PathEnd.ClientID, clientState.GetLatestHeight())
if err != nil {
return 0, err
}

exportedConsState, err := clienttypes.UnpackConsensusState(consensusStateResp.ConsensusState)
if err != nil {
return 0, err
}

consensusState, ok := exportedConsState.(*ibctmtypes.ConsensusState)
if !ok {
return 0, fmt.Errorf("consensus state with clientID %s from chain %s is not IBC tendermint type",
src.PathEnd.ClientID, src.PathEnd.ChainID)
}

expirationTime := consensusState.Timestamp.Add(clientState.TrustingPeriod)

timeToExpiry := time.Until(expirationTime)
colin-axner marked this conversation as resolved.
Show resolved Hide resolved

if timeToExpiry > thresholdTime {
return timeToExpiry, nil
}

if clientState.IsExpired(consensusState.Timestamp, time.Now()) {
return 0, fmt.Errorf("client (%s) is already expired on chain: %s", src.PathEnd.ClientID, src.ChainID)
}

sh, err := NewSyncHeaders(src, dst)
if err != nil {
return 0, err
}

dstUH, err := sh.GetTrustedHeader(dst, src)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the logic here is switched. Nonetheless this will be replaced with src.GetIBCUpdateHeader(dst) after merging my refactor pr

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just merged that pr to master, so you can update now

if err != nil {
return 0, err
}

msgs := []sdk.Msg{
src.UpdateClient(dstUH),
}

res, success, err := src.SendMsgs(msgs)
if err != nil {
return 0, err
}
if !success {
return 0, fmt.Errorf("tx failed: %s", res.RawLog)
}
src.Log(fmt.Sprintf("★ Client updated: [%s]client(%s) {%d}->{%d}",
src.ChainID,
src.PathEnd.ClientID,
MustGetHeight(dstUH.TrustedHeight),
dstUH.Header.Height,
))

return clientState.TrustingPeriod, nil
}