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

Add 02-client implementation for Recover client. #4499

Merged
merged 3 commits into from
Aug 31, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 {
DimitrisJim marked this conversation as resolved.
Show resolved Hide resolved
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() {
DimitrisJim marked this conversation as resolved.
Show resolved Hide resolved
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)
DimitrisJim marked this conversation as resolved.
Show resolved Hide resolved
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)
DimitrisJim marked this conversation as resolved.
Show resolved Hide resolved

// 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) {
DimitrisJim marked this conversation as resolved.
Show resolved Hide resolved
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"
DimitrisJim marked this conversation as resolved.
Show resolved Hide resolved

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