-
Notifications
You must be signed in to change notification settings - Fork 0
/
client.go
397 lines (338 loc) · 12.1 KB
/
client.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
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
package collactor
import (
"fmt"
sdk "github.com/ci123chain/ci123chain/pkg/abci/types"
clienttypes "github.com/ci123chain/ci123chain/pkg/ibc/core/clients/types"
clientutils "github.com/ci123chain/ci123chain/pkg/ibc/core/clients/utils"
commitmenttypes "github.com/ci123chain/ci123chain/pkg/ibc/core/commitment/types"
ibctmtypes "github.com/ci123chain/ci123chain/pkg/ibc/light-clients/07-tendermint/types"
"github.com/tendermint/tendermint/light"
"reflect"
"time"
)
// CreateClients creates clients for src on dst and dst on src if the client ids are unspecified.
// TODO: de-duplicate code
func (c *Chain) CreateClients(dst *Chain) (modified bool, err error) {
// Handle off chain light clients
if err := c.ValidateLightInitialized(); err != nil {
return false, err
}
if err = dst.ValidateLightInitialized(); err != nil {
return false, err
}
srcUpdateHeader, dstUpdateHeader, err := GetIBCCreateClientHeaders(c, dst)
if err != nil {
return false, err
}
// Create client for the destination chain on the source chain if client id is unspecified
if c.PathEnd.ClientID == "" {
if c.debug {
c.logCreateClient(dst, dstUpdateHeader.Header.Height)
}
ubdPeriod, err := dst.QueryUnbondingPeriod()
if err != nil {
return modified, err
}
// Create the ClientState we want on 'c' tracking 'dst'
clientState := ibctmtypes.NewClientState(
dstUpdateHeader.GetHeader().Header.ChainID,
ibctmtypes.NewFractionFromTm(light.DefaultTrustLevel),
dst.GetTrustingPeriod(),
ubdPeriod,
time.Minute*10,
dstUpdateHeader.GetHeight().(clienttypes.Height),
commitmenttypes.GetSDKSpecs(),
DefaultUpgradePath,
AllowUpdateAfterExpiry,
AllowUpdateAfterMisbehaviour,
)
// Check if an identical light client already exists
clientID, found := FindMatchingClient(c, dst, clientState)
if !found {
msgs := []sdk.Msg{
c.CreateClient(
clientState,
dstUpdateHeader,
),
}
// if a matching client does not exist, create one
res, success, err := c.SendMsgs(msgs)
if err != nil {
return modified, err
}
if !success {
return modified, fmt.Errorf("tx failed: %s", res.RawLog)
}
// update the client identifier
// use index 0, the transaction only has one message
clientID, err = ParseClientIDFromEvents(res.Logs[0].Events)
if err != nil {
return modified, err
}
}
c.PathEnd.ClientID = clientID
modified = true
} else {
// Ensure client exists in the event of user inputted identifiers
// TODO: check client is not expired
_, err := c.QueryClientState(srcUpdateHeader.Header.Height)
if err != nil {
return false, fmt.Errorf("please ensure provided on-chain client (%s) exists on the chain (%s): %v",
c.PathEnd.ClientID, c.ChainID, err)
}
}
// Create client for the source chain on destination chain if client id is unspecified
if dst.PathEnd.ClientID == "" {
if dst.debug {
dst.logCreateClient(c, srcUpdateHeader.Header.Height)
}
ubdPeriod, err := c.QueryUnbondingPeriod()
if err != nil {
return modified, err
}
// Create the ClientState we want on 'dst' tracking 'c'
clientState := ibctmtypes.NewClientState(
srcUpdateHeader.GetHeader().Header.ChainID,
ibctmtypes.NewFractionFromTm(light.DefaultTrustLevel),
c.GetTrustingPeriod(),
ubdPeriod,
time.Minute*10,
srcUpdateHeader.GetHeight().(clienttypes.Height),
commitmenttypes.GetSDKSpecs(),
DefaultUpgradePath,
AllowUpdateAfterExpiry,
AllowUpdateAfterMisbehaviour,
)
// Check if an identical light client already exists
// NOTE: we pass in 'dst' as the source and 'c' as the
// counterparty.
clientID, found := FindMatchingClient(dst, c, clientState)
if !found {
msgs := []sdk.Msg{
dst.CreateClient(
clientState,
srcUpdateHeader,
),
}
// if a matching client does not exist, create one
res, success, err := dst.SendMsgs(msgs)
if err != nil {
return modified, err
}
if !success {
return modified, fmt.Errorf("tx failed: %s", res.RawLog)
}
// update client identifier
clientID, err = ParseClientIDFromEvents(res.Logs[0].Events)
if err != nil {
return modified, err
}
}
dst.PathEnd.ClientID = clientID
modified = true
} else {
// Ensure client exists in the event of user inputted identifiers
// TODO: check client is not expired
_, err := dst.QueryClientState(dstUpdateHeader.Header.Height)
if err != nil {
return false, fmt.Errorf("please ensure provided on-chain client (%s) exists on the chain (%s): %v",
dst.PathEnd.ClientID, dst.ChainID, err)
}
}
c.Log(fmt.Sprintf("★ Clients created: client(%s) on chain[%s] and client(%s) on chain[%s]",
c.PathEnd.ClientID, c.ChainID, dst.PathEnd.ClientID, dst.ChainID))
return modified, nil
}
// FindMatchingClient will determine if there exists a client with identical client and consensus states
// to the client which would have been created. Source is the chain that would be adding a client
// which would track the counterparty. Therefore we query source for the existing clients
// and check if any match the counterparty. The counterparty must have a matching consensus state
// to the latest consensus state of a potential match. The provided client state is the client
// state that will be created if there exist no matches.
func FindMatchingClient(source, counterparty *Chain, clientState *ibctmtypes.ClientState) (string, bool) {
// TODO: add appropriate offset and limits, along with retries
clientsResp, err := source.QueryClients(0, 1000)
if err != nil {
if source.debug {
source.Log(fmt.Sprintf("Error: querying clients on %s failed: %v", source.PathEnd.ChainID, err))
}
return "", false
}
for _, identifiedClientState := range clientsResp.ClientStates {
// unpack any into ibc tendermint client state
existingClientState, err := CastClientStateToTMType(identifiedClientState.ClientState)
if err != nil {
return "", false
}
// check if the client states match
// NOTE: IsFrozen is a sanity check, the client to be created should always
// have a zero frozen height and therefore should never match with a frozen client
if IsMatchingClient(*clientState, *existingClientState) && !existingClientState.IsFrozen() {
// query the latest consensus state of the potential matching client
consensusStateResp, err := clientutils.QueryConsensusStateABCI(source.CLIContext(0),
identifiedClientState.ClientId, existingClientState.GetLatestHeight())
if err != nil {
if source.debug {
source.Log(fmt.Sprintf("Error: failed to query latest consensus state for existing client on chain %s: %v",
source.PathEnd.ChainID, err))
}
continue
}
//nolint:lll
header, err := counterparty.GetLightSignedHeaderAtHeight(int64(existingClientState.GetLatestHeight().GetRevisionHeight()))
if err != nil {
if source.debug {
source.Log(fmt.Sprintf("Error: failed to query header for chain %s at height %d: %v",
counterparty.PathEnd.ChainID, existingClientState.GetLatestHeight().GetRevisionHeight(), err))
}
continue
}
exportedConsState, err := clienttypes.UnpackConsensusState(consensusStateResp.ConsensusState)
if err != nil {
if source.debug {
source.Log(fmt.Sprintf("Error: failed to consensus state on chain %s: %v", counterparty.PathEnd.ChainID, err))
}
continue
}
existingConsensusState, ok := exportedConsState.(*ibctmtypes.ConsensusState)
if !ok {
if source.debug {
source.Log(fmt.Sprintf("Error:consensus state is not tendermint type on chain %s", counterparty.PathEnd.ChainID))
}
continue
}
if existingClientState.IsExpired(existingConsensusState.Timestamp, time.Now()) {
continue
}
if IsMatchingConsensusState(existingConsensusState, header.ConsensusState()) {
// found matching client
return identifiedClientState.ClientId, true
}
}
}
return "", false
}
// IsMatchingClient determines if the two provided clients match in all fields
// except latest height. They are assumed to be IBC tendermint light clients.
// NOTE: we don't pass in a pointer so upstream references don't have a modified
// latest height set to zero.
func IsMatchingClient(clientStateA, clientStateB ibctmtypes.ClientState) bool {
// zero out latest client height since this is determined and incremented
// by on-chain updates. Changing the latest height does not fundamentally
// change the client. The associated consensus state at the latest height
// determines this last check
clientStateA.LatestHeight = clienttypes.ZeroHeight()
clientStateB.LatestHeight = clienttypes.ZeroHeight()
return reflect.DeepEqual(clientStateA, clientStateB)
}
// IsMatchingConsensusState determines if the two provided consensus states are
// identical. They are assumed to be IBC tendermint light clients.
func IsMatchingConsensusState(consensusStateA, consensusStateB *ibctmtypes.ConsensusState) bool {
return reflect.DeepEqual(*consensusStateA, *consensusStateB)
}
// UpdateClients updates clients for src on dst and dst on src given the configured paths
func (c *Chain) UpdateClients(dst *Chain) (err error) {
clients := &RelayMsgs{Src: []sdk.Msg{}, Dst: []sdk.Msg{}}
srcUpdateHeader, dstUpdateHeader, err := GetIBCUpdateHeaders(c, dst)
if err != nil {
return err
}
srcUpdateMsg, err := c.UpdateClient(dst)
if err != nil {
return err
}
dstUpdateMsg, err := dst.UpdateClient(c)
if err != nil {
return err
}
clients.Src = append(clients.Src, srcUpdateMsg)
clients.Dst = append(clients.Dst, dstUpdateMsg)
// Send msgs to both chains
if clients.Ready() {
if clients.Send(c, dst); clients.Success() {
c.Log(fmt.Sprintf("★ Clients updated: [%s]client(%s) {%d}->{%d} and [%s]client(%s) {%d}->{%d}",
c.ChainID,
c.PathEnd.ClientID,
MustGetHeight(srcUpdateHeader.TrustedHeight),
srcUpdateHeader.Header.Height,
dst.ChainID,
dst.PathEnd.ClientID,
MustGetHeight(dstUpdateHeader.TrustedHeight),
dstUpdateHeader.Header.Height,
),
)
}
}
return nil
}
// AutoUpdateClient update client automatically to prevent expiry
func AutoUpdateClient(src, dst *Chain, thresholdTime time.Duration) (time.Duration, error) {
height, err := src.QueryLatestHeight()
if err != nil {
return 0, err
}
clientStateRes, err := src.QueryClientState(height)
if err != nil {
return 0, err
}
// unpack any into ibc tendermint client state
//clientStateExported := clientStateRes.ClientState
//if err != nil {
// return 0, err
//}
// cast from interface to concrete type
clientState, err := CastClientStateToTMType(clientStateRes.ClientState)
if err != nil {
return 0, err
}
if clientState.TrustingPeriod <= thresholdTime {
return 0, fmt.Errorf("client (%s) trusting period time is less than or equal to threshold time",
src.PathEnd.ClientID)
}
// query the latest consensus state of the potential matching client
consensusStateResp, err := clientutils.QueryConsensusStateABCI(src.CLIContext(0),
src.PathEnd.ClientID, clientState.GetLatestHeight())
if err != nil {
return 0, err
}
exportedConsState, err := clienttypes.UnpackConsensusState(consensusStateResp.ConsensusState)
if err != nil {
return 0, err
}
consensusState, ok := exportedConsState.(*ibctmtypes.ConsensusState)
if !ok {
return 0, fmt.Errorf("consensus state with clientID %s from chain %s is not IBC tendermint type",
src.PathEnd.ClientID, src.PathEnd.ChainID)
}
expirationTime := consensusState.Timestamp.Add(clientState.TrustingPeriod)
timeToExpiry := time.Until(expirationTime)
if timeToExpiry > thresholdTime {
return timeToExpiry, nil
}
if clientState.IsExpired(consensusState.Timestamp, time.Now()) {
return 0, fmt.Errorf("client (%s) is already expired on chain: %s", src.PathEnd.ClientID, src.ChainID)
}
srcUpdateHeader, err := src.GetIBCUpdateHeader(dst)
if err != nil {
return 0, err
}
updateMsg, err := src.UpdateClient(dst)
if err != nil {
return 0, err
}
msgs := []sdk.Msg{updateMsg}
res, success, err := src.SendMsgs(msgs)
if err != nil {
return 0, err
}
if !success {
return 0, fmt.Errorf("tx failed: %s", res.RawLog)
}
src.Log(fmt.Sprintf("★ Client updated: [%s]client(%s) {%d}->{%d}",
src.ChainID,
src.PathEnd.ClientID,
MustGetHeight(srcUpdateHeader.TrustedHeight),
srcUpdateHeader.Header.Height,
))
return clientState.TrustingPeriod, nil
}