/
block_reader.go
223 lines (200 loc) · 7.29 KB
/
block_reader.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
package execution
import (
"context"
"fmt"
"math/big"
"github.com/ethereum/go-ethereum/common"
"github.com/pkg/errors"
"github.com/prysmaticlabs/prysm/v3/beacon-chain/execution/types"
"github.com/prysmaticlabs/prysm/v3/config/params"
"github.com/prysmaticlabs/prysm/v3/monitoring/tracing"
"go.opencensus.io/trace"
)
// searchThreshold to apply for when searching for blocks of a particular time. If the buffer
// is exceeded we recalibrate the search again.
const searchThreshold = 5
// amount of times we repeat a failed search till is satisfies the conditional.
const repeatedSearches = 2 * searchThreshold
var errBlockTimeTooLate = errors.New("provided time is later than the current eth1 head")
// BlockExists returns true if the block exists, its height and any possible error encountered.
func (s *Service) BlockExists(ctx context.Context, hash common.Hash) (bool, *big.Int, error) {
ctx, span := trace.StartSpan(ctx, "powchain.BlockExists")
defer span.End()
if exists, hdrInfo, err := s.headerCache.HeaderInfoByHash(hash); exists || err != nil {
if err != nil {
return false, nil, err
}
span.AddAttributes(trace.BoolAttribute("blockCacheHit", true))
return true, hdrInfo.Number, nil
}
span.AddAttributes(trace.BoolAttribute("blockCacheHit", false))
header, err := s.HeaderByHash(ctx, hash)
if err != nil {
return false, big.NewInt(0), errors.Wrap(err, "could not query block with given hash")
}
if err := s.headerCache.AddHeader(header); err != nil {
return false, big.NewInt(0), err
}
return true, new(big.Int).Set(header.Number), nil
}
// BlockHashByHeight returns the block hash of the block at the given height.
func (s *Service) BlockHashByHeight(ctx context.Context, height *big.Int) (common.Hash, error) {
ctx, span := trace.StartSpan(ctx, "powchain.BlockHashByHeight")
defer span.End()
if exists, hInfo, err := s.headerCache.HeaderInfoByHeight(height); exists || err != nil {
if err != nil {
return [32]byte{}, err
}
span.AddAttributes(trace.BoolAttribute("headerCacheHit", true))
return hInfo.Hash, nil
}
span.AddAttributes(trace.BoolAttribute("headerCacheHit", false))
if s.rpcClient == nil {
err := errors.New("nil rpc client")
tracing.AnnotateError(span, err)
return [32]byte{}, err
}
header, err := s.HeaderByNumber(ctx, height)
if err != nil {
return [32]byte{}, errors.Wrap(err, fmt.Sprintf("could not query header with height %d", height.Uint64()))
}
if err := s.headerCache.AddHeader(header); err != nil {
return [32]byte{}, err
}
return header.Hash, nil
}
// BlockTimeByHeight fetches an eth1 block timestamp by its height.
func (s *Service) BlockTimeByHeight(ctx context.Context, height *big.Int) (uint64, error) {
ctx, span := trace.StartSpan(ctx, "powchain.BlockTimeByHeight")
defer span.End()
if s.rpcClient == nil {
err := errors.New("nil rpc client")
tracing.AnnotateError(span, err)
return 0, err
}
header, err := s.HeaderByNumber(ctx, height)
if err != nil {
return 0, errors.Wrap(err, fmt.Sprintf("could not query block with height %d", height.Uint64()))
}
return header.Time, nil
}
// BlockByTimestamp returns the most recent block number up to a given timestamp.
// This is an optimized version with the worst case being O(2*repeatedSearches) number of calls
// while in best case search for the block is performed in O(1).
func (s *Service) BlockByTimestamp(ctx context.Context, time uint64) (*types.HeaderInfo, error) {
ctx, span := trace.StartSpan(ctx, "powchain.BlockByTimestamp")
defer span.End()
s.latestEth1DataLock.RLock()
latestBlkHeight := s.latestEth1Data.BlockHeight
latestBlkTime := s.latestEth1Data.BlockTime
s.latestEth1DataLock.RUnlock()
if time > latestBlkTime {
return nil, errors.Wrap(errBlockTimeTooLate, fmt.Sprintf("(%d > %d)", time, latestBlkTime))
}
// Initialize a pointer to eth1 chain's history to start our search from.
cursorNum := big.NewInt(0).SetUint64(latestBlkHeight)
cursorTime := latestBlkTime
numOfBlocks := uint64(0)
estimatedBlk := cursorNum.Uint64()
maxTimeBuffer := searchThreshold * params.BeaconConfig().SecondsPerETH1Block
// Terminate if we can't find an acceptable block after
// repeated searches.
for i := 0; i < repeatedSearches; i++ {
if ctx.Err() != nil {
return nil, ctx.Err()
}
if time > cursorTime+maxTimeBuffer {
numOfBlocks = (time - cursorTime) / params.BeaconConfig().SecondsPerETH1Block
// In the event we have an infeasible estimated block, this is a defensive
// check to ensure it does not exceed rational bounds.
if cursorNum.Uint64()+numOfBlocks > latestBlkHeight {
break
}
estimatedBlk = cursorNum.Uint64() + numOfBlocks
} else if time+maxTimeBuffer < cursorTime {
numOfBlocks = (cursorTime - time) / params.BeaconConfig().SecondsPerETH1Block
// In the event we have an infeasible number of blocks
// we exit early.
if numOfBlocks >= cursorNum.Uint64() {
break
}
estimatedBlk = cursorNum.Uint64() - numOfBlocks
} else {
// Exit if we are in the range of
// time - buffer <= head.time <= time + buffer
break
}
hInfo, err := s.retrieveHeaderInfo(ctx, estimatedBlk)
if err != nil {
return nil, err
}
cursorNum = hInfo.Number
cursorTime = hInfo.Time
}
// Exit early if we get the desired block.
if cursorTime == time {
return s.retrieveHeaderInfo(ctx, cursorNum.Uint64())
}
if cursorTime > time {
return s.findMaxTargetEth1Block(ctx, big.NewInt(0).SetUint64(estimatedBlk), time)
}
return s.findMinTargetEth1Block(ctx, big.NewInt(0).SetUint64(estimatedBlk), time)
}
// Performs a search to find a target eth1 block which is earlier than or equal to the
// target time. This method is used when head.time > targetTime
func (s *Service) findMaxTargetEth1Block(ctx context.Context, upperBoundBlk *big.Int, targetTime uint64) (*types.HeaderInfo, error) {
for bn := upperBoundBlk; ; bn = big.NewInt(0).Sub(bn, big.NewInt(1)) {
if ctx.Err() != nil {
return nil, ctx.Err()
}
info, err := s.retrieveHeaderInfo(ctx, bn.Uint64())
if err != nil {
return nil, err
}
if info.Time <= targetTime {
return info, nil
}
}
}
// Performs a search to find a target eth1 block which is just earlier than or equal to the
// target time. This method is used when head.time < targetTime
func (s *Service) findMinTargetEth1Block(ctx context.Context, lowerBoundBlk *big.Int, targetTime uint64) (*types.HeaderInfo, error) {
for bn := lowerBoundBlk; ; bn = big.NewInt(0).Add(bn, big.NewInt(1)) {
if ctx.Err() != nil {
return nil, ctx.Err()
}
info, err := s.retrieveHeaderInfo(ctx, bn.Uint64())
if err != nil {
return nil, err
}
// Return the last block before we hit the threshold time.
if info.Time > targetTime {
return s.retrieveHeaderInfo(ctx, info.Number.Uint64()-1)
}
// If time is equal, this is our target block.
if info.Time == targetTime {
return info, nil
}
}
}
func (s *Service) retrieveHeaderInfo(ctx context.Context, bNum uint64) (*types.HeaderInfo, error) {
bn := big.NewInt(0).SetUint64(bNum)
exists, info, err := s.headerCache.HeaderInfoByHeight(bn)
if err != nil {
return nil, err
}
if !exists {
hdr, err := s.HeaderByNumber(ctx, bn)
if err != nil {
return nil, err
}
if hdr == nil {
return nil, errors.Errorf("header with the number %d does not exist", bNum)
}
if err := s.headerCache.AddHeader(hdr); err != nil {
return nil, err
}
info = hdr
}
return info, nil
}