Skip to content

Commit

Permalink
Add 02-client implementation for Recover client.
Browse files Browse the repository at this point in the history
  • Loading branch information
DimitrisJim committed Aug 29, 2023
1 parent 51417ee commit 6f75769
Show file tree
Hide file tree
Showing 5 changed files with 197 additions and 0 deletions.
48 changes: 48 additions & 0 deletions modules/core/02-client/keeper/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -149,3 +149,51 @@ func (k Keeper) UpgradeClient(ctx sdk.Context, clientID string, upgradedClient e

return nil
}

func (k Keeper) RecoverClient(ctx sdk.Context, subjectClientID, substituteClientID string) error {
subjectClientState, found := k.GetClientState(ctx, subjectClientID)
if !found {
return errorsmod.Wrapf(types.ErrClientNotFound, "subject client with ID %s", subjectClientID)
}

subjectClientStore := k.ClientStore(ctx, subjectClientID)

if status := k.GetClientStatus(ctx, subjectClientState, subjectClientID); status == exported.Active {
return errorsmod.Wrap(types.ErrInvalidRecoveryClient, "cannot recover Active subject client")
}

substituteClientState, found := k.GetClientState(ctx, substituteClientID)
if !found {
return errorsmod.Wrapf(types.ErrClientNotFound, "substitute client with ID %s", substituteClientID)
}

if subjectClientState.GetLatestHeight().GTE(substituteClientState.GetLatestHeight()) {
return errorsmod.Wrapf(types.ErrInvalidHeight, "subject client state latest height is greater or equal to substitute client state latest height (%s >= %s)", subjectClientState.GetLatestHeight(), substituteClientState.GetLatestHeight())
}

substituteClientStore := k.ClientStore(ctx, substituteClientID)

if status := k.GetClientStatus(ctx, substituteClientState, substituteClientID); status != exported.Active {
return errorsmod.Wrapf(types.ErrClientNotActive, "substitute client is not Active, status is %s", status)
}

if err := subjectClientState.CheckSubstituteAndUpdateState(ctx, k.cdc, subjectClientStore, substituteClientStore, substituteClientState); err != nil {
return err
}

k.Logger(ctx).Info("client recovered", "client-id", subjectClientID)

defer telemetry.IncrCounterWithLabels(
[]string{"ibc", "client", "recover"},
1,
[]metrics.Label{
telemetry.NewLabel(types.LabelClientType, substituteClientState.ClientType()),
telemetry.NewLabel(types.LabelClientID, subjectClientID),
},
)

// emitting events in the keeper for recovering clients
emitRecoverClientEvent(ctx, subjectClientID, substituteClientState.ClientType())

return nil
}
132 changes: 132 additions & 0 deletions modules/core/02-client/keeper/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -508,3 +508,135 @@ func (suite *KeeperTestSuite) TestUpdateClientEventEmission() {
}
suite.Require().True(contains)
}

