-
Notifications
You must be signed in to change notification settings - Fork 176
/
blocks.go
276 lines (234 loc) · 7.89 KB
/
blocks.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
package rest
import (
"context"
"fmt"
"net/http"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"github.com/onflow/flow-go/model/flow"
"github.com/onflow/flow-go/access"
"github.com/onflow/flow-go/engine/access/rest/generated"
)
const (
ExpandableFieldPayload = "payload"
ExpandableExecutionResult = "execution_result"
sealedHeightQueryParam = "sealed"
finalHeightQueryParam = "final"
startHeightQueryParam = "start_height"
endHeightQueryParam = "end_height"
heightQueryParam = "height"
)
// getBlocksByID gets blocks by provided ID or list of IDs.
func getBlocksByIDs(r *request, backend access.API, link LinkGenerator) (interface{}, error) {
ids, err := r.ids()
if err != nil {
return nil, NewBadRequestError(err)
}
blocks := make([]*generated.Block, len(ids))
for i, id := range ids {
block, err := getBlock(forID(&id), r, backend, link)
if err != nil {
return nil, err
}
blocks[i] = block
}
return blocks, nil
}
func getBlocksByHeight(r *request, backend access.API, link LinkGenerator) (interface{}, error) {
heights, err := r.getQueryParams(heightQueryParam)
if err != nil {
return nil, NewBadRequestError(fmt.Errorf("block heights invalid: %w", err))
}
startHeight := r.getQueryParam(startHeightQueryParam)
endHeight := r.getQueryParam(endHeightQueryParam)
// if both height and one or both of start and end height are provided
if len(heights) > 0 && (startHeight != "" || endHeight != "") {
err := fmt.Errorf("can only provide either heights or start and end height range")
return nil, NewBadRequestError(err)
}
// if neither height nor start and end height are provided
if len(heights) == 0 && (startHeight == "" || endHeight == "") {
err := fmt.Errorf("must provide either heights or start and end height range")
return nil, NewBadRequestError(err)
}
// if the query is /blocks?height=final or /blocks?height=sealed, lookup the last finalized or the last sealed block
if len(heights) == 1 && (heights[0] == finalHeightQueryParam || heights[0] == sealedHeightQueryParam) {
block, err := getBlock(forFinalized(heights[0]), r, backend, link)
if err != nil {
return nil, err
}
return []*generated.Block{block}, nil
}
// if the query is /blocks/height=1000,1008,1049...
if len(heights) > 0 {
uintHeights, err := toHeights(heights)
if err != nil {
heightError := fmt.Errorf("invalid height specified: %w", err)
return nil, NewBadRequestError(heightError)
}
blocks := make([]*generated.Block, len(uintHeights))
for i, h := range uintHeights {
block, err := getBlock(forHeight(h), r, backend, link)
if err != nil {
return nil, err
}
blocks[i] = block
}
return blocks, nil
}
// support providing end height as "sealed" or "final"
if endHeight == finalHeightQueryParam || endHeight == sealedHeightQueryParam {
latest, err := backend.GetLatestBlock(r.Context(), endHeight == sealedHeightQueryParam)
if err != nil {
return nil, err
}
endHeight = fmt.Sprintf("%d", latest.Header.Height)
}
// lookup block by start and end height range
start, err := toHeight(startHeight)
if err != nil {
heightError := fmt.Errorf("invalid start height: %w", err)
return nil, NewBadRequestError(heightError)
}
end, err := toHeight(endHeight)
if err != nil {
heightError := fmt.Errorf("invalid end height: %w", err)
return nil, NewBadRequestError(heightError)
}
if start > end {
err := fmt.Errorf("start height must be less than or equal to end height")
return nil, NewBadRequestError(err)
}
if end-start > MaxAllowedHeights {
err := fmt.Errorf("height range %d exceeds maximum allowed of %d", end-start, MaxAllowedHeights)
return nil, NewBadRequestError(err)
}
blocks := make([]*generated.Block, 0)
// start and end height inclusive
for i := start; i <= end; i++ {
block, err := getBlock(forHeight(i), r, backend, link)
if err != nil {
return nil, err
}
blocks = append(blocks, block)
}
return blocks, nil
}
// getBlockPayloadByID gets block payload by ID
func getBlockPayloadByID(req *request, backend access.API, _ LinkGenerator) (interface{}, error) {
id, err := req.id()
if err != nil {
return nil, NewBadRequestError(err)
}
blkProvider := NewBlockProvider(backend, forID(&id))
blk, statusErr := blkProvider.getBlock(req.Context())
if statusErr != nil {
return nil, statusErr
}
payload, err := blockPayloadResponse(blk.Payload)
if err != nil {
return nil, err
}
return payload, nil
}
func getBlock(option blockProviderOption, req *request, backend access.API, link LinkGenerator) (*generated.Block, error) {
// lookup block
blkProvider := NewBlockProvider(backend, option)
blk, err := blkProvider.getBlock(req.Context())
if err != nil {
return nil, err
}
// lookup execution result
// (even if not specified as expandable, since we need the execution result ID to generate its expandable link)
executionResult, err := backend.GetExecutionResultForBlockID(req.Context(), blk.ID())
if err != nil {
// handle case where execution result is not yet available
if se, ok := status.FromError(err); ok {
if se.Code() == codes.NotFound {
return blockResponse(blk, nil, link, req.expandFields)
}
}
return nil, err
}
return blockResponse(blk, executionResult, link, req.expandFields)
}
// blockProvider is a layer of abstraction on top of the backend access.API and provides a uniform way to
// look up a block or a block header either by ID or by height
type blockProvider struct {
id *flow.Identifier
height uint64
latest bool
sealed bool
backend access.API
}
type blockProviderOption func(blkProvider *blockProvider)
func forID(id *flow.Identifier) blockProviderOption {
return func(blkProvider *blockProvider) {
blkProvider.id = id
}
}
func forHeight(height uint64) blockProviderOption {
return func(blkProvider *blockProvider) {
blkProvider.height = height
}
}
func forFinalized(queryParam string) blockProviderOption {
return func(blkProvider *blockProvider) {
switch queryParam {
case sealedHeightQueryParam:
blkProvider.sealed = true
fallthrough
case finalHeightQueryParam:
blkProvider.latest = true
}
}
}
func NewBlockProvider(backend access.API, options ...blockProviderOption) *blockProvider {
blkProvider := &blockProvider{
backend: backend,
}
for _, o := range options {
o(blkProvider)
}
return blkProvider
}
func (blkProvider *blockProvider) getBlock(ctx context.Context) (*flow.Block, error) {
if blkProvider.id != nil {
blk, err := blkProvider.backend.GetBlockByID(ctx, *blkProvider.id)
if err != nil {
return nil, idLookupError(blkProvider.id, "block", err)
}
return blk, nil
}
if blkProvider.latest {
blk, err := blkProvider.backend.GetLatestBlock(ctx, blkProvider.sealed)
if err != nil {
// cannot be a 'not found' error since final and sealed block should always be found
return nil, NewRestError(http.StatusInternalServerError, "block lookup failed", err)
}
return blk, nil
}
blk, err := blkProvider.backend.GetBlockByHeight(ctx, blkProvider.height)
if err != nil {
return nil, heightLookupError(blkProvider.height, "block", err)
}
return blk, nil
}
// idLookupError adds ID to the error message
func idLookupError(id *flow.Identifier, entityType string, err error) StatusError {
msg := fmt.Sprintf("error looking up %s with ID %s", entityType, id.String())
// if error has GRPC code NotFound, then return HTTP NotFound error
if status.Code(err) == codes.NotFound {
return NewNotFoundError(msg, err)
}
return NewRestError(http.StatusInternalServerError, msg, err)
}
// heightLookupError adds height to the error message
func heightLookupError(height uint64, entityType string, err error) StatusError {
msg := fmt.Sprintf("error looking up %s at height %d", entityType, height)
// if error has GRPC code NotFound, then return HTTP NotFound error
if status.Code(err) == codes.NotFound {
return NewNotFoundError(msg, err)
}
return NewRestError(http.StatusInternalServerError, msg, err)
}