-
Notifications
You must be signed in to change notification settings - Fork 51
Expand file tree
/
Copy pathcmd-x-index-gsfa.go
More file actions
454 lines (423 loc) · 13.8 KB
/
Copy pathcmd-x-index-gsfa.go
File metadata and controls
454 lines (423 loc) · 13.8 KB
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
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
package main
import (
"context"
"errors"
"fmt"
"log/slog"
"os"
"path/filepath"
"runtime"
"slices"
"sync/atomic"
"time"
"github.com/davecgh/go-spew/spew"
"github.com/dustin/go-humanize"
"github.com/ipfs/go-cid"
"github.com/rpcpool/yellowstone-faithful/accum"
"github.com/rpcpool/yellowstone-faithful/carreader"
"github.com/rpcpool/yellowstone-faithful/gsfa"
"github.com/rpcpool/yellowstone-faithful/indexes"
"github.com/rpcpool/yellowstone-faithful/indexmeta"
"github.com/rpcpool/yellowstone-faithful/ipld/ipldbindcode"
"github.com/rpcpool/yellowstone-faithful/iplddecoders"
serde_agave "github.com/rpcpool/yellowstone-faithful/parse_legacy_transaction_status_meta"
"github.com/rpcpool/yellowstone-faithful/readasonecar"
"github.com/rpcpool/yellowstone-faithful/slottools"
"github.com/urfave/cli/v2"
"golang.org/x/sync/errgroup"
"k8s.io/klog/v2"
"github.com/gagliardetto/solana-go"
)
func newCmd_Index_gsfa() *cli.Command {
var epoch uint64
var network indexes.Network
var pubkeysExclude solana.PublicKeySlice
return &cli.Command{
Name: "gsfa",
Description: "Create GSFA index from a CAR file",
ArgsUsage: "--index-dir=<index-dir> --car=<car-path>",
Before: func(c *cli.Context) error {
if network == "" {
network = indexes.NetworkMainnet
}
return nil
},
Flags: []cli.Flag{
// verify hash of transactions:
&cli.BoolFlag{
Name: "verify-hash",
Usage: "verify hash of transactions",
Value: false,
},
// w number of workers:
&cli.UintFlag{
Name: "w",
Usage: "number of workers",
Value: uint(runtime.NumCPU()) * 3,
},
&cli.Uint64Flag{
Name: "epoch",
Usage: "epoch",
Destination: &epoch,
Required: true,
},
&cli.StringFlag{
Name: "network",
Usage: "network",
Destination: (*string)(&network),
Action: func(c *cli.Context, v string) error {
if !indexes.IsValidNetwork(indexes.Network(v)) {
return fmt.Errorf("invalid network: %s", v)
}
return nil
},
},
&cli.StringFlag{
Name: "tmp-dir",
Usage: "temporary directory to use for storing intermediate files",
Value: os.TempDir(),
},
&cli.StringSliceFlag{
Name: "car",
Usage: "Path to a CAR file containing a single Solana epoch, or multiple split CAR files (in order) containing a single Solana epoch",
},
&cli.StringFlag{
Name: "index-dir",
Usage: "Destination directory for the output files",
},
&cli.BoolFlag{
Name: "require-tx-metadata",
Usage: "Require transaction metadata to be present in the CAR file",
Value: true,
},
&cli.BoolFlag{
Name: "sigverify",
Usage: "Verify signatures of transactions",
Value: true,
},
&cli.StringSliceFlag{
Name: "exclude-pubkey",
Usage: "Exclude transactions that contain these public keys in their account keys",
Action: func(c *cli.Context, v []string) error {
for _, pk := range v {
parsed, err := solana.PublicKeyFromBase58(pk)
if err != nil {
return fmt.Errorf("failed to parse public key %q: %w", pk, err)
}
pubkeysExclude = append(pubkeysExclude, parsed)
}
return nil
},
},
},
Action: func(c *cli.Context) error {
carPaths := c.StringSlice("car")
if len(carPaths) == 0 {
klog.Exit("Please provide a CAR file")
}
rd, err := readasonecar.NewFromFilepaths(carPaths...)
if err != nil {
klog.Exitf("Failed to open CAR: %s", err)
}
defer rd.Close()
indexDir := c.String("index-dir")
if indexDir == "" {
klog.Exit("Please provide an --index-dir=<dir to store the index>")
}
if ok, err := isDirectory(indexDir); err != nil {
if errors.Is(err, os.ErrNotExist) {
if err := os.MkdirAll(indexDir, 0o755); err != nil {
return fmt.Errorf("failed to create index-dir: %w", err)
} else {
klog.Infof("Created index-dir: %s", indexDir)
}
} else {
return err
}
} else if !ok {
return fmt.Errorf("index-dir is not a directory")
}
rootCID, err := rd.FindRoot()
if err != nil {
return fmt.Errorf("failed to find root CID: %w", err)
}
// Use the car file name and root CID to name the gsfa index dir:
gsfaIndexDir := filepath.Join(indexDir, formatIndexDirname_gsfa(
epoch,
rootCID,
network,
))
klog.Infof("Creating gsfa index dir at %s", gsfaIndexDir)
if fileExists, err := isFileOrDirExists(gsfaIndexDir); err != nil {
return fmt.Errorf("failed to check if gsfa index dir exists: %w", err)
} else if fileExists {
if isNonEmpty, err := isDirNonEmpty(gsfaIndexDir); err != nil {
return fmt.Errorf("failed to check if gsfa index dir is non-empty: %w", err)
} else if isNonEmpty {
return fmt.Errorf("gsfa index dir already exists and is not empty: %s", gsfaIndexDir)
}
} else {
err = os.Mkdir(gsfaIndexDir, 0o755)
if err != nil {
return fmt.Errorf("failed to create gsfa index dir: %w", err)
}
}
meta := indexmeta.Meta{}
{
if err := meta.AddUint64(indexmeta.MetadataKey_Epoch, epoch); err != nil {
return fmt.Errorf("failed to add epoch to sig_exists index metadata: %w", err)
}
if err := meta.AddCid(indexmeta.MetadataKey_RootCid, rootCID); err != nil {
return fmt.Errorf("failed to add root cid to sig_exists index metadata: %w", err)
}
if err := meta.AddString(indexmeta.MetadataKey_Network, string(network)); err != nil {
return fmt.Errorf("failed to add network to sig_exists index metadata: %w", err)
}
}
tmpDir := c.String("tmp-dir")
tmpDir, err = os.MkdirTemp(tmpDir, "gsfa_indexer_*")
if err != nil {
return fmt.Errorf("failed to create temporary directory: %w", err)
}
indexW, err := gsfa.NewGsfaWriter(
gsfaIndexDir,
meta,
epoch,
rootCID,
network,
tmpDir,
)
if err != nil {
return fmt.Errorf("error while opening gsfa index writer: %w", err)
}
numProcessedTransactions := new(atomic.Int64)
startedAt := time.Now()
verifyHash := c.Bool("verify-hash")
sigverify := c.Bool("sigverify")
ipldbindcode.DisableHashVerification = !verifyHash
epochStart, epochEnd := slottools.CalcEpochLimits(epoch)
numSlots := uint64(0)
numMaxObjects := uint64(0)
lastPrintedAt := time.Now()
numMissingMetadata := new(atomic.Int64)
numMissingMetadata.Store(0)
requireTxMetadata := c.Bool("require-tx-metadata")
if len(pubkeysExclude) > 0 {
slog.Info("Excluding transactions with the following public keys:", "pubkeys", pubkeysExclude)
}
numTransactionsWithMoreThanOnePieceForMetadata := new(atomic.Uint64)
defer func() {
n := numTransactionsWithMoreThanOnePieceForMetadata.Load()
if n > 0 {
slog.Info(
"num transactions with more than one piece for metadata",
"num", numTransactionsWithMoreThanOnePieceForMetadata.Load(),
)
}
}()
accum := accum.NewObjectAccumulator(
rd,
iplddecoders.KindBlock,
accum.IgnoreKinds(
// Ignore these kinds in the accumulator (only need Transactions and DataFrames):
iplddecoders.KindEntry,
iplddecoders.KindRewards,
),
func(parent *accum.ObjectWithMetadata, children accum.ObjectsWithMetadata) error {
defer func() {
carreader.PutBuffer(parent.ObjectData)
children.Put()
}()
numSlots++
numObjects := len(children) + 1
if numObjects > int(numMaxObjects) {
numMaxObjects = uint64(numObjects)
}
if parent == nil {
transactions, err := accum.ObjectsToTransactionsAndMetadata(
&ipldbindcode.Block{
Meta: ipldbindcode.SlotMeta{
Blocktime: 0,
},
}, children)
if err != nil {
return fmt.Errorf("error while converting objects to transactions: %w", err)
}
if len(transactions) == 0 {
return nil
}
spew.Dump(parent, transactions, len(children))
}
// decode the block:
block, err := iplddecoders.DecodeBlock(parent.ObjectData.Bytes())
if err != nil {
return fmt.Errorf("error while decoding block: %w", err)
}
defer iplddecoders.PutBlock(block)
transactions, err := accum.ObjectsToTransactionsAndMetadata(block, children)
if err != nil {
return fmt.Errorf("error while converting objects to transactions: %w", err)
}
defer accum.PutTransactionWithSlotSlice(transactions)
if sigverify {
wg := new(errgroup.Group)
for ii := range transactions {
txWithInfo := transactions[ii]
wg.Go(func() error {
if err := txWithInfo.Transaction.VerifySignatures(); err != nil {
return fmt.Errorf(
"error while verifying signatures for transaction %s: %w",
txWithInfo.Transaction.Signatures[0],
err,
)
}
{
if len(txWithInfo.MetadataPieces) > 0 {
numTransactionsWithMoreThanOnePieceForMetadata.Add(1)
}
}
return nil
})
}
if err := wg.Wait(); err != nil {
klog.Exitf("Error while verifying signatures: %s", err)
}
}
for ii := range transactions {
txWithInfo := transactions[ii]
numProcessedTransactions.Add(1)
accountKeys := txWithInfo.Transaction.Message.AccountKeys
if txWithInfo.Metadata != nil && txWithInfo.Metadata.IsProtobuf() {
meta := txWithInfo.Metadata.GetProtobuf()
accountKeys = append(accountKeys, byteSlicesToKeySlice(meta.LoadedReadonlyAddresses)...)
accountKeys = append(accountKeys, byteSlicesToKeySlice(meta.LoadedWritableAddresses)...)
}
hasMeta := txWithInfo.Metadata != nil // We include this to know whether isSuccess is valid.
if txWithInfo.Metadata == nil || !txWithInfo.Metadata.HasMeta() {
numMissingMetadata.Add(1)
if requireTxMetadata {
klog.Errorf("Transaction %s has no metadata", txWithInfo.Transaction.Signatures[0])
spew.Dump(txWithInfo.Error, txWithInfo.IsMetaParseError())
panic("Transaction has no metadata, but --require-tx-metadata=true")
}
}
isSuccess := func() bool {
// check if the transaction is a success:
if txWithInfo.Metadata == nil {
// NOTE: if there is no metadata, we have NO WAY of knowing if the transaction was successful.
return false
}
if txWithInfo.Metadata.IsProtobuf() {
meta := txWithInfo.Metadata.GetProtobuf()
if meta.Err == nil {
return true
}
}
if txWithInfo.Metadata.IsSerde() {
meta := txWithInfo.Metadata.GetSerde()
_, ok := meta.Status.(*serde_agave.Result__Ok)
if ok {
return true
}
}
return false
}()
isVote := IsVote(txWithInfo.Transaction)
// v2:
if len(pubkeysExclude) > 0 {
accountKeys = slices.DeleteFunc(
accountKeys,
func(pk solana.PublicKey) bool {
return slices.Contains(pubkeysExclude, pk)
},
)
}
err = indexW.Push(
txWithInfo.Offset,
txWithInfo.Length,
txWithInfo.Slot,
accountKeys,
hasMeta,
isSuccess,
isVote,
)
if err != nil {
klog.Exitf("Error while pushing to gsfa index: %s", err)
}
if time.Since(lastPrintedAt) > time.Second {
percentDone := float64(txWithInfo.Slot-epochStart) / float64(epochEnd-epochStart) * 100
// clear line, then print progress
msg := fmt.Sprintf(
"\rCreating gSFA index for epoch %d - %s | %s | %.2f%% | slot %s | tx %s",
epoch,
time.Now().Format("2006-01-02 15:04:05"),
time.Since(startedAt).Truncate(time.Second),
percentDone,
humanize.Comma(int64(txWithInfo.Slot)),
humanize.Comma(int64(numProcessedTransactions.Load())),
)
var eta time.Duration
timePast := time.Since(startedAt).Truncate(time.Second).Round(time.Second)
if percentDone > 0 && timePast > 0 {
// it took timePast to get percentDone done
remainingPercent := 100 - percentDone
msForOnePercent := float64(timePast.Milliseconds()) / percentDone
eta = time.Millisecond * time.Duration(msForOnePercent*remainingPercent)
eta = eta.Truncate(time.Second).Round(time.Second)
}
if eta > 0 {
msg += fmt.Sprintf(" | ETA %s", eta.Truncate(time.Second))
}
fmt.Print(msg)
lastPrintedAt = time.Now()
}
}
return nil
},
)
if err := accum.Run(context.Background()); err != nil {
return fmt.Errorf("error while accumulating objects: %w", err)
}
{
klog.Infof("Indexed %s transactions", humanize.Comma(int64(numProcessedTransactions.Load())))
klog.Info("Finalizing index -- this may take a while, DO NOT EXIT")
klog.Info("Closing index")
if err := indexW.Close(); err != nil {
klog.Fatalf("Error while closing: %s", err)
}
klog.Infof("Success: gSFA index created at %s with %s transactions", gsfaIndexDir, humanize.Comma(int64(numProcessedTransactions.Load())))
klog.Infof("Finished in %s", time.Since(startedAt))
}
return nil
},
}
}
func formatIndexDirname_gsfa(epoch uint64, rootCid cid.Cid, network indexes.Network) string {
return fmt.Sprintf(
"epoch-%d-%s-%s-%s",
epoch,
rootCid.String(),
network,
"gsfa.indexdir",
)
}
func isFileOrDirExists(path string) (bool, error) {
info, err := os.Stat(path)
if err != nil {
if os.IsNotExist(err) {
return false, nil // File or directory does not exist
}
return false, err // Other error occurred
}
return info.IsDir() || info.Mode().IsRegular(), nil // Return true if it's a directory or a regular file
}
func isDirNonEmpty(path string) (bool, error) {
entries, err := os.ReadDir(path)
if err != nil {
if os.IsNotExist(err) {
return false, nil // Directory does not exist
}
return false, err // Other error occurred
}
return len(entries) > 0, nil // Return true if there are any entries in the directory
}