-
Notifications
You must be signed in to change notification settings - Fork 580
/
update.go
263 lines (239 loc) · 11.2 KB
/
update.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
package types
import (
"bytes"
"reflect"
"time"
"github.com/cosmos/cosmos-sdk/codec"
sdk "github.com/cosmos/cosmos-sdk/types"
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
"github.com/tendermint/tendermint/light"
tmtypes "github.com/tendermint/tendermint/types"
clienttypes "github.com/cosmos/ibc-go/v4/modules/core/02-client/types"
commitmenttypes "github.com/cosmos/ibc-go/v4/modules/core/23-commitment/types"
"github.com/cosmos/ibc-go/v4/modules/core/exported"
)
// CheckHeaderAndUpdateState checks if the provided header is valid, and if valid it will:
// create the consensus state for the header.Height
// and update the client state if the header height is greater than the latest client state height
// It returns an error if:
// - the client or header provided are not parseable to tendermint types
// - the header is invalid
// - header height is less than or equal to the trusted header height
// - header revision is not equal to trusted header revision
// - header valset commit verification fails
// - header timestamp is past the trusting period in relation to the consensus state
// - header timestamp is less than or equal to the consensus state timestamp
//
// UpdateClient may be used to either create a consensus state for:
// - a future height greater than the latest client state height
// - a past height that was skipped during bisection
// If we are updating to a past height, a consensus state is created for that height to be persisted in client store
// If we are updating to a future height, the consensus state is created and the client state is updated to reflect
// the new latest height
// UpdateClient must only be used to update within a single revision, thus header revision number and trusted height's revision
// number must be the same. To update to a new revision, use a separate upgrade path
// Tendermint client validity checking uses the bisection algorithm described
// in the [Tendermint spec](https://github.com/tendermint/spec/blob/master/spec/consensus/light-client.md).
//
// Misbehaviour Detection:
// UpdateClient will detect implicit misbehaviour by enforcing certain invariants on any new update call and will return a frozen client.
// 1. Any valid update that creates a different consensus state for an already existing height is evidence of misbehaviour and will freeze client.
// 2. Any valid update that breaks time monotonicity with respect to its neighboring consensus states is evidence of misbehaviour and will freeze client.
// Misbehaviour sets frozen height to {0, 1} since it is only used as a boolean value (zero or non-zero).
//
// Pruning:
// UpdateClient will additionally retrieve the earliest consensus state for this clientID and check if it is expired. If it is,
// that consensus state will be pruned from store along with all associated metadata. This will prevent the client store from
// becoming bloated with expired consensus states that can no longer be used for updates and packet verification.
func (cs ClientState) CheckHeaderAndUpdateState(
ctx sdk.Context, cdc codec.BinaryCodec, clientStore sdk.KVStore,
header exported.Header,
) (exported.ClientState, exported.ConsensusState, error) {
tmHeader, ok := header.(*Header)
if !ok {
return nil, nil, sdkerrors.Wrapf(
clienttypes.ErrInvalidHeader, "expected type %T, got %T", &Header{}, header,
)
}
// Check if the Client store already has a consensus state for the header's height
// If the consensus state exists, and it matches the header then we return early
// since header has already been submitted in a previous UpdateClient.
var conflictingHeader bool
prevConsState, _ := GetConsensusState(clientStore, cdc, header.GetHeight())
if prevConsState != nil {
// This header has already been submitted and the necessary state is already stored
// in client store, thus we can return early without further validation.
if reflect.DeepEqual(prevConsState, tmHeader.ConsensusState()) {
return &cs, prevConsState, nil
}
// A consensus state already exists for this height, but it does not match the provided header.
// Thus, we must check that this header is valid, and if so we will freeze the client.
conflictingHeader = true
}
// get consensus state from clientStore
trustedConsState, err := GetConsensusState(clientStore, cdc, tmHeader.TrustedHeight)
if err != nil {
return nil, nil, sdkerrors.Wrapf(
err, "could not get consensus state from clientstore at TrustedHeight: %s", tmHeader.TrustedHeight,
)
}
if err := checkValidity(&cs, trustedConsState, tmHeader, ctx.BlockTime()); err != nil {
return nil, nil, err
}
consState := tmHeader.ConsensusState()
// Header is different from existing consensus state and also valid, so freeze the client and return
if conflictingHeader {
cs.FrozenHeight = FrozenHeight
return &cs, consState, nil
}
// Check that consensus state timestamps are monotonic
prevCons, prevOk := GetPreviousConsensusState(clientStore, cdc, header.GetHeight())
nextCons, nextOk := GetNextConsensusState(clientStore, cdc, header.GetHeight())
// if previous consensus state exists, check consensus state time is greater than previous consensus state time
// if previous consensus state is not before current consensus state, freeze the client and return.
if prevOk && !prevCons.Timestamp.Before(consState.Timestamp) {
cs.FrozenHeight = FrozenHeight
return &cs, consState, nil
}
// if next consensus state exists, check consensus state time is less than next consensus state time
// if next consensus state is not after current consensus state, freeze the client and return.
if nextOk && !nextCons.Timestamp.After(consState.Timestamp) {
cs.FrozenHeight = FrozenHeight
return &cs, consState, nil
}
// Check the earliest consensus state to see if it is expired, if so then set the prune height
// so that we can delete consensus state and all associated metadata.
var (
pruneHeight exported.Height
pruneError error
)
pruneCb := func(height exported.Height) bool {
consState, err := GetConsensusState(clientStore, cdc, height)
// this error should never occur
if err != nil {
pruneError = err
return true
}
if cs.IsExpired(consState.Timestamp, ctx.BlockTime()) {
pruneHeight = height
}
return true
}
IterateConsensusStateAscending(clientStore, pruneCb)
if pruneError != nil {
return nil, nil, pruneError
}
// if pruneHeight is set, delete consensus state and metadata
if pruneHeight != nil {
deleteConsensusState(clientStore, pruneHeight)
deleteConsensusMetadata(clientStore, pruneHeight)
}
newClientState, consensusState := update(ctx, clientStore, &cs, tmHeader)
return newClientState, consensusState, nil
}
// checkTrustedHeader checks that consensus state matches trusted fields of Header
func checkTrustedHeader(header *Header, consState *ConsensusState) error {
tmTrustedValidators, err := tmtypes.ValidatorSetFromProto(header.TrustedValidators)
if err != nil {
return sdkerrors.Wrap(err, "trusted validator set in not tendermint validator set type")
}
// assert that trustedVals is NextValidators of last trusted header
// to do this, we check that trustedVals.Hash() == consState.NextValidatorsHash
tvalHash := tmTrustedValidators.Hash()
if !bytes.Equal(consState.NextValidatorsHash, tvalHash) {
return sdkerrors.Wrapf(
ErrInvalidValidatorSet,
"trusted validators %s, does not hash to latest trusted validators. Expected: %X, got: %X",
header.TrustedValidators, consState.NextValidatorsHash, tvalHash,
)
}
return nil
}
// checkValidity checks if the Tendermint header is valid.
// CONTRACT: consState.Height == header.TrustedHeight
func checkValidity(
clientState *ClientState, consState *ConsensusState,
header *Header, currentTimestamp time.Time,
) error {
if err := checkTrustedHeader(header, consState); err != nil {
return err
}
// UpdateClient only accepts updates with a header at the same revision
// as the trusted consensus state
if header.GetHeight().GetRevisionNumber() != header.TrustedHeight.RevisionNumber {
return sdkerrors.Wrapf(
ErrInvalidHeaderHeight,
"header height revision %d does not match trusted header revision %d",
header.GetHeight().GetRevisionNumber(), header.TrustedHeight.RevisionNumber,
)
}
tmTrustedValidators, err := tmtypes.ValidatorSetFromProto(header.TrustedValidators)
if err != nil {
return sdkerrors.Wrap(err, "trusted validator set in not tendermint validator set type")
}
tmSignedHeader, err := tmtypes.SignedHeaderFromProto(header.SignedHeader)
if err != nil {
return sdkerrors.Wrap(err, "signed header in not tendermint signed header type")
}
tmValidatorSet, err := tmtypes.ValidatorSetFromProto(header.ValidatorSet)
if err != nil {
return sdkerrors.Wrap(err, "validator set in not tendermint validator set type")
}
// assert header height is newer than consensus state
if header.GetHeight().LTE(header.TrustedHeight) {
return sdkerrors.Wrapf(
clienttypes.ErrInvalidHeader,
"header height ≤ consensus state height (%s ≤ %s)", header.GetHeight(), header.TrustedHeight,
)
}
chainID := clientState.GetChainID()
// If chainID is in revision format, then set revision number of chainID with the revision number
// of the header we are verifying
// This is useful if the update is at a previous revision rather than an update to the latest revision
// of the client.
// The chainID must be set correctly for the previous revision before attempting verification.
// Updates for previous revisions are not supported if the chainID is not in revision format.
if clienttypes.IsRevisionFormat(chainID) {
chainID, _ = clienttypes.SetRevisionNumber(chainID, header.GetHeight().GetRevisionNumber())
}
// Construct a trusted header using the fields in consensus state
// Only Height, Time, and NextValidatorsHash are necessary for verification
trustedHeader := tmtypes.Header{
ChainID: chainID,
Height: int64(header.TrustedHeight.RevisionHeight),
Time: consState.Timestamp,
NextValidatorsHash: consState.NextValidatorsHash,
}
signedHeader := tmtypes.SignedHeader{
Header: &trustedHeader,
}
// Verify next header with the passed-in trustedVals
// - asserts trusting period not passed
// - assert header timestamp is not past the trusting period
// - assert header timestamp is past latest stored consensus state timestamp
// - assert that a TrustLevel proportion of TrustedValidators signed new Commit
err = light.Verify(
&signedHeader,
tmTrustedValidators, tmSignedHeader, tmValidatorSet,
clientState.TrustingPeriod, currentTimestamp, clientState.MaxClockDrift, clientState.TrustLevel.ToTendermint(),
)
if err != nil {
return sdkerrors.Wrap(err, "failed to verify header")
}
return nil
}
// update the consensus state from a new header and set processed time metadata
func update(ctx sdk.Context, clientStore sdk.KVStore, clientState *ClientState, header *Header) (*ClientState, *ConsensusState) {
height := header.GetHeight().(clienttypes.Height)
if height.GT(clientState.LatestHeight) {
clientState.LatestHeight = height
}
consensusState := &ConsensusState{
Timestamp: header.GetTime(),
Root: commitmenttypes.NewMerkleRoot(header.Header.GetAppHash()),
NextValidatorsHash: header.Header.NextValidatorsHash,
}
// set metadata for this consensus state
setConsensusMetadata(ctx, clientStore, header.GetHeight())
return clientState, consensusState
}