func (suite *KeeperTestSuite) TestRecoverClient() {
var (
subject, substitute string
subjectClientState, substituteClientState exported.ClientState
)

testCases := []struct {
msg string
malleate func()
expErr error
}{
{
"success",
func() {},
nil,
},
{
"subject client does not exist",
func() {
subject = ibctesting.InvalidID
},
clienttypes.ErrClientNotFound,
},
{
"subject is Active",
func() {
tmClientState, ok := subjectClientState.(*ibctm.ClientState)
suite.Require().True(ok)
// Set FrozenHeight to zero to ensure client is reported as Active
tmClientState.FrozenHeight = clienttypes.NewHeight(0, 0)
suite.chainA.App.GetIBCKeeper().ClientKeeper.SetClientState(suite.chainA.GetContext(), subject, tmClientState)
},
clienttypes.ErrInvalidRecoveryClient,
},
{
"substitute client does not exist",
func() {
substitute = ibctesting.InvalidID
},
clienttypes.ErrClientNotFound,
},
{
"subject height is equal than substitute height",
func() {
tmClientState, ok := subjectClientState.(*ibctm.ClientState)
suite.Require().True(ok)
tmClientState.LatestHeight = substituteClientState.GetLatestHeight().(clienttypes.Height)
suite.chainA.App.GetIBCKeeper().ClientKeeper.SetClientState(suite.chainA.GetContext(), subject, tmClientState)
},
clienttypes.ErrInvalidHeight,
},
{
"subject height is greater than substitute height",
func() {
tmClientState, ok := subjectClientState.(*ibctm.ClientState)
suite.Require().True(ok)
tmClientState.LatestHeight = substituteClientState.GetLatestHeight().Increment().(clienttypes.Height)
suite.chainA.App.GetIBCKeeper().ClientKeeper.SetClientState(suite.chainA.GetContext(), subject, tmClientState)
},
clienttypes.ErrInvalidHeight,
},
{
"substitute is frozen",
func() {
tmClientState, ok := substituteClientState.(*ibctm.ClientState)
suite.Require().True(ok)
tmClientState.FrozenHeight = clienttypes.NewHeight(0, 1)
suite.chainA.App.GetIBCKeeper().ClientKeeper.SetClientState(suite.chainA.GetContext(), substitute, tmClientState)
},
clienttypes.ErrClientNotActive,
},
{
"CheckSubstituteAndUpdateState fails, substitute client trust level doesn't match subject client trust level",
func() {
tmClientState, ok := substituteClientState.(*ibctm.ClientState)
suite.Require().True(ok)
tmClientState.UnbondingPeriod += time.Minute
suite.chainA.App.GetIBCKeeper().ClientKeeper.SetClientState(suite.chainA.GetContext(), substitute, tmClientState)
},
clienttypes.ErrInvalidSubstitute,
},
}

for _, tc := range testCases {
tc := tc

suite.Run(tc.msg, func() {
suite.SetupTest() // reset

subjectPath := ibctesting.NewPath(suite.chainA, suite.chainB)
suite.coordinator.SetupClients(subjectPath)
subject = subjectPath.EndpointA.ClientID
subjectClientState = suite.chainA.GetClientState(subject)

substitutePath := ibctesting.NewPath(suite.chainA, suite.chainB)
suite.coordinator.SetupClients(substitutePath)
substitute = substitutePath.EndpointA.ClientID

// update substitute twice
err := substitutePath.EndpointA.UpdateClient()
suite.Require().NoError(err)
err = substitutePath.EndpointA.UpdateClient()
suite.Require().NoError(err)
substituteClientState = suite.chainA.GetClientState(substitute)

tmClientState, ok := subjectClientState.(*ibctm.ClientState)
suite.Require().True(ok)
tmClientState.AllowUpdateAfterMisbehaviour = true
tmClientState.AllowUpdateAfterExpiry = true
tmClientState.FrozenHeight = tmClientState.LatestHeight
suite.chainA.App.GetIBCKeeper().ClientKeeper.SetClientState(suite.chainA.GetContext(), subject, tmClientState)

tmClientState, ok = substituteClientState.(*ibctm.ClientState)
suite.Require().True(ok)
tmClientState.AllowUpdateAfterMisbehaviour = true
tmClientState.AllowUpdateAfterExpiry = true

tc.malleate()

err = suite.chainA.App.GetIBCKeeper().ClientKeeper.RecoverClient(suite.chainA.GetContext(), subject, substitute)

expPass := tc.expErr == nil
if expPass {
suite.Require().NoError(err)
} else {
suite.Require().Error(err)
suite.Require().ErrorIs(err, tc.expErr)
}
})
}
}
15 changes: 15 additions & 0 deletions modules/core/02-client/keeper/events.go
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,21 @@ func emitUpdateClientProposalEvent(ctx sdk.Context, clientID, clientType string)
})
}

// emitRecoverClientEvent emits a recover client event
func emitRecoverClientEvent(ctx sdk.Context, clientID, clientType string) {
ctx.EventManager().EmitEvents(sdk.Events{
sdk.NewEvent(
types.EventTypeRecoverClient,
sdk.NewAttribute(types.AttributeKeySubjectClientID, clientID),
sdk.NewAttribute(types.AttributeKeyClientType, clientType),
),
sdk.NewEvent(
sdk.EventTypeMessage,
sdk.NewAttribute(sdk.AttributeKeyModule, types.AttributeValueCategory),
),
})
}

// emitUpgradeClientProposalEvent emits an upgrade client proposal event
func emitUpgradeClientProposalEvent(ctx sdk.Context, title string, height int64) {
ctx.EventManager().EmitEvents(sdk.Events{
Expand Down
1 change: 1 addition & 0 deletions modules/core/02-client/types/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,4 +36,5 @@ var (
ErrClientNotActive = errorsmod.Register(SubModuleName, 29, "client state is not active")
ErrFailedMembershipVerification = errorsmod.Register(SubModuleName, 30, "membership verification failed")
ErrFailedNonMembershipVerification = errorsmod.Register(SubModuleName, 31, "non-membership verification failed")
ErrInvalidRecoveryClient = errorsmod.Register(SubModuleName, 32, "invalid recovery client")
)
1 change: 1 addition & 0 deletions modules/core/02-client/types/events.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ var (
EventTypeUpdateClientProposal = "update_client_proposal"
EventTypeUpgradeChain = "upgrade_chain"
EventTypeUpgradeClientProposal = "upgrade_client_proposal"
EventTypeRecoverClient = "recover_client"

AttributeValueCategory = fmt.Sprintf("%s_%s", ibcexported.ModuleName, SubModuleName)
)

0 comments on commit 6f75769

Please sign in to comment.