Skip to content

Commit

Permalink
Add 02-client implementation for Recover client. (#4499)
Browse files Browse the repository at this point in the history
* Add 02-client implementation for Recover client.

* Partially address feedback.

* Docu RecoverClient, add label, re-use error.
  • Loading branch information
DimitrisJim authored Aug 31, 2023
1 parent d9f6200 commit 9c5499e
Show file tree
Hide file tree
Showing 6 changed files with 230 additions and 2 deletions.
56 changes: 56 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,59 @@ func (k Keeper) UpgradeClient(ctx sdk.Context, clientID string, upgradedClient e

return nil
}

// RecoverClient will retrieve the subject and substitute client.
// A callback will occur to the subject client state with the client
// prefixed store being provided for both the subject and the substitute client.
// The IBC client implementations are responsible for validating the parameters of the
// substitute (ensuring they match the subject's parameters) as well as copying
// the necessary consensus states from the substitute to the subject client
// store. The substitute must be Active and the subject must not be Active.
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", "update"},
1,
[]metrics.Label{
telemetry.NewLabel(types.LabelClientType, substituteClientState.ClientType()),
telemetry.NewLabel(types.LabelClientID, subjectClientID),
telemetry.NewLabel(types.LabelUpdateType, "recovery"),
},
)

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

return nil
}
156 changes: 156 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,159 @@ 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,
},
{
"success, subject and substitute use different revision number",
func() {
tmClientState, ok := substituteClientState.(*ibctm.ClientState)
suite.Require().True(ok)
consState, found := suite.chainA.App.GetIBCKeeper().ClientKeeper.GetClientConsensusState(suite.chainA.GetContext(), substitute, tmClientState.LatestHeight)
suite.Require().True(found)
newRevisionNumber := tmClientState.GetLatestHeight().GetRevisionNumber() + 1

tmClientState.LatestHeight = clienttypes.NewHeight(newRevisionNumber, tmClientState.GetLatestHeight().GetRevisionHeight())

suite.chainA.App.GetIBCKeeper().ClientKeeper.SetClientConsensusState(suite.chainA.GetContext(), substitute, tmClientState.LatestHeight, consState)
clientStore := suite.chainA.App.GetIBCKeeper().ClientKeeper.ClientStore(suite.chainA.GetContext(), substitute)
ibctm.SetProcessedTime(clientStore, tmClientState.LatestHeight, 100)
ibctm.SetProcessedHeight(clientStore, tmClientState.LatestHeight, clienttypes.NewHeight(0, 1))
suite.chainA.App.GetIBCKeeper().ClientKeeper.SetClientState(suite.chainA.GetContext(), substitute, tmClientState)
},
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.ZeroHeight()
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 and substitute have equal latest 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)

// Assert that client status is now Active
clientStore := suite.chainA.App.GetIBCKeeper().ClientKeeper.ClientStore(suite.chainA.GetContext(), subjectPath.EndpointA.ClientID)
tmClientState := subjectPath.EndpointA.GetClientState().(*ibctm.ClientState)
suite.Require().Equal(tmClientState.Status(suite.chainA.GetContext(), clientStore, suite.chainA.App.AppCodec()), exported.Active)
} 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
2 changes: 1 addition & 1 deletion modules/core/02-client/keeper/proposal.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ func (k Keeper) ClientUpdateProposal(ctx sdk.Context, p *types.ClientUpdatePropo
subjectClientStore := k.ClientStore(ctx, p.SubjectClientId)

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

substituteClientState, found := k.GetClientState(ctx, p.SubstituteClientId)
Expand Down
2 changes: 1 addition & 1 deletion modules/core/02-client/types/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ var (
ErrFailedNextSeqRecvVerification = errorsmod.Register(SubModuleName, 21, "next sequence receive verification failed")
ErrSelfConsensusStateNotFound = errorsmod.Register(SubModuleName, 22, "self consensus state not found")
ErrUpdateClientFailed = errorsmod.Register(SubModuleName, 23, "unable to update light client")
ErrInvalidUpdateClientProposal = errorsmod.Register(SubModuleName, 24, "invalid update client proposal")
ErrInvalidRecoveryClient = errorsmod.Register(SubModuleName, 24, "invalid recovery client")
ErrInvalidUpgradeClient = errorsmod.Register(SubModuleName, 25, "invalid client upgrade")
ErrInvalidHeight = errorsmod.Register(SubModuleName, 26, "invalid height")
ErrInvalidSubstitute = errorsmod.Register(SubModuleName, 27, "invalid client state substitute")
Expand Down
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 9c5499e

Please sign in to comment.