Skip to content

Commit d6fb90a

Browse files
peterargueillia-malachyn
authored andcommitted
Merge pull request #6525 from The-K-R-O-K/UlyanaAndrukhiv/6413-backfill-tx-error-messages
[Access] Add util command to backfill tx error messages db
1 parent 83dbe69 commit d6fb90a

17 files changed

+885
-50
lines changed

admin/README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,3 +115,8 @@ curl localhost:9002/admin/run_command -H 'Content-Type: application/json' -d '{"
115115
curl localhost:9002/admin/run_command -H 'Content-Type: application/json' -d '{"commandName": "protocol-snapshot"}'
116116
curl localhost:9002/admin/run_command -H 'Content-Type: application/json' -d '{"commandName": "protocol-snapshot", "data": { "blocks-to-skip": 10 }}'
117117
```
118+
119+
### To backfill transaction error messages
120+
```
121+
curl localhost:9002/admin/run_command -H 'Content-Type: application/json' -d '{"commandName": "backfill-tx-error-messages", "data": { "start-height": 340, "end-height": 343, "execution-node-ids":["ec7b934df29248d574ae1cc33ae77f22f0fcf96a79e009224c46374d1837824e", "8cbdc8d24a28899a33140cb68d4146cd6f2f6c18c57f54c299f26351d126919e"] }}'
122+
```
Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
package storage
2+
3+
import (
4+
"context"
5+
"fmt"
6+
7+
"github.com/onflow/flow-go/admin"
8+
"github.com/onflow/flow-go/admin/commands"
9+
"github.com/onflow/flow-go/engine/access/ingestion/tx_error_messages"
10+
commonrpc "github.com/onflow/flow-go/engine/common/rpc"
11+
"github.com/onflow/flow-go/model/flow"
12+
"github.com/onflow/flow-go/model/flow/filter"
13+
"github.com/onflow/flow-go/state/protocol"
14+
)
15+
16+
var _ commands.AdminCommand = (*BackfillTxErrorMessagesCommand)(nil)
17+
18+
// backfillTxErrorMessagesRequest represents the input parameters for
19+
// backfilling transaction error messages.
20+
type backfillTxErrorMessagesRequest struct {
21+
startHeight uint64 // Start height from which to begin backfilling.
22+
endHeight uint64 // End height up to which backfilling is performed.
23+
executionNodeIds flow.IdentitySkeletonList // List of execution node IDs to be used for backfilling.
24+
}
25+
26+
// BackfillTxErrorMessagesCommand executes a command to backfill
27+
// transaction error messages by fetching them from execution nodes.
28+
type BackfillTxErrorMessagesCommand struct {
29+
state protocol.State
30+
txErrorMessagesCore *tx_error_messages.TxErrorMessagesCore
31+
}
32+
33+
// NewBackfillTxErrorMessagesCommand creates a new instance of BackfillTxErrorMessagesCommand
34+
func NewBackfillTxErrorMessagesCommand(
35+
state protocol.State,
36+
txErrorMessagesCore *tx_error_messages.TxErrorMessagesCore,
37+
) commands.AdminCommand {
38+
return &BackfillTxErrorMessagesCommand{
39+
state: state,
40+
txErrorMessagesCore: txErrorMessagesCore,
41+
}
42+
}
43+
44+
// Validator validates the input for the backfill command. The input is validated
45+
// for field types, boundaries, and coherence of start and end heights.
46+
//
47+
// Expected errors during normal operation:
48+
// - admin.InvalidAdminReqError - if start-height is greater than end-height or
49+
// if the input format is invalid, if an invalid execution node ID is provided.
50+
func (b *BackfillTxErrorMessagesCommand) Validator(request *admin.CommandRequest) error {
51+
input, ok := request.Data.(map[string]interface{})
52+
if !ok {
53+
return admin.NewInvalidAdminReqFormatError("expected map[string]any")
54+
}
55+
56+
data := &backfillTxErrorMessagesRequest{}
57+
58+
rootHeight := b.state.Params().SealedRoot().Height
59+
data.startHeight = rootHeight // Default value
60+
61+
sealed, err := b.state.Sealed().Head()
62+
if err != nil {
63+
return fmt.Errorf("failed to lookup sealed header: %w", err)
64+
}
65+
66+
lastSealedHeight := sealed.Height
67+
if startHeightIn, ok := input["start-height"]; ok {
68+
startHeight, err := parseN(startHeightIn)
69+
if err != nil {
70+
return admin.NewInvalidAdminReqErrorf("invalid 'start-height' field: %w", err)
71+
}
72+
73+
if startHeight > lastSealedHeight {
74+
return admin.NewInvalidAdminReqErrorf(
75+
"'start-height' %d must not be greater than latest sealed block %d",
76+
startHeight,
77+
lastSealedHeight,
78+
)
79+
}
80+
81+
if startHeight < rootHeight {
82+
return admin.NewInvalidAdminReqErrorf(
83+
"'start-height' %d must not be less than root block %d",
84+
startHeight,
85+
rootHeight,
86+
)
87+
}
88+
89+
data.startHeight = startHeight
90+
}
91+
92+
data.endHeight = lastSealedHeight // Default value
93+
if endHeightIn, ok := input["end-height"]; ok {
94+
endHeight, err := parseN(endHeightIn)
95+
if err != nil {
96+
return admin.NewInvalidAdminReqErrorf("invalid 'end-height' field: %w", err)
97+
}
98+
99+
if endHeight > lastSealedHeight {
100+
return admin.NewInvalidAdminReqErrorf(
101+
"'end-height' %d must not be greater than latest sealed block %d",
102+
endHeight,
103+
lastSealedHeight,
104+
)
105+
}
106+
107+
data.endHeight = endHeight
108+
}
109+
110+
if data.endHeight < data.startHeight {
111+
return admin.NewInvalidAdminReqErrorf(
112+
"'start-height' %d must not be less than 'end-height' %d",
113+
data.startHeight,
114+
data.endHeight,
115+
)
116+
}
117+
118+
identities, err := b.state.Final().Identities(filter.HasRole[flow.Identity](flow.RoleExecution))
119+
if err != nil {
120+
return fmt.Errorf("failed to retreive execution IDs: %w", err)
121+
}
122+
123+
if executionNodeIdsIn, ok := input["execution-node-ids"]; ok {
124+
executionNodeIds, err := b.parseExecutionNodeIds(executionNodeIdsIn, identities)
125+
if err != nil {
126+
return err
127+
}
128+
data.executionNodeIds = executionNodeIds
129+
} else {
130+
// in case no execution node ids provided, the command will use any valid execution node
131+
data.executionNodeIds = identities.ToSkeleton()
132+
}
133+
134+
request.ValidatorData = data
135+
136+
return nil
137+
}
138+
139+
// Handler performs the backfilling operation by fetching missing transaction
140+
// error messages for blocks within the specified height range. Uses execution nodes
141+
// from data.executionNodeIds if available, otherwise defaults to valid execution nodes.
142+
//
143+
// No errors are expected during normal operation.
144+
func (b *BackfillTxErrorMessagesCommand) Handler(ctx context.Context, request *admin.CommandRequest) (interface{}, error) {
145+
if b.txErrorMessagesCore == nil {
146+
return nil, fmt.Errorf("failed to backfill, could not get transaction error messages storage")
147+
}
148+
149+
data := request.ValidatorData.(*backfillTxErrorMessagesRequest)
150+
151+
for height := data.startHeight; height <= data.endHeight; height++ {
152+
header, err := b.state.AtHeight(height).Head()
153+
if err != nil {
154+
return nil, fmt.Errorf("failed to get block header: %w", err)
155+
}
156+
157+
blockID := header.ID()
158+
err = b.txErrorMessagesCore.HandleTransactionResultErrorMessagesByENs(ctx, blockID, data.executionNodeIds)
159+
if err != nil {
160+
return nil, fmt.Errorf("error encountered while processing transaction result error message for block: %d, %w", height, err)
161+
}
162+
}
163+
164+
return nil, nil
165+
}
166+
167+
// parseExecutionNodeIds converts a list of node IDs from input to flow.IdentitySkeletonList.
168+
// Returns an error if the IDs are invalid or empty.
169+
//
170+
// Expected errors during normal operation:
171+
// - admin.InvalidAdminReqParameterError - if execution-node-ids is empty or has an invalid format.
172+
func (b *BackfillTxErrorMessagesCommand) parseExecutionNodeIds(executionNodeIdsIn interface{}, allIdentities flow.IdentityList) (flow.IdentitySkeletonList, error) {
173+
var ids flow.IdentityList
174+
175+
switch executionNodeIds := executionNodeIdsIn.(type) {
176+
case []string:
177+
if len(executionNodeIds) == 0 {
178+
return nil, admin.NewInvalidAdminReqParameterError("execution-node-ids", "must be a non empty list of strings", executionNodeIdsIn)
179+
}
180+
requestedENIdentifiers, err := commonrpc.IdentifierList(executionNodeIds)
181+
if err != nil {
182+
return nil, admin.NewInvalidAdminReqParameterError("execution-node-ids", err.Error(), executionNodeIdsIn)
183+
}
184+
185+
for _, enId := range requestedENIdentifiers {
186+
id, exists := allIdentities.ByNodeID(enId)
187+
if !exists {
188+
return nil, admin.NewInvalidAdminReqParameterError("execution-node-ids", "could not find execution node by provided id", enId)
189+
}
190+
ids = append(ids, id)
191+
}
192+
default:
193+
return nil, admin.NewInvalidAdminReqParameterError("execution-node-ids", "must be a list of strings", executionNodeIdsIn)
194+
}
195+
196+
return ids.ToSkeleton(), nil
197+
}

0 commit comments

Comments
 (0)