-
Notifications
You must be signed in to change notification settings - Fork 3.8k
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
Modify IBC client governance unfreezing to reflect ADR changes #8405
Changes from 22 commits
a654f27
9aa500c
51b5001
a47dbbb
73b2821
8d27dcf
fcb97ff
0e93fb6
bbd6d26
0282996
ce4fe37
c4cb7de
a7ef263
d15f901
0c6d98d
fa28622
4c6688c
a041987
eefccd5
a5bc8d5
861b1cb
908e5a0
51905db
c74a5af
dbe1341
0c96f99
0fda643
f17b5e5
e141f19
18b82ba
2369e07
bc6a359
2b55583
ef2fb8a
c9bca48
6a96b6d
78d04f4
20b7a5d
696b5f2
0d60013
b14ea6c
bbb4acf
10dfe43
1edf9ab
eb5823c
e01e8c5
1869fbd
84ebde6
912fdf5
613cf69
b7834c6
1ba2c3d
9fae3ce
26837e2
a5c09a7
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
<!-- | ||
order: 5 | ||
--> | ||
|
||
# Governance Proposals | ||
|
||
In uncommon situtations, a highly valued client may become frozen due to uncontrollable | ||
circumstances. For example, the chain the light client represents might fork into two | ||
new chains. The light client will now have two valid, but conflicting headers at the same | ||
height. Evidence of such misbehaviour is likely to be submitted resulting in a frozen light | ||
client. | ||
|
||
Frozen light clients cannot be updated under any circumstance except via a governance proposal. | ||
Since validators can arbitarily agree to make state transitions that defy the written code, a | ||
governance proposal has been added to ease the complexity of unfreezing or updating clients | ||
which have become "stuck". Unfreezing clients, re-enables all of the channels built upon that | ||
client. This may result in recovery of otherwise lost funds. | ||
|
||
In the case that a highly valued light client is frozen, expired, or rendered unupdateable, a | ||
governance proposal may be submitted to update this client, known as the subject client. The | ||
proposal includes the client identifier for the subject, the client identifier for a substitute | ||
client, and an initial height to reference the substitute client from. Light client implementations | ||
may implement custom updating logic, but in most cases, the subject will be updated with information | ||
from the substitute client, if the proposal passes. The substitute client is used as a "stand in" | ||
while the subject is on trial. It is best practice to create a substitute client *after* the subject | ||
has become frozen to avoid the substitute from also becoming frozen. An active substitute client | ||
allows headers to be submitted during the voting period to prevent accidental expiry once the proposal | ||
passes. | ||
|
||
Example: | ||
|
||
There exists a very common client called "ethereum-0" which is a light client for the Ethereum chain. | ||
Ethereum undergoes a fork, creating two valid headers. As a result, misbehaviour evidence is submitted | ||
to "ethereum-0" rendering it frozen. The Cosmos Hub decides that "Ethereum 2.0" fork is the desired | ||
counterparty chain for this client. The proposer of the governance proposal to unfreeze "ethereum-0" | ||
then creates a new "ethereum-{N}" client with the exact same parameters (except for latest height, | ||
frozen height, and chain-id). Since the substitute client was created *after* the fork, there are | ||
colin-axner marked this conversation as resolved.
Show resolved
Hide resolved
|
||
no conflicting headers to freeze the client. During the voting period, the substitute client can | ||
constantly be updated to prevent the subject from being expired at the end of the voting period, | ||
if the vote passes. If the vote passed, the "ethereum-0" client would have an updated chain-id | ||
and consensus states copied directly from the substitute client's store. | ||
|
||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -33,19 +33,23 @@ message ClientConsensusStates { | |
[(gogoproto.moretags) = "yaml:\"consensus_states\"", (gogoproto.nullable) = false]; | ||
} | ||
|
||
// ClientUpdateProposal is a governance proposal. If it passes, the client is | ||
// updated with the provided header. The update may fail if the header is not | ||
// valid given certain conditions specified by the client implementation. | ||
// ClientUpdateProposal is a governance proposal. If it passes, the substitute client's | ||
// consensus states starting from the 'initial height' are copied over to the subjects | ||
// client state. The proposal handler may fail if the subject and the substitute do not | ||
// match in client and chain parameters (with exception to latest and frozen height). | ||
// The updated client must also be valid (cannot be expired). | ||
message ClientUpdateProposal { | ||
option (gogoproto.goproto_getters) = false; | ||
// the title of the update proposal | ||
string title = 1; | ||
// the description of the proposal | ||
string description = 2; | ||
// the client identifier for the client to be updated if the proposal passes | ||
string client_id = 3 [(gogoproto.moretags) = "yaml:\"client_id\""]; | ||
// the header used to update the client if the proposal passes | ||
google.protobuf.Any header = 4; | ||
string subject_client_id = 3 [(gogoproto.moretags) = "yaml:\"subject_client_id\""]; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This should be |
||
// the substitute client identifier for the client standing in for the subject client | ||
string substitute_client_id = 4 [(gogoproto.moretags) = "yaml:\"susbtitute_client_id\""]; | ||
// the intital height to copy consensus states from the substitute to the subject | ||
Height initial_height = 5 [(gogoproto.moretags) = "yaml:\"initial_height\"", (gogoproto.nullable) = false]; | ||
} | ||
|
||
// Height is a monotonically increasing data type | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -10,40 +10,60 @@ import ( | |
"github.com/cosmos/cosmos-sdk/x/ibc/core/exported" | ||
) | ||
|
||
// ClientUpdateProposal will try to update the client with the new header if and only if | ||
// the proposal passes. The localhost client is not allowed to be modified with a proposal. | ||
// ClientUpdateProposal will retrieve the subject and substitute client. | ||
// The initial height must be greater than the latest height of the subject | ||
// 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 localhost client is not allowed to be modified with a proposal. The IBC | ||
// client implementations are responsible for validating the parameters of the | ||
// subtitute (enusring they match the subject's parameters) as well as copying | ||
// the necessary consensus states from the subtitute to the subject client | ||
// store. | ||
// | ||
// NOTE: Substitute clients with revision numbers not equal to the revision | ||
// number of the subject client is explicityly disallowed. We cannot support | ||
// this until there is a way to range query for the all the consensus | ||
// states which occurred between two IBC Revision heights. | ||
// https://github.com/cosmos/cosmos-sdk/issues/7712 | ||
func (k Keeper) ClientUpdateProposal(ctx sdk.Context, p *types.ClientUpdateProposal) error { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I thought of trying to enforce subject and substitute client types being equal here but just doing It is up to governance never to pass a vote where the subject belongs to a malicious light client implementation. Ideally, best practice (which I will document/recommend) would be to create an entirely new substitute client after the subject became frozen/expired. This way no other connection/channels are relying on the substitute. |
||
if p.ClientId == exported.Localhost { | ||
if p.SubjectClientId == exported.Localhost || p.SubstituteClientId == exported.Localhost { | ||
return sdkerrors.Wrap(types.ErrInvalidUpdateClientProposal, "cannot update localhost client with proposal") | ||
} | ||
|
||
clientState, found := k.GetClientState(ctx, p.ClientId) | ||
subjectClientState, found := k.GetClientState(ctx, p.SubjectClientId) | ||
if !found { | ||
return sdkerrors.Wrapf(types.ErrClientNotFound, "cannot update client with ID %s", p.ClientId) | ||
return sdkerrors.Wrapf(types.ErrClientNotFound, "subject client with ID %s", p.SubjectClientId) | ||
} | ||
|
||
header, err := types.UnpackHeader(p.Header) | ||
if err != nil { | ||
return err | ||
if subjectClientState.GetLatestHeight().GTE(p.InitialHeight) { | ||
return sdkerrors.Wrapf(types.ErrInvalidHeight, "subject client state latest height is greater or equal to initial height (%s >= %s)", subjectClientState.GetLatestHeight(), p.InitialHeight) | ||
} | ||
|
||
substituteClientState, found := k.GetClientState(ctx, p.SubstituteClientId) | ||
if !found { | ||
return sdkerrors.Wrapf(types.ErrClientNotFound, "substitute client with ID %s", p.SubstituteClientId) | ||
} | ||
|
||
// substitute clients with height across revision numbers is not allowed | ||
if subjectClientState.GetLatestHeight().GetRevisionNumber() != substituteClientState.GetLatestHeight().GetRevisionNumber() { | ||
colin-axner marked this conversation as resolved.
Show resolved
Hide resolved
|
||
return sdkerrors.Wrapf(types.ErrInvalidHeight, "subject client state and substitute client state must have the same revision number (%d != %d)", subjectClientState.GetLatestHeight().GetRevisionNumber(), substituteClientState.GetLatestHeight().GetRevisionNumber()) | ||
} | ||
|
||
clientState, consensusState, err := clientState.CheckProposedHeaderAndUpdateState(ctx, k.cdc, k.ClientStore(ctx, p.ClientId), header) | ||
clientState, err := subjectClientState.CheckSubstituteAndUpdateState(ctx, k.cdc, k.ClientStore(ctx, p.SubjectClientId), k.ClientStore(ctx, p.SubstituteClientId), substituteClientState, p.InitialHeight) | ||
if err != nil { | ||
return err | ||
} | ||
k.SetClientState(ctx, p.SubjectClientId, clientState) | ||
|
||
k.SetClientState(ctx, p.ClientId, clientState) | ||
k.SetClientConsensusState(ctx, p.ClientId, header.GetHeight(), consensusState) | ||
|
||
k.Logger(ctx).Info("client updated after governance proposal passed", "client-id", p.ClientId, "height", clientState.GetLatestHeight().String()) | ||
k.Logger(ctx).Info("client updated after governance proposal passed", "client-id", p.SubjectClientId, "height", clientState.GetLatestHeight().String()) | ||
|
||
defer func() { | ||
telemetry.IncrCounterWithLabels( | ||
[]string{"ibc", "client", "update"}, | ||
1, | ||
[]metrics.Label{ | ||
telemetry.NewLabel("client-type", clientState.ClientType()), | ||
telemetry.NewLabel("client-id", p.ClientId), | ||
telemetry.NewLabel("client-id", p.SubjectClientId), | ||
telemetry.NewLabel("update-type", "proposal"), | ||
}, | ||
) | ||
|
@@ -53,9 +73,9 @@ func (k Keeper) ClientUpdateProposal(ctx sdk.Context, p *types.ClientUpdatePropo | |
ctx.EventManager().EmitEvent( | ||
sdk.NewEvent( | ||
types.EventTypeUpdateClientProposal, | ||
sdk.NewAttribute(types.AttributeKeyClientID, p.ClientId), | ||
sdk.NewAttribute(types.AttributeKeySubjectClientID, p.SubjectClientId), | ||
sdk.NewAttribute(types.AttributeKeyClientType, clientState.ClientType()), | ||
sdk.NewAttribute(types.AttributeKeyConsensusHeight, header.GetHeight().String()), | ||
sdk.NewAttribute(types.AttributeKeyConsensusHeight, clientState.GetLatestHeight().String()), | ||
), | ||
) | ||
|
||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't understand this sentence, what is the relation? Do you mean that the security model would be the same?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, I was just trying to refer to the security model outlined in the ADR. Why this feature is safe and why it requires a governance proposal. Is there a way I could clarify this better?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
"a quorum validators can sign arbitrary state roots which may not be valid executions of the state machine"
something like this is clearer I think