raw tx binary
This commit is contained in:
302
cmd/collect_yellowstone_rawtx_binary/main.go
Normal file
302
cmd/collect_yellowstone_rawtx_binary/main.go
Normal file
@@ -0,0 +1,302 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"os/signal"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/gagliardetto/solana-go"
|
||||
pump_parser "github.com/thloyi/pump-parser"
|
||||
pb "go.onsig.ai/onsig/yellowstone-proto"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/credentials"
|
||||
"google.golang.org/grpc/credentials/insecure"
|
||||
"google.golang.org/grpc/keepalive"
|
||||
"google.golang.org/grpc/metadata"
|
||||
)
|
||||
|
||||
type collector struct {
|
||||
endpoint string
|
||||
xToken string
|
||||
plaintext bool
|
||||
|
||||
blocks map[uint64][]pump_parser.RawTx
|
||||
seen map[string]struct{}
|
||||
|
||||
totalUpdates uint64
|
||||
txUpdates uint64
|
||||
savedNonVote uint64
|
||||
duplicates uint64
|
||||
voteFiltered uint64
|
||||
convertErrs uint64
|
||||
reconnects uint64
|
||||
|
||||
firstSlot uint64
|
||||
lastSlot uint64
|
||||
}
|
||||
|
||||
func main() {
|
||||
var (
|
||||
endpoint = flag.String("endpoint", "ams.rpc.orbitflare.com:10000", "Yellowstone gRPC endpoint")
|
||||
xToken = flag.String("x-token", os.Getenv("YELLOWSTONE_X_TOKEN"), "Yellowstone x-token; defaults to YELLOWSTONE_X_TOKEN")
|
||||
duration = flag.Duration("duration", 5*time.Minute, "collection duration")
|
||||
output = flag.String("output", "", "output .prbs file path")
|
||||
plaintext = flag.Bool("plaintext", true, "use plaintext gRPC instead of TLS")
|
||||
)
|
||||
flag.Parse()
|
||||
|
||||
if *xToken == "" {
|
||||
exitf("missing -x-token or YELLOWSTONE_X_TOKEN")
|
||||
}
|
||||
if *duration <= 0 {
|
||||
exitf("-duration must be positive")
|
||||
}
|
||||
if *output == "" {
|
||||
*output = filepath.Join("testdata", "rawtx-binary", fmt.Sprintf("rawtx-yellowstone-%s.prbs", time.Now().Format("20060102-150405")))
|
||||
}
|
||||
|
||||
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
|
||||
defer stop()
|
||||
ctx, cancel := context.WithTimeout(ctx, *duration)
|
||||
defer cancel()
|
||||
|
||||
c := &collector{
|
||||
endpoint: *endpoint,
|
||||
xToken: *xToken,
|
||||
plaintext: *plaintext,
|
||||
blocks: make(map[uint64][]pump_parser.RawTx),
|
||||
seen: make(map[string]struct{}),
|
||||
}
|
||||
|
||||
started := time.Now()
|
||||
done := make(chan error, 1)
|
||||
go func() {
|
||||
done <- c.run(ctx)
|
||||
}()
|
||||
|
||||
ticker := time.NewTicker(30 * time.Second)
|
||||
defer ticker.Stop()
|
||||
|
||||
loop:
|
||||
for {
|
||||
select {
|
||||
case err := <-done:
|
||||
if err != nil {
|
||||
exitf("%v", err)
|
||||
}
|
||||
break loop
|
||||
case <-ctx.Done():
|
||||
if err := <-done; err != nil {
|
||||
exitf("%v", err)
|
||||
}
|
||||
break loop
|
||||
case <-ticker.C:
|
||||
c.printProgress(started)
|
||||
}
|
||||
}
|
||||
|
||||
encoded, decodedCount, err := encodeAndVerify(c.blocks)
|
||||
if err != nil {
|
||||
exitf("raw tx binary encode/decode: %v", err)
|
||||
}
|
||||
if decodedCount != int(c.savedNonVote) {
|
||||
exitf("decoded tx count mismatch: got=%d want=%d", decodedCount, c.savedNonVote)
|
||||
}
|
||||
if err := os.MkdirAll(filepath.Dir(*output), 0o755); err != nil {
|
||||
exitf("mkdir output dir: %v", err)
|
||||
}
|
||||
if err := os.WriteFile(*output, encoded, 0o644); err != nil {
|
||||
exitf("write output: %v", err)
|
||||
}
|
||||
|
||||
fmt.Printf("output=%s\n", *output)
|
||||
fmt.Printf("duration=%s elapsed=%s\n", *duration, time.Since(started).Truncate(time.Second))
|
||||
fmt.Printf("updates=%d tx_updates=%d converted_nonvote=%d duplicate=%d vote_filtered=%d convert_err=%d reconnects=%d\n",
|
||||
c.totalUpdates, c.txUpdates, c.savedNonVote, c.duplicates, c.voteFiltered, c.convertErrs, c.reconnects)
|
||||
fmt.Printf("slots=%d first_slot=%d last_slot=%d decoded=%d\n", len(c.blocks), c.firstSlot, c.lastSlot, decodedCount)
|
||||
fmt.Printf("bytes=%d bytes_per_tx=%.2f\n", len(encoded), float64(len(encoded))/float64(max(int(c.savedNonVote), 1)))
|
||||
}
|
||||
|
||||
func (c *collector) run(ctx context.Context) error {
|
||||
for ctx.Err() == nil {
|
||||
if err := c.recvOnce(ctx); err != nil && ctx.Err() == nil {
|
||||
c.reconnects++
|
||||
fmt.Fprintf(os.Stderr, "stream_err reconnect=%d err=%v\n", c.reconnects, err)
|
||||
time.Sleep(time.Second)
|
||||
continue
|
||||
}
|
||||
if ctx.Err() == nil {
|
||||
c.reconnects++
|
||||
time.Sleep(time.Second)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *collector) recvOnce(ctx context.Context) error {
|
||||
conn, err := grpc.NewClient(
|
||||
c.endpoint,
|
||||
c.transportOption(),
|
||||
grpc.WithKeepaliveParams(keepalive.ClientParameters{
|
||||
Time: 10 * time.Second,
|
||||
Timeout: time.Second,
|
||||
PermitWithoutStream: true,
|
||||
}),
|
||||
grpc.WithDefaultCallOptions(grpc.MaxCallRecvMsgSize(64*1024*1024)),
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
ctx = metadata.NewOutgoingContext(ctx, metadata.New(map[string]string{"x-token": c.xToken}))
|
||||
stream, err := pb.NewGeyserClient(conn).Subscribe(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
vote := false
|
||||
subscription := &pb.SubscribeRequest{
|
||||
Transactions: map[string]*pb.SubscribeRequestFilterTransactions{
|
||||
"nonvote": {Vote: &vote},
|
||||
},
|
||||
}
|
||||
if err := stream.Send(subscription); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for {
|
||||
resp, err := stream.Recv()
|
||||
if err != nil {
|
||||
if err == io.EOF || ctx.Err() != nil {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
c.totalUpdates++
|
||||
txn := resp.GetTransaction()
|
||||
if txn == nil {
|
||||
continue
|
||||
}
|
||||
c.txUpdates++
|
||||
|
||||
created := time.Now().Unix()
|
||||
if resp.GetCreatedAt() != nil {
|
||||
created = resp.GetCreatedAt().Seconds
|
||||
}
|
||||
rawTx, err := pump_parser.ConvertYellowstoneGrpcTransactionToSolanaTransaction(txn, created)
|
||||
if err != nil {
|
||||
c.convertErrs++
|
||||
continue
|
||||
}
|
||||
txHash := rawTx.TxHash()
|
||||
if txHash != "" {
|
||||
if _, exists := c.seen[txHash]; exists {
|
||||
c.duplicates++
|
||||
continue
|
||||
}
|
||||
c.seen[txHash] = struct{}{}
|
||||
}
|
||||
if c.firstSlot == 0 || rawTx.Slot < c.firstSlot {
|
||||
c.firstSlot = rawTx.Slot
|
||||
}
|
||||
if rawTx.Slot > c.lastSlot {
|
||||
c.lastSlot = rawTx.Slot
|
||||
}
|
||||
if isVoteTx(rawTx) {
|
||||
c.voteFiltered++
|
||||
continue
|
||||
}
|
||||
c.blocks[rawTx.Slot] = append(c.blocks[rawTx.Slot], *rawTx)
|
||||
c.savedNonVote++
|
||||
}
|
||||
}
|
||||
|
||||
func (c *collector) transportOption() grpc.DialOption {
|
||||
if c.plaintext {
|
||||
return grpc.WithTransportCredentials(insecure.NewCredentials())
|
||||
}
|
||||
return grpc.WithTransportCredentials(credentials.NewClientTLSFromCert(nil, ""))
|
||||
}
|
||||
|
||||
func (c *collector) printProgress(started time.Time) {
|
||||
fmt.Fprintf(os.Stderr,
|
||||
"progress elapsed=%s updates=%d tx_updates=%d saved_nonvote=%d slots=%d duplicate=%d vote_filtered=%d convert_err=%d reconnects=%d\n",
|
||||
time.Since(started).Truncate(time.Second),
|
||||
c.totalUpdates,
|
||||
c.txUpdates,
|
||||
c.savedNonVote,
|
||||
len(c.blocks),
|
||||
c.duplicates,
|
||||
c.voteFiltered,
|
||||
c.convertErrs,
|
||||
c.reconnects,
|
||||
)
|
||||
}
|
||||
|
||||
func encodeAndVerify(blocks map[uint64][]pump_parser.RawTx) ([]byte, int, error) {
|
||||
slots := make([]uint64, 0, len(blocks))
|
||||
for slot := range blocks {
|
||||
slots = append(slots, slot)
|
||||
}
|
||||
sort.Slice(slots, func(i, j int) bool { return slots[i] < slots[j] })
|
||||
|
||||
ordered := make([][]pump_parser.RawTx, 0, len(slots))
|
||||
for _, slot := range slots {
|
||||
txs := blocks[slot]
|
||||
blockTime := int64(0)
|
||||
if len(txs) > 0 {
|
||||
blockTime = txs[0].BlockTime
|
||||
}
|
||||
for i := range txs {
|
||||
txs[i].BlockTime = blockTime
|
||||
}
|
||||
ordered = append(ordered, txs)
|
||||
}
|
||||
|
||||
encoded, err := pump_parser.EncodeRawTxBlocksBinary(ordered)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
decoded, err := pump_parser.DecodeRawTxBlocksBinary(encoded)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
decodedCount := 0
|
||||
for _, block := range decoded {
|
||||
decodedCount += len(block)
|
||||
}
|
||||
return encoded, decodedCount, nil
|
||||
}
|
||||
|
||||
func isVoteTx(tx *pump_parser.RawTx) bool {
|
||||
if tx == nil {
|
||||
return false
|
||||
}
|
||||
accountList := tx.GetAccountList()
|
||||
for _, instr := range tx.Transaction.Message.Instructions {
|
||||
if instr.ProgramIDIndex >= 0 && instr.ProgramIDIndex < len(accountList) && accountList[instr.ProgramIDIndex] == solana.VoteProgramID {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func max(a, b int) int {
|
||||
if a > b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
func exitf(format string, args ...any) {
|
||||
fmt.Fprintf(os.Stderr, format+"\n", args...)
|
||||
os.Exit(1)
|
||||
}
|
||||
Reference in New Issue
Block a user