-
Notifications
You must be signed in to change notification settings - Fork 13
/
fetcher.go
237 lines (200 loc) · 9.32 KB
/
fetcher.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
package trusted_headers
import (
"context"
"fmt"
"github.com/avast/retry-go/v4"
ibcexported "github.com/cosmos/ibc-go/v7/modules/core/exported"
"github.com/cosmos/relayer/v2/relayer/provider"
"time"
"github.com/cosmos/relayer/v2/relayer/chains/cosmos"
neutronmetrics "github.com/neutron-org/neutron-query-relayer/internal/metrics"
"github.com/cosmos/cosmos-sdk/types/query"
clienttypes "github.com/cosmos/ibc-go/v7/modules/core/02-client/types"
tmclient "github.com/cosmos/ibc-go/v7/modules/light-clients/07-tendermint"
"github.com/cosmos/relayer/v2/relayer"
"go.uber.org/zap"
)
// consensusPageSize is how many consensusStates to retrieve for each page in `qc.ConsensusStates(...)` call
const consensusPageSize = 10
// submissionMarginPeriod is a lag period, because we need consensusState to be valid until we approve it on the chain
const submissionMarginPeriod = time.Minute * 5
// retries configuration for fetching light header
var (
RtyAttNum = uint(5)
RtyAtt = retry.Attempts(RtyAttNum)
RtyDel = retry.Delay(time.Millisecond * 400)
RtyErr = retry.LastErrorOnly(true)
)
// TrustedHeaderFetcher able to get trusted headers for a given height
// Trusted headers are needed in Neutron along with proofs to verify that transactions are:
// - included in the block (inclusion proof)
// - successfully executed (delivery proof)
type TrustedHeaderFetcher struct {
neutronChain *relayer.Chain
targetChain *relayer.Chain
logger *zap.Logger
revisionNumber uint64
}
// NewTrustedHeaderFetcher constructs a new TrustedHeaderFetcher
func NewTrustedHeaderFetcher(neutronChain *relayer.Chain, targetChain *relayer.Chain, logger *zap.Logger) *TrustedHeaderFetcher {
return &TrustedHeaderFetcher{
neutronChain: neutronChain,
targetChain: targetChain,
logger: logger,
revisionNumber: clienttypes.ParseChainID(targetChain.ChainID()),
}
}
// FetchTrustedHeaderForHeight returns the best suitable TrustedHeader for given height
// Arguments:
// `height` - remote chain block height X = transaction with such block height
func (thf *TrustedHeaderFetcher) Fetch(ctx context.Context, height uint64) (header *tmclient.Header, err error) {
start := time.Now()
// tries to find height of the closest consensus state height that is less or equal than provided height
trustedHeight, err := thf.getTrustedHeight(ctx, height)
if err != nil {
err = fmt.Errorf("no satisfying consensus state found: %w", err)
return
}
thf.logger.Debug("Found suitable consensus state with trusted height", zap.Uint64("height", trustedHeight.RevisionHeight))
header, err = thf.trustedHeaderAtHeight(ctx, trustedHeight, height)
if err != nil {
err = fmt.Errorf("failed to get header for src chain: %w", err)
return
}
neutronmetrics.RecordActionDuration("TrustedHeaderFetcher", time.Since(start).Seconds())
return
}
// trustedHeaderAtHeight returns a Header with injected necessary trusted fields (TrustedHeight and TrustedValidators) for a height
// to update a light client stored on the Neutron, using the information provided by the target chain.
//
// TrustedHeight is a height of the IBC client on Neutron for the provided height to trust
// TrustedValidators is the validator set of target chain at the TrustedHeight + 1.
//
// The function is very similar to InjectTrustedFields (https://github.com/cosmos/relayer/blob/v2.0.0-beta7/relayer/provider/cosmos/provider.go#L727)
// but with one big difference: trustedHeaderAtHeight injects trusted fields for a particular trusted height, not the latest one in IBC light client.
// This allows us to send UpdateClient msg not only for new heights, but for the old ones (which are still in the trusting period).
//
// Arguments:
// `trustedHeight` - height of any consensus state that's height < supplied height
// `height` - remote chain height for a header
func (thf *TrustedHeaderFetcher) trustedHeaderAtHeight(ctx context.Context, trustedHeight *clienttypes.Height, height uint64) (*tmclient.Header, error) {
header, err := thf.retryGetLightSignedHeaderAtHeight(ctx, height)
if err != nil {
return nil, fmt.Errorf("could not get light header: %w", err)
}
// NOTE: We need to get validators from the source chain at height: trustedHeight+1
// since the last trusted validators for a header at height h is the NextValidators
// at h+1 committed to in header h by NextValidatorsHash
nextHeader, err := thf.retryGetLightSignedHeaderAtHeight(ctx, trustedHeight.RevisionHeight+1)
if err != nil {
return nil, fmt.Errorf("could not get next light header: %w", err)
}
// inject TrustedHeight and TrustedValidators into header
header.TrustedHeight = *trustedHeight
header.TrustedValidators = nextHeader.ValidatorSet
return header, nil
}
// getTrustedHeight tries to find height of any consensusState within trusting period with a height < supplied height
// To do this, it simply iterates over all consensus states and checks each that it's within trusting period AND < supplied_height
// Note that we cannot optimize this search due to consensus states being stored in a tree with *STRING* key `RevisionNumber-RevisionHeight`
//
// Arguments:
// `height` - found consensus state will be with a height <= than it
func (thf *TrustedHeaderFetcher) getTrustedHeight(ctx context.Context, height uint64) (*clienttypes.Height, error) {
// Without this hack it doesn't want to work with NewQueryClient
neutronProvider, ok := thf.neutronChain.ChainProvider.(*cosmos.CosmosProvider)
if !ok {
return nil, fmt.Errorf("failed to cast ChainProvider to concrete type (cosmos.CosmosProvider)")
}
qc := clienttypes.NewQueryClient(neutronProvider)
trustingPeriod, err := thf.fetchTrustingPeriod(ctx)
if err != nil {
return nil, fmt.Errorf("failed to fetch trusting period: %w", err)
}
thf.logger.Debug("fetched trusting period", zap.Float64("trusting_period_hours", trustingPeriod.Hours()))
nextKey := make([]byte, 0)
for {
page, err := qc.ConsensusStates(ctx, requestPage(thf.neutronChain.ClientID(), nextKey))
if err != nil {
return nil, fmt.Errorf("failed to get consensus states for client ID %s: %w", thf.neutronChain.ClientID(), err)
}
for _, cs := range page.ConsensusStates {
suitable, err := thf.isSuitableCS(cs, trustingPeriod, height)
if err != nil {
return nil, err
} else if suitable {
return &cs.Height, nil
}
}
nextKey = page.GetPagination().NextKey
if len(nextKey) == 0 {
break
}
}
return nil, fmt.Errorf("could not find any trusted consensus state for height=%d", height)
}
// fetchTrustingPeriod fetches trusting period of the client
func (thf *TrustedHeaderFetcher) fetchTrustingPeriod(ctx context.Context) (time.Duration, error) {
clientState, err := thf.neutronChain.ChainProvider.QueryClientState(ctx, 0, thf.neutronChain.PathEnd.ClientID)
if err != nil {
return 0, fmt.Errorf("could not fetch client state for ClientId=%s: %w", thf.neutronChain.PathEnd.ClientID, err)
}
tmClientState, ok := clientState.(*tmclient.ClientState)
if !ok {
return 0, fmt.Errorf("expected client state of type *tmclient.ClientState, got %T", clientState)
}
if tmClientState.TrustingPeriod == 0 {
return 0, fmt.Errorf("got empty TrustingPeriod")
}
return tmClientState.TrustingPeriod, nil
}
func (thf *TrustedHeaderFetcher) retryGetLightSignedHeaderAtHeight(ctx context.Context, height uint64) (*tmclient.Header, error) {
var tmHeader *tmclient.Header
if err := retry.Do(func() error {
header, err := thf.targetChain.ChainProvider.QueryIBCHeader(ctx, int64(height))
if err != nil {
return err
}
tmp, ok := header.(provider.TendermintIBCHeader)
if !ok {
return fmt.Errorf("expected header of type provider.TendermintIBCHeader, got %T", header)
}
tmHeader, err = tmp.TMHeader()
if err != nil {
return fmt.Errorf("failed to convert provider.TendermintIBCHeader to tmclient.Header: %w", err)
}
return nil
}, retry.Context(ctx), RtyAtt, RtyDel, RtyErr); err != nil {
return nil, fmt.Errorf(
"failed to get trusted header, please ensure header at the height %d has not been pruned by the connected node: %w",
height, err,
)
}
return tmHeader, nil
}
// isSuitableCS parses the given consensus state and checks if its height can be used as trusted.
// The condition for this check is the following:
//
// 1. The consensus state height is in the same revision and is less than the given height;
// 2. The consensus state timestamp is within the trusting period.
func (thf *TrustedHeaderFetcher) isSuitableCS(cs clienttypes.ConsensusStateWithHeight, trustingPeriod time.Duration, height uint64) (bool, error) {
ibcCS, ok := cs.ConsensusState.GetCachedValue().(ibcexported.ConsensusState)
if !ok {
return false, fmt.Errorf("couldn't cast consensus state value of type %T to ibcexported.ConsensusState", cs.ConsensusState.GetCachedValue())
}
olderThanHeightInSameRevision := cs.Height.RevisionNumber == thf.revisionNumber && cs.Height.RevisionHeight < height
consensusTimestamp := time.Unix(0, int64(ibcCS.GetTimestamp()))
inTrustingPeriod := consensusTimestamp.Add(trustingPeriod).Add(-submissionMarginPeriod).After(time.Now())
return olderThanHeightInSameRevision && inTrustingPeriod, nil
}
func requestPage(clientID string, nextKey []byte) *clienttypes.QueryConsensusStatesRequest {
return &clienttypes.QueryConsensusStatesRequest{
ClientId: clientID,
Pagination: &query.PageRequest{
Key: nextKey,
Limit: consensusPageSize,
Reverse: true,
CountTotal: false,
},
}
}