From a7fe87938b45f2d36ad4d023b09c273c2e60484c Mon Sep 17 00:00:00 2001 From: Alok Date: Wed, 22 Oct 2025 18:28:15 +0530 Subject: [PATCH 01/13] feat: add RPC endpoints for bid options --- tools/preconf-rpc/sender/sender.go | 25 +++-- tools/preconf-rpc/sender/sender_test.go | 2 - tools/preconf-rpc/service/service.go | 136 +++++++++++++++++------- x/opt-in-bidder/bidder.go | 13 +++ 4 files changed, 129 insertions(+), 47 deletions(-) diff --git a/tools/preconf-rpc/sender/sender.go b/tools/preconf-rpc/sender/sender.go index a08d46b69..47df5572c 100644 --- a/tools/preconf-rpc/sender/sender.go +++ b/tools/preconf-rpc/sender/sender.go @@ -56,6 +56,8 @@ var ( ErrTimeoutExceeded = errors.New("timeout exceeded while waiting for transaction to be processed") ) +type PositionConstraintKey struct{} + type Transaction struct { *types.Transaction Sender common.Address @@ -151,7 +153,6 @@ func NewTxSender( transferer Transferer, notifier Notifier, settlementChainId *big.Int, - fastTrack func(cmts []*bidderapiv1.Commitment, optedInSlot bool) bool, logger *slog.Logger, ) (*TxSender, error) { txnAttemptHistory, err := lru.New[common.Hash, *txnAttempt](1000) @@ -160,10 +161,6 @@ func NewTxSender( return nil, fmt.Errorf("failed to create transaction attempt history cache: %w", err) } - if fastTrack == nil { - fastTrack = noOpFastTrack - } - return &TxSender{ store: st, bidder: bidder, @@ -178,7 +175,7 @@ func NewTxSender( inflightAccount: make(map[common.Address]struct{}), txnAttemptHistory: txnAttemptHistory, notifier: notifier, - fastTrack: fastTrack, + fastTrack: noOpFastTrack, bidTimeout: bidTimeout, }, nil } @@ -205,6 +202,17 @@ func validateTransaction(tx *Transaction) error { return nil } +func getConstraintFromCtx(ctx context.Context) *bidderapiv1.PositionConstraint { + optVal := ctx.Value(PositionConstraintKey{}) + if optVal != nil { + constraint, ok := optVal.(*bidderapiv1.PositionConstraint) + if ok { + return constraint + } + } + return nil +} + func (t *TxSender) hasLowerNonce(ctx context.Context, tx *Transaction) bool { currentNonce := t.store.GetCurrentNonce(ctx, tx.Sender) return tx.Nonce() < currentNonce @@ -218,6 +226,10 @@ func (t *TxSender) triggerSender() { } } +func (t *TxSender) SetFastTrackFunc(fastTrack func(cmts []*bidderapiv1.Commitment, optedInSlot bool) bool) { + t.fastTrack = fastTrack +} + func (t *TxSender) Enqueue(ctx context.Context, tx *Transaction) error { if err := validateTransaction(tx); err != nil { t.logger.Error("Invalid transaction", "error", err, "transaction", tx.Raw) @@ -695,6 +707,7 @@ func (t *TxSender) sendBid( BlockNumber: uint64(bidBlockNo), RevertingTxHashes: []string{txn.Hash().Hex()}, DecayDuration: t.getBidTimeout() * 2, + Constraint: getConstraintFromCtx(ctx), }, ) if err != nil { diff --git a/tools/preconf-rpc/sender/sender_test.go b/tools/preconf-rpc/sender/sender_test.go index 57f92f11f..9f616923f 100644 --- a/tools/preconf-rpc/sender/sender_test.go +++ b/tools/preconf-rpc/sender/sender_test.go @@ -294,7 +294,6 @@ func TestSender(t *testing.T) { &mockTransferer{}, notifier, big.NewInt(1), // Settlement chain ID - nil, util.NewTestLogger(os.Stdout), ) if err != nil { @@ -573,7 +572,6 @@ func TestCancelTransaction(t *testing.T) { &mockTransferer{}, &mockNotifier{}, big.NewInt(1), // Settlement chain ID - nil, util.NewTestLogger(os.Stdout), ) if err != nil { diff --git a/tools/preconf-rpc/service/service.go b/tools/preconf-rpc/service/service.go index fe6b8e205..f7bb30ae7 100644 --- a/tools/preconf-rpc/service/service.go +++ b/tools/preconf-rpc/service/service.go @@ -12,7 +12,9 @@ import ( "math/big" "net/http" "slices" + "strconv" "strings" + "sync" "time" "github.com/ethereum/go-ethereum/common" @@ -242,25 +244,6 @@ func New(config *Config) (*Service, error) { blockTrackerDone := blockTracker.Start(ctx) healthChecker.Register(health.CloseChannelHealthCheck("BlockTracker", blockTrackerDone)) - allSlots := false - providers := []common.Address{} - fastTrackFn := func(cmts []*bidderapiv1.Commitment, optedInSlot bool) bool { - if !allSlots && !optedInSlot { - return false - } - if len(providers) == 0 { - return false - } - for _, p := range providers { - if !slices.ContainsFunc(cmts, func(cmt *bidderapiv1.Commitment) bool { - return common.HexToAddress(cmt.ProviderAddress).Cmp(p) == 0 - }) { - return false - } - } - return true - } - sndr, err := sender.NewTxSender( rpcstore, bidderClient, @@ -269,7 +252,6 @@ func New(config *Config) (*Service, error) { transferer, notifier, settlementChainID, - fastTrackFn, config.Logger.With("module", "txsender"), ) if err != nil { @@ -279,7 +261,7 @@ func New(config *Config) (*Service, error) { senderDone := sndr.Start(ctx) healthChecker.Register(health.CloseChannelHealthCheck("TxSender", senderDone)) - handlers := handlers.NewRPCMethodHandler( + rpcHandlers := handlers.NewRPCMethodHandler( config.Logger.With("module", "handlers"), bidpricer, bidderClient, @@ -291,7 +273,7 @@ func New(config *Config) (*Service, error) { l1ChainID, ) - handlers.RegisterMethods(rpcServer) + rpcHandlers.RegisterMethods(rpcServer) mux := http.NewServeMux() mux.HandleFunc("GET /health", func(w http.ResponseWriter, r *http.Request) { @@ -303,9 +285,71 @@ func New(config *Config) (*Service, error) { _, _ = w.Write([]byte("OK")) }) mux.Handle("/", rpcServer) + mux.HandleFunc("/{option...}", func(w http.ResponseWriter, r *http.Request) { + options := r.PathValue("option") + splits := strings.Split(options, "/") + if len(splits) != 3 { + http.Error(w, "invalid position constraint format", http.StatusBadRequest) + return + } + constraint := new(bidderapiv1.PositionConstraint) + switch splits[0] { + case "top": + constraint.Anchor = bidderapiv1.PositionConstraint_ANCHOR_TOP + case "bottom": + constraint.Anchor = bidderapiv1.PositionConstraint_ANCHOR_BOTTOM + default: + http.Error(w, "invalid position constraint", http.StatusBadRequest) + return + } + + switch splits[1] { + case "absolute": + constraint.Basis = bidderapiv1.PositionConstraint_BASIS_ABSOLUTE + case "percentile": + constraint.Basis = bidderapiv1.PositionConstraint_BASIS_PERCENTILE + case "gas_percentile": + constraint.Basis = bidderapiv1.PositionConstraint_BASIS_GAS_PERCENTILE + default: + http.Error(w, "invalid position constraint type", http.StatusBadRequest) + return + } + + value, err := strconv.Atoi(splits[2]) + if err != nil { + http.Error(w, "invalid position constraint value", http.StatusBadRequest) + return + } + constraint.Value = int32(value) + + r = r.WithContext(context.WithValue(r.Context(), sender.PositionConstraintKey{}, constraint)) + rpcServer.ServeHTTP(w, r) + }) + + srv := http.Server{ + Addr: fmt.Sprintf(":%d", config.HTTPPort), + Handler: mux, + } + + go func() { + if err := srv.ListenAndServe(); err != nil { + config.Logger.Error("failed to start HTTP server", "error", err) + } + }() + + s.closers = append(s.closers, &srv) + + return s, nil +} + +type RPCStore interface { + AddSubsidy(ctx context.Context, account common.Address, amount *big.Int) error +} + +func registerAdminAPIs(mux *http.ServeMux, token string, sndr *sender.TxSender, rpcstore RPCStore) { checkAuthorization := func(r *http.Request) error { - if config.Token == "" { + if token == "" { return errors.New("server not configured with authorization token") } @@ -320,13 +364,39 @@ func New(config *Config) (*Service, error) { return errors.New("invalid authorization header format") } - if headerToken != config.Token { + if headerToken != token { return errors.New("unauthorized: invalid token") } return nil } + var fastTrackMutex sync.RWMutex + allSlots := false + providers := []common.Address{} + + fastTrackFn := func(cmts []*bidderapiv1.Commitment, optedInSlot bool) bool { + fastTrackMutex.RLock() + defer fastTrackMutex.RUnlock() + + if !allSlots && !optedInSlot { + return false + } + if len(providers) == 0 { + return false + } + for _, p := range providers { + if !slices.ContainsFunc(cmts, func(cmt *bidderapiv1.Commitment) bool { + return common.HexToAddress(cmt.ProviderAddress).Cmp(p) == 0 + }) { + return false + } + } + return true + } + + sndr.SetFastTrackFunc(fastTrackFn) + mux.HandleFunc("POST /fast-track/enable", func(w http.ResponseWriter, r *http.Request) { if err := checkAuthorization(r); err != nil { http.Error(w, err.Error(), http.StatusUnauthorized) @@ -350,6 +420,9 @@ func New(config *Config) (*Service, error) { return } + fastTrackMutex.Lock() + defer fastTrackMutex.Unlock() + allSlots = req.AllSlots providers = make([]common.Address, 0, len(req.Providers)) for _, p := range req.Providers { @@ -449,21 +522,6 @@ func New(config *Config) (*Service, error) { w.WriteHeader(http.StatusOK) _, _ = w.Write([]byte("OK")) }) - - srv := http.Server{ - Addr: fmt.Sprintf(":%d", config.HTTPPort), - Handler: mux, - } - - go func() { - if err := srv.ListenAndServe(); err != nil { - config.Logger.Error("failed to start HTTP server", "error", err) - } - }() - - s.closers = append(s.closers, &srv) - - return s, nil } func (s *Service) Close() error { diff --git a/x/opt-in-bidder/bidder.go b/x/opt-in-bidder/bidder.go index 992020dcf..cac41c4b4 100644 --- a/x/opt-in-bidder/bidder.go +++ b/x/opt-in-bidder/bidder.go @@ -189,6 +189,7 @@ type BidOpts struct { BlockNumber uint64 RevertingTxHashes []string DecayDuration time.Duration + Constraint *bidderapiv1.PositionConstraint } var defaultBidOpts = &BidOpts{ @@ -285,6 +286,18 @@ func (b *BidderClient) Bid( bidReq.DecayEndTimestamp = time.UnixMilli(bidReq.DecayStartTimestamp).Add(12 * time.Second).UnixMilli() } + if opts.Constraint != nil { + bidReq.BidOptions = &bidderapiv1.BidOptions{ + Options: []*bidderapiv1.BidOption{ + { + Opt: &bidderapiv1.BidOption_PositionConstraint{ + PositionConstraint: opts.Constraint, + }, + }, + }, + } + } + pc, err := b.bidderClient.SendBid(ctx, bidReq) if err != nil { b.logger.Error("failed to send bid", "error", err) From 5a4eae2f83157f9a03e15d23157eb01ddcf3aa0d Mon Sep 17 00:00:00 2001 From: Alok Date: Wed, 22 Oct 2025 18:30:11 +0530 Subject: [PATCH 02/13] feat: add RPC endpoints for bid options --- tools/preconf-rpc/service/service.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tools/preconf-rpc/service/service.go b/tools/preconf-rpc/service/service.go index f7bb30ae7..b50885201 100644 --- a/tools/preconf-rpc/service/service.go +++ b/tools/preconf-rpc/service/service.go @@ -327,6 +327,8 @@ func New(config *Config) (*Service, error) { rpcServer.ServeHTTP(w, r) }) + registerAdminAPIs(mux, config.Token, sndr, rpcstore) + srv := http.Server{ Addr: fmt.Sprintf(":%d", config.HTTPPort), Handler: mux, From f232f26857fc2ad8e9e79d6e854692d4c6a9bd16 Mon Sep 17 00:00:00 2001 From: Alok Nerurkar Date: Sat, 25 Oct 2025 02:20:31 +0530 Subject: [PATCH 03/13] feat: add simulation --- tools/preconf-rpc/sender/sender.go | 90 +++++++----- tools/preconf-rpc/sim/simulator.go | 225 +++++++++++++++++++++++++++++ 2 files changed, 281 insertions(+), 34 deletions(-) create mode 100644 tools/preconf-rpc/sim/simulator.go diff --git a/tools/preconf-rpc/sender/sender.go b/tools/preconf-rpc/sender/sender.go index 47df5572c..3596f1437 100644 --- a/tools/preconf-rpc/sender/sender.go +++ b/tools/preconf-rpc/sender/sender.go @@ -43,17 +43,19 @@ const ( confidenceSecondAttempt = 95 // confidence level for the second attempt confidenceSubsequentAttempts = 99 // confidence level for subsequent attempts transactionTimeout = 10 * time.Minute // timeout for transaction processing + maxAttemptsPerBlock = 10 // maximum attempts per block ) var ( - ErrInvalidTransaction = errors.New("invalid transaction") - ErrUnsupportedTxType = errors.New("unsupported transaction type") - ErrEmptyRawTransaction = errors.New("empty raw transaction") - ErrEmptyTransactionTo = errors.New("empty transaction 'to' address") - ErrNegativeTransactionValue = errors.New("negative transaction value") - ErrZeroGasLimit = errors.New("zero gas limit") - ErrTransactionCancelled = errors.New("transaction cancelled by user") - ErrTimeoutExceeded = errors.New("timeout exceeded while waiting for transaction to be processed") + ErrInvalidTransaction = errors.New("invalid transaction") + ErrUnsupportedTxType = errors.New("unsupported transaction type") + ErrEmptyRawTransaction = errors.New("empty raw transaction") + ErrEmptyTransactionTo = errors.New("empty transaction 'to' address") + ErrNegativeTransactionValue = errors.New("negative transaction value") + ErrZeroGasLimit = errors.New("zero gas limit") + ErrTransactionCancelled = errors.New("transaction cancelled by user") + ErrTimeoutExceeded = errors.New("timeout exceeded while waiting for transaction to be processed") + ErrMaxAttemptsPerBlockExceeded = errors.New("maximum attempts exceeded for transaction in the current block") ) type PositionConstraintKey struct{} @@ -103,6 +105,10 @@ type Transferer interface { Transfer(ctx context.Context, to common.Address, chainID *big.Int, amount *big.Int) error } +type Simulator interface { + Simulate(ctx context.Context, txRaw string) ([][]byte, error) +} + type blockAttempt struct { blockNumber uint64 attempts int @@ -136,6 +142,7 @@ type TxSender struct { processMu sync.RWMutex txnAttemptHistory *lru.Cache[common.Hash, *txnAttempt] notifier Notifier + simulator Simulator fastTrack func(cmts []*bidderapiv1.Commitment, optedInSlot bool) bool bidTimeout time.Duration timeoutMtx sync.RWMutex @@ -441,6 +448,11 @@ func (t *TxSender) processTransaction(ctx context.Context, txn *Transaction, can result bidResult err error ) + logger := t.logger.With( + "transactionHash", txn.Hash().Hex(), + "sender", txn.Sender.Hex(), + "type", txn.Type, + ) BID_LOOP: for { select { @@ -452,11 +464,13 @@ BID_LOOP: } preConfirmed := false + maxAttemptsPerBlockExceeded := false + result, err = t.sendBid(ctx, txn) switch { case err != nil: if retryErr, ok := err.(*errRetry); ok { - t.logger.Warn( + logger.Warn( "Retrying bid due to error", "error", retryErr.err, "retryAfter", retryErr.retryAfter, @@ -471,17 +485,20 @@ BID_LOOP: } continue } - return err + // If we exceeded max attempts per block, we retry for the next block but + // also check for inclusion in case the transaction got included + if !errors.Is(err, ErrMaxAttemptsPerBlockExceeded) { + return err + } else { + maxAttemptsPerBlockExceeded = true + } case t.fastTrack(result.commitments, result.optedInSlot): // If the commitments indicate that the transaction can be fast-tracked, // we consider it pre-confirmed and skip further checks txn.Status = TxStatusPreConfirmed txn.BlockNumber = int64(result.blockNumber) - t.logger.Info( + logger.Info( "Transaction fast-tracked based on commitments", - "transactionHash", txn.Hash().Hex(), - "sender", txn.Sender.Hex(), - "type", txn.Type, "blockNumber", result.blockNumber, "bidAmount", result.bidAmount.String(), ) @@ -496,11 +513,8 @@ BID_LOOP: // user that the txn was successfully sent and will be processed txn.Status = TxStatusPreConfirmed txn.BlockNumber = int64(result.blockNumber) - t.logger.Info( + logger.Info( "Transaction pre-confirmed", - "transactionHash", txn.Hash().Hex(), - "sender", txn.Sender.Hex(), - "type", txn.Type, "blockNumber", result.blockNumber, "bidAmount", result.bidAmount.String(), ) @@ -512,20 +526,17 @@ BID_LOOP: default: } - if !preConfirmed && result.noOfProviders > len(result.commitments) { - t.logger.Warn( + if !preConfirmed && result.noOfProviders > len(result.commitments) && !maxAttemptsPerBlockExceeded { + logger.Warn( "Not all builders committed to the bid", - "transactionHash", txn.Hash().Hex(), "noOfProviders", result.noOfProviders, "noOfCommitments", len(result.commitments), - "sender", txn.Sender.Hex(), - "type", txn.Type, "blockNumber", result.blockNumber, "bidAmount", result.bidAmount.String(), ) - if (result.timeUntillNextBlock - time.Second) > time.Since(result.startTime) { + if (result.timeUntillNextBlock - 2*time.Second) > time.Since(result.startTime) { // If not all builders committed, we will retry the bid process - // immediately if we have atleast 1 second left before the next block + // immediately if we have atleast 2 seconds left before the next block continue } } @@ -534,18 +545,15 @@ BID_LOOP: // we will retry the bid process till user cancels the operation included, err := t.blockTracker.CheckTxnInclusion(ctx, txn.Hash(), result.blockNumber) if err != nil { - t.logger.Error("Failed to check transaction inclusion", "error", err) + logger.Error("Failed to check transaction inclusion", "error", err) return fmt.Errorf("failed to check transaction inclusion: %w", err) } if included { if !preConfirmed { txn.Status = TxStatusConfirmed txn.BlockNumber = int64(result.blockNumber) - t.logger.Info( + logger.Info( "Transaction confirmed", - "transactionHash", txn.Hash().Hex(), - "sender", txn.Sender.Hex(), - "type", txn.Type, "blockNumber", result.blockNumber, "bidAmount", result.bidAmount.String(), ) @@ -565,19 +573,19 @@ BID_LOOP: switch txn.Type { case TxTypeRegular: if err := t.store.DeductBalance(ctx, txn.Sender, result.bidAmount); err != nil { - t.logger.Error("Failed to deduct balance for sender", "sender", txn.Sender.Hex(), "error", err) + logger.Error("Failed to deduct balance for sender", "sender", txn.Sender.Hex(), "error", err) return fmt.Errorf("failed to deduct balance for sender: %w", err) } case TxTypeDeposit: balanceToAdd := new(big.Int).Sub(txn.Value(), result.bidAmount) if err := t.store.AddBalance(ctx, txn.Sender, balanceToAdd); err != nil { - t.logger.Error("Failed to add balance for sender", "sender", txn.Sender.Hex(), "error", err) + logger.Error("Failed to add balance for sender", "sender", txn.Sender.Hex(), "error", err) return fmt.Errorf("failed to add balance for sender: %w", err) } case TxTypeInstantBridge: amountToBridge := new(big.Int).Sub(txn.Value(), new(big.Int).Mul(result.bidAmount, big.NewInt(2))) if err := t.transferer.Transfer(ctx, txn.Sender, t.settlementChainId, amountToBridge); err != nil { - t.logger.Error("Failed to transfer funds for instant bridge", "sender", txn.Sender.Hex(), "error", err) + logger.Error("Failed to transfer funds for instant bridge", "sender", txn.Sender.Hex(), "error", err) return fmt.Errorf("failed to transfer funds for instant bridge: %w", err) } } @@ -602,12 +610,15 @@ type bidResult struct { optedInSlot bool bidAmount *big.Int commitments []*bidderapiv1.Commitment + logs [][]byte } func (t *TxSender) sendBid( ctx context.Context, txn *Transaction, ) (bidResult, error) { + start := time.Now() + timeToOptIn, err := t.bidder.Estimate() if err != nil { t.logger.Warn("Failed to estimate time to opt-in", "error", err) @@ -617,7 +628,12 @@ func (t *TxSender) sendBid( timeToOptIn = blockTime * 32 } - start := time.Now() + logs, err := t.simulator.Simulate(ctx, txn.Raw) + if err != nil { + t.logger.Error("Failed to simulate transaction", "error", err) + return bidResult{}, fmt.Errorf("failed to simulate transaction: %w", err) + } + bidBlockNo, timeUntilNextBlock, err := t.blockTracker.NextBlockNumber() if err != nil { t.logger.Error("Failed to get next block number", "error", err) @@ -650,6 +666,9 @@ func (t *TxSender) sendBid( t.logger.Warn("Timeout exceeded while trying to process transaction", "txnHash", txn.Hash().Hex()) return bidResult{}, ErrTimeoutExceeded } + if errors.Is(err, ErrMaxAttemptsPerBlockExceeded) { + return bidResult{}, err + } return bidResult{}, &errRetry{ err: fmt.Errorf("failed to calculate price: %w", err), retryAfter: time.Second, @@ -720,6 +739,7 @@ func (t *TxSender) sendBid( bidAmount: cost, startTime: start, timeUntillNextBlock: timeUntilNextBlock, + logs: logs, } BID_LOOP: for { @@ -794,6 +814,8 @@ func (t *TxSender) calculatePriceForNextBlock( confidence = confidenceSecondAttempt case attempts.attempts[i].attempts > 2: confidence = confidenceSubsequentAttempts + case attempts.attempts[i].attempts > maxAttemptsPerBlock: + return nil, fmt.Errorf("%w: block %d", ErrMaxAttemptsPerBlockExceeded, bidBlockNo) } break // No need to check further attempts for the same block } diff --git a/tools/preconf-rpc/sim/simulator.go b/tools/preconf-rpc/sim/simulator.go new file mode 100644 index 000000000..c0fa0f1c3 --- /dev/null +++ b/tools/preconf-rpc/sim/simulator.go @@ -0,0 +1,225 @@ +package sim + +import ( + "context" + "encoding/hex" + "encoding/json" + "errors" + "fmt" + "io" + "math/big" + "net" + "net/http" + "strings" + "time" +) + +type SimLog struct { + Address string `json:"address"` + Topics []string `json:"topics"` + Data string `json:"data"` + BlockHash *string `json:"blockHash"` + BlockNumber string `json:"blockNumber"` + BlockTimestamp string `json:"blockTimestamp"` + LogIndex string `json:"logIndex"` + Removed bool `json:"removed"` + TransactionHash string `json:"transactionHash"` + TransactionIndex string `json:"transactionIndex"` +} + +type SimCall struct { + Status string `json:"status"` + GasUsed string `json:"gasUsed"` + ReturnData string `json:"returnData"` + Logs []SimLog `json:"logs"` + Calls []SimCall `json:"calls,omitempty"` +} + +type SimBlock struct { + Number string `json:"number"` + Hash string `json:"hash"` + BaseFeePerGas string `json:"baseFeePerGas"` + LogsBloom string `json:"logsBloom"` + Transactions []string `json:"transactions"` + Calls []SimCall `json:"calls"` +} + +type Simulator struct { + apiURL string + client *http.Client +} + +func NewSimulator(apiURL string) *Simulator { + return &Simulator{ + apiURL: apiURL, + client: &http.Client{ + Transport: &http.Transport{ + Proxy: http.ProxyFromEnvironment, + MaxIdleConns: 256, + MaxIdleConnsPerHost: 256, + IdleConnTimeout: 90 * time.Second, + ForceAttemptHTTP2: true, + DialContext: (&net.Dialer{ + Timeout: 5 * time.Second, + KeepAlive: 30 * time.Second, + }).DialContext, + TLSHandshakeTimeout: 5 * time.Second, + }, + Timeout: 15 * time.Second, + }, + } +} + +type reqBody struct { + TxRaw string `json:"raw"` + Block string `json:"block,omitempty"` +} + +func (s *Simulator) Simulate(ctx context.Context, txRaw string) ([][]byte, error) { + body := reqBody{ + TxRaw: txRaw, + Block: "latest", + } + bodyJSON, err := json.Marshal(body) + if err != nil { + return nil, fmt.Errorf("marshal request: %w", err) + } + req, err := http.NewRequestWithContext( + ctx, + http.MethodPost, + fmt.Sprintf("%s/rethsim/simulate/raw", s.apiURL), + strings.NewReader(string(bodyJSON)), + ) + if err != nil { + return nil, fmt.Errorf("create request: %w", err) + } + + req.Header.Set("Content-Type", "application/json") + resp, err := s.client.Do(req) + if err != nil { + return nil, fmt.Errorf("do request: %w", err) + } + defer func() { + _ = resp.Body.Close() + }() + + respBody, err := io.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("read response: %w", err) + } + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("bad status %d: %s", resp.StatusCode, string(respBody)) + } + + return parseResponse(respBody) +} + +func parseResponse(body []byte) ([][]byte, error) { + trim := strings.TrimSpace(string(body)) + if len(trim) == 0 { + return nil, errors.New("empty response") + } + + var blk SimBlock + if strings.HasPrefix(trim, "[") { + var arr []SimBlock + if err := json.Unmarshal(body, &arr); err != nil { + return nil, fmt.Errorf("decode array: %w", err) + } + if len(arr) == 0 { + return nil, errors.New("no blocks in response") + } + blk = arr[0] + } else { + if err := json.Unmarshal(body, &blk); err != nil { + return nil, fmt.Errorf("decode object: %w", err) + } + } + + if len(blk.Calls) == 0 { + return nil, errors.New("no calls in response") + } + root := blk.Calls[0] + + // Failure → build extended error + if strings.EqualFold(root.Status, "0x0") { + reason := decodeRevert(root.ReturnData, "execution reverted") + return nil, fmt.Errorf("reverted: %s", reason) + } + + // Success → collect all logs (depth-first, execution order) + var out [][]byte + collectLogs(&root, &out) + return out, nil +} + +func collectLogs(n *SimCall, acc *[][]byte) { + for i := range n.Logs { + b, _ := json.Marshal(n.Logs[i]) // preserve original shape + *acc = append(*acc, b) + } + for i := range n.Calls { + collectLogs(&n.Calls[i], acc) + } +} + +func decodeRevert(dataHex string, fallback string) string { + h := extract0x(dataHex) + if h == "" || h == "0x" { + return fallback + } + if len(h) < 10 { + return fallback + } + sel := strings.ToLower(h[:10]) // 0x + 8 hex + args := "0x" + h[10:] + + switch sel { + case "0x08c379a0": // Error(string) + if s, ok := abiDecodeString(args); ok { + return s + } + return "execution reverted (Error)" + case "0x4e487b71": // Panic(uint256) + if n, ok := abiDecodeUint256(args); ok { + return fmt.Sprintf("panic(0x%x)", n) + } + return "execution reverted (Panic)" + default: + return fmt.Sprintf("reverted (selector %s)", sel) + } +} + +func extract0x(x string) string { + x = strings.TrimSpace(x) + if strings.HasPrefix(x, "0x") { + return x + } + // sometimes servers embed the hex in a quoted JSON string + var s string + if err := json.Unmarshal([]byte(x), &s); err == nil && strings.HasPrefix(s, "0x") { + return s + } + return "" +} + +func abiDecodeString(args string) (string, bool) { + b, err := hex.DecodeString(strings.TrimPrefix(args, "0x")) + if err != nil || len(b) < 64 { + return "", false + } + // length at bytes 32..64 + l := new(big.Int).SetBytes(b[32:64]).Int64() + if l < 0 || int(64+l) > len(b) { + return "", false + } + return string(b[64 : 64+l]), true +} + +func abiDecodeUint256(args string) (*big.Int, bool) { + b, err := hex.DecodeString(strings.TrimPrefix(args, "0x")) + if err != nil || len(b) < 32 { + return nil, false + } + return new(big.Int).SetBytes(b[len(b)-32:]), true +} From 6a60fb5b0fec89bf83ac135a1e2aca0616497777 Mon Sep 17 00:00:00 2001 From: Alok Date: Sat, 25 Oct 2025 13:03:22 +0530 Subject: [PATCH 04/13] feat: add tests --- tools/preconf-rpc/sim/simulator_test.go | 81 +++++++++++++++++++++++++ 1 file changed, 81 insertions(+) create mode 100644 tools/preconf-rpc/sim/simulator_test.go diff --git a/tools/preconf-rpc/sim/simulator_test.go b/tools/preconf-rpc/sim/simulator_test.go new file mode 100644 index 000000000..26571b209 --- /dev/null +++ b/tools/preconf-rpc/sim/simulator_test.go @@ -0,0 +1,81 @@ +package sim_test + +import ( + "context" + "encoding/json" + "net/http" + "net/http/httptest" + "strings" + "testing" + + "github.com/primev/mev-commit/tools/preconf-rpc/sim" +) + +var jsonRPCResponse1 = []byte(`[{"baseFeePerGas":"0x2d04732e","blobGasUsed":"0x0","calls":[{"gasUsed":"0x5208","logs":[],"returnData":"0x","status":"0x1"}],"difficulty":"0x0","excessBlobGas":"0x100000","extraData":"0x726574682f76312e382e322f6c696e7578","gasLimit":"0x2aea540","gasUsed":"0x5208","hash":"0xaf51a6fb4ea22f5175baeaeac9fb15c55bf96f5a42974fec5cdc0e9b5e41e7a0","logsBloom":"0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000","miner":"0x1f9090aae28b8a3dceadf281b0f12828e676c326","mixHash":"0x8f829a3c6bfe34dc0224d2b4bf80190fdabc184c5f56f70a5d462f4696f1309c","nonce":"0x0000000000000000","number":"0x168d7c9","parentBeaconBlockRoot":"0x0000000000000000000000000000000000000000000000000000000000000000","parentHash":"0x63eea0ad2621b94f49340c8b7ef7ead45967083894af3aab1ff7b67d9dbd7df7","receiptsRoot":"0xf78dfb743fbd92ade140711c8bbc542b5e307f0ab7984eff35d751969fe57efa","requestsHash":"0xe3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855","sha3Uncles":"0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347","size":"0x2af","stateRoot":"0x0000000000000000000000000000000000000000000000000000000000000000","timestamp":"0x68fb9a4b","transactions":["0xe0310102ffb7651c28cb3114f1e9137f6b6fd9ccf8326ba9657c333b0cb0ad1a"],"transactionsRoot":"0x8ae72c26cb403244bb8de6039212a55eb00bb7372c2459b0967df5c5e97a526c","uncles":[],"withdrawals":[],"withdrawalsRoot":"0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421"}]`) + +var jsonRPCResponse2 = []byte(`[{"baseFeePerGas":"0x2d04732e","blobGasUsed":"0x0","calls":[{"gasUsed":"0x5208","logs":[{"address":"0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee","blockHash":null,"blockNumber":"0x168d7c9","blockTimestamp":"0x68fb9a4b","data":"0x0000000000000000000000000000000000000000000000000000000000000007","logIndex":"0x0","removed":false,"topics":["0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef","0x000000000000000000000000ae2885e0e7a6c5f99b93b4dbc43d206c7cf67c7e","0x000000000000000000000000ae2885e0e7a6c5f99b93b4dbc43d206c7cf67c7e"],"transactionHash":"0xe0310102ffb7651c28cb3114f1e9137f6b6fd9ccf8326ba9657c333b0cb0ad1a","transactionIndex":"0x0"}],"returnData":"0x","status":"0x1"}],"difficulty":"0x0","excessBlobGas":"0x100000","extraData":"0x726574682f76312e382e322f6c696e7578","gasLimit":"0x2aea540","gasUsed":"0x5208","hash":"0x0b99997c087f1ffd50628a2a4e3e147c0c728a5bffdb1bb2d069f1e4254ba1d3","logsBloom":"0x00000000000000001000000000000000001000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000000000000000000008000000000000000000040000000000000000000000000000000000000000000000000000000000000000002000000010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000040000000002000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000","miner":"0x1f9090aae28b8a3dceadf281b0f12828e676c326","mixHash":"0x71b40dd1254adf5a9834608fc6e73325eb1ddacff14ee2b3d6bfd031b678c30e","nonce":"0x0000000000000000","number":"0x168d7c9","parentBeaconBlockRoot":"0x0000000000000000000000000000000000000000000000000000000000000000","parentHash":"0x63eea0ad2621b94f49340c8b7ef7ead45967083894af3aab1ff7b67d9dbd7df7","receiptsRoot":"0x7df755b885cf2890db03b44cacb889bda231ba87a572a4cec66a06bdaff31f5d","requestsHash":"0xe3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855","sha3Uncles":"0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347","size":"0x2af","stateRoot":"0x0000000000000000000000000000000000000000000000000000000000000000","timestamp":"0x68fb9a4b","transactions":["0xe0310102ffb7651c28cb3114f1e9137f6b6fd9ccf8326ba9657c333b0cb0ad1a"],"transactionsRoot":"0x8ae72c26cb403244bb8de6039212a55eb00bb7372c2459b0967df5c5e97a526c","uncles":[],"withdrawals":[],"withdrawalsRoot":"0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421"}]`) + +var errResponse = []byte(`[{"baseFeePerGas":"0xddacfa4","blobGasUsed":"0x0","calls":[{"error":{"code":3,"message":"execution reverted"},"gasUsed":"0x194714","logs":[],"returnData":"0x064a4ec600000000000000000000000000000000000000000000000000000038af10569100000000000000000000000000000000000000000000000000000038ddca493d","status":"0x0"}],"difficulty":"0x0","excessBlobGas":"0x620000","extraData":"0x726574682f76312e382e322f6c696e7578","gasLimit":"0x2adf7c0","gasUsed":"0x194714","hash":"0xcdb8b3f1cda9f148c44e51e9f9400568fb8479120c8b2da0599207cd9b9846b5","logsBloom":"0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000","miner":"0xdadb0d80178819f2319190d340ce9a924f783711","mixHash":"0x4f51b3d0ea08e01914bb86339aec2c2ab66b382711059331fbc0c2f56e0e7134","nonce":"0x0000000000000000","number":"0x168bac7","parentBeaconBlockRoot":"0x0000000000000000000000000000000000000000000000000000000000000000","parentHash":"0xb54623fcd9d11cb692091209c4feb1988dc2a9aef1c9e54ce7cee4a8332eeba7","receiptsRoot":"0x7b3251ff8a2414ac1cbcc4b091493cffeaecb6530daf2c2012e7269f07a80cab","requestsHash":"0xe3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855","sha3Uncles":"0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347","size":"0x1b41","stateRoot":"0x0000000000000000000000000000000000000000000000000000000000000000","timestamp":"0x68fa3a5b","transactions":["0x61d03e5e6754f7e13a2d09024a5be07a44bf1b68e5d363275fb75fa45a224918"],"transactionsRoot":"0xa255a72d9dfcb7143b10ec681f5d5804b1aa3f1f12f67715665a9d90fdcb1370","uncles":[],"withdrawals":[],"withdrawalsRoot":"0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421"}]`) + +func TestSimulator(t *testing.T) { + txns := map[string][]byte{ + "1234": jsonRPCResponse1, + "5678": jsonRPCResponse2, + "abcd": errResponse, + } + srv := httptest.NewServer( + http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case "/rethsim/simulate/raw": + var req map[string]interface{} + defer func() { _ = r.Body.Close() }() + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + http.Error(w, "bad request", http.StatusBadRequest) + return + } + rawTx := req["raw"].(string) + if resp, ok := txns[rawTx]; ok { + w.Header().Set("Content-Type", "application/json") + w.Write(resp) + return + } + http.Error(w, "transaction not found", http.StatusNotFound) + default: + http.NotFound(w, r) + } + }), + ) + + defer srv.Close() + + t.Logf("Test server running at %s", srv.URL) + simulator := sim.NewSimulator(srv.URL) + + t.Run("SuccessfulSimulation1", func(t *testing.T) { + result, err := simulator.Simulate(context.Background(), "1234") + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + if len(result) != 0 { + t.Fatalf("expected non-empty result") + } + }) + t.Run("SuccessfulSimulation2", func(t *testing.T) { + result, err := simulator.Simulate(context.Background(), "5678") + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + if len(result) == 0 { + t.Fatalf("expected non-empty result") + } + }) + t.Run("ErrorSimulation", func(t *testing.T) { + _, err := simulator.Simulate(context.Background(), "abcd") + if err == nil { + t.Fatalf("expected error, got none") + } + if !strings.Contains(err.Error(), "reverted") { + t.Fatalf("unexpected error") + } + }) +} From b8dd11934e65e07b4825f3dc6a55b670e9a58632 Mon Sep 17 00:00:00 2001 From: Alok Date: Sat, 25 Oct 2025 13:39:09 +0530 Subject: [PATCH 05/13] feat: add logs to DB --- tools/preconf-rpc/handlers/handlers.go | 12 ++++- tools/preconf-rpc/main.go | 7 +++ tools/preconf-rpc/sender/sender.go | 63 +++++++++++++------------ tools/preconf-rpc/sender/sender_test.go | 11 +++++ tools/preconf-rpc/service/service.go | 5 ++ tools/preconf-rpc/sim/simulator.go | 25 +++++----- tools/preconf-rpc/store/store.go | 52 ++++++++++++++++++++ tools/preconf-rpc/store/store_test.go | 22 ++++++++- 8 files changed, 153 insertions(+), 44 deletions(-) diff --git a/tools/preconf-rpc/handlers/handlers.go b/tools/preconf-rpc/handlers/handlers.go index 5d51d7436..aa8060f87 100644 --- a/tools/preconf-rpc/handlers/handlers.go +++ b/tools/preconf-rpc/handlers/handlers.go @@ -32,6 +32,7 @@ type Store interface { GetTransactionByHash(ctx context.Context, txnHash common.Hash) (*sender.Transaction, error) GetTransactionsForBlock(ctx context.Context, blockNumber int64) ([]*sender.Transaction, error) GetTransactionCommitments(ctx context.Context, txnHash common.Hash) ([]*bidderapiv1.Commitment, error) + GetTransactionLogs(ctx context.Context, txnHash common.Hash) ([]*types.Log, error) GetBalance(ctx context.Context, account common.Address) (*big.Int, error) GetCurrentNonce(ctx context.Context, account common.Address) uint64 } @@ -415,6 +416,15 @@ func (h *rpcMethodHandler) handleGetTxReceipt(ctx context.Context, params ...any return nil, true, nil } + logs, err := h.store.GetTransactionLogs(ctx, txHash) + if err != nil { + h.logger.Error("Failed to get transaction logs", "error", err, "txHash", txHash) + return nil, false, rpcserver.NewJSONErr( + rpcserver.CodeCustomError, + "failed to get transaction logs", + ) + } + result := map[string]interface{}{ "type": hexutil.Uint(txn.Transaction.Type()), "transactionHash": txn.Hash().Hex(), @@ -424,7 +434,7 @@ func (h *rpcMethodHandler) handleGetTxReceipt(ctx context.Context, params ...any "contractAddress": (common.Address{}).Hex(), "gasUsed": hexutil.Uint64(0), "cumulativeGasUsed": hexutil.Uint64(1), - "logs": []*types.Log{}, // should be [] not null + "logs": logs, "logsBloom": hexutil.Bytes(types.Bloom{}.Bytes()), "effectiveGasPrice": hexutil.EncodeBig(big.NewInt(0)), } diff --git a/tools/preconf-rpc/main.go b/tools/preconf-rpc/main.go index 2bac4dd96..982592014 100644 --- a/tools/preconf-rpc/main.go +++ b/tools/preconf-rpc/main.go @@ -210,6 +210,13 @@ var ( Value: "", } + optionSimulationURL = &cli.StringFlag{ + Name: "simulation-url", + Usage: "URL for the transaction simulation service", + EnvVars: []string{"PRECONF_RPC_SIMULATION_URL"}, + Required: true, + } + optionLogFmt = &cli.StringFlag{ Name: "log-fmt", Usage: "log format to use, options are 'text' or 'json'", diff --git a/tools/preconf-rpc/sender/sender.go b/tools/preconf-rpc/sender/sender.go index 3596f1437..79d366e99 100644 --- a/tools/preconf-rpc/sender/sender.go +++ b/tools/preconf-rpc/sender/sender.go @@ -77,7 +77,7 @@ type Store interface { HasBalance(ctx context.Context, sender common.Address, amount *big.Int) bool AddBalance(ctx context.Context, account common.Address, amount *big.Int) error DeductBalance(ctx context.Context, account common.Address, amount *big.Int) error - StoreTransaction(ctx context.Context, txn *Transaction, commitments []*bidderapiv1.Commitment) error + StoreTransaction(ctx context.Context, txn *Transaction, commitments []*bidderapiv1.Commitment, logs []*types.Log) error GetTransactionByHash(ctx context.Context, txnHash common.Hash) (*Transaction, error) } @@ -106,7 +106,7 @@ type Transferer interface { } type Simulator interface { - Simulate(ctx context.Context, txRaw string) ([][]byte, error) + Simulate(ctx context.Context, txRaw string) ([]*types.Log, error) } type blockAttempt struct { @@ -159,6 +159,7 @@ func NewTxSender( blockTracker BlockTracker, transferer Transferer, notifier Notifier, + simulator Simulator, settlementChainId *big.Int, logger *slog.Logger, ) (*TxSender, error) { @@ -182,6 +183,7 @@ func NewTxSender( inflightAccount: make(map[common.Address]struct{}), txnAttemptHistory: txnAttemptHistory, notifier: notifier, + simulator: simulator, fastTrack: noOpFastTrack, bidTimeout: bidTimeout, }, nil @@ -274,7 +276,7 @@ func (t *TxSender) CancelTransaction(ctx context.Context, txnHash common.Hash) ( if txn.Status == TxStatusPending { txn.Status = TxStatusFailed txn.Details = ErrTransactionCancelled.Error() - if err := t.store.StoreTransaction(ctx, txn, nil); err != nil { + if err := t.store.StoreTransaction(ctx, txn, nil, nil); err != nil { t.logger.Error("Failed to store cancelled transaction", "hash", txnHash.Hex(), "error", err) return false, fmt.Errorf("failed to store cancelled transaction: %w", err) } @@ -435,7 +437,7 @@ func (t *TxSender) processQueuedTransactions(ctx context.Context) { txn.Status = TxStatusFailed txn.Details = err.Error() t.clearBlockAttemptHistory(txn, time.Now()) - return t.store.StoreTransaction(ctx, txn, nil) + return t.store.StoreTransaction(ctx, txn, nil, nil) } return nil }) @@ -502,7 +504,7 @@ BID_LOOP: "blockNumber", result.blockNumber, "bidAmount", result.bidAmount.String(), ) - if err := t.store.StoreTransaction(ctx, txn, result.commitments); err != nil { + if err := t.store.StoreTransaction(ctx, txn, result.commitments, result.logs); err != nil { return fmt.Errorf("failed to store fast-tracked transaction: %w", err) } preConfirmed = true @@ -518,7 +520,7 @@ BID_LOOP: "blockNumber", result.blockNumber, "bidAmount", result.bidAmount.String(), ) - if err := t.store.StoreTransaction(ctx, txn, result.commitments); err != nil { + if err := t.store.StoreTransaction(ctx, txn, result.commitments, result.logs); err != nil { return fmt.Errorf("failed to store preconfirmed transaction: %w", err) } preConfirmed = true @@ -557,7 +559,7 @@ BID_LOOP: "blockNumber", result.blockNumber, "bidAmount", result.bidAmount.String(), ) - if err := t.store.StoreTransaction(ctx, txn, result.commitments); err != nil { + if err := t.store.StoreTransaction(ctx, txn, result.commitments, result.logs); err != nil { return fmt.Errorf("failed to store preconfirmed transaction: %w", err) } } @@ -573,19 +575,19 @@ BID_LOOP: switch txn.Type { case TxTypeRegular: if err := t.store.DeductBalance(ctx, txn.Sender, result.bidAmount); err != nil { - logger.Error("Failed to deduct balance for sender", "sender", txn.Sender.Hex(), "error", err) + logger.Error("Failed to deduct balance for sender", "error", err) return fmt.Errorf("failed to deduct balance for sender: %w", err) } case TxTypeDeposit: balanceToAdd := new(big.Int).Sub(txn.Value(), result.bidAmount) if err := t.store.AddBalance(ctx, txn.Sender, balanceToAdd); err != nil { - logger.Error("Failed to add balance for sender", "sender", txn.Sender.Hex(), "error", err) + logger.Error("Failed to add balance for sender", "error", err) return fmt.Errorf("failed to add balance for sender: %w", err) } case TxTypeInstantBridge: amountToBridge := new(big.Int).Sub(txn.Value(), new(big.Int).Mul(result.bidAmount, big.NewInt(2))) if err := t.transferer.Transfer(ctx, txn.Sender, t.settlementChainId, amountToBridge); err != nil { - logger.Error("Failed to transfer funds for instant bridge", "sender", txn.Sender.Hex(), "error", err) + logger.Error("Failed to transfer funds for instant bridge", "error", err) return fmt.Errorf("failed to transfer funds for instant bridge: %w", err) } } @@ -610,7 +612,7 @@ type bidResult struct { optedInSlot bool bidAmount *big.Int commitments []*bidderapiv1.Commitment - logs [][]byte + logs []*types.Log } func (t *TxSender) sendBid( @@ -618,10 +620,15 @@ func (t *TxSender) sendBid( txn *Transaction, ) (bidResult, error) { start := time.Now() + logger := t.logger.With( + "transactionHash", txn.Hash().Hex(), + "sender", txn.Sender.Hex(), + "type", txn.Type, + ) timeToOptIn, err := t.bidder.Estimate() if err != nil { - t.logger.Warn("Failed to estimate time to opt-in", "error", err) + logger.Warn("Failed to estimate time to opt-in", "error", err) // If we cannot estimate the time to opt-in, we assume a default value and // proceed with the bid process. The default value should be higher than // the typical block time to ensure we consider the next slot as a non-opt-in slot. @@ -630,13 +637,13 @@ func (t *TxSender) sendBid( logs, err := t.simulator.Simulate(ctx, txn.Raw) if err != nil { - t.logger.Error("Failed to simulate transaction", "error", err) + logger.Error("Failed to simulate transaction", "error", err) return bidResult{}, fmt.Errorf("failed to simulate transaction: %w", err) } bidBlockNo, timeUntilNextBlock, err := t.blockTracker.NextBlockNumber() if err != nil { - t.logger.Error("Failed to get next block number", "error", err) + logger.Error("Failed to get next block number", "error", err) return bidResult{}, &errRetry{ err: fmt.Errorf("failed to get next block number: %w", err), retryAfter: time.Second, @@ -644,7 +651,7 @@ func (t *TxSender) sendBid( } if timeUntilNextBlock <= time.Second { - t.logger.Warn("Next block time is too short, skipping bid", "timeUntilNextBlock", timeUntilNextBlock) + logger.Warn("Next block time is too short, skipping bid", "timeUntilNextBlock", timeUntilNextBlock) return bidResult{}, &errRetry{ err: fmt.Errorf("next block time is too short: %s", timeUntilNextBlock), retryAfter: time.Second, @@ -661,9 +668,9 @@ func (t *TxSender) sendBid( cost, err := t.calculatePriceForNextBlock(txn, bidBlockNo, prices, optedInSlot) if err != nil { - t.logger.Error("Failed to calculate price for next block", "error", err) + logger.Error("Failed to calculate price for next block", "error", err) if errors.Is(err, ErrTimeoutExceeded) { - t.logger.Warn("Timeout exceeded while trying to process transaction", "txnHash", txn.Hash().Hex()) + logger.Warn("Timeout exceeded while trying to process transaction") return bidResult{}, ErrTimeoutExceeded } if errors.Is(err, ErrMaxAttemptsPerBlockExceeded) { @@ -679,14 +686,13 @@ func (t *TxSender) sendBid( switch txn.Type { case TxTypeRegular: if !t.store.HasBalance(ctx, txn.Sender, cost) { - t.logger.Error("Insufficient balance for sender", "sender", txn.Sender.Hex()) + logger.Error("Insufficient balance for sender") return bidResult{}, fmt.Errorf("insufficient balance for sender: %s", txn.Sender.Hex()) } case TxTypeDeposit: if txn.Value().Cmp(cost) < 0 { - t.logger.Error( + logger.Error( "Deposit amount is less than price of deposit", - "sender", txn.Sender.Hex(), "deposit", txn.Value().String(), "price", cost.String(), ) @@ -700,9 +706,8 @@ func (t *TxSender) sendBid( case TxTypeInstantBridge: costOfBridge := new(big.Int).Mul(cost, big.NewInt(2)) // 2x the price for instant bridge if txn.Value().Cmp(costOfBridge) < 0 { - t.logger.Error( + logger.Error( "Instant bridge amount is less than price of bridge", - "sender", txn.Sender.Hex(), "bridge", txn.Value().String(), "price", costOfBridge.String(), ) @@ -730,7 +735,7 @@ func (t *TxSender) sendBid( }, ) if err != nil { - t.logger.Error("Failed to place bid", "error", err) + logger.Error("Failed to place bid", "error", err) return bidResult{}, fmt.Errorf("failed to place bid: %w", err) } @@ -745,11 +750,11 @@ BID_LOOP: for { select { case <-ctx.Done(): - t.logger.Info("Context cancelled while waiting for bid status") + logger.Info("Context cancelled while waiting for bid status") return bidResult{}, ctx.Err() case bidStatus, more := <-bidC: if !more { - t.logger.Info("Bid channel closed, no more bid statuses") + logger.Info("Bid channel closed, no more bid statuses") break BID_LOOP } switch bidStatus.Type { @@ -760,15 +765,15 @@ BID_LOOP: case optinbidder.BidStatusCommitment: result.commitments = append(result.commitments, bidStatus.Arg.(*bidderapiv1.Commitment)) case optinbidder.BidStatusCancelled: - t.logger.Warn("Bid context cancelled by the bidder") + logger.Warn("Bid context cancelled by the bidder") break BID_LOOP case optinbidder.BidStatusFailed: - t.logger.Error("Bid failed", "error", bidStatus.Arg) + logger.Error("Bid failed", "error", bidStatus.Arg) break BID_LOOP } } } - t.logger.Info( + logger.Info( "Bid operation complete", "noOfProviders", result.noOfProviders, "noOfCommitments", len(result.commitments), @@ -861,7 +866,7 @@ func (t *TxSender) clearBlockAttemptHistory(txn *Transaction, endTime time.Time) t.logger.Info( "Clearing block attempt history for transaction", - "hash", txn.Hash().Hex(), + "transactionHash", txn.Hash().Hex(), "blockNumber", txn.BlockNumber, "blockAttempts", len(attempts.attempts), "startTime", attempts.startTime.Format(time.RFC3339), diff --git a/tools/preconf-rpc/sender/sender_test.go b/tools/preconf-rpc/sender/sender_test.go index 9f616923f..806e91b6b 100644 --- a/tools/preconf-rpc/sender/sender_test.go +++ b/tools/preconf-rpc/sender/sender_test.go @@ -21,6 +21,7 @@ type result struct { txn *sender.Transaction commitments []*bidderapiv1.Commitment blockNumber int64 + logs []*types.Log } type mockStore struct { @@ -123,11 +124,13 @@ func (m *mockStore) StoreTransaction( ctx context.Context, txn *sender.Transaction, commitments []*bidderapiv1.Commitment, + logs []*types.Log, ) error { m.preconfirmedTxns <- result{ txn: txn, commitments: commitments, blockNumber: txn.BlockNumber, + logs: logs, } m.mu.Lock() defer m.mu.Unlock() @@ -265,6 +268,12 @@ func (m *mockNotifier) NotifyTransactionStatus(txn *sender.Transaction, attempts m.notifications = append(m.notifications, txn.Hash().Hex()) } +type mockSimulator struct{} + +func (m *mockSimulator) Simulate(ctx context.Context, rawTx string) ([]*types.Log, error) { + return []*types.Log{}, nil +} + func TestSender(t *testing.T) { t.Parallel() @@ -293,6 +302,7 @@ func TestSender(t *testing.T) { blockTracker, &mockTransferer{}, notifier, + &mockSimulator{}, big.NewInt(1), // Settlement chain ID util.NewTestLogger(os.Stdout), ) @@ -571,6 +581,7 @@ func TestCancelTransaction(t *testing.T) { blockTracker, &mockTransferer{}, &mockNotifier{}, + &mockSimulator{}, big.NewInt(1), // Settlement chain ID util.NewTestLogger(os.Stdout), ) diff --git a/tools/preconf-rpc/service/service.go b/tools/preconf-rpc/service/service.go index b50885201..887e28db2 100644 --- a/tools/preconf-rpc/service/service.go +++ b/tools/preconf-rpc/service/service.go @@ -29,6 +29,7 @@ import ( "github.com/primev/mev-commit/tools/preconf-rpc/pricer" "github.com/primev/mev-commit/tools/preconf-rpc/rpcserver" "github.com/primev/mev-commit/tools/preconf-rpc/sender" + "github.com/primev/mev-commit/tools/preconf-rpc/sim" "github.com/primev/mev-commit/tools/preconf-rpc/store" "github.com/primev/mev-commit/x/accountsync" "github.com/primev/mev-commit/x/contracts/ethwrapper" @@ -69,6 +70,7 @@ type Config struct { PricerAPIKey string Webhooks []string Token string + SimulatorURL string } type Service struct { @@ -244,6 +246,8 @@ func New(config *Config) (*Service, error) { blockTrackerDone := blockTracker.Start(ctx) healthChecker.Register(health.CloseChannelHealthCheck("BlockTracker", blockTrackerDone)) + simulator := sim.NewSimulator(config.SimulatorURL) + sndr, err := sender.NewTxSender( rpcstore, bidderClient, @@ -251,6 +255,7 @@ func New(config *Config) (*Service, error) { blockTracker, transferer, notifier, + simulator, settlementChainID, config.Logger.With("module", "txsender"), ) diff --git a/tools/preconf-rpc/sim/simulator.go b/tools/preconf-rpc/sim/simulator.go index c0fa0f1c3..4b6f9dabb 100644 --- a/tools/preconf-rpc/sim/simulator.go +++ b/tools/preconf-rpc/sim/simulator.go @@ -12,6 +12,8 @@ import ( "net/http" "strings" "time" + + "github.com/ethereum/go-ethereum/core/types" ) type SimLog struct { @@ -28,11 +30,11 @@ type SimLog struct { } type SimCall struct { - Status string `json:"status"` - GasUsed string `json:"gasUsed"` - ReturnData string `json:"returnData"` - Logs []SimLog `json:"logs"` - Calls []SimCall `json:"calls,omitempty"` + Status string `json:"status"` + GasUsed string `json:"gasUsed"` + ReturnData string `json:"returnData"` + Logs []*types.Log `json:"logs"` + Calls []SimCall `json:"calls,omitempty"` } type SimBlock struct { @@ -75,7 +77,7 @@ type reqBody struct { Block string `json:"block,omitempty"` } -func (s *Simulator) Simulate(ctx context.Context, txRaw string) ([][]byte, error) { +func (s *Simulator) Simulate(ctx context.Context, txRaw string) ([]*types.Log, error) { body := reqBody{ TxRaw: txRaw, Block: "latest", @@ -114,7 +116,7 @@ func (s *Simulator) Simulate(ctx context.Context, txRaw string) ([][]byte, error return parseResponse(respBody) } -func parseResponse(body []byte) ([][]byte, error) { +func parseResponse(body []byte) ([]*types.Log, error) { trim := strings.TrimSpace(string(body)) if len(trim) == 0 { return nil, errors.New("empty response") @@ -148,15 +150,14 @@ func parseResponse(body []byte) ([][]byte, error) { } // Success → collect all logs (depth-first, execution order) - var out [][]byte + var out []*types.Log collectLogs(&root, &out) return out, nil } -func collectLogs(n *SimCall, acc *[][]byte) { - for i := range n.Logs { - b, _ := json.Marshal(n.Logs[i]) // preserve original shape - *acc = append(*acc, b) +func collectLogs(n *SimCall, acc *[]*types.Log) { + for _, log := range n.Logs { + *acc = append(*acc, log) } for i := range n.Calls { collectLogs(&n.Calls[i], acc) diff --git a/tools/preconf-rpc/store/store.go b/tools/preconf-rpc/store/store.go index f54d47f05..513fbdba5 100644 --- a/tools/preconf-rpc/store/store.go +++ b/tools/preconf-rpc/store/store.go @@ -4,6 +4,7 @@ import ( "context" "database/sql" "encoding/hex" + "encoding/json" "errors" "fmt" "math/big" @@ -54,6 +55,13 @@ CREATE TABLE IF NOT EXISTS subsidies ( balance NUMERIC(24, 0) );` +var simulationLogs = ` +CREATE TABLE IF NOT EXISTS simulationLogs ( + transaction_hash TEXT PRIMARY KEY, + logs TEXT, + FOREIGN KEY (transaction_hash) REFERENCES mcTransactions (hash) ON DELETE CASCADE +);` + type rpcstore struct { db *sql.DB } @@ -64,6 +72,7 @@ func New(db *sql.DB) (*rpcstore, error) { commitmentsTable, balancesTable, subsidiesTable, + simulationLogs, } { _, err := db.Exec(table) if err != nil { @@ -255,6 +264,7 @@ func (s *rpcstore) StoreTransaction( ctx context.Context, txn *sender.Transaction, commitments []*bidderapiv1.Commitment, + logs []*types.Log, ) error { if txn.Status == sender.TxStatusPending { return fmt.Errorf("transaction must not be in pending status, got %s", txn.Status) @@ -309,6 +319,24 @@ func (s *rpcstore) StoreTransaction( } } + if logs != nil { + logBuf, err := json.Marshal(logs) + if err != nil { + _ = dbTxn.Rollback() + return fmt.Errorf("failed to marshal simulation logs for transaction %s: %w", txn.Hash().Hex(), err) + } + insertLogs := ` + INSERT INTO simulationLogs (transaction_hash, logs) + VALUES ($1, $2) + ON CONFLICT (transaction_hash) DO UPDATE SET logs = EXCLUDED.logs; + ` + _, err = dbTxn.ExecContext(ctx, insertLogs, txn.Hash().Hex(), string(logBuf)) + if err != nil { + _ = dbTxn.Rollback() + return fmt.Errorf("failed to insert simulation logs for transaction %s: %w", txn.Hash().Hex(), err) + } + } + if err := dbTxn.Commit(); err != nil { return fmt.Errorf("failed to commit transaction: %w", err) } @@ -316,6 +344,30 @@ func (s *rpcstore) StoreTransaction( return nil } +func (s *rpcstore) GetTransactionLogs(ctx context.Context, txnHash common.Hash) ([]*types.Log, error) { + query := ` + SELECT logs + FROM simulationLogs + WHERE transaction_hash = $1; + ` + row := s.db.QueryRowContext(ctx, query, txnHash.Hex()) + var logsData string + err := row.Scan(&logsData) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + return []*types.Log{}, nil // No logs found, return empty slice + } + return nil, fmt.Errorf("failed to get logs for transaction %s: %w", txnHash.Hex(), err) + } + + var logs []*types.Log + if err := json.Unmarshal([]byte(logsData), &logs); err != nil { + return nil, fmt.Errorf("failed to unmarshal logs for transaction %s: %w", txnHash.Hex(), err) + } + + return logs, nil +} + func (s *rpcstore) GetTransactionCommitments(ctx context.Context, txnHash common.Hash) ([]*bidderapiv1.Commitment, error) { query := ` SELECT commitment_data diff --git a/tools/preconf-rpc/store/store_test.go b/tools/preconf-rpc/store/store_test.go index 6104a96bc..498989fe1 100644 --- a/tools/preconf-rpc/store/store_test.go +++ b/tools/preconf-rpc/store/store_test.go @@ -101,6 +101,16 @@ func TestStore(t *testing.T) { Type: sender.TxTypeRegular, Status: sender.TxStatusPending, } + txn1Logs := []*types.Log{ + { + Address: common.HexToAddress("0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef"), + Topics: []common.Hash{common.HexToHash("0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")}, + Data: []byte{0x01, 0x02, 0x03}, + BlockNumber: 1, + TxHash: txn1.Hash(), + TxIndex: 0, + }, + } txn2 := types.NewTransaction( 1, @@ -199,7 +209,7 @@ func TestStore(t *testing.T) { wrappedTxn1.Status = sender.TxStatusPreConfirmed wrappedTxn1.BlockNumber = 1 - err := st.StoreTransaction(context.Background(), wrappedTxn1, commitments) + err := st.StoreTransaction(context.Background(), wrappedTxn1, commitments, txn1Logs) if err != nil { t.Errorf("failed to store preconfirmed transaction: %v", err) } @@ -239,10 +249,18 @@ func TestStore(t *testing.T) { t.Errorf("transaction mismatch (-want +got):\n%s", diff) } + logs, err := st.GetTransactionLogs(context.Background(), wrappedTxn1.Hash()) + if err != nil { + t.Errorf("failed to get transaction logs: %v", err) + } + if diff := cmp.Diff(txn1Logs, logs, cmpopts.IgnoreUnexported(types.Log{})); diff != "" { + t.Errorf("transaction logs mismatch (-want +got):\n%s", diff) + } + wrappedTxn2.Status = sender.TxStatusFailed wrappedTxn2.Details = "Transaction failed due to insufficient funds" wrappedTxn2.BlockNumber = 2 - err = st.StoreTransaction(context.Background(), wrappedTxn2, nil) + err = st.StoreTransaction(context.Background(), wrappedTxn2, nil, nil) if err != nil { t.Errorf("failed to store failed transaction: %v", err) } From 1196b3548c2e46d2888f952b44af499e58f803ce Mon Sep 17 00:00:00 2001 From: Alok Date: Sat, 25 Oct 2025 13:53:42 +0530 Subject: [PATCH 06/13] feat: lint --- p2p/pkg/rpc/provider/service_test.go | 11 ++++++----- tools/preconf-rpc/main.go | 2 ++ tools/preconf-rpc/sim/simulator.go | 4 +--- tools/preconf-rpc/sim/simulator_test.go | 2 +- 4 files changed, 10 insertions(+), 9 deletions(-) diff --git a/p2p/pkg/rpc/provider/service_test.go b/p2p/pkg/rpc/provider/service_test.go index f253375b7..535dea49a 100644 --- a/p2p/pkg/rpc/provider/service_test.go +++ b/p2p/pkg/rpc/provider/service_test.go @@ -925,7 +925,8 @@ func TestShutterisedBidOptionsProcessing(t *testing.T) { var bidOptionsBytes []byte var err error - if tc.name == "mixed bid options" { + switch tc.name { + case "mixed bid options": // Mixed options with position constraint and shutterised option bidderOpts = &bidderapiv1.BidOptions{ Options: []*bidderapiv1.BidOption{ @@ -948,7 +949,7 @@ func TestShutterisedBidOptionsProcessing(t *testing.T) { }, }, } - } else if tc.name == "multiple shutterised bid options" { + case "multiple shutterised bid options": // Multiple shutterised options - parse comma-separated values identityPrefixes := strings.Split(tc.identityPrefix, ",") encryptedTxs := strings.Split(tc.encryptedTx, ",") @@ -973,13 +974,13 @@ func TestShutterisedBidOptionsProcessing(t *testing.T) { }, }, } - } else if tc.name == "invalid bid options marshaling" { + case "invalid bid options marshaling": // Invalid protobuf data bidOptionsBytes = []byte("invalid protobuf data") - } else if tc.name == "nil shutterised bid option" { + case "nil shutterised bid option": // Nil bid options for nil case bidderOpts = nil - } else { + default: // Single valid shutterised option bidderOpts = &bidderapiv1.BidOptions{ Options: []*bidderapiv1.BidOption{ diff --git a/tools/preconf-rpc/main.go b/tools/preconf-rpc/main.go index 982592014..c8f501884 100644 --- a/tools/preconf-rpc/main.go +++ b/tools/preconf-rpc/main.go @@ -292,6 +292,7 @@ func main() { optionBidderThreshold, optionBidderTopup, optionAuthToken, + optionSimulationURL, }, Action: func(c *cli.Context) error { logger, err := util.NewLogger( @@ -377,6 +378,7 @@ func main() { PricerAPIKey: c.String(optionBlocknativeAPIKey.Name), Webhooks: c.StringSlice(optionWebhookURLs.Name), Token: c.String(optionAuthToken.Name), + SimulatorURL: c.String(optionSimulationURL.Name), } s, err := service.New(&config) diff --git a/tools/preconf-rpc/sim/simulator.go b/tools/preconf-rpc/sim/simulator.go index 4b6f9dabb..41bfed69e 100644 --- a/tools/preconf-rpc/sim/simulator.go +++ b/tools/preconf-rpc/sim/simulator.go @@ -156,9 +156,7 @@ func parseResponse(body []byte) ([]*types.Log, error) { } func collectLogs(n *SimCall, acc *[]*types.Log) { - for _, log := range n.Logs { - *acc = append(*acc, log) - } + *acc = append(*acc, n.Logs...) for i := range n.Calls { collectLogs(&n.Calls[i], acc) } diff --git a/tools/preconf-rpc/sim/simulator_test.go b/tools/preconf-rpc/sim/simulator_test.go index 26571b209..e1c9f3efe 100644 --- a/tools/preconf-rpc/sim/simulator_test.go +++ b/tools/preconf-rpc/sim/simulator_test.go @@ -36,7 +36,7 @@ func TestSimulator(t *testing.T) { rawTx := req["raw"].(string) if resp, ok := txns[rawTx]; ok { w.Header().Set("Content-Type", "application/json") - w.Write(resp) + _, _ = w.Write(resp) return } http.Error(w, "transaction not found", http.StatusNotFound) From 5834d595f3972736de50c33f9380303e35332134 Mon Sep 17 00:00:00 2001 From: Alok Date: Sat, 25 Oct 2025 16:09:36 +0530 Subject: [PATCH 07/13] fix: pb gen issue --- p2p/gen/openapi/debugapi/v1/debugapi.swagger.yaml | 2 +- p2p/gen/openapi/notificationsapi/v1/notifications.swagger.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/p2p/gen/openapi/debugapi/v1/debugapi.swagger.yaml b/p2p/gen/openapi/debugapi/v1/debugapi.swagger.yaml index 7f1033586..6273729b4 100644 --- a/p2p/gen/openapi/debugapi/v1/debugapi.swagger.yaml +++ b/p2p/gen/openapi/debugapi/v1/debugapi.swagger.yaml @@ -86,7 +86,7 @@ definitions: `NullValue` is a singleton enumeration to represent the null value for the `Value` type union. - The JSON representation for `NullValue` is JSON `null`. + The JSON representation for `NullValue` is JSON `null`. v1CancelTransactionResponse: type: object example: diff --git a/p2p/gen/openapi/notificationsapi/v1/notifications.swagger.yaml b/p2p/gen/openapi/notificationsapi/v1/notifications.swagger.yaml index 0fc3d83c1..58f9c5163 100644 --- a/p2p/gen/openapi/notificationsapi/v1/notifications.swagger.yaml +++ b/p2p/gen/openapi/notificationsapi/v1/notifications.swagger.yaml @@ -123,7 +123,7 @@ definitions: `NullValue` is a singleton enumeration to represent the null value for the `Value` type union. - The JSON representation for `NullValue` is JSON `null`. + The JSON representation for `NullValue` is JSON `null`. v1Notification: type: object properties: From 94e248058e4c9b88ad64f86c902b394c7ceb69b4 Mon Sep 17 00:00:00 2001 From: Alok Nerurkar Date: Sun, 26 Oct 2025 00:11:02 +0530 Subject: [PATCH 08/13] feat: add simulation --- tools/preconf-rpc/service/service.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/preconf-rpc/service/service.go b/tools/preconf-rpc/service/service.go index 887e28db2..047d8e9c6 100644 --- a/tools/preconf-rpc/service/service.go +++ b/tools/preconf-rpc/service/service.go @@ -290,7 +290,7 @@ func New(config *Config) (*Service, error) { _, _ = w.Write([]byte("OK")) }) mux.Handle("/", rpcServer) - mux.HandleFunc("/{option...}", func(w http.ResponseWriter, r *http.Request) { + mux.HandleFunc("/opt/{option...}", func(w http.ResponseWriter, r *http.Request) { options := r.PathValue("option") splits := strings.Split(options, "/") From 65ffbc8ac9ef7918cc46d637184703c0ca564ba8 Mon Sep 17 00:00:00 2001 From: harshsingh1002 Date: Sun, 26 Oct 2025 00:31:35 +0530 Subject: [PATCH 09/13] chore: add simulation url --- .../charts/mev-commit-preconf-rpc/templates/deployment.yaml | 1 + infrastructure/charts/mev-commit-preconf-rpc/values.yaml | 1 + 2 files changed, 2 insertions(+) diff --git a/infrastructure/charts/mev-commit-preconf-rpc/templates/deployment.yaml b/infrastructure/charts/mev-commit-preconf-rpc/templates/deployment.yaml index b7e3ce7f9..002e21fe9 100644 --- a/infrastructure/charts/mev-commit-preconf-rpc/templates/deployment.yaml +++ b/infrastructure/charts/mev-commit-preconf-rpc/templates/deployment.yaml @@ -34,6 +34,7 @@ spec: - --log-tags={{ .Values.config.logTags }} {{- end }} - --keystore-dir={{ .Values.keystore.dir }} + - --simulation-url={{ .Values.config.simulationUrl }} - --keystore-password=$(KEYSTORE_PASSWORD) - --l1-rpc-urls={{ .Values.config.l1RpcUrls | join "," }} - --settlement-rpc-url={{ .Values.config.settlementRpcUrl }} diff --git a/infrastructure/charts/mev-commit-preconf-rpc/values.yaml b/infrastructure/charts/mev-commit-preconf-rpc/values.yaml index 0f6e81565..e48ad6f0a 100644 --- a/infrastructure/charts/mev-commit-preconf-rpc/values.yaml +++ b/infrastructure/charts/mev-commit-preconf-rpc/values.yaml @@ -63,6 +63,7 @@ config: httpPort: 8080 logLevel: "info" logFormat: "json" + simulationUrl: http://1.2.3.4 logTags: "" l1RpcUrls: - "" From c6f609e8b680e23a28503321c2769896c202dd9f Mon Sep 17 00:00:00 2001 From: Alok Nerurkar Date: Sun, 26 Oct 2025 17:02:10 +0530 Subject: [PATCH 10/13] fix: bid opts --- tools/preconf-rpc/handlers/handlers.go | 21 +++++++- tools/preconf-rpc/sender/sender.go | 23 ++------- tools/preconf-rpc/service/service.go | 71 +++++++++++++------------- 3 files changed, 59 insertions(+), 56 deletions(-) diff --git a/tools/preconf-rpc/handlers/handlers.go b/tools/preconf-rpc/handlers/handlers.go index aa8060f87..1ce0b637a 100644 --- a/tools/preconf-rpc/handlers/handlers.go +++ b/tools/preconf-rpc/handlers/handlers.go @@ -20,6 +20,8 @@ const ( bridgeLimitWei = 1000000000000000000 // 1 ETH ) +type positionConstraintKey struct{} + type Bidder interface { Estimate() (int64, error) } @@ -46,6 +48,15 @@ type Sender interface { CancelTransaction(ctx context.Context, txHash common.Hash) (bool, error) } +func SetPositionConstraint(ctx context.Context, constraint *bidderapiv1.PositionConstraint) context.Context { + return context.WithValue(ctx, positionConstraintKey{}, constraint) +} + +func getPositionConstraint(ctx context.Context) (*bidderapiv1.PositionConstraint, bool) { + value, ok := ctx.Value(positionConstraintKey{}).(*bidderapiv1.PositionConstraint) + return value, ok +} + type rpcMethodHandler struct { logger *slog.Logger pricer Pricer @@ -357,12 +368,18 @@ func (h *rpcMethodHandler) handleSendRawTx( } } - err = h.sndr.Enqueue(ctx, &sender.Transaction{ + txnToEnqueue := &sender.Transaction{ Transaction: txn, Raw: rawTxHex, Sender: txSender, Type: txType, - }) + } + constraint, ok := getPositionConstraint(ctx) + if ok { + txnToEnqueue.Constraint = constraint + } + + err = h.sndr.Enqueue(ctx, txnToEnqueue) if err != nil { h.logger.Error("Failed to enqueue transaction for sending", "error", err, "sender", txSender.Hex()) return nil, false, rpcserver.NewJSONErr( diff --git a/tools/preconf-rpc/sender/sender.go b/tools/preconf-rpc/sender/sender.go index 79d366e99..8f781e23d 100644 --- a/tools/preconf-rpc/sender/sender.go +++ b/tools/preconf-rpc/sender/sender.go @@ -58,8 +58,6 @@ var ( ErrMaxAttemptsPerBlockExceeded = errors.New("maximum attempts exceeded for transaction in the current block") ) -type PositionConstraintKey struct{} - type Transaction struct { *types.Transaction Sender common.Address @@ -68,6 +66,7 @@ type Transaction struct { Status TxStatus Details string BlockNumber int64 + Constraint *bidderapiv1.PositionConstraint } type Store interface { @@ -211,17 +210,6 @@ func validateTransaction(tx *Transaction) error { return nil } -func getConstraintFromCtx(ctx context.Context) *bidderapiv1.PositionConstraint { - optVal := ctx.Value(PositionConstraintKey{}) - if optVal != nil { - constraint, ok := optVal.(*bidderapiv1.PositionConstraint) - if ok { - return constraint - } - } - return nil -} - func (t *TxSender) hasLowerNonce(ctx context.Context, tx *Transaction) bool { currentNonce := t.store.GetCurrentNonce(ctx, tx.Sender) return tx.Nonce() < currentNonce @@ -669,11 +657,8 @@ func (t *TxSender) sendBid( cost, err := t.calculatePriceForNextBlock(txn, bidBlockNo, prices, optedInSlot) if err != nil { logger.Error("Failed to calculate price for next block", "error", err) - if errors.Is(err, ErrTimeoutExceeded) { - logger.Warn("Timeout exceeded while trying to process transaction") - return bidResult{}, ErrTimeoutExceeded - } - if errors.Is(err, ErrMaxAttemptsPerBlockExceeded) { + if errors.Is(err, ErrTimeoutExceeded) || errors.Is(err, ErrMaxAttemptsPerBlockExceeded) { + // We propagate these errors as is return bidResult{}, err } return bidResult{}, &errRetry{ @@ -731,7 +716,7 @@ func (t *TxSender) sendBid( BlockNumber: uint64(bidBlockNo), RevertingTxHashes: []string{txn.Hash().Hex()}, DecayDuration: t.getBidTimeout() * 2, - Constraint: getConstraintFromCtx(ctx), + Constraint: txn.Constraint, }, ) if err != nil { diff --git a/tools/preconf-rpc/service/service.go b/tools/preconf-rpc/service/service.go index 047d8e9c6..b9b6dd714 100644 --- a/tools/preconf-rpc/service/service.go +++ b/tools/preconf-rpc/service/service.go @@ -289,46 +289,47 @@ func New(config *Config) (*Service, error) { w.WriteHeader(http.StatusOK) _, _ = w.Write([]byte("OK")) }) - mux.Handle("/", rpcServer) - mux.HandleFunc("/opt/{option...}", func(w http.ResponseWriter, r *http.Request) { + mux.HandleFunc("/{option...}", func(w http.ResponseWriter, r *http.Request) { options := r.PathValue("option") - splits := strings.Split(options, "/") - if len(splits) != 3 { - http.Error(w, "invalid position constraint format", http.StatusBadRequest) - return - } - constraint := new(bidderapiv1.PositionConstraint) - switch splits[0] { - case "top": - constraint.Anchor = bidderapiv1.PositionConstraint_ANCHOR_TOP - case "bottom": - constraint.Anchor = bidderapiv1.PositionConstraint_ANCHOR_BOTTOM - default: - http.Error(w, "invalid position constraint", http.StatusBadRequest) - return - } + if options != "" { + splits := strings.Split(options, "/") + if len(splits) != 3 { + http.Error(w, "invalid position constraint format", http.StatusBadRequest) + return + } + constraint := new(bidderapiv1.PositionConstraint) + switch splits[0] { + case "top": + constraint.Anchor = bidderapiv1.PositionConstraint_ANCHOR_TOP + case "bottom": + constraint.Anchor = bidderapiv1.PositionConstraint_ANCHOR_BOTTOM + default: + http.Error(w, "invalid position constraint", http.StatusBadRequest) + return + } - switch splits[1] { - case "absolute": - constraint.Basis = bidderapiv1.PositionConstraint_BASIS_ABSOLUTE - case "percentile": - constraint.Basis = bidderapiv1.PositionConstraint_BASIS_PERCENTILE - case "gas_percentile": - constraint.Basis = bidderapiv1.PositionConstraint_BASIS_GAS_PERCENTILE - default: - http.Error(w, "invalid position constraint type", http.StatusBadRequest) - return - } + switch splits[1] { + case "absolute": + constraint.Basis = bidderapiv1.PositionConstraint_BASIS_ABSOLUTE + case "percentile": + constraint.Basis = bidderapiv1.PositionConstraint_BASIS_PERCENTILE + case "gas_percentile": + constraint.Basis = bidderapiv1.PositionConstraint_BASIS_GAS_PERCENTILE + default: + http.Error(w, "invalid position constraint type", http.StatusBadRequest) + return + } - value, err := strconv.Atoi(splits[2]) - if err != nil { - http.Error(w, "invalid position constraint value", http.StatusBadRequest) - return - } - constraint.Value = int32(value) + value, err := strconv.Atoi(splits[2]) + if err != nil { + http.Error(w, "invalid position constraint value", http.StatusBadRequest) + return + } + constraint.Value = int32(value) - r = r.WithContext(context.WithValue(r.Context(), sender.PositionConstraintKey{}, constraint)) + r = r.WithContext(handlers.SetPositionConstraint(r.Context(), constraint)) + } rpcServer.ServeHTTP(w, r) }) From c7843456a3bc5328a7ca2ae24292bf924a2e172d Mon Sep 17 00:00:00 2001 From: Alok Nerurkar Date: Tue, 28 Oct 2025 02:10:59 +0530 Subject: [PATCH 11/13] fix: bid opts --- tools/preconf-rpc/notifier/notifier.go | 43 ++++++++++++++++++++++++++ tools/preconf-rpc/store/store.go | 40 ++++++++++++++++++++---- tools/preconf-rpc/store/store_test.go | 5 +++ 3 files changed, 82 insertions(+), 6 deletions(-) diff --git a/tools/preconf-rpc/notifier/notifier.go b/tools/preconf-rpc/notifier/notifier.go index 185b134de..4060d5d02 100644 --- a/tools/preconf-rpc/notifier/notifier.go +++ b/tools/preconf-rpc/notifier/notifier.go @@ -10,11 +10,13 @@ import ( "log/slog" "math/big" "net/http" + "strings" "sync" "time" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/params" + bidderapiv1 "github.com/primev/mev-commit/p2p/gen/go/bidderapi/v1" "github.com/primev/mev-commit/tools/preconf-rpc/sender" "github.com/primev/mev-commit/x/util" ) @@ -222,6 +224,42 @@ func buildType(t txnInfo) string { } } +func buildConstraint(t txnInfo) string { + if t.txn.Constraint == nil { + return "None" + } + str := strings.Builder{} + switch t.txn.Constraint.Basis { + case bidderapiv1.PositionConstraint_BASIS_ABSOLUTE: + str.WriteString(fmt.Sprintf("Position %d", t.txn.Constraint.Value)) + switch t.txn.Constraint.Anchor { + case bidderapiv1.PositionConstraint_ANCHOR_TOP: + str.WriteString(" from Top") + case bidderapiv1.PositionConstraint_ANCHOR_BOTTOM: + str.WriteString(" from Bottom") + } + case bidderapiv1.PositionConstraint_BASIS_PERCENTILE: + switch t.txn.Constraint.Anchor { + case bidderapiv1.PositionConstraint_ANCHOR_TOP: + str.WriteString("Top ") + case bidderapiv1.PositionConstraint_ANCHOR_BOTTOM: + str.WriteString("Bottom ") + } + str.WriteString(fmt.Sprintf("%d%%", t.txn.Constraint.Value)) + case bidderapiv1.PositionConstraint_BASIS_GAS_PERCENTILE: + switch t.txn.Constraint.Anchor { + case bidderapiv1.PositionConstraint_ANCHOR_TOP: + str.WriteString("Top ") + case bidderapiv1.PositionConstraint_ANCHOR_BOTTOM: + str.WriteString("Bottom ") + } + str.WriteString(fmt.Sprintf("Gas Percentile %d%%", t.txn.Constraint.Value)) + default: + str.WriteString(" Unknown Basis") + } + return str.String() +} + func (n *Notifier) StartTransactionNotifier( ctx context.Context, ) <-chan struct{} { @@ -258,6 +296,11 @@ func (n *Notifier) StartTransactionNotifier( Field{Title: "Attempts", Value: fmt.Sprintf("%d", t.noOfAttempts), Short: true}, Field{Title: "Duration", Value: t.timeTaken.String(), Short: true}, ) + if t.txn.Constraint != nil { + fields = append(fields, + Field{Title: "Constraint", Value: buildConstraint(t), Short: true}, + ) + } } message := Message{ Text: "🚀 Transaction Report", diff --git a/tools/preconf-rpc/store/store.go b/tools/preconf-rpc/store/store.go index 513fbdba5..b6c26202d 100644 --- a/tools/preconf-rpc/store/store.go +++ b/tools/preconf-rpc/store/store.go @@ -31,7 +31,8 @@ CREATE TABLE IF NOT EXISTS mcTransactions ( sender TEXT, tx_type INTEGER, status TEXT, - details TEXT + details TEXT, + options BYTEA );` var commitmentsTable = ` @@ -90,11 +91,21 @@ func (s *rpcstore) Close() error { } func (s *rpcstore) AddQueuedTransaction(ctx context.Context, tx *sender.Transaction) error { + var ( + cBuf []byte + err error + ) + if tx.Constraint != nil { + cBuf, err = proto.Marshal(tx.Constraint) + if err != nil { + return fmt.Errorf("failed to marshal transaction constraint: %w", err) + } + } insertQuery := ` - INSERT INTO mcTransactions (hash, nonce, raw_transaction, sender, tx_type, status) - VALUES ($1, $2, $3, $4, $5, $6); + INSERT INTO mcTransactions (hash, nonce, raw_transaction, sender, tx_type, status, options) + VALUES ($1, $2, $3, $4, $5, $6, $7); ` - _, err := s.db.ExecContext( + _, err = s.db.ExecContext( ctx, insertQuery, tx.Hash().Hex(), @@ -103,6 +114,7 @@ func (s *rpcstore) AddQueuedTransaction(ctx context.Context, tx *sender.Transact tx.Sender.Hex(), int(tx.Type), string(sender.TxStatusPending), + cBuf, ) if err != nil { return fmt.Errorf("failed to add queued transaction: %w", err) @@ -121,8 +133,10 @@ func parseTransactionsFromRows(rows *sql.Rows) ([]*sender.Transaction, error) { blockNum sql.NullInt64 status string details sql.NullString + options []byte + pbOption *bidderapiv1.PositionConstraint ) - err := rows.Scan(&rawTransaction, &blockNum, &senderAddress, &txType, &status, &details) + err := rows.Scan(&rawTransaction, &blockNum, &senderAddress, &txType, &status, &details, &options) if err != nil { return nil, fmt.Errorf("failed to scan row: %w", err) } @@ -134,6 +148,11 @@ func parseTransactionsFromRows(rows *sql.Rows) ([]*sender.Transaction, error) { if err := parsedTxn.UnmarshalBinary(txStr); err != nil { return nil, fmt.Errorf("failed to unmarshal transaction: %w", err) } + if len(options) > 0 { + if err := proto.Unmarshal(options, pbOption); err != nil { + return nil, fmt.Errorf("failed to unmarshal transaction options: %w", err) + } + } txn := &sender.Transaction{ Transaction: parsedTxn, Raw: rawTransaction, @@ -142,6 +161,7 @@ func parseTransactionsFromRows(rows *sql.Rows) ([]*sender.Transaction, error) { Type: sender.TxType(txType), Status: sender.TxStatus(status), Details: details.String, + Constraint: pbOption, } transactions = append(transactions, txn) } @@ -155,7 +175,7 @@ func parseTransactionsFromRows(rows *sql.Rows) ([]*sender.Transaction, error) { // GetQueuedTransactions retrieves the next pending transaction for each sender. func (s *rpcstore) GetQueuedTransactions(ctx context.Context) ([]*sender.Transaction, error) { query := ` - SELECT t1.raw_transaction, t1.block_number, t1.sender, t1.tx_type, t1.status, t1.details + SELECT t1.raw_transaction, t1.block_number, t1.sender, t1.tx_type, t1.status, t1.details, t1.options FROM mcTransactions t1 INNER JOIN ( SELECT sender, MIN(nonce) AS min_nonce @@ -201,6 +221,8 @@ func (s *rpcstore) GetTransactionByHash(ctx context.Context, txnHash common.Hash status string blockNum sql.NullInt64 details sql.NullString + options []byte + pbOption *bidderapiv1.PositionConstraint ) err := row.Scan(&rawTransaction, &blockNum, &senderAddress, &txType, &status, &details) if err != nil { @@ -217,6 +239,11 @@ func (s *rpcstore) GetTransactionByHash(ctx context.Context, txnHash common.Hash if err := parsedTxn.UnmarshalBinary(txStr); err != nil { return nil, fmt.Errorf("failed to unmarshal transaction: %w", err) } + if len(options) > 0 { + if err := proto.Unmarshal(options, pbOption); err != nil { + return nil, fmt.Errorf("failed to unmarshal transaction options: %w", err) + } + } txn := &sender.Transaction{ Transaction: parsedTxn, Raw: rawTransaction, @@ -225,6 +252,7 @@ func (s *rpcstore) GetTransactionByHash(ctx context.Context, txnHash common.Hash Type: sender.TxType(txType), Status: sender.TxStatus(status), Details: details.String, + Constraint: pbOption, } return txn, nil diff --git a/tools/preconf-rpc/store/store_test.go b/tools/preconf-rpc/store/store_test.go index 498989fe1..eabbcbc2b 100644 --- a/tools/preconf-rpc/store/store_test.go +++ b/tools/preconf-rpc/store/store_test.go @@ -130,6 +130,11 @@ func TestStore(t *testing.T) { Sender: common.HexToAddress("0xabcdefabcdefabcdefabcdefabcdefabcdefabcd"), Type: sender.TxTypeRegular, Status: sender.TxStatusPending, + Constraint: &bidderapiv1.PositionConstraint{ + Anchor: bidderapiv1.PositionConstraint_ANCHOR_TOP, + Basis: bidderapiv1.PositionConstraint_BASIS_PERCENTILE, + Value: 10, + }, } commitments := []*bidderapiv1.Commitment{ From 6908e6def91d5d3f91641b2194ae3c10d2a2d402 Mon Sep 17 00:00:00 2001 From: Alok Nerurkar Date: Tue, 28 Oct 2025 02:13:06 +0530 Subject: [PATCH 12/13] fix: bid opts --- tools/preconf-rpc/sim/simulator.go | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/tools/preconf-rpc/sim/simulator.go b/tools/preconf-rpc/sim/simulator.go index 41bfed69e..36bb9c198 100644 --- a/tools/preconf-rpc/sim/simulator.go +++ b/tools/preconf-rpc/sim/simulator.go @@ -16,19 +16,6 @@ import ( "github.com/ethereum/go-ethereum/core/types" ) -type SimLog struct { - Address string `json:"address"` - Topics []string `json:"topics"` - Data string `json:"data"` - BlockHash *string `json:"blockHash"` - BlockNumber string `json:"blockNumber"` - BlockTimestamp string `json:"blockTimestamp"` - LogIndex string `json:"logIndex"` - Removed bool `json:"removed"` - TransactionHash string `json:"transactionHash"` - TransactionIndex string `json:"transactionIndex"` -} - type SimCall struct { Status string `json:"status"` GasUsed string `json:"gasUsed"` From d17b1437f55c373bfea5a2885071812b901c67fb Mon Sep 17 00:00:00 2001 From: Alok Date: Tue, 28 Oct 2025 15:36:04 +0530 Subject: [PATCH 13/13] fix: options to DB --- tools/preconf-rpc/store/store.go | 8 +++++--- tools/preconf-rpc/store/store_test.go | 8 ++++---- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/tools/preconf-rpc/store/store.go b/tools/preconf-rpc/store/store.go index b6c26202d..cd7198666 100644 --- a/tools/preconf-rpc/store/store.go +++ b/tools/preconf-rpc/store/store.go @@ -149,6 +149,7 @@ func parseTransactionsFromRows(rows *sql.Rows) ([]*sender.Transaction, error) { return nil, fmt.Errorf("failed to unmarshal transaction: %w", err) } if len(options) > 0 { + pbOption = &bidderapiv1.PositionConstraint{} if err := proto.Unmarshal(options, pbOption); err != nil { return nil, fmt.Errorf("failed to unmarshal transaction options: %w", err) } @@ -209,7 +210,7 @@ func (s *rpcstore) GetQueuedTransactions(ctx context.Context) ([]*sender.Transac func (s *rpcstore) GetTransactionByHash(ctx context.Context, txnHash common.Hash) (*sender.Transaction, error) { query := ` - SELECT raw_transaction, block_number, sender, tx_type, status, details + SELECT raw_transaction, block_number, sender, tx_type, status, details, options FROM mcTransactions WHERE hash = $1; ` @@ -224,7 +225,7 @@ func (s *rpcstore) GetTransactionByHash(ctx context.Context, txnHash common.Hash options []byte pbOption *bidderapiv1.PositionConstraint ) - err := row.Scan(&rawTransaction, &blockNum, &senderAddress, &txType, &status, &details) + err := row.Scan(&rawTransaction, &blockNum, &senderAddress, &txType, &status, &details, &options) if err != nil { if errors.Is(err, sql.ErrNoRows) { return nil, fmt.Errorf("transaction %s not found: %w", txnHash.Hex(), ErrNotFound) @@ -240,6 +241,7 @@ func (s *rpcstore) GetTransactionByHash(ctx context.Context, txnHash common.Hash return nil, fmt.Errorf("failed to unmarshal transaction: %w", err) } if len(options) > 0 { + pbOption = &bidderapiv1.PositionConstraint{} if err := proto.Unmarshal(options, pbOption); err != nil { return nil, fmt.Errorf("failed to unmarshal transaction options: %w", err) } @@ -260,7 +262,7 @@ func (s *rpcstore) GetTransactionByHash(ctx context.Context, txnHash common.Hash func (s *rpcstore) GetTransactionsForBlock(ctx context.Context, blockNumber int64) ([]*sender.Transaction, error) { query := ` - SELECT raw_transaction, block_number, sender, tx_type, status, details + SELECT raw_transaction, block_number, sender, tx_type, status, details, options FROM mcTransactions WHERE block_number = $1 AND status = 'pre-confirmed'; ` diff --git a/tools/preconf-rpc/store/store_test.go b/tools/preconf-rpc/store/store_test.go index eabbcbc2b..aca5e97fa 100644 --- a/tools/preconf-rpc/store/store_test.go +++ b/tools/preconf-rpc/store/store_test.go @@ -205,7 +205,7 @@ func TestStore(t *testing.T) { if len(retrievedTxns) != 1 { t.Fatalf("expected 1 queued transaction, got %d", len(retrievedTxns)) } - if diff := cmp.Diff(wrappedTxn1, retrievedTxns[0], cmpopts.IgnoreUnexported(sender.Transaction{}, types.Transaction{})); diff != "" { + if diff := cmp.Diff(wrappedTxn1, retrievedTxns[0], cmpopts.IgnoreUnexported(sender.Transaction{}, types.Transaction{}, bidderapiv1.PositionConstraint{})); diff != "" { t.Fatalf("queued transaction mismatch (-want +got):\n%s", diff) } }) @@ -227,7 +227,7 @@ func TestStore(t *testing.T) { t.Errorf("expected 2 commitments, got %d", len(commitments)) } for i, commitment := range commitments { - if diff := cmp.Diff(commitment, commitments[i], cmpopts.IgnoreUnexported(bidderapiv1.Commitment{}, types.Transaction{})); diff != "" { + if diff := cmp.Diff(commitment, commitments[i], cmpopts.IgnoreUnexported(bidderapiv1.Commitment{}, types.Transaction{}, bidderapiv1.PositionConstraint{})); diff != "" { t.Errorf("commitment mismatch (-want +got):\n%s", diff) } } @@ -239,7 +239,7 @@ func TestStore(t *testing.T) { if len(nextTxns) != 1 { t.Errorf("expected 1 queued transaction, got %d", len(nextTxns)) } - if diff := cmp.Diff(wrappedTxn2, nextTxns[0], cmpopts.IgnoreUnexported(sender.Transaction{}, types.Transaction{})); diff != "" { + if diff := cmp.Diff(wrappedTxn2, nextTxns[0], cmpopts.IgnoreUnexported(sender.Transaction{}, types.Transaction{}, bidderapiv1.PositionConstraint{})); diff != "" { t.Errorf("queued transaction mismatch (-want +got):\n%s", diff) } @@ -275,7 +275,7 @@ func TestStore(t *testing.T) { t.Errorf("failed to get failed transaction by hash: %v", err) } - if diff := cmp.Diff(wrappedTxn2, failedTxn, cmpopts.IgnoreUnexported(sender.Transaction{}, types.Transaction{})); diff != "" { + if diff := cmp.Diff(wrappedTxn2, failedTxn, cmpopts.IgnoreUnexported(sender.Transaction{}, types.Transaction{}, bidderapiv1.PositionConstraint{})); diff != "" { t.Errorf("failed transaction mismatch (-want +got):\n%s", diff) }