From dc73f5c2df4e79db6120532df4644e9f15308380 Mon Sep 17 00:00:00 2001 From: Alok Nerurkar Date: Thu, 6 Nov 2025 10:50:02 +0530 Subject: [PATCH 01/16] fix: rpc improvements --- .../preconf-rpc/blocktracker/blocktracker.go | 106 ++++----- tools/preconf-rpc/sender/sender.go | 221 ++++++++++++++++++ 2 files changed, 271 insertions(+), 56 deletions(-) diff --git a/tools/preconf-rpc/blocktracker/blocktracker.go b/tools/preconf-rpc/blocktracker/blocktracker.go index 3c6780efa..a6ad30e01 100644 --- a/tools/preconf-rpc/blocktracker/blocktracker.go +++ b/tools/preconf-rpc/blocktracker/blocktracker.go @@ -5,13 +5,13 @@ import ( "errors" "log/slog" "math/big" - "sync" "sync/atomic" "time" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" lru "github.com/hashicorp/golang-lru/v2" + "golang.org/x/sync/errgroup" ) type EthClient interface { @@ -24,7 +24,8 @@ type blockTracker struct { blocks *lru.Cache[uint64, *types.Block] client EthClient log *slog.Logger - checkCond *sync.Cond + txnsToCheck map[common.Hash]chan uint64 + newBlockChan chan uint64 } func NewBlockTracker(client EthClient, log *slog.Logger) (*blockTracker, error) { @@ -38,27 +39,28 @@ func NewBlockTracker(client EthClient, log *slog.Logger) (*blockTracker, error) blocks: cache, client: client, log: log, - checkCond: sync.NewCond(&sync.Mutex{}), + txnsToCheck: make(map[common.Hash]chan uint64), + newBlockChan: make(chan uint64, 1), }, nil } func (b *blockTracker) Start(ctx context.Context) <-chan struct{} { done := make(chan struct{}) - ticker := time.NewTicker(500 * time.Millisecond) - go func() { - defer close(done) + eg, egCtx := errgroup.WithContext(ctx) + eg.Go(func() error { + ticker := time.NewTicker(500 * time.Millisecond) for { select { - case <-ctx.Done(): - return + case <-egCtx.Done(): + return egCtx.Err() case <-ticker.C: - blockNo, err := b.client.BlockNumber(ctx) + blockNo, err := b.client.BlockNumber(egCtx) if err != nil { b.log.Error("Failed to get block number", "error", err) continue } if blockNo > b.latestBlockNo.Load() { - block, err := b.client.BlockByNumber(ctx, big.NewInt(int64(blockNo))) + block, err := b.client.BlockByNumber(egCtx, big.NewInt(int64(blockNo))) if err != nil { b.log.Error("Failed to get block by number", "error", err) continue @@ -70,14 +72,43 @@ func (b *blockTracker) Start(ctx context.Context) <-chan struct{} { } } } + }) + eg.Go(func() error { + for { + select { + case <-egCtx.Done(): + return egCtx.Err() + case bNo := <-b.newBlockChan: + block, ok := b.blocks.Get(bNo) + if !ok { + b.log.Error("Block not found in cache", "blockNumber", bNo) + continue + } + for txHash, resultCh := range b.txnsToCheck { + if txn := block.Transaction(txHash); txn != nil { + resultCh <- bNo + delete(b.txnsToCheck, txHash) + } + } + } + } + }) + + go func() { + defer close(done) + if err := eg.Wait(); err != nil { + b.log.Error("Block tracker exited with error", "error", err) + } }() + return done } func (b *blockTracker) triggerCheck() { - b.checkCond.L.Lock() - b.checkCond.Broadcast() - b.checkCond.L.Unlock() + select { + case b.newBlockChan <- b.latestBlockNo.Load(): + default: + } } func (b *blockTracker) LatestBlockNumber() uint64 { @@ -91,54 +122,17 @@ func (b *blockTracker) NextBlockNumber() (uint64, time.Duration, error) { return 0, 0, errors.New("latest block not found in cache") } blockTime := time.Unix(int64(block.Time()), 0) - if time.Since(blockTime) >= 11*time.Second { + if time.Since(blockTime) >= 12*time.Second { return latestBlockNo + 2, time.Until(blockTime.Add(24 * time.Second)), nil } return latestBlockNo + 1, time.Until(blockTime.Add(12 * time.Second)), nil } -func (b *blockTracker) CheckTxnInclusion( +func (b *blockTracker) WaitForTxnInclusion( ctx context.Context, txHash common.Hash, - blockNumber uint64, -) (bool, error) { - if blockNumber <= b.latestBlockNo.Load() { - return b.checkTxnInclusion(ctx, txHash, blockNumber) - } - - waitCh := make(chan struct{}) - go func() { - b.checkCond.L.Lock() - defer b.checkCond.L.Unlock() - for blockNumber > b.latestBlockNo.Load() { - b.checkCond.Wait() - } - close(waitCh) - }() - - select { - case <-ctx.Done(): - return false, ctx.Err() - case <-waitCh: - return b.checkTxnInclusion(ctx, txHash, blockNumber) - } -} - -func (b *blockTracker) checkTxnInclusion(ctx context.Context, txHash common.Hash, blockNumber uint64) (bool, error) { - var err error - block, ok := b.blocks.Get(blockNumber) - if !ok { - block, err = b.client.BlockByNumber(ctx, big.NewInt(int64(blockNumber))) - if err != nil { - b.log.Error("Failed to get block by number", "error", err, "blockNumber", blockNumber) - return false, err - } - _ = b.blocks.Add(blockNumber, block) - } - - if txn := block.Transaction(txHash); txn != nil { - return true, nil - } - - return false, nil +) chan uint64 { + resultCh := make(chan uint64, 1) + b.txnsToCheck[txHash] = resultCh + return resultCh } diff --git a/tools/preconf-rpc/sender/sender.go b/tools/preconf-rpc/sender/sender.go index 8f781e23d..4e4c78682 100644 --- a/tools/preconf-rpc/sender/sender.go +++ b/tools/preconf-rpc/sender/sender.go @@ -44,6 +44,7 @@ const ( confidenceSubsequentAttempts = 99 // confidence level for subsequent attempts transactionTimeout = 10 * time.Minute // timeout for transaction processing maxAttemptsPerBlock = 10 // maximum attempts per block + defaultRetryDelay = 500 * time.Millisecond ) var ( @@ -583,6 +584,226 @@ BID_LOOP: return nil } +func (t *TxSender) processTransaction2(ctx context.Context, txn *Transaction, cancel <-chan struct{}) error { + var ( + result bidResult + err error + ) + logger := t.logger.With( + "transactionHash", txn.Hash().Hex(), + "sender", txn.Sender.Hex(), + "type", txn.Type, + ) + + retryTicker := time.NewTicker(defaultRetryDelay) + defer retryTicker.Stop() + + for { + result, err = t.sendBid(ctx, txn) + switch { + case err != nil: + if retryErr, ok := err.(*errRetry); ok { + logger.Warn( + "Retrying bid due to error", + "error", retryErr.err, + "retryAfter", retryErr.retryAfter, + ) + retryTicker.Reset(retryErr.retryAfter) + } else if errors.Is(err, ErrMaxAttemptsPerBlockExceeded) { + retryTicker.Reset(result.timeUntillNextBlock + 500*time.Millisecond) + } else { + return err + } + 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) + logger.Info( + "Transaction fast-tracked based on commitments", + "blockNumber", result.blockNumber, + "bidAmount", result.bidAmount.String(), + ) + if err := t.store.StoreTransaction(ctx, txn, result.commitments, result.logs); err != nil { + return fmt.Errorf("failed to store fast-tracked transaction: %w", err) + } + retryTicker.Reset(result.timeUntillNextBlock + 500*time.Millisecond) + case result.noOfProviders == len(result.commitments): + if result.optedInSlot { + // This means that all builders have committed to the bid and it + // is a primev opted in slot. We can safely proceed to inform the + // user that the txn was successfully sent and will be processed + txn.Status = TxStatusPreConfirmed + txn.BlockNumber = int64(result.blockNumber) + logger.Info( + "Transaction pre-confirmed", + "blockNumber", result.blockNumber, + "bidAmount", result.bidAmount.String(), + ) + if err := t.store.StoreTransaction(ctx, txn, result.commitments, result.logs); err != nil { + return fmt.Errorf("failed to store preconfirmed transaction: %w", err) + } + } + retryTicker.Reset(result.timeUntillNextBlock + 500*time.Millisecond) + default: + logger.Warn( + "Not all builders committed to the bid", + "noOfProviders", result.noOfProviders, + "noOfCommitments", len(result.commitments), + "blockNumber", result.blockNumber, + "bidAmount", result.bidAmount.String(), + ) + retryTicker.Reset(defaultRetryDelay) + } + select { + case <-ctx.Done(): + return ctx.Err() + case <-cancel: + return ErrTransactionCancelled + case <-retryTicker.C: + // Continue to the next iteration after the retry delay + } + } +BID_LOOP: + for { + select { + case <-ctx.Done(): + return ctx.Err() + case <-cancel: + return ErrTransactionCancelled + default: + } + + preConfirmed := false + maxAttemptsPerBlockExceeded := false + + result, err = t.sendBid(ctx, txn) + switch { + case err != nil: + if retryErr, ok := err.(*errRetry); ok { + logger.Warn( + "Retrying bid due to error", + "error", retryErr.err, + "retryAfter", retryErr.retryAfter, + ) + select { + case <-ctx.Done(): + return ctx.Err() + case <-time.After(retryErr.retryAfter): + // Wait for the specified retry duration before retrying + case <-cancel: + return ErrTransactionCancelled + } + continue + } + // 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) + logger.Info( + "Transaction fast-tracked based on commitments", + "blockNumber", result.blockNumber, + "bidAmount", result.bidAmount.String(), + ) + 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 + case result.optedInSlot: + if result.noOfProviders == len(result.commitments) { + // This means that all builders have committed to the bid and it + // is a primev opted in slot. We can safely proceed to inform the + // user that the txn was successfully sent and will be processed + txn.Status = TxStatusPreConfirmed + txn.BlockNumber = int64(result.blockNumber) + logger.Info( + "Transaction pre-confirmed", + "blockNumber", result.blockNumber, + "bidAmount", result.bidAmount.String(), + ) + 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 + } + default: + } + + if !preConfirmed && result.noOfProviders > len(result.commitments) && !maxAttemptsPerBlockExceeded { + logger.Warn( + "Not all builders committed to the bid", + "noOfProviders", result.noOfProviders, + "noOfCommitments", len(result.commitments), + "blockNumber", result.blockNumber, + "bidAmount", result.bidAmount.String(), + ) + 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 2 seconds left before the next block + continue + } + } + + // Wait for block number to be updated to confirm transaction. If failed + // we will retry the bid process till user cancels the operation + included, err := t.blockTracker.CheckTxnInclusion(ctx, txn.Hash(), result.blockNumber) + if err != nil { + 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) + logger.Info( + "Transaction confirmed", + "blockNumber", result.blockNumber, + "bidAmount", result.bidAmount.String(), + ) + if err := t.store.StoreTransaction(ctx, txn, result.commitments, result.logs); err != nil { + return fmt.Errorf("failed to store preconfirmed transaction: %w", err) + } + } + endTime := time.Now() + if len(result.commitments) > 0 { + endTime = time.UnixMilli(result.commitments[len(result.commitments)-1].DispatchTimestamp) + } + t.clearBlockAttemptHistory(txn, endTime) + break 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", "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", "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", "error", err) + return fmt.Errorf("failed to transfer funds for instant bridge: %w", err) + } + } + + return nil +} + type errRetry struct { err error retryAfter time.Duration From e30c6ea207d6c25fcd8a1a1db466316359928bb6 Mon Sep 17 00:00:00 2001 From: Alok Date: Thu, 6 Nov 2025 17:13:29 +0530 Subject: [PATCH 02/16] fix: fixes for RPC --- .../preconf-rpc/blocktracker/blocktracker.go | 15 +- tools/preconf-rpc/sender/sender.go | 260 +----------------- 2 files changed, 13 insertions(+), 262 deletions(-) diff --git a/tools/preconf-rpc/blocktracker/blocktracker.go b/tools/preconf-rpc/blocktracker/blocktracker.go index a6ad30e01..ca62d1768 100644 --- a/tools/preconf-rpc/blocktracker/blocktracker.go +++ b/tools/preconf-rpc/blocktracker/blocktracker.go @@ -67,7 +67,11 @@ func (b *blockTracker) Start(ctx context.Context) <-chan struct{} { } _ = b.blocks.Add(blockNo, block) b.latestBlockNo.Store(block.NumberU64()) - b.triggerCheck() + select { + case b.newBlockChan <- blockNo: + case <-egCtx.Done(): + return egCtx.Err() + } b.log.Debug("New block detected", "number", block.NumberU64(), "hash", block.Hash().Hex()) } } @@ -87,6 +91,7 @@ func (b *blockTracker) Start(ctx context.Context) <-chan struct{} { for txHash, resultCh := range b.txnsToCheck { if txn := block.Transaction(txHash); txn != nil { resultCh <- bNo + close(resultCh) delete(b.txnsToCheck, txHash) } } @@ -104,13 +109,6 @@ func (b *blockTracker) Start(ctx context.Context) <-chan struct{} { return done } -func (b *blockTracker) triggerCheck() { - select { - case b.newBlockChan <- b.latestBlockNo.Load(): - default: - } -} - func (b *blockTracker) LatestBlockNumber() uint64 { return b.latestBlockNo.Load() } @@ -129,7 +127,6 @@ func (b *blockTracker) NextBlockNumber() (uint64, time.Duration, error) { } func (b *blockTracker) WaitForTxnInclusion( - ctx context.Context, txHash common.Hash, ) chan uint64 { resultCh := make(chan uint64, 1) diff --git a/tools/preconf-rpc/sender/sender.go b/tools/preconf-rpc/sender/sender.go index 4e4c78682..c0d388ddf 100644 --- a/tools/preconf-rpc/sender/sender.go +++ b/tools/preconf-rpc/sender/sender.go @@ -97,7 +97,7 @@ type Pricer interface { } type BlockTracker interface { - CheckTxnInclusion(ctx context.Context, txnHash common.Hash, blockNumber uint64) (bool, error) + WaitForTxnInclusion(txnHash common.Hash) chan uint64 NextBlockNumber() (uint64, time.Duration, error) } @@ -444,159 +444,10 @@ func (t *TxSender) processTransaction(ctx context.Context, txn *Transaction, can "sender", txn.Sender.Hex(), "type", txn.Type, ) -BID_LOOP: - for { - select { - case <-ctx.Done(): - return ctx.Err() - case <-cancel: - return ErrTransactionCancelled - default: - } - - preConfirmed := false - maxAttemptsPerBlockExceeded := false - - result, err = t.sendBid(ctx, txn) - switch { - case err != nil: - if retryErr, ok := err.(*errRetry); ok { - logger.Warn( - "Retrying bid due to error", - "error", retryErr.err, - "retryAfter", retryErr.retryAfter, - ) - select { - case <-ctx.Done(): - return ctx.Err() - case <-time.After(retryErr.retryAfter): - // Wait for the specified retry duration before retrying - case <-cancel: - return ErrTransactionCancelled - } - continue - } - // 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) - logger.Info( - "Transaction fast-tracked based on commitments", - "blockNumber", result.blockNumber, - "bidAmount", result.bidAmount.String(), - ) - 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 - case result.optedInSlot: - if result.noOfProviders == len(result.commitments) { - // This means that all builders have committed to the bid and it - // is a primev opted in slot. We can safely proceed to inform the - // user that the txn was successfully sent and will be processed - txn.Status = TxStatusPreConfirmed - txn.BlockNumber = int64(result.blockNumber) - logger.Info( - "Transaction pre-confirmed", - "blockNumber", result.blockNumber, - "bidAmount", result.bidAmount.String(), - ) - 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 - } - default: - } - - if !preConfirmed && result.noOfProviders > len(result.commitments) && !maxAttemptsPerBlockExceeded { - logger.Warn( - "Not all builders committed to the bid", - "noOfProviders", result.noOfProviders, - "noOfCommitments", len(result.commitments), - "blockNumber", result.blockNumber, - "bidAmount", result.bidAmount.String(), - ) - 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 2 seconds left before the next block - continue - } - } - - // Wait for block number to be updated to confirm transaction. If failed - // we will retry the bid process till user cancels the operation - included, err := t.blockTracker.CheckTxnInclusion(ctx, txn.Hash(), result.blockNumber) - if err != nil { - 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) - logger.Info( - "Transaction confirmed", - "blockNumber", result.blockNumber, - "bidAmount", result.bidAmount.String(), - ) - if err := t.store.StoreTransaction(ctx, txn, result.commitments, result.logs); err != nil { - return fmt.Errorf("failed to store preconfirmed transaction: %w", err) - } - } - endTime := time.Now() - if len(result.commitments) > 0 { - endTime = time.UnixMilli(result.commitments[len(result.commitments)-1].DispatchTimestamp) - } - t.clearBlockAttemptHistory(txn, endTime) - break 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", "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", "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", "error", err) - return fmt.Errorf("failed to transfer funds for instant bridge: %w", err) - } - } - - return nil -} - -func (t *TxSender) processTransaction2(ctx context.Context, txn *Transaction, cancel <-chan struct{}) error { - var ( - result bidResult - err error - ) - logger := t.logger.With( - "transactionHash", txn.Hash().Hex(), - "sender", txn.Sender.Hex(), - "type", txn.Type, - ) retryTicker := time.NewTicker(defaultRetryDelay) defer retryTicker.Stop() + inclusion := t.blockTracker.WaitForTxnInclusion(txn.Hash()) for { result, err = t.sendBid(ctx, txn) @@ -627,7 +478,7 @@ func (t *TxSender) processTransaction2(ctx context.Context, txn *Transaction, ca if err := t.store.StoreTransaction(ctx, txn, result.commitments, result.logs); err != nil { return fmt.Errorf("failed to store fast-tracked transaction: %w", err) } - retryTicker.Reset(result.timeUntillNextBlock + 500*time.Millisecond) + retryTicker.Reset(result.timeUntillNextBlock + 1*time.Second) case result.noOfProviders == len(result.commitments): if result.optedInSlot { // This means that all builders have committed to the bid and it @@ -644,7 +495,7 @@ func (t *TxSender) processTransaction2(ctx context.Context, txn *Transaction, ca return fmt.Errorf("failed to store preconfirmed transaction: %w", err) } } - retryTicker.Reset(result.timeUntillNextBlock + 500*time.Millisecond) + retryTicker.Reset(result.timeUntillNextBlock + 1*time.Second) default: logger.Warn( "Not all builders committed to the bid", @@ -662,105 +513,8 @@ func (t *TxSender) processTransaction2(ctx context.Context, txn *Transaction, ca return ErrTransactionCancelled case <-retryTicker.C: // Continue to the next iteration after the retry delay - } - } -BID_LOOP: - for { - select { - case <-ctx.Done(): - return ctx.Err() - case <-cancel: - return ErrTransactionCancelled - default: - } - - preConfirmed := false - maxAttemptsPerBlockExceeded := false - - result, err = t.sendBid(ctx, txn) - switch { - case err != nil: - if retryErr, ok := err.(*errRetry); ok { - logger.Warn( - "Retrying bid due to error", - "error", retryErr.err, - "retryAfter", retryErr.retryAfter, - ) - select { - case <-ctx.Done(): - return ctx.Err() - case <-time.After(retryErr.retryAfter): - // Wait for the specified retry duration before retrying - case <-cancel: - return ErrTransactionCancelled - } - continue - } - // 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) - logger.Info( - "Transaction fast-tracked based on commitments", - "blockNumber", result.blockNumber, - "bidAmount", result.bidAmount.String(), - ) - 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 - case result.optedInSlot: - if result.noOfProviders == len(result.commitments) { - // This means that all builders have committed to the bid and it - // is a primev opted in slot. We can safely proceed to inform the - // user that the txn was successfully sent and will be processed - txn.Status = TxStatusPreConfirmed - txn.BlockNumber = int64(result.blockNumber) - logger.Info( - "Transaction pre-confirmed", - "blockNumber", result.blockNumber, - "bidAmount", result.bidAmount.String(), - ) - 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 - } - default: - } - - if !preConfirmed && result.noOfProviders > len(result.commitments) && !maxAttemptsPerBlockExceeded { - logger.Warn( - "Not all builders committed to the bid", - "noOfProviders", result.noOfProviders, - "noOfCommitments", len(result.commitments), - "blockNumber", result.blockNumber, - "bidAmount", result.bidAmount.String(), - ) - 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 2 seconds left before the next block - continue - } - } - - // Wait for block number to be updated to confirm transaction. If failed - // we will retry the bid process till user cancels the operation - included, err := t.blockTracker.CheckTxnInclusion(ctx, txn.Hash(), result.blockNumber) - if err != nil { - logger.Error("Failed to check transaction inclusion", "error", err) - return fmt.Errorf("failed to check transaction inclusion: %w", err) - } - if included { - if !preConfirmed { + case <-inclusion: + if txn.Status != TxStatusPreConfirmed { txn.Status = TxStatusConfirmed txn.BlockNumber = int64(result.blockNumber) logger.Info( @@ -777,7 +531,7 @@ BID_LOOP: endTime = time.UnixMilli(result.commitments[len(result.commitments)-1].DispatchTimestamp) } t.clearBlockAttemptHistory(txn, endTime) - break BID_LOOP + break } } From b340dfe34dbe763a359bdd0c01a80336f1aed9de Mon Sep 17 00:00:00 2001 From: Alok Date: Fri, 7 Nov 2025 17:44:08 +0530 Subject: [PATCH 03/16] fix: fixes for RPC --- .../blocktracker/blocktracker_test.go | 30 +++++---- tools/preconf-rpc/sender/sender.go | 12 ++-- tools/preconf-rpc/sender/sender_test.go | 61 ++++++------------- 3 files changed, 44 insertions(+), 59 deletions(-) diff --git a/tools/preconf-rpc/blocktracker/blocktracker_test.go b/tools/preconf-rpc/blocktracker/blocktracker_test.go index 54f395de9..9f58ee114 100644 --- a/tools/preconf-rpc/blocktracker/blocktracker_test.go +++ b/tools/preconf-rpc/blocktracker/blocktracker_test.go @@ -112,6 +112,11 @@ func TestBlockTracker(t *testing.T) { t.Fatalf("Expected latest block number to be 0, got %d", blkNo) } + included1 := tracker.WaitForTxnInclusion(tx1.Hash()) + included2 := tracker.WaitForTxnInclusion(tx2.Hash()) + included3 := tracker.WaitForTxnInclusion(tx3.Hash()) + included4 := tracker.WaitForTxnInclusion(tx4.Hash()) + client.blockNumber <- 100 start := time.Now() @@ -134,13 +139,13 @@ func TestBlockTracker(t *testing.T) { time.Sleep(100 * time.Millisecond) } - included, err := tracker.CheckTxnInclusion(ctx, tx1.Hash(), 100) - if err != nil { - t.Fatalf("Error checking transaction inclusion: %v", err) + bNo1 := <-included1 + if bNo1 != 100 { + t.Fatalf("Expected transaction %s to be included in block 100, got %d", tx1.Hash().Hex(), bNo1) } - - if !included { - t.Fatalf("Expected transaction %s to be included in block 100", tx1.Hash().Hex()) + bNo2 := <-included2 + if bNo2 != 100 { + t.Fatalf("Expected transaction %s to be included in block 100, got %d", tx2.Hash().Hex(), bNo2) } blkNo = tracker.LatestBlockNumber() @@ -166,13 +171,16 @@ func TestBlockTracker(t *testing.T) { time.Sleep(100 * time.Millisecond) } - included, err = tracker.CheckTxnInclusion(ctx, tx4.Hash(), 101) - if err != nil { - t.Fatalf("Error checking transaction inclusion: %v", err) + bNo3 := <-included3 + if bNo3 != 101 { + t.Fatalf("Expected transaction %s to be included in block 101, got %d", tx3.Hash().Hex(), bNo3) } - if included { - t.Fatalf("Expected transaction %s not to be included in block 101", tx4.Hash().Hex()) + select { + case bNo4 := <-included4: + t.Fatalf("Did not expect transaction %s to be included, but got block number %d", tx4.Hash().Hex(), bNo4) + case <-time.After(1 * time.Second): + // Expected timeout } cancel() diff --git a/tools/preconf-rpc/sender/sender.go b/tools/preconf-rpc/sender/sender.go index c0d388ddf..ae195e117 100644 --- a/tools/preconf-rpc/sender/sender.go +++ b/tools/preconf-rpc/sender/sender.go @@ -110,8 +110,9 @@ type Simulator interface { } type blockAttempt struct { - blockNumber uint64 - attempts int + blockNumber uint64 + attempts int + committedProviders []common.Address } type txnAttempt struct { @@ -449,6 +450,7 @@ func (t *TxSender) processTransaction(ctx context.Context, txn *Transaction, can defer retryTicker.Stop() inclusion := t.blockTracker.WaitForTxnInclusion(txn.Hash()) +BID_LOOP: for { result, err = t.sendBid(ctx, txn) switch { @@ -531,7 +533,7 @@ func (t *TxSender) processTransaction(ctx context.Context, txn *Transaction, can endTime = time.UnixMilli(result.commitments[len(result.commitments)-1].DispatchTimestamp) } t.clearBlockAttemptHistory(txn, endTime) - break + break BID_LOOP } } @@ -613,11 +615,11 @@ func (t *TxSender) sendBid( } } - if timeUntilNextBlock <= time.Second { + if timeUntilNextBlock <= 500*time.Millisecond { 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, + retryAfter: defaultRetryDelay, } } diff --git a/tools/preconf-rpc/sender/sender_test.go b/tools/preconf-rpc/sender/sender_test.go index 806e91b6b..e0f22affa 100644 --- a/tools/preconf-rpc/sender/sender_test.go +++ b/tools/preconf-rpc/sender/sender_test.go @@ -3,6 +3,7 @@ package sender_test import ( "context" "errors" + "io" "math/big" "os" "sync" @@ -212,35 +213,25 @@ func (m *mockPricer) EstimatePrice(ctx context.Context) map[int64]float64 { } } -type op struct { - hash common.Hash - block uint64 -} - type blockNoOp struct { block uint64 timeTillNextBlock time.Duration } type mockBlockTracker struct { - in chan op - out chan bool + out chan uint64 bnIn chan struct{} bnOut chan blockNoOp bnErr chan error } -func (m *mockBlockTracker) CheckTxnInclusion(ctx context.Context, txnHash common.Hash, blockNumber uint64) (bool, error) { - m.in <- op{ - hash: txnHash, - block: blockNumber, - } - select { - case included := <-m.out: - return included, nil - case <-ctx.Done(): - return false, ctx.Err() - } +func (m *mockBlockTracker) WaitForTxnInclusion(txnHash common.Hash) chan uint64 { + includedCh := make(chan uint64, 1) + go func() { + included := <-m.out + includedCh <- included + }() + return includedCh } func (m *mockBlockTracker) NextBlockNumber() (uint64, time.Duration, error) { @@ -287,8 +278,7 @@ func TestSender(t *testing.T) { out: make(chan chan optinbidder.BidStatus, 10), } blockTracker := &mockBlockTracker{ - in: make(chan op, 10), - out: make(chan bool, 10), + out: make(chan uint64, 10), bnIn: make(chan struct{}, 10), bnOut: make(chan blockNoOp, 10), bnErr: make(chan error, 1), @@ -304,7 +294,7 @@ func TestSender(t *testing.T) { notifier, &mockSimulator{}, big.NewInt(1), // Settlement chain ID - util.NewTestLogger(os.Stdout), + util.NewTestLogger(io.Discard), ) if err != nil { t.Fatalf("failed to create sender: %v", err) @@ -358,6 +348,9 @@ func TestSender(t *testing.T) { 99: 2.0, } + // Simulate transaction inclusion + blockTracker.out <- 1 + // Simulate a bid response bidOp := <-bidder.in if bidOp.rawTx != tx1.Raw[2:] { @@ -408,16 +401,6 @@ func TestSender(t *testing.T) { t.Fatalf("expected 1 commitment, got %d", len(res.commitments)) } - checkOp := <-blockTracker.in - if checkOp.hash != tx1.Hash() { - t.Fatalf("expected transaction hash %s, got %s", tx1.Hash().Hex(), checkOp.hash.Hex()) - } - if checkOp.block != 1 { - t.Fatalf("expected block number 1, got %d", checkOp.block) - } - // Simulate transaction inclusion - blockTracker.out <- true - tx2 := &sender.Transaction{ Transaction: types.NewTransaction( 2, @@ -486,6 +469,9 @@ func TestSender(t *testing.T) { 99: 2.0, } + // Simulate transaction inclusion + blockTracker.out <- 2 + // Simulate a bid response bidOp = <-bidder.in if bidOp.rawTx != tx2.Raw[2:] { @@ -512,16 +498,6 @@ func TestSender(t *testing.T) { close(resC) bidder.out <- resC - checkOp = <-blockTracker.in - if checkOp.hash != tx2.Hash() { - t.Fatalf("expected transaction hash %s, got %s", tx2.Hash().Hex(), checkOp.hash.Hex()) - } - if checkOp.block != 2 { - t.Fatalf("expected block number 2, got %d", checkOp.block) - } - // Simulate transaction inclusion - blockTracker.out <- true - res = <-st.preconfirmedTxns if res.txn == nil { t.Fatal("expected a preconfirmed transaction, got nil") @@ -567,8 +543,7 @@ func TestCancelTransaction(t *testing.T) { out: make(chan chan optinbidder.BidStatus, 10), } blockTracker := &mockBlockTracker{ - in: make(chan op, 10), - out: make(chan bool, 10), + out: make(chan uint64, 10), bnIn: make(chan struct{}, 10), bnOut: make(chan blockNoOp, 10), bnErr: make(chan error, 3), From cfeb7698135dab99d744257c6416918799392366 Mon Sep 17 00:00:00 2001 From: Alok Date: Fri, 7 Nov 2025 18:02:24 +0530 Subject: [PATCH 04/16] fix: fixes for RPC --- .../preconf-rpc/blocktracker/blocktracker.go | 6 +++++ tools/preconf-rpc/sender/sender.go | 22 ++++++++++++++----- 2 files changed, 22 insertions(+), 6 deletions(-) diff --git a/tools/preconf-rpc/blocktracker/blocktracker.go b/tools/preconf-rpc/blocktracker/blocktracker.go index ca62d1768..7e344e0f0 100644 --- a/tools/preconf-rpc/blocktracker/blocktracker.go +++ b/tools/preconf-rpc/blocktracker/blocktracker.go @@ -5,6 +5,7 @@ import ( "errors" "log/slog" "math/big" + "sync" "sync/atomic" "time" @@ -24,6 +25,7 @@ type blockTracker struct { blocks *lru.Cache[uint64, *types.Block] client EthClient log *slog.Logger + txnToCheckMu sync.Mutex txnsToCheck map[common.Hash]chan uint64 newBlockChan chan uint64 } @@ -92,7 +94,9 @@ func (b *blockTracker) Start(ctx context.Context) <-chan struct{} { if txn := block.Transaction(txHash); txn != nil { resultCh <- bNo close(resultCh) + b.txnToCheckMu.Lock() delete(b.txnsToCheck, txHash) + b.txnToCheckMu.Unlock() } } } @@ -130,6 +134,8 @@ func (b *blockTracker) WaitForTxnInclusion( txHash common.Hash, ) chan uint64 { resultCh := make(chan uint64, 1) + b.txnToCheckMu.Lock() b.txnsToCheck[txHash] = resultCh + b.txnToCheckMu.Unlock() return resultCh } diff --git a/tools/preconf-rpc/sender/sender.go b/tools/preconf-rpc/sender/sender.go index ae195e117..d9aecd839 100644 --- a/tools/preconf-rpc/sender/sender.go +++ b/tools/preconf-rpc/sender/sender.go @@ -68,6 +68,9 @@ type Transaction struct { Details string BlockNumber int64 Constraint *bidderapiv1.PositionConstraint + // local fields not stored in DB + commitments []*bidderapiv1.Commitment + logs []*types.Log } type Store interface { @@ -497,6 +500,8 @@ BID_LOOP: return fmt.Errorf("failed to store preconfirmed transaction: %w", err) } } + txn.commitments = result.commitments + txn.logs = result.logs retryTicker.Reset(result.timeUntillNextBlock + 1*time.Second) default: logger.Warn( @@ -506,6 +511,8 @@ BID_LOOP: "blockNumber", result.blockNumber, "bidAmount", result.bidAmount.String(), ) + txn.commitments = result.commitments + txn.logs = result.logs retryTicker.Reset(defaultRetryDelay) } select { @@ -515,22 +522,25 @@ BID_LOOP: return ErrTransactionCancelled case <-retryTicker.C: // Continue to the next iteration after the retry delay - case <-inclusion: + case bNo := <-inclusion: if txn.Status != TxStatusPreConfirmed { + // It could happen that the transaction got included but we got the signal + // late and made a failed attempt. So we should update the commitments and + // logs from the last successful bid attempt. txn.Status = TxStatusConfirmed - txn.BlockNumber = int64(result.blockNumber) + txn.BlockNumber = int64(bNo) logger.Info( "Transaction confirmed", - "blockNumber", result.blockNumber, + "blockNumber", bNo, "bidAmount", result.bidAmount.String(), ) - if err := t.store.StoreTransaction(ctx, txn, result.commitments, result.logs); err != nil { + if err := t.store.StoreTransaction(ctx, txn, txn.commitments, txn.logs); err != nil { return fmt.Errorf("failed to store preconfirmed transaction: %w", err) } } endTime := time.Now() - if len(result.commitments) > 0 { - endTime = time.UnixMilli(result.commitments[len(result.commitments)-1].DispatchTimestamp) + if len(txn.commitments) > 0 { + endTime = time.UnixMilli(txn.commitments[len(txn.commitments)-1].DispatchTimestamp) } t.clearBlockAttemptHistory(txn, endTime) break BID_LOOP From 03b3bf548cfae6ff2ae6727428abf22d938b8364 Mon Sep 17 00:00:00 2001 From: Alok Date: Fri, 7 Nov 2025 18:15:58 +0530 Subject: [PATCH 05/16] fix: fixes for RPC --- tools/preconf-rpc/sender/sender.go | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/tools/preconf-rpc/sender/sender.go b/tools/preconf-rpc/sender/sender.go index d9aecd839..455ed1039 100644 --- a/tools/preconf-rpc/sender/sender.go +++ b/tools/preconf-rpc/sender/sender.go @@ -113,9 +113,8 @@ type Simulator interface { } type blockAttempt struct { - blockNumber uint64 - attempts int - committedProviders []common.Address + blockNumber uint64 + attempts int } type txnAttempt struct { From 8f315092173fa29543276e1126aaa209b7f3b9e8 Mon Sep 17 00:00:00 2001 From: Alok Date: Mon, 10 Nov 2025 18:06:47 +0530 Subject: [PATCH 06/16] fix: fixes for RPC --- p2p/pkg/preconfirmation/preconfirmation.go | 16 + tools/preconf-rpc/bidder/bidder.go | 398 +++++++++++++++++++++ tools/preconf-rpc/bidder/bidder_test.go | 266 ++++++++++++++ tools/preconf-rpc/bidder/export_test.go | 7 + tools/preconf-rpc/sender/sender.go | 90 +++-- x/opt-in-bidder/bidder.go | 41 ++- 6 files changed, 790 insertions(+), 28 deletions(-) create mode 100644 tools/preconf-rpc/bidder/bidder.go create mode 100644 tools/preconf-rpc/bidder/bidder_test.go create mode 100644 tools/preconf-rpc/bidder/export_test.go diff --git a/p2p/pkg/preconfirmation/preconfirmation.go b/p2p/pkg/preconfirmation/preconfirmation.go index 306e42182..07bede61b 100644 --- a/p2p/pkg/preconfirmation/preconfirmation.go +++ b/p2p/pkg/preconfirmation/preconfirmation.go @@ -20,6 +20,7 @@ import ( providerapi "github.com/primev/mev-commit/p2p/pkg/rpc/provider" "github.com/primev/mev-commit/p2p/pkg/topology" "google.golang.org/grpc/codes" + "google.golang.org/grpc/metadata" "google.golang.org/grpc/status" ) @@ -147,6 +148,21 @@ func (p *Preconfirmation) SendBid( return nil, errors.New("no providers available") } + md, ok := metadata.FromIncomingContext(ctx) + if ok { + ignoredProviders := md.Get("ignore-provider") + for _, ip := range ignoredProviders { + ignoredAddr := common.HexToAddress(ip) + filteredProviders := make([]p2p.Peer, 0, len(providers)) + for _, provider := range providers { + if provider.EthAddress != ignoredAddr { + filteredProviders = append(filteredProviders, provider) + } + } + providers = filteredProviders + } + } + // Create a new channel to receive preConfirmations preConfirmations := make(chan *preconfpb.PreConfirmation, len(providers)) diff --git a/tools/preconf-rpc/bidder/bidder.go b/tools/preconf-rpc/bidder/bidder.go new file mode 100644 index 000000000..6180a49dd --- /dev/null +++ b/tools/preconf-rpc/bidder/bidder.go @@ -0,0 +1,398 @@ +package bidder + +import ( + "context" + "errors" + "fmt" + "io" + "log/slog" + "math/big" + "sync" + "sync/atomic" + "time" + + bidderapiv1 "github.com/primev/mev-commit/p2p/gen/go/bidderapi/v1" + debugapiv1 "github.com/primev/mev-commit/p2p/gen/go/debugapi/v1" + notificationsapiv1 "github.com/primev/mev-commit/p2p/gen/go/notificationsapi/v1" + "grpc.go4.org/metadata" +) + +const ( + epochNotificationTopic = "epoch_validators_opted_in" + slotDuration = 12 * time.Second +) + +var ( + ErrNoEpochInfo = errors.New("no epoch info available") + ErrNoSlotInCurrentEpoch = errors.New("no slot available in current epoch") + ErrNoProviders = errors.New("no connected providers found") +) + +var nowFunc = time.Now + +type slotInfo struct { + slot uint64 + startTime time.Time + blsKey string +} + +type epochInfo struct { + epoch uint64 + startTime time.Time + slots []slotInfo +} + +type BlockNumberGetter interface { + BlockNumber(ctx context.Context) (uint64, error) +} + +type BidderClient struct { + logger *slog.Logger + bigWg sync.WaitGroup + bidderClient bidderapiv1.BidderClient + topologyClient debugapiv1.DebugServiceClient + notificationsClient notificationsapiv1.NotificationsClient + currentEpoch atomic.Pointer[epochInfo] + blkNumberGetter BlockNumberGetter +} + +func NewBidderClient( + logger *slog.Logger, + bidderClient bidderapiv1.BidderClient, + topologyClient debugapiv1.DebugServiceClient, + notificationsClient notificationsapiv1.NotificationsClient, + blkNumberGetter BlockNumberGetter, +) *BidderClient { + return &BidderClient{ + logger: logger, + bidderClient: bidderClient, + topologyClient: topologyClient, + notificationsClient: notificationsClient, + blkNumberGetter: blkNumberGetter, + } +} + +func (b *BidderClient) Start(ctx context.Context) <-chan struct{} { + done := make(chan struct{}) + go func() { + defer close(done) + + lastMsg := nowFunc() + RESTART: + sub, err := b.notificationsClient.Subscribe(ctx, ¬ificationsapiv1.SubscribeRequest{ + Topics: []string{epochNotificationTopic}, + }) + if err != nil { + b.logger.Error("failed to subscribe to notifications", "error", err) + return + } + + if time.Since(lastMsg) > 15*time.Minute { + b.logger.Error("no messages received for 15 minutes, closing subscription") + return + } + + for { + select { + case <-ctx.Done(): + b.logger.Info("context done") + return + default: + } + + msg, err := sub.Recv() + if err != nil { + b.logger.Error("failed to receive message", "error", err) + goto RESTART + } + + lastMsg = nowFunc() + + b.logger.Debug("received message", "msg", msg) + + if msg.Topic != epochNotificationTopic { + b.logger.Error("unexpected topic", "topic", msg.Topic) + continue + } + + epoch, err := parseEpochInfo(msg) + if err != nil { + b.logger.Error("failed to parse epoch info", "error", err, "msg", msg) + continue + } + + b.currentEpoch.Store(epoch) + b.logger.Info("current epoch info updated", "epoch", epoch.epoch) + } + }() + return done +} + +func parseEpochInfo(msg *notificationsapiv1.Notification) (*epochInfo, error) { + epochIdx := msg.Value.Fields["epoch"].GetNumberValue() + if epochIdx == 0 { + return nil, errors.New("failed to parse epoch index") + } + startTime := msg.Value.Fields["epoch_start_time"].GetNumberValue() + if startTime == 0 { + return nil, errors.New("failed to parse start time") + } + slots := msg.Value.Fields["slots"].GetListValue() + if slots == nil { + return nil, errors.New("failed to parse slots") + } + epoch := &epochInfo{ + epoch: uint64(epochIdx), + startTime: time.Unix(int64(startTime), 0), + } + baseSlot := epochIdx * 32 + for _, slot := range slots.Values { + slotIdx := slot.GetStructValue().Fields["slot"].GetNumberValue() + if slotIdx == 0 { + return nil, errors.New("failed to parse slot index") + } + if slotIdx < baseSlot || slotIdx >= baseSlot+32 { + return nil, errors.New("slot index out of range") + } + blsKey := slot.GetStructValue().Fields["bls_key"].GetStringValue() + if blsKey == "" { + return nil, errors.New("failed to parse BLS key") + } + idx := slotIdx - baseSlot + epoch.slots = append(epoch.slots, slotInfo{ + slot: uint64(slotIdx), + startTime: epoch.startTime.Add(time.Duration(idx) * slotDuration), + blsKey: blsKey, + }) + } + + return epoch, nil +} + +type BidStatusType int + +const ( + BidStatusNoOfProviders BidStatusType = iota + BidStatusWaitSecs + BidStatusAttempted + BidStatusFailed + BidStatusCancelled + BidStatusCommitment +) + +type BidStatus struct { + Type BidStatusType + Arg any +} + +type BidOpts struct { + WaitForOptIn bool + BlockNumber uint64 + RevertingTxHashes []string + DecayDuration time.Duration + Constraint *bidderapiv1.PositionConstraint + IgnoreProviders []string +} + +var defaultBidOpts = &BidOpts{ + WaitForOptIn: true, +} + +func (b *BidderClient) Bid( + ctx context.Context, + bidAmount *big.Int, + slashAmount *big.Int, + rawTx string, + opts *BidOpts, +) (chan BidStatus, error) { + if opts == nil { + opts = defaultBidOpts + } + + topo, err := b.topologyClient.GetTopology(ctx, &debugapiv1.EmptyMessage{}) + if err != nil { + b.logger.Error("failed to get topology", "error", err) + return nil, err + } + + providers := topo.Topology.Fields["connected_providers"].GetListValue() + if providers == nil || len(providers.Values) == 0 { + return nil, ErrNoProviders + } + + filteredProviders := make([]any, 0, len(providers.Values)) + for _, ip := range opts.IgnoreProviders { + for _, p := range providers.Values { + if p.GetStringValue() != ip { + filteredProviders = append(filteredProviders, p) + } + } + } + + // Channel length chosen is 3 so that sending the bid is not blocked by the first + // status message. + res := make(chan BidStatus, 3) + b.bigWg.Add(1) + go func() { + defer fmt.Println("BidderClient goroutine exiting") + defer close(res) + defer b.bigWg.Done() + + res <- BidStatus{Type: BidStatusNoOfProviders, Arg: len(filteredProviders)} + + if opts.WaitForOptIn { + nextSlot, err := b.getNextSlot() + if err != nil { + b.logger.Error("failed to get next slot", "error", err) + res <- BidStatus{Type: BidStatusFailed, Arg: err.Error()} + return + } + + bidTime := nextSlot.startTime.Add(-1 * time.Second) + wait := bidTime.Sub(nowFunc()) + res <- BidStatus{Type: BidStatusWaitSecs, Arg: int(wait.Seconds())} + + if wait > 0 { + b.logger.Info("waiting for next slot", "wait", wait) + select { + case <-time.After(wait): + case <-ctx.Done(): + res <- BidStatus{Type: BidStatusCancelled, Arg: ctx.Err().Error()} + return + } + } + } + + blkNumber := opts.BlockNumber + if blkNumber == 0 { + bNo, err := b.blkNumberGetter.BlockNumber(ctx) + if err != nil { + b.logger.Error("failed to get block number", "error", err) + res <- BidStatus{Type: BidStatusFailed, Arg: err.Error()} + return + } + blkNumber = bNo + 1 + } + + res <- BidStatus{Type: BidStatusAttempted, Arg: blkNumber} + b.logger.Info( + "attempting to send bid", + "blockNumber", blkNumber, + "bidAmount", bidAmount, + "slashAmount", slashAmount, + ) + + bidReq := &bidderapiv1.Bid{ + Amount: bidAmount.String(), + BlockNumber: int64(blkNumber), + RawTransactions: []string{rawTx}, + DecayStartTimestamp: nowFunc().Add(200 * time.Millisecond).UnixMilli(), + SlashAmount: slashAmount.String(), + RevertingTxHashes: opts.RevertingTxHashes, + } + + if opts.DecayDuration > 0 { + bidReq.DecayEndTimestamp = time.UnixMilli(bidReq.DecayStartTimestamp).Add(opts.DecayDuration).UnixMilli() + } else { + 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, + }, + }, + }, + } + } + + if len(opts.IgnoreProviders) > 0 { + mdMap := make(map[string]string) + for _, ip := range opts.IgnoreProviders { + mdMap["ignore-provider"] = ip + } + md := metadata.New(mdMap) + ctx = metadata.NewContext(ctx, md) + } + + pc, err := b.bidderClient.SendBid(ctx, bidReq) + if err != nil { + b.logger.Error("failed to send bid", "error", err) + res <- BidStatus{Type: BidStatusFailed, Arg: err.Error()} + return + } + + for { + select { + case <-ctx.Done(): + res <- BidStatus{Type: BidStatusCancelled, Arg: ctx.Err().Error()} + return + default: + } + + msg, err := pc.Recv() + if err != nil { + if errors.Is(err, io.EOF) { + return + } + if errors.Is(err, context.Canceled) { + res <- BidStatus{Type: BidStatusCancelled, Arg: err.Error()} + return + } + b.logger.Error("failed to receive commitment", "error", err) + res <- BidStatus{Type: BidStatusFailed, Arg: err.Error()} + return + } + res <- BidStatus{Type: BidStatusCommitment, Arg: msg} + } + }() + + return res, nil +} + +func (b *BidderClient) ConnectedProviders(ctx context.Context) ([]string, error) { + topo, err := b.topologyClient.GetTopology(ctx, &debugapiv1.EmptyMessage{}) + if err != nil { + b.logger.Error("failed to get topology", "error", err) + return []string{}, err + } + + providers := topo.Topology.Fields["connected_providers"].GetListValue() + if providers == nil { + return []string{}, nil + } + + prvs := make([]string, 0, len(providers.Values)) + for _, p := range providers.Values { + prvs = append(prvs, p.GetStringValue()) + } + return prvs, nil +} + +func (b *BidderClient) Estimate() (int64, error) { + nextSlot, err := b.getNextSlot() + if err != nil { + return 0, err + } + + return int64(nextSlot.startTime.Sub(nowFunc()).Seconds()), nil +} + +func (b *BidderClient) getNextSlot() (slotInfo, error) { + epochInfo := b.currentEpoch.Load() + if epochInfo == nil { + return slotInfo{}, ErrNoEpochInfo + } + + now := nowFunc() + for _, slot := range epochInfo.slots { + if now.Before(slot.startTime) { + return slot, nil + } + } + + return slotInfo{}, ErrNoSlotInCurrentEpoch +} diff --git a/tools/preconf-rpc/bidder/bidder_test.go b/tools/preconf-rpc/bidder/bidder_test.go new file mode 100644 index 000000000..37792e67a --- /dev/null +++ b/tools/preconf-rpc/bidder/bidder_test.go @@ -0,0 +1,266 @@ +package bidder_test + +import ( + "context" + "crypto/rand" + "encoding/hex" + "io" + "math/big" + "os" + "testing" + "time" + + bidderapiv1 "github.com/primev/mev-commit/p2p/gen/go/bidderapi/v1" + debugapiv1 "github.com/primev/mev-commit/p2p/gen/go/debugapi/v1" + notificationsapiv1 "github.com/primev/mev-commit/p2p/gen/go/notificationsapi/v1" + "github.com/primev/mev-commit/tools/preconf-rpc/bidder" + optinbidder "github.com/primev/mev-commit/tools/preconf-rpc/bidder" + "github.com/primev/mev-commit/x/util" + "google.golang.org/grpc" + "google.golang.org/protobuf/types/known/structpb" +) + +type testRPCServices struct { + bidderapiv1.BidderClient + debugapiv1.DebugServiceClient + notificationsapiv1.NotificationsClient + + notificationChan chan *notificationsapiv1.Notification + bidChan chan *bidderapiv1.Bid + commitmentChan chan *bidderapiv1.Commitment + topo *debugapiv1.TopologyResponse +} + +type testNotificationStream struct { + grpc.ClientStream + + ctx context.Context + notificationChan chan *notificationsapiv1.Notification +} + +func (t *testNotificationStream) Recv() (*notificationsapiv1.Notification, error) { + select { + case <-t.ctx.Done(): + return nil, io.EOF + case n := <-t.notificationChan: + return n, nil + } +} + +type testCommitmentStream struct { + grpc.ClientStream + + ctx context.Context + commitmentChan chan *bidderapiv1.Commitment +} + +func (t *testCommitmentStream) Recv() (*bidderapiv1.Commitment, error) { + select { + case <-t.ctx.Done(): + return nil, io.EOF + case c, more := <-t.commitmentChan: + if !more { + return nil, io.EOF + } + return c, nil + } +} + +func (t *testRPCServices) SendBid( + ctx context.Context, + in *bidderapiv1.Bid, + _ ...grpc.CallOption, +) (grpc.ServerStreamingClient[bidderapiv1.Commitment], error) { + select { + case t.bidChan <- in: + case <-ctx.Done(): + return nil, ctx.Err() + } + return &testCommitmentStream{ctx: ctx, commitmentChan: t.commitmentChan}, nil +} + +func (t *testRPCServices) GetTopology( + _ context.Context, + _ *debugapiv1.EmptyMessage, + _ ...grpc.CallOption, +) (*debugapiv1.TopologyResponse, error) { + return t.topo, nil +} + +func (t *testRPCServices) Subscribe( + ctx context.Context, + in *notificationsapiv1.SubscribeRequest, + _ ...grpc.CallOption, +) (grpc.ServerStreamingClient[notificationsapiv1.Notification], error) { + return &testNotificationStream{ctx: ctx, notificationChan: t.notificationChan}, nil +} + +type testBlockNumberGetter struct { + blockNumber uint64 +} + +func (t *testBlockNumberGetter) BlockNumber(ctx context.Context) (uint64, error) { + return t.blockNumber, nil +} + +type testTimeSetter struct { + now time.Time +} + +func (t *testTimeSetter) Now() time.Time { + return t.now +} + +func TestBidderClient(t *testing.T) { + t.Parallel() + + clock := time.Now() + timeSetter := &testTimeSetter{ + now: clock, + } + + optinbidder.SetNowFunc(timeSetter.Now) + + topoVal, err := structpb.NewStruct(map[string]interface{}{ + "connected_providers": []any{"provider1", "provider2"}, + }) + if err != nil { + t.Fatal(err) + } + // Create a new test RPC services. + rpcServices := &testRPCServices{ + notificationChan: make(chan *notificationsapiv1.Notification), + bidChan: make(chan *bidderapiv1.Bid), + commitmentChan: make(chan *bidderapiv1.Commitment), + topo: &debugapiv1.TopologyResponse{ + Topology: &structpb.Struct{}, + }, + } + + blockNumberGetter := &testBlockNumberGetter{blockNumber: 10} + bidderClient := bidder.NewBidderClient( + util.NewTestLogger(os.Stdout), + rpcServices, + rpcServices, + rpcServices, + blockNumberGetter, + ) + + ctx, cancel := context.WithCancel(context.Background()) + done := bidderClient.Start(ctx) + + _, err = bidderClient.Estimate() + if err != optinbidder.ErrNoEpochInfo { + t.Fatalf("expected error %v, got %v", optinbidder.ErrNoEpochInfo, err) + } + + // Send a notification. + nVal, err := structpb.NewStruct(map[string]interface{}{ + "epoch": 1, + "epoch_start_time": clock.Add(2 * time.Second).Unix(), + "slots": []any{ + map[string]interface{}{ + "slot": 33, + "start_time": clock.Add(14 * time.Second).Unix(), + "bls_key": "key2", + "opted_in": true, + }, + }, + }) + if err != nil { + t.Fatal(err) + } + + rpcServices.notificationChan <- ¬ificationsapiv1.Notification{ + Topic: "epoch_validators_opted_in", + Value: nVal, + } + + for { + if _, err := bidderClient.Estimate(); err == nil { + break + } + } + + estimate, err := bidderClient.Estimate() + if err != nil { + t.Fatal(err) + } + if estimate != 13 { + t.Fatalf("expected estimate 13, got %d", estimate) + } + + timeSetter.now = clock.Add(10 * time.Second) + + buf := make([]byte, 32) + _, _ = rand.Read(buf) + txString := hex.EncodeToString(buf) + + _, err = bidderClient.Bid(ctx, big.NewInt(1), big.NewInt(1), txString, nil) + if err == nil { + t.Fatal("expected error, got nil") + } + + rpcServices.topo = &debugapiv1.TopologyResponse{ + Topology: topoVal, + } + + statusC, err := bidderClient.Bid(ctx, big.NewInt(1), big.NewInt(1), txString, nil) + if err != nil { + t.Fatal(err) + } + + commitments := 0 +waitLoop: + for { + select { + case status, more := <-statusC: + if !more { + break waitLoop + } + switch status.Type { + case optinbidder.BidStatusNoOfProviders: + if status.Arg.(int) != 2 { + t.Fatalf("expected 2 providers, got %d", status.Arg) + } + case optinbidder.BidStatusWaitSecs: + if status.Arg.(int) != 2 { + t.Fatalf("expected 2 seconds, got %d", status.Arg) + } + case optinbidder.BidStatusAttempted: + if status.Arg.(uint64) != 11 { + t.Fatalf("expected 11, got %d", status.Arg) + } + case optinbidder.BidStatusCommitment: + if status.Arg.(*bidderapiv1.Commitment).BlockNumber != 11 { + t.Fatalf("expected block number 11, got %d", status.Arg.(*bidderapiv1.Commitment).BlockNumber) + } + commitments++ + } + case bid := <-rpcServices.bidChan: + if bid.Amount != big.NewInt(1).String() { + t.Fatalf("expected amount 1, got %s", bid.Amount) + } + if bid.BlockNumber != 11 { + t.Fatalf("expected block number 11, got %d", bid.BlockNumber) + } + if bid.RawTransactions[0] != txString { + t.Fatalf("expected raw transaction %x, got %s", buf, bid.RawTransactions[0]) + } + rpcServices.commitmentChan <- &bidderapiv1.Commitment{ + BlockNumber: 11, + } + rpcServices.commitmentChan <- &bidderapiv1.Commitment{ + BlockNumber: 11, + } + close(rpcServices.commitmentChan) + } + } + + if commitments != 2 { + t.Fatalf("expected 2 commitments, got %d", commitments) + } + + cancel() + <-done +} diff --git a/tools/preconf-rpc/bidder/export_test.go b/tools/preconf-rpc/bidder/export_test.go new file mode 100644 index 000000000..35e7047a0 --- /dev/null +++ b/tools/preconf-rpc/bidder/export_test.go @@ -0,0 +1,7 @@ +package bidder + +import "time" + +func SetNowFunc(f func() time.Time) { + nowFunc = f +} diff --git a/tools/preconf-rpc/sender/sender.go b/tools/preconf-rpc/sender/sender.go index 455ed1039..a1e5ccbcf 100644 --- a/tools/preconf-rpc/sender/sender.go +++ b/tools/preconf-rpc/sender/sender.go @@ -69,8 +69,9 @@ type Transaction struct { BlockNumber int64 Constraint *bidderapiv1.PositionConstraint // local fields not stored in DB - commitments []*bidderapiv1.Commitment - logs []*types.Log + noOfProviders int + commitments []*bidderapiv1.Commitment + logs []*types.Log } type Store interface { @@ -93,6 +94,7 @@ type Bidder interface { rawTx string, opts *optinbidder.BidOpts, ) (chan optinbidder.BidStatus, error) + ConnectedProviders(ctx context.Context) ([]string, error) } type Pricer interface { @@ -469,20 +471,21 @@ BID_LOOP: } else { return err } - 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) - logger.Info( - "Transaction fast-tracked based on commitments", + case len(result.commitments) == 0: + logger.Warn( + "No commitments received for the bid", "blockNumber", result.blockNumber, "bidAmount", result.bidAmount.String(), ) - if err := t.store.StoreTransaction(ctx, txn, result.commitments, result.logs); err != nil { - return fmt.Errorf("failed to store fast-tracked transaction: %w", err) - } - retryTicker.Reset(result.timeUntillNextBlock + 1*time.Second) + retryTicker.Reset(defaultRetryDelay) + // default: + // for _, cmt := range result.commitments { + // if !slices.ContainsFunc(txn.commitments, func(existing *bidderapiv1.Commitment) bool { + // return existing.ProviderAddress == cmt.ProviderAddress + // }) { + // txn.commitments = append(txn.commitments, cmt) + // } + // } case result.noOfProviders == len(result.commitments): if result.optedInSlot { // This means that all builders have committed to the bid and it @@ -586,7 +589,6 @@ type bidResult struct { optedInSlot bool bidAmount *big.Int commitments []*bidderapiv1.Commitment - logs []*types.Log } func (t *TxSender) sendBid( @@ -609,12 +611,6 @@ func (t *TxSender) sendBid( timeToOptIn = blockTime * 32 } - logs, err := t.simulator.Simulate(ctx, txn.Raw) - if err != nil { - 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 { logger.Error("Failed to get next block number", "error", err) @@ -640,7 +636,7 @@ func (t *TxSender) sendBid( cctx, cancel := context.WithTimeout(ctx, t.getBidTimeout()) defer cancel() - cost, err := t.calculatePriceForNextBlock(txn, bidBlockNo, prices, optedInSlot) + cost, isRetry, 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) || errors.Is(err, ErrMaxAttemptsPerBlockExceeded) { @@ -653,6 +649,17 @@ func (t *TxSender) sendBid( } } + var ignoreProviders []string + if isRetry && len(txn.commitments) > 0 { + for _, cmt := range txn.commitments { + ignoreProviders = append(ignoreProviders, cmt.ProviderAddress) + } + logger.Info( + "Retrying bid, ignoring previously committed providers", + "ignoreProviders", ignoreProviders, + ) + } + slashAmount := big.NewInt(0) switch txn.Type { case TxTypeRegular: @@ -692,6 +699,22 @@ func (t *TxSender) sendBid( slashAmount = new(big.Int).Set(txn.Value()) } + if !isRetry { + logs, err := t.simulator.Simulate(ctx, txn.Raw) + if err != nil { + logger.Error("Failed to simulate transaction", "error", err, "blockNumber", bidBlockNo) + return bidResult{}, fmt.Errorf("failed to simulate transaction: %w", err) + } + providers, err := t.bidder.ConnectedProviders(ctx) + if err != nil { + logger.Error("Failed to get connected providers", "error", err) + return bidResult{}, fmt.Errorf("failed to get connected providers: %w", err) + } + txn.logs = logs + txn.commitments = nil + txn.noOfProviders = len(providers) + } + bidC, err := t.bidder.Bid( cctx, cost, @@ -703,6 +726,7 @@ func (t *TxSender) sendBid( RevertingTxHashes: []string{txn.Hash().Hex()}, DecayDuration: t.getBidTimeout() * 2, Constraint: txn.Constraint, + IgnoreProviders: ignoreProviders, }, ) if err != nil { @@ -715,7 +739,6 @@ func (t *TxSender) sendBid( bidAmount: cost, startTime: start, timeUntillNextBlock: timeUntilNextBlock, - logs: logs, } BID_LOOP: for { @@ -735,6 +758,18 @@ BID_LOOP: result.blockNumber = bidStatus.Arg.(uint64) case optinbidder.BidStatusCommitment: result.commitments = append(result.commitments, bidStatus.Arg.(*bidderapiv1.Commitment)) + if t.fastTrack(result.commitments, optedInSlot) && txn.Status != TxStatusPreConfirmed { + txn.Status = TxStatusPreConfirmed + txn.BlockNumber = int64(result.blockNumber) + logger.Info( + "Transaction fast-tracked based on commitments", + "blockNumber", result.blockNumber, + "bidAmount", result.bidAmount.String(), + ) + if err := t.store.StoreTransaction(ctx, txn, result.commitments, result.logs); err != nil { + logger.Error("Failed to store fast-tracked transaction", "error", err) + } + } case optinbidder.BidStatusCancelled: logger.Warn("Bid context cancelled by the bidder") break BID_LOOP @@ -761,7 +796,7 @@ func (t *TxSender) calculatePriceForNextBlock( bidBlockNo uint64, prices map[int64]float64, optedInSlot bool, -) (*big.Int, error) { +) (*big.Int, bool, error) { attempts, found := t.txnAttemptHistory.Get(txn.Hash()) if !found { attempts = &txnAttempt{ @@ -771,7 +806,7 @@ func (t *TxSender) calculatePriceForNextBlock( } if time.Since(attempts.startTime) > transactionTimeout { - return nil, ErrTimeoutExceeded + return nil, false, ErrTimeoutExceeded } // default confidence level for the next block @@ -791,7 +826,7 @@ func (t *TxSender) calculatePriceForNextBlock( case attempts.attempts[i].attempts > 2: confidence = confidenceSubsequentAttempts case attempts.attempts[i].attempts > maxAttemptsPerBlock: - return nil, fmt.Errorf("%w: block %d", ErrMaxAttemptsPerBlockExceeded, bidBlockNo) + return nil, false, fmt.Errorf("%w: block %d", ErrMaxAttemptsPerBlockExceeded, bidBlockNo) } break // No need to check further attempts for the same block } @@ -815,11 +850,11 @@ func (t *TxSender) calculatePriceForNextBlock( if conf == int64(confidence) { // the gwei value is in float, so we need to convert it to wei before multiplying with gas limit priceInWei := price * 1e9 // Convert Gwei to Wei - return new(big.Int).Mul(big.NewInt(int64(priceInWei)), big.NewInt(int64(txn.Gas()))), nil + return new(big.Int).Mul(big.NewInt(int64(priceInWei)), big.NewInt(int64(txn.Gas()))), isRetry, nil } } - return nil, fmt.Errorf( + return nil, false, fmt.Errorf( "no estimated price found for block %d with confidence %d", bidBlockNo, confidence, ) } @@ -843,6 +878,7 @@ func (t *TxSender) clearBlockAttemptHistory(txn *Transaction, endTime time.Time) "startTime", attempts.startTime.Format(time.RFC3339), "startBlockNumber", attempts.attempts[0].blockNumber, "totalAttempts", totalAttempts, + "totalBlockAttempts", len(attempts.attempts), ) _ = t.txnAttemptHistory.Remove(txn.Hash()) diff --git a/x/opt-in-bidder/bidder.go b/x/opt-in-bidder/bidder.go index cac41c4b4..6d4eded6e 100644 --- a/x/opt-in-bidder/bidder.go +++ b/x/opt-in-bidder/bidder.go @@ -14,6 +14,7 @@ import ( bidderapiv1 "github.com/primev/mev-commit/p2p/gen/go/bidderapi/v1" debugapiv1 "github.com/primev/mev-commit/p2p/gen/go/debugapi/v1" notificationsapiv1 "github.com/primev/mev-commit/p2p/gen/go/notificationsapi/v1" + "grpc.go4.org/metadata" ) const ( @@ -190,6 +191,7 @@ type BidOpts struct { RevertingTxHashes []string DecayDuration time.Duration Constraint *bidderapiv1.PositionConstraint + IgnoreProviders []string } var defaultBidOpts = &BidOpts{ @@ -218,6 +220,15 @@ func (b *BidderClient) Bid( return nil, ErrNoProviders } + filteredProviders := make([]any, 0, len(providers.Values)) + for _, ip := range opts.IgnoreProviders { + for _, p := range providers.Values { + if p.GetStringValue() != ip { + filteredProviders = append(filteredProviders, p) + } + } + } + // Channel length chosen is 3 so that sending the bid is not blocked by the first // status message. res := make(chan BidStatus, 3) @@ -227,7 +238,7 @@ func (b *BidderClient) Bid( defer close(res) defer b.bigWg.Done() - res <- BidStatus{Type: BidStatusNoOfProviders, Arg: len(providers.Values)} + res <- BidStatus{Type: BidStatusNoOfProviders, Arg: len(filteredProviders)} if opts.WaitForOptIn { nextSlot, err := b.getNextSlot() @@ -298,6 +309,15 @@ func (b *BidderClient) Bid( } } + if len(opts.IgnoreProviders) > 0 { + mdMap := make(map[string]string) + for _, ip := range opts.IgnoreProviders { + mdMap["ignore-provider"] = ip + } + md := metadata.New(mdMap) + ctx = metadata.NewContext(ctx, md) + } + pc, err := b.bidderClient.SendBid(ctx, bidReq) if err != nil { b.logger.Error("failed to send bid", "error", err) @@ -333,6 +353,25 @@ func (b *BidderClient) Bid( return res, nil } +func (b *BidderClient) ConnectedProviders(ctx context.Context) ([]string, error) { + topo, err := b.topologyClient.GetTopology(ctx, &debugapiv1.EmptyMessage{}) + if err != nil { + b.logger.Error("failed to get topology", "error", err) + return []string{}, err + } + + providers := topo.Topology.Fields["connected_providers"].GetListValue() + if providers == nil { + return []string{}, nil + } + + prvs := make([]string, 0, len(providers.Values)) + for _, p := range providers.Values { + prvs = append(prvs, p.GetStringValue()) + } + return prvs, nil +} + func (b *BidderClient) Estimate() (int64, error) { nextSlot, err := b.getNextSlot() if err != nil { From e564225384dfeb07d0c3e90a15ccee3fd81257b7 Mon Sep 17 00:00:00 2001 From: Alok Nerurkar Date: Tue, 11 Nov 2025 11:30:36 +0530 Subject: [PATCH 07/16] fix: rpc improvements --- tools/preconf-rpc/bidder/bidder.go | 33 ++++--------------------- tools/preconf-rpc/bidder/bidder_test.go | 30 +++++++++++----------- tools/preconf-rpc/sender/sender.go | 18 +++++++------- 3 files changed, 28 insertions(+), 53 deletions(-) diff --git a/tools/preconf-rpc/bidder/bidder.go b/tools/preconf-rpc/bidder/bidder.go index 6180a49dd..13f6f5995 100644 --- a/tools/preconf-rpc/bidder/bidder.go +++ b/tools/preconf-rpc/bidder/bidder.go @@ -172,8 +172,7 @@ func parseEpochInfo(msg *notificationsapiv1.Notification) (*epochInfo, error) { type BidStatusType int const ( - BidStatusNoOfProviders BidStatusType = iota - BidStatusWaitSecs + BidStatusWaitSecs BidStatusType = iota BidStatusAttempted BidStatusFailed BidStatusCancelled @@ -209,26 +208,6 @@ func (b *BidderClient) Bid( opts = defaultBidOpts } - topo, err := b.topologyClient.GetTopology(ctx, &debugapiv1.EmptyMessage{}) - if err != nil { - b.logger.Error("failed to get topology", "error", err) - return nil, err - } - - providers := topo.Topology.Fields["connected_providers"].GetListValue() - if providers == nil || len(providers.Values) == 0 { - return nil, ErrNoProviders - } - - filteredProviders := make([]any, 0, len(providers.Values)) - for _, ip := range opts.IgnoreProviders { - for _, p := range providers.Values { - if p.GetStringValue() != ip { - filteredProviders = append(filteredProviders, p) - } - } - } - // Channel length chosen is 3 so that sending the bid is not blocked by the first // status message. res := make(chan BidStatus, 3) @@ -238,8 +217,6 @@ func (b *BidderClient) Bid( defer close(res) defer b.bigWg.Done() - res <- BidStatus{Type: BidStatusNoOfProviders, Arg: len(filteredProviders)} - if opts.WaitForOptIn { nextSlot, err := b.getNextSlot() if err != nil { @@ -272,9 +249,9 @@ func (b *BidderClient) Bid( return } blkNumber = bNo + 1 + res <- BidStatus{Type: BidStatusAttempted, Arg: blkNumber} } - res <- BidStatus{Type: BidStatusAttempted, Arg: blkNumber} b.logger.Info( "attempting to send bid", "blockNumber", blkNumber, @@ -310,11 +287,11 @@ func (b *BidderClient) Bid( } if len(opts.IgnoreProviders) > 0 { - mdMap := make(map[string]string) + var pairs []string for _, ip := range opts.IgnoreProviders { - mdMap["ignore-provider"] = ip + pairs = append(pairs, "ignore-provider", ip) } - md := metadata.New(mdMap) + md := metadata.Pairs(pairs...) ctx = metadata.NewContext(ctx, md) } diff --git a/tools/preconf-rpc/bidder/bidder_test.go b/tools/preconf-rpc/bidder/bidder_test.go index 37792e67a..16a909576 100644 --- a/tools/preconf-rpc/bidder/bidder_test.go +++ b/tools/preconf-rpc/bidder/bidder_test.go @@ -14,7 +14,6 @@ import ( debugapiv1 "github.com/primev/mev-commit/p2p/gen/go/debugapi/v1" notificationsapiv1 "github.com/primev/mev-commit/p2p/gen/go/notificationsapi/v1" "github.com/primev/mev-commit/tools/preconf-rpc/bidder" - optinbidder "github.com/primev/mev-commit/tools/preconf-rpc/bidder" "github.com/primev/mev-commit/x/util" "google.golang.org/grpc" "google.golang.org/protobuf/types/known/structpb" @@ -119,7 +118,7 @@ func TestBidderClient(t *testing.T) { now: clock, } - optinbidder.SetNowFunc(timeSetter.Now) + bidder.SetNowFunc(timeSetter.Now) topoVal, err := structpb.NewStruct(map[string]interface{}{ "connected_providers": []any{"provider1", "provider2"}, @@ -150,8 +149,8 @@ func TestBidderClient(t *testing.T) { done := bidderClient.Start(ctx) _, err = bidderClient.Estimate() - if err != optinbidder.ErrNoEpochInfo { - t.Fatalf("expected error %v, got %v", optinbidder.ErrNoEpochInfo, err) + if err != bidder.ErrNoEpochInfo { + t.Fatalf("expected error %v, got %v", bidder.ErrNoEpochInfo, err) } // Send a notification. @@ -196,15 +195,18 @@ func TestBidderClient(t *testing.T) { _, _ = rand.Read(buf) txString := hex.EncodeToString(buf) - _, err = bidderClient.Bid(ctx, big.NewInt(1), big.NewInt(1), txString, nil) - if err == nil { - t.Fatal("expected error, got nil") - } - rpcServices.topo = &debugapiv1.TopologyResponse{ Topology: topoVal, } + providers, err := bidderClient.ConnectedProviders(ctx) + if err != nil { + t.Fatal(err) + } + if len(providers) != 2 { + t.Fatalf("expected 2 providers, got %d", len(providers)) + } + statusC, err := bidderClient.Bid(ctx, big.NewInt(1), big.NewInt(1), txString, nil) if err != nil { t.Fatal(err) @@ -219,19 +221,15 @@ waitLoop: break waitLoop } switch status.Type { - case optinbidder.BidStatusNoOfProviders: - if status.Arg.(int) != 2 { - t.Fatalf("expected 2 providers, got %d", status.Arg) - } - case optinbidder.BidStatusWaitSecs: + case bidder.BidStatusWaitSecs: if status.Arg.(int) != 2 { t.Fatalf("expected 2 seconds, got %d", status.Arg) } - case optinbidder.BidStatusAttempted: + case bidder.BidStatusAttempted: if status.Arg.(uint64) != 11 { t.Fatalf("expected 11, got %d", status.Arg) } - case optinbidder.BidStatusCommitment: + case bidder.BidStatusCommitment: if status.Arg.(*bidderapiv1.Commitment).BlockNumber != 11 { t.Fatalf("expected block number 11, got %d", status.Arg.(*bidderapiv1.Commitment).BlockNumber) } diff --git a/tools/preconf-rpc/sender/sender.go b/tools/preconf-rpc/sender/sender.go index a1e5ccbcf..8bf73996a 100644 --- a/tools/preconf-rpc/sender/sender.go +++ b/tools/preconf-rpc/sender/sender.go @@ -15,7 +15,7 @@ import ( "github.com/ethereum/go-ethereum/core/types" lru "github.com/hashicorp/golang-lru/v2" bidderapiv1 "github.com/primev/mev-commit/p2p/gen/go/bidderapi/v1" - optinbidder "github.com/primev/mev-commit/x/opt-in-bidder" + "github.com/primev/mev-commit/tools/preconf-rpc/bidder" "golang.org/x/sync/errgroup" ) @@ -92,8 +92,8 @@ type Bidder interface { bidAmount *big.Int, slashAmount *big.Int, rawTx string, - opts *optinbidder.BidOpts, - ) (chan optinbidder.BidStatus, error) + opts *bidder.BidOpts, + ) (chan bidder.BidStatus, error) ConnectedProviders(ctx context.Context) ([]string, error) } @@ -720,7 +720,7 @@ func (t *TxSender) sendBid( cost, slashAmount, strings.TrimPrefix(txn.Raw, "0x"), - &optinbidder.BidOpts{ + &bidder.BidOpts{ WaitForOptIn: false, BlockNumber: uint64(bidBlockNo), RevertingTxHashes: []string{txn.Hash().Hex()}, @@ -752,11 +752,11 @@ BID_LOOP: break BID_LOOP } switch bidStatus.Type { - case optinbidder.BidStatusNoOfProviders: + case bidder.BidStatusNoOfProviders: result.noOfProviders = bidStatus.Arg.(int) - case optinbidder.BidStatusAttempted: + case bidder.BidStatusAttempted: result.blockNumber = bidStatus.Arg.(uint64) - case optinbidder.BidStatusCommitment: + case bidder.BidStatusCommitment: result.commitments = append(result.commitments, bidStatus.Arg.(*bidderapiv1.Commitment)) if t.fastTrack(result.commitments, optedInSlot) && txn.Status != TxStatusPreConfirmed { txn.Status = TxStatusPreConfirmed @@ -770,10 +770,10 @@ BID_LOOP: logger.Error("Failed to store fast-tracked transaction", "error", err) } } - case optinbidder.BidStatusCancelled: + case bidder.BidStatusCancelled: logger.Warn("Bid context cancelled by the bidder") break BID_LOOP - case optinbidder.BidStatusFailed: + case bidder.BidStatusFailed: logger.Error("Bid failed", "error", bidStatus.Arg) break BID_LOOP } From 814929947e22bbeb14de82f67a608f918140104b Mon Sep 17 00:00:00 2001 From: Alok Date: Tue, 11 Nov 2025 15:20:29 +0530 Subject: [PATCH 08/16] fix: fixes for RPC --- tools/preconf-rpc/sender/sender.go | 47 ++--- tools/preconf-rpc/sender/sender_test.go | 239 ++++++++++++++++++------ tools/preconf-rpc/service/service.go | 2 +- 3 files changed, 195 insertions(+), 93 deletions(-) diff --git a/tools/preconf-rpc/sender/sender.go b/tools/preconf-rpc/sender/sender.go index 8bf73996a..ab37bb358 100644 --- a/tools/preconf-rpc/sender/sender.go +++ b/tools/preconf-rpc/sender/sender.go @@ -471,22 +471,7 @@ BID_LOOP: } else { return err } - case len(result.commitments) == 0: - logger.Warn( - "No commitments received for the bid", - "blockNumber", result.blockNumber, - "bidAmount", result.bidAmount.String(), - ) - retryTicker.Reset(defaultRetryDelay) - // default: - // for _, cmt := range result.commitments { - // if !slices.ContainsFunc(txn.commitments, func(existing *bidderapiv1.Commitment) bool { - // return existing.ProviderAddress == cmt.ProviderAddress - // }) { - // txn.commitments = append(txn.commitments, cmt) - // } - // } - case result.noOfProviders == len(result.commitments): + case txn.noOfProviders == len(txn.commitments): if result.optedInSlot { // This means that all builders have committed to the bid and it // is a primev opted in slot. We can safely proceed to inform the @@ -498,23 +483,19 @@ BID_LOOP: "blockNumber", result.blockNumber, "bidAmount", result.bidAmount.String(), ) - if err := t.store.StoreTransaction(ctx, txn, result.commitments, result.logs); err != nil { + if err := t.store.StoreTransaction(ctx, txn, txn.commitments, txn.logs); err != nil { return fmt.Errorf("failed to store preconfirmed transaction: %w", err) } } - txn.commitments = result.commitments - txn.logs = result.logs retryTicker.Reset(result.timeUntillNextBlock + 1*time.Second) default: logger.Warn( "Not all builders committed to the bid", - "noOfProviders", result.noOfProviders, - "noOfCommitments", len(result.commitments), + "noOfProviders", txn.noOfProviders, + "noOfCommitments", len(txn.commitments), "blockNumber", result.blockNumber, "bidAmount", result.bidAmount.String(), ) - txn.commitments = result.commitments - txn.logs = result.logs retryTicker.Reset(defaultRetryDelay) } select { @@ -584,11 +565,9 @@ func (e *errRetry) Error() string { type bidResult struct { startTime time.Time timeUntillNextBlock time.Duration - noOfProviders int blockNumber uint64 optedInSlot bool bidAmount *big.Int - commitments []*bidderapiv1.Commitment } func (t *TxSender) sendBid( @@ -735,8 +714,8 @@ func (t *TxSender) sendBid( } result := bidResult{ - commitments: make([]*bidderapiv1.Commitment, 0), bidAmount: cost, + blockNumber: bidBlockNo, startTime: start, timeUntillNextBlock: timeUntilNextBlock, } @@ -752,21 +731,17 @@ BID_LOOP: break BID_LOOP } switch bidStatus.Type { - case bidder.BidStatusNoOfProviders: - result.noOfProviders = bidStatus.Arg.(int) - case bidder.BidStatusAttempted: - result.blockNumber = bidStatus.Arg.(uint64) case bidder.BidStatusCommitment: - result.commitments = append(result.commitments, bidStatus.Arg.(*bidderapiv1.Commitment)) - if t.fastTrack(result.commitments, optedInSlot) && txn.Status != TxStatusPreConfirmed { + txn.commitments = append(txn.commitments, bidStatus.Arg.(*bidderapiv1.Commitment)) + if t.fastTrack(txn.commitments, optedInSlot) && txn.Status != TxStatusPreConfirmed { txn.Status = TxStatusPreConfirmed - txn.BlockNumber = int64(result.blockNumber) + txn.BlockNumber = int64(bidBlockNo) logger.Info( "Transaction fast-tracked based on commitments", "blockNumber", result.blockNumber, "bidAmount", result.bidAmount.String(), ) - if err := t.store.StoreTransaction(ctx, txn, result.commitments, result.logs); err != nil { + if err := t.store.StoreTransaction(ctx, txn, txn.commitments, txn.logs); err != nil { logger.Error("Failed to store fast-tracked transaction", "error", err) } } @@ -781,8 +756,8 @@ BID_LOOP: } logger.Info( "Bid operation complete", - "noOfProviders", result.noOfProviders, - "noOfCommitments", len(result.commitments), + "noOfProviders", txn.noOfProviders, + "noOfCommitments", len(txn.commitments), "blockNumber", result.blockNumber, "optedInSlot", optedInSlot, ) diff --git a/tools/preconf-rpc/sender/sender_test.go b/tools/preconf-rpc/sender/sender_test.go index e0f22affa..b18f2143f 100644 --- a/tools/preconf-rpc/sender/sender_test.go +++ b/tools/preconf-rpc/sender/sender_test.go @@ -13,8 +13,8 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" bidderapiv1 "github.com/primev/mev-commit/p2p/gen/go/bidderapi/v1" + "github.com/primev/mev-commit/tools/preconf-rpc/bidder" "github.com/primev/mev-commit/tools/preconf-rpc/sender" - optinbidder "github.com/primev/mev-commit/x/opt-in-bidder" "github.com/primev/mev-commit/x/util" ) @@ -165,13 +165,13 @@ type bidOp struct { bidAmount *big.Int slashAmount *big.Int rawTx string - opts *optinbidder.BidOpts + opts *bidder.BidOpts } type mockBidder struct { optinEstimate chan int64 in chan bidOp - out chan chan optinbidder.BidStatus + out chan chan bidder.BidStatus } func (m *mockBidder) Estimate() (int64, error) { @@ -184,8 +184,8 @@ func (m *mockBidder) Bid( bidAmount *big.Int, slashAmount *big.Int, rawTx string, - opts *optinbidder.BidOpts, -) (chan optinbidder.BidStatus, error) { + opts *bidder.BidOpts, +) (chan bidder.BidStatus, error) { m.in <- bidOp{ bidAmount: bidAmount, slashAmount: slashAmount, @@ -197,6 +197,10 @@ func (m *mockBidder) Bid( return res, nil } +func (m *mockBidder) ConnectedProviders(ctx context.Context) ([]string, error) { + return []string{"provider1", "provider2"}, nil +} + type mockPricer struct { out chan map[int64]float64 } @@ -272,10 +276,10 @@ func TestSender(t *testing.T) { testPricer := &mockPricer{ out: make(chan map[int64]float64, 10), } - bidder := &mockBidder{ + bidderImpl := &mockBidder{ optinEstimate: make(chan int64, 10), in: make(chan bidOp, 10), - out: make(chan chan optinbidder.BidStatus, 10), + out: make(chan chan bidder.BidStatus, 10), } blockTracker := &mockBlockTracker{ out: make(chan uint64, 10), @@ -287,14 +291,14 @@ func TestSender(t *testing.T) { sndr, err := sender.NewTxSender( st, - bidder, + bidderImpl, testPricer, blockTracker, &mockTransferer{}, notifier, &mockSimulator{}, big.NewInt(1), // Settlement chain ID - util.NewTestLogger(io.Discard), + util.NewTestLogger(os.Stdout), ) if err != nil { t.Fatalf("failed to create sender: %v", err) @@ -327,12 +331,12 @@ func TestSender(t *testing.T) { } // Simulate opted in block - bidder.optinEstimate <- 2 + bidderImpl.optinEstimate <- 2 <-blockTracker.bnIn blockTracker.bnErr <- errors.New("simulated error for testing") - bidder.optinEstimate <- 7 + bidderImpl.optinEstimate <- 7 <-blockTracker.bnIn @@ -352,21 +356,13 @@ func TestSender(t *testing.T) { blockTracker.out <- 1 // Simulate a bid response - bidOp := <-bidder.in + bidOp := <-bidderImpl.in if bidOp.rawTx != tx1.Raw[2:] { t.Fatalf("expected raw transaction %s, got %s", tx1.Raw, bidOp.rawTx) } - resC := make(chan optinbidder.BidStatus, 3) - resC <- optinbidder.BidStatus{ - Type: optinbidder.BidStatusNoOfProviders, - Arg: 1, - } - resC <- optinbidder.BidStatus{ - Type: optinbidder.BidStatusAttempted, - Arg: uint64(1), - } - resC <- optinbidder.BidStatus{ - Type: optinbidder.BidStatusCommitment, + resC := make(chan bidder.BidStatus, 3) + resC <- bidder.BidStatus{ + Type: bidder.BidStatusCommitment, Arg: &bidderapiv1.Commitment{ TxHashes: []string{tx1.Hash().Hex()}, BidAmount: big.NewInt(100).String(), @@ -374,8 +370,17 @@ func TestSender(t *testing.T) { ProviderAddress: "provider1", }, } + resC <- bidder.BidStatus{ + Type: bidder.BidStatusCommitment, + Arg: &bidderapiv1.Commitment{ + TxHashes: []string{tx1.Hash().Hex()}, + BidAmount: big.NewInt(100).String(), + BlockNumber: 1, + ProviderAddress: "provider2", + }, + } close(resC) - bidder.out <- resC + bidderImpl.out <- resC res := <-st.preconfirmedTxns if res.txn == nil { @@ -397,8 +402,8 @@ func TestSender(t *testing.T) { t.Fatalf("expected transaction hash %s, got %s", tx1.Hash().Hex(), res.txn.Hash().Hex()) } // Check that the commitments are as expected - if len(res.commitments) != 1 { - t.Fatalf("expected 1 commitment, got %d", len(res.commitments)) + if len(res.commitments) != 2 { + t.Fatalf("expected 2 commitments, got %d", len(res.commitments)) } tx2 := &sender.Transaction{ @@ -420,7 +425,7 @@ func TestSender(t *testing.T) { } // Simulate non opted in block - bidder.optinEstimate <- 20 + bidderImpl.optinEstimate <- 20 <-blockTracker.bnIn blockTracker.bnOut <- blockNoOp{ @@ -436,25 +441,17 @@ func TestSender(t *testing.T) { } // Simulate a bid response - bidOp = <-bidder.in + bidOp = <-bidderImpl.in if bidOp.rawTx != tx2.Raw[2:] { t.Fatalf("expected raw transaction %s, got %s", tx1.Raw, bidOp.rawTx) } - resC = make(chan optinbidder.BidStatus, 3) - resC <- optinbidder.BidStatus{ - Type: optinbidder.BidStatusNoOfProviders, - Arg: 1, - } - resC <- optinbidder.BidStatus{ - Type: optinbidder.BidStatusAttempted, - Arg: uint64(2), - } + resC = make(chan bidder.BidStatus, 3) // Simulate retry due to incomplete commitments close(resC) - bidder.out <- resC + bidderImpl.out <- resC // Simulate non opted in block - bidder.optinEstimate <- 18 + bidderImpl.optinEstimate <- 18 <-blockTracker.bnIn blockTracker.bnOut <- blockNoOp{ @@ -473,21 +470,13 @@ func TestSender(t *testing.T) { blockTracker.out <- 2 // Simulate a bid response - bidOp = <-bidder.in + bidOp = <-bidderImpl.in if bidOp.rawTx != tx2.Raw[2:] { t.Fatalf("expected raw transaction %s, got %s", tx1.Raw, bidOp.rawTx) } - resC = make(chan optinbidder.BidStatus, 3) - resC <- optinbidder.BidStatus{ - Type: optinbidder.BidStatusNoOfProviders, - Arg: 1, - } - resC <- optinbidder.BidStatus{ - Type: optinbidder.BidStatusAttempted, - Arg: uint64(2), - } - resC <- optinbidder.BidStatus{ - Type: optinbidder.BidStatusCommitment, + resC = make(chan bidder.BidStatus, 3) + resC <- bidder.BidStatus{ + Type: bidder.BidStatusCommitment, Arg: &bidderapiv1.Commitment{ TxHashes: []string{tx1.Hash().Hex()}, BidAmount: big.NewInt(100).String(), @@ -496,7 +485,7 @@ func TestSender(t *testing.T) { }, } close(resC) - bidder.out <- resC + bidderImpl.out <- resC res = <-st.preconfirmedTxns if res.txn == nil { @@ -537,10 +526,10 @@ func TestCancelTransaction(t *testing.T) { testPricer := &mockPricer{ out: make(chan map[int64]float64, 10), } - bidder := &mockBidder{ + bidderImpl := &mockBidder{ optinEstimate: make(chan int64), in: make(chan bidOp, 10), - out: make(chan chan optinbidder.BidStatus, 10), + out: make(chan chan bidder.BidStatus, 10), } blockTracker := &mockBlockTracker{ out: make(chan uint64, 10), @@ -551,7 +540,7 @@ func TestCancelTransaction(t *testing.T) { sndr, err := sender.NewTxSender( st, - bidder, + bidderImpl, testPricer, blockTracker, &mockTransferer{}, @@ -602,7 +591,7 @@ func TestCancelTransaction(t *testing.T) { blockTracker.bnErr <- errors.New("simulated error for testing") blockTracker.bnErr <- errors.New("simulated error for testing") - bidder.optinEstimate <- 2 + bidderImpl.optinEstimate <- 2 cancelled, err := sndr.CancelTransaction(ctx, tx1.Hash()) if err != nil { @@ -615,3 +604,141 @@ func TestCancelTransaction(t *testing.T) { cancel() <-done } + +func TestIgnoreProvidersOnRetry(t *testing.T) { + t.Parallel() + + st := newMockStore() + testPricer := &mockPricer{ + out: make(chan map[int64]float64, 10), + } + bidderImpl := &mockBidder{ + optinEstimate: make(chan int64, 10), + in: make(chan bidOp, 10), + out: make(chan chan bidder.BidStatus, 10), + } + blockTracker := &mockBlockTracker{ + out: make(chan uint64, 10), + bnIn: make(chan struct{}, 10), + bnOut: make(chan blockNoOp, 10), + bnErr: make(chan error, 1), + } + notifier := &mockNotifier{} + + sndr, err := sender.NewTxSender( + st, + bidderImpl, + testPricer, + blockTracker, + &mockTransferer{}, + notifier, + &mockSimulator{}, + big.NewInt(1), // Settlement chain ID + util.NewTestLogger(io.Discard), + ) + if err != nil { + t.Fatalf("failed to create sender: %v", err) + } + + ctx, cancel := context.WithCancel(context.Background()) + + done := sndr.Start(ctx) + + tx1 := &sender.Transaction{ + Transaction: types.NewTransaction( + 1, + common.HexToAddress("0x1234567890123456789012345678901234567890"), + big.NewInt(100), + 21000, + big.NewInt(1), + nil, + ), + Sender: common.HexToAddress("0x1234567890123456789012345678901234567890"), + Type: sender.TxTypeRegular, + Raw: "0x1234567890123456789012345678901234567890", + } + + if err := st.AddBalance(ctx, tx1.Sender, big.NewInt(5e18)); err != nil { + t.Fatalf("failed to add balance: %v", err) + } + + if err := sndr.Enqueue(ctx, tx1); err != nil { + t.Fatalf("failed to enqueue transaction: %v", err) + } + + // Simulate opted in block + bidderImpl.optinEstimate <- 2 + + <-blockTracker.bnIn + + blockTracker.bnOut <- blockNoOp{ + block: 1, + timeTillNextBlock: 5 * time.Second, + } + + // Simulate a price estimate + testPricer.out <- map[int64]float64{ + 90: 1.0, + 95: 1.5, + 99: 2.0, + } + + // Simulate a bid response + bidOp := <-bidderImpl.in + if bidOp.rawTx != tx1.Raw[2:] { + t.Fatalf("expected raw transaction %s, got %s", tx1.Raw, bidOp.rawTx) + } + resC := make(chan bidder.BidStatus, 3) + resC <- bidder.BidStatus{ + Type: bidder.BidStatusCommitment, + Arg: &bidderapiv1.Commitment{ + TxHashes: []string{tx1.Hash().Hex()}, + BidAmount: big.NewInt(100).String(), + BlockNumber: 1, + ProviderAddress: "provider1", + }, + } + close(resC) + bidderImpl.out <- resC + + bidderImpl.optinEstimate <- 2 + + <-blockTracker.bnIn + + blockTracker.bnOut <- blockNoOp{ + block: 1, + timeTillNextBlock: 2 * time.Second, + } + + // Simulate a price estimate + testPricer.out <- map[int64]float64{ + 90: 1.0, + 95: 1.5, + 99: 2.0, + } + + bidOp = <-bidderImpl.in + if len(bidOp.opts.IgnoreProviders) != 1 { + t.Fatalf("expected 1 ignored provider, got %d", len(bidOp.opts.IgnoreProviders)) + } + + resC = make(chan bidder.BidStatus, 3) + resC <- bidder.BidStatus{ + Type: bidder.BidStatusCommitment, + Arg: &bidderapiv1.Commitment{ + TxHashes: []string{tx1.Hash().Hex()}, + BidAmount: big.NewInt(100).String(), + BlockNumber: 1, + ProviderAddress: "provider2", + }, + } + close(resC) + bidderImpl.out <- resC + res := <-st.preconfirmedTxns + if res.txn == nil { + t.Fatal("expected a preconfirmed transaction, got nil") + } + + cancel() + <-done +} diff --git a/tools/preconf-rpc/service/service.go b/tools/preconf-rpc/service/service.go index b9b6dd714..47d3c9d87 100644 --- a/tools/preconf-rpc/service/service.go +++ b/tools/preconf-rpc/service/service.go @@ -23,6 +23,7 @@ import ( bidderapiv1 "github.com/primev/mev-commit/p2p/gen/go/bidderapi/v1" debugapiv1 "github.com/primev/mev-commit/p2p/gen/go/debugapi/v1" notificationsapiv1 "github.com/primev/mev-commit/p2p/gen/go/notificationsapi/v1" + bidder "github.com/primev/mev-commit/tools/preconf-rpc/bidder" "github.com/primev/mev-commit/tools/preconf-rpc/blocktracker" "github.com/primev/mev-commit/tools/preconf-rpc/handlers" "github.com/primev/mev-commit/tools/preconf-rpc/notifier" @@ -35,7 +36,6 @@ import ( "github.com/primev/mev-commit/x/contracts/ethwrapper" "github.com/primev/mev-commit/x/health" "github.com/primev/mev-commit/x/keysigner" - bidder "github.com/primev/mev-commit/x/opt-in-bidder" "github.com/primev/mev-commit/x/transfer" "google.golang.org/grpc" "google.golang.org/grpc/credentials" From 937edcacf7fe57d6f030c8572fdfe086f7050a7d Mon Sep 17 00:00:00 2001 From: Alok Date: Tue, 11 Nov 2025 15:25:27 +0530 Subject: [PATCH 09/16] fix: remove old instant bridge --- .../templates/jobs/instant-bridge.nomad.j2 | 204 --------- .../nomad/playbooks/variables/profiles.yml | 57 --- tools/instant-bridge/.goreleaser.yml | 64 --- tools/instant-bridge/api/api.go | 337 --------------- tools/instant-bridge/main.go | 266 ------------ tools/instant-bridge/service/service.go | 218 ---------- x/opt-in-bidder/bidder.go | 398 ------------------ x/opt-in-bidder/bidder_test.go | 265 ------------ x/opt-in-bidder/export_test.go | 7 - 9 files changed, 1816 deletions(-) delete mode 100644 infrastructure/nomad/playbooks/templates/jobs/instant-bridge.nomad.j2 delete mode 100644 tools/instant-bridge/.goreleaser.yml delete mode 100644 tools/instant-bridge/api/api.go delete mode 100644 tools/instant-bridge/main.go delete mode 100644 tools/instant-bridge/service/service.go delete mode 100644 x/opt-in-bidder/bidder.go delete mode 100644 x/opt-in-bidder/bidder_test.go delete mode 100644 x/opt-in-bidder/export_test.go diff --git a/infrastructure/nomad/playbooks/templates/jobs/instant-bridge.nomad.j2 b/infrastructure/nomad/playbooks/templates/jobs/instant-bridge.nomad.j2 deleted file mode 100644 index b20d0af61..000000000 --- a/infrastructure/nomad/playbooks/templates/jobs/instant-bridge.nomad.j2 +++ /dev/null @@ -1,204 +0,0 @@ -#jinja2: trim_blocks:True, lstrip_blocks:True -job "{{ job.name }}" { - datacenters = ["{{ datacenter }}"] - - group "{{ job.name }}-group" { - count = {{ job.count }} - - {% if env == 'devenv' %} - restart { - attempts = 0 - mode = "fail" - } - - reschedule { - attempts = 0 - unlimited = false - } - {% endif %} - - network { - mode = "bridge" - - dns { - servers = {{ (ansible_facts['dns']['nameservers'] + ['1.1.1.1']) | tojson }} - } - - {% for port_name, port_details in job.ports[0].items() %} - port "{{ port_name }}" { - {% if port_details.get('static') %} - static = {{ port_details['static'] }} - {% endif %} - {% if port_details.get('to') %} - to = {{ port_details['to'] }} - {% endif %} - } - {% endfor %} - } - - {% for port_name in job.ports[0] %} - service { - name = "{{ job.name }}" - port = "{{ port_name }}" - tags = ["{{ port_name }}"] - provider = "nomad" - {% if port_name == "http" %} - check { - type = "http" - path = "/health" - interval = "10s" - timeout = "2s" - } - {% endif %} - } - {% endfor %} - - task "instantbridge" { - driver = "exec" - - resources { - cpu = 4000 - memory = 4096 - } - - artifact { - source = "https://foundry.paradigm.xyz" - destination = "local/foundry.sh" - } - - {% if env != 'devenv' %} - artifact { - source = "https://primev-infrastructure-artifacts.s3.us-west-2.amazonaws.com/instant-bridge_{{ version }}_Linux_{{ target_system_architecture }}.tar.gz" - } - {% else %} - artifact { - source = "http://{{ ansible_facts['default_ipv4']['address'] }}:1111/instant-bridge_{{ version }}_Linux_{{ target_system_architecture }}.tar.gz" - } - {% endif %} - - template { - data = <<-EOH - XDG_CONFIG_HOME="local/.config" - INSTANT_BRIDGE_LOG_LEVEL="{{ job.env.get('log-level', 'info') }}" - INSTANT_BRIDGE_LOG_FMT="{{ job.env.get('log-format', 'json') }}" - INSTANT_BRIDGE_LOG_TAGS="{{ 'service.name:' + job.name + '-{{ env "NOMAD_ALLOC_INDEX" }}' + ',service.version:' + version }}" - CONTRACTS_JSON_URL="{{ job.env.get('contracts_json_url', '') }}" - INSTANT_BRIDGE_SETTLEMENT_RPC_URL="{{ job.env.get('settlement_rpc_url', '') }}" - {%- raw %} - INSTANT_BRIDGE_KEYSTORE_DIR="/local/data-{{ env "NOMAD_ALLOC_INDEX" }}/keystore" - INSTANT_BRIDGE_KEYSTORE_FILENAME="{{ with secret "secret/data/mev-commit" }}{{ .Data.data.instant_bridge_keystore_filename }}{{ end }}" - INSTANT_BRIDGE_KEYSTORE_PASSWORD="{{ with secret "secret/data/mev-commit" }}{{ .Data.data.instant_bridge_keystore_password }}{{ end }}" - {{- range nomadService "mev-commit-geth-bootnode1" }} - {{- if contains "http" .Tags }} - INSTANT_BRIDGE_SETTLEMENT_RPC_URL="http://{{ .Address }}:{{ .Port }}" - {{- end }} - {{- end }} - {{- range nomadService "{% endraw %}{{ job.target.name }}{% raw %}" }} - {{- if contains "rpc" .Tags }} - INSTANT_BRIDGE_BIDDER_RPC_URL="{{ .Address }}:{{ .Port }}" - {{- end }} - {{- end }} - {% endraw %} - XDG_CONFIG_HOME="local/.config" - {% if profile == 'instant-bridge-test' %} - {%- raw %} - {{- $secret := secret "secret/data/mev-commit" }} - CONTRACT_DEPLOYER_KEYSTORE_PATH="/local/data-{{ env "NOMAD_ALLOC_INDEX" }}/deployer_keystore" - CONTRACT_DEPLOYER_KEYSTORE_FILENAME="{{ $secret.Data.data.contract_deployer_keystore_filename }}" - CONTRACT_DEPLOYER_KEYSTORE_PASSWORD="{{ $secret.Data.data.contract_deployer_keystore_password }}" - {% endraw %} - {% endif %} - INSTANT_BRIDGE_L1_RPC_URLS="{{ job.env['l1_rpc_urls'] }}" - CONTRACTS_PATH="local/contracts" - ARTIFACT_OUT_PATH="local" - EOH - destination = "secrets/.env" - env = true - } - - template { - data = <<-EOH - #!/usr/bin/env bash - - {% raw %} - {{- range nomadService "datadog-agent-logs-collector" }} - {{ if contains "tcp" .Tags }} - exec > >(nc {{ .Address }} {{ .Port }}) 2>&1 - {{ end }} - {{- end }} - mkdir -p "${INSTANT_BRIDGE_KEYSTORE_DIR}" > /dev/null 2>&1 - {{- with secret "secret/data/mev-commit" }} - INSTANT_BRIDGE_KEYSTORE_FILE="${INSTANT_BRIDGE_KEYSTORE_DIR}/${INSTANT_BRIDGE_KEYSTORE_FILENAME}" - echo '{{ .Data.data.instant_bridge_keystore }}' > "${INSTANT_BRIDGE_KEYSTORE_FILE}" - {{ end }} - {% endraw %} - - {% if profile == 'instant-bridge-test' %} - mkdir -p "${CONTRACT_DEPLOYER_KEYSTORE_PATH}" > /dev/null 2>&1 - CONTRACT_DEPLOYER_KEYSTORE_FILE="${CONTRACT_DEPLOYER_KEYSTORE_PATH}/${CONTRACT_DEPLOYER_KEYSTORE_FILENAME}" - {%- raw %} - {{- $secret := secret "secret/data/mev-commit" }} - echo '{{ $secret.Data.data.contract_deployer_keystore }}' > "${CONTRACT_DEPLOYER_KEYSTORE_FILE}" - {%- endraw %} - {% endif %} - - {% raw %} - {{- range nomadService "contracts-deployer" }} - {{ if contains "http" .Tags }} - CONTRACTS_JSON_URL="http://{{ .Address }}:{{ .Port }}/contracts.json" - {{ end }} - {{- end }} - {% endraw %} - CONTRACTS_FILE="/local/contracts.json" - curl -s -o "${CONTRACTS_FILE}" "${CONTRACTS_JSON_URL}" - export INSTANT_BRIDGE_SETTLEMENT_CONTRACT_ADDR="$(jq -r '.SettlementGateway' ${CONTRACTS_FILE})" - export INSTANT_BRIDGE_L1_CONTRACT_ADDR="$(jq -r '.L1Gateway' ${CONTRACTS_FILE})" - - chmod +x local/foundry.sh && local/foundry.sh - chmod +x ${XDG_CONFIG_HOME}/.foundry/bin/foundryup - ${XDG_CONFIG_HOME}/.foundry/bin/foundryup 2>&1 - if [ $? -ne 0 ]; then - echo "Failed to install foundry tools" - exit 1 - fi - export PATH="${XDG_CONFIG_HOME}/.foundry/bin:$PATH" - {%- raw %} - {{- range nomadService "mock-l1" }} - {{- if contains "ws" .Tags }} - L1_RPC_URL="ws://{{ .Address}}:{{ .Port }}" - {{- end }} - {{- with secret "secret/data/mev-commit" }} - ADDRESS="$(cat "${INSTANT_BRIDGE_KEYSTORE_FILE}" | jq -r '.address')" - {{ end }} - cast send \ - --keystore "${CONTRACT_DEPLOYER_KEYSTORE_FILE}" \ - --password "${CONTRACT_DEPLOYER_KEYSTORE_PASSWORD}" \ - --priority-gas-price 2000000000 \ - --gas-price 5000000000 \ - --value 100ether \ - --rpc-url "${L1_RPC_URL}" \ - "${ADDRESS}" - - if [ $? -eq 0 ]; then - echo "Funds successfully sent to: ${ADDRESS}" - else - echo "Failed to send funds to: ${ADDRESS}" - fi - {{- end }} - {% endraw %} - - chmod +x local/instant-bridge - exec ./local/instant-bridge - EOH - destination = "local/run.sh" - change_mode = "noop" - perms = "0755" - } - - config { - command = "bash" - args = ["-c", "exec local/run.sh"] - } - } - } -} diff --git a/infrastructure/nomad/playbooks/variables/profiles.yml b/infrastructure/nomad/playbooks/variables/profiles.yml index 2f651a713..b928cb447 100644 --- a/infrastructure/nomad/playbooks/variables/profiles.yml +++ b/infrastructure/nomad/playbooks/variables/profiles.yml @@ -39,9 +39,6 @@ artifacts: beacon-emulator: &beacon_emulator_artifact type: binary path: tools/beacon-emulator - instant-bridge: &instant_bridge_artifact - type: binary - path: tools/instant-bridge preconf-rpc: &preconf_rpc_artifact type: binary path: tools/preconf-rpc @@ -723,24 +720,6 @@ jobs: env: l1_rpc_url: "{{ resolved_l1_rpc_urls.split(',')[0] }}" - instant_bridge: &instant_bridge_job - name: instant-bridge - template: instant-bridge.nomad.j2 - artifacts: - - *instant_bridge_artifact - - keystores: - instant_bridge_keystore: - count: 1 - target: *mev_commit_bidder_node1_job - ports: - - http: - to: 8080 - env: - l1_chain_id: "{{ environments[env].chain_id }}" - l1_rpc_urls: "{{ resolved_l1_rpc_urls }}" - settlement_rpc_url: "{{ settlement_rpc_url if settlement_rpc_url is defined else '' }}" - contracts_json_url: "{{ contracts_json_url if contracts_json_url is defined else '' }}" - preconf_rpc: &preconf_rpc_job name: preconf-rpc template: preconf-rpc.nomad.j2 @@ -941,35 +920,6 @@ profiles: - *mev_commit_faucet_job - *datadog_agent_metrics_collector_job - instant-bridge-test: - jobs: - - *artifacts_job - - *datadog_agent_logs_collector_job - - *otel_collector_job - - *beacon_emulator_job - - *mock_l1_job - - *l1_transactor_job - - *mev_commit_geth_bootnode1_job - - *mev_commit_geth_signer_node1_job - - *mev_commit_geth_member_node_job - - *relay_emulator_job - - *contracts_deployer_job - - *mev_commit_bridge_job - - *mev_commit_dashboard_job - - *mev_commit_bootnode1_job - - *mev_commit_provider_node1_job - - *mev_commit_provider_node1_funder_job - - *mev_commit_provider_node2_job - - *mev_commit_provider_node2_funder_job - - *mev_commit_provider_node3_job - - *mev_commit_provider_node3_funder_job - - *mev_commit_provider_emulator_nodes_job - - *mev_commit_oracle_job - - *mev_commit_bidder_node1_job - - *mev_commit_bidder_node1_funder_job - - *instant_bridge_job - - *datadog_agent_metrics_collector_job - preconf-rpc-test: jobs: - *artifacts_job @@ -998,13 +948,6 @@ profiles: - *preconf_rpc_job - *datadog_agent_metrics_collector_job - instant-bridge: - jobs: - - *artifacts_job - - *datadog_agent_logs_collector_job - - *mev_commit_bidder_node1_job - - *instant_bridge_job - archive: jobs: - *artifacts_job diff --git a/tools/instant-bridge/.goreleaser.yml b/tools/instant-bridge/.goreleaser.yml deleted file mode 100644 index f4c2001de..000000000 --- a/tools/instant-bridge/.goreleaser.yml +++ /dev/null @@ -1,64 +0,0 @@ -version: 1 - -project_name: instant-bridge -dist: /tmp/dist/instant-bridge - -builds: - - env: - - CGO_ENABLED=0 - goos: - - linux - goarch: - - amd64 - - arm64 - dir: ./tools/instant-bridge - binary: "{{ .ProjectName }}" - flags: - - -v - - -trimpath - -archives: - - format: tar.gz - name_template: >- - {{- .Binary }}_ - {{- with index .Env "RELEASE_VERSION" -}} - {{ . }} - {{- else -}} - {{- if .IsSnapshot }}{{ .ShortCommit }} - {{- else }}{{ .Version }} - {{- end }} - {{- end -}} - {{- with index .Env "DIRTY_SUFFIX" -}} - {{ . }} - {{- end -}}_ - {{- title .Os }}_ - {{- if eq .Arch "amd64" }}x86_64 - {{- else if eq .Arch "386" }}i386 - {{- else }}{{ .Arch }} - {{- end }} - {{- if .Arm }}v{{ .Arm }}{{ end }} - format_overrides: - - goos: windows - format: zip - -checksum: - name_template: >- - {{ .ProjectName }}_ - {{- with index .Env "RELEASE_VERSION" -}} - {{ . }} - {{- else -}} - {{- if .IsSnapshot }}{{ .ShortCommit }} - {{- else }}{{ .Version }} - {{- end }} - {{- end -}} - {{- with index .Env "DIRTY_SUFFIX" -}} - {{ . }} - {{- end -}} - _checksums.txt - -changelog: - sort: asc - filters: - exclude: - - "^docs:" - - "^test:" diff --git a/tools/instant-bridge/api/api.go b/tools/instant-bridge/api/api.go deleted file mode 100644 index eb079b5e8..000000000 --- a/tools/instant-bridge/api/api.go +++ /dev/null @@ -1,337 +0,0 @@ -package api - -import ( - "bufio" - "context" - "errors" - "fmt" - "log/slog" - "math/big" - "net" - "net/http" - "os" - "sync/atomic" - "time" - - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/ethclient" - "github.com/primev/mev-commit/p2p/pkg/apiserver" - "github.com/primev/mev-commit/x/health" - bidder "github.com/primev/mev-commit/x/opt-in-bidder" - "github.com/primev/mev-commit/x/transfer" -) - -type API struct { - logger *slog.Logger - mux *http.ServeMux - port int - srv *http.Server - health health.Health - bidder *bidder.BidderClient - transferer *transfer.Transferer - minServiceFee *big.Int - status *status - owner common.Address - l1Client *ethclient.Client - settlementClient *ethclient.Client - l1ChainID *big.Int - settlementChainID *big.Int -} - -type bid struct { - BridgeAmount string `json:"bridgeAmount"` - RawTx string `json:"rawTx"` - DestAddress string `json:"destAddress"` -} - -type status struct { - bidsAttempted atomic.Int64 - bidsSucceeded atomic.Int64 - transfersAttempted atomic.Int64 - transfersSucceeded atomic.Int64 - bridgedAmount atomic.Pointer[big.Int] - bidAmountSpent atomic.Pointer[big.Int] - feesAccumulated atomic.Pointer[big.Int] -} - -func NewAPI( - logger *slog.Logger, - port int, - health health.Health, - bdr *bidder.BidderClient, - transferer *transfer.Transferer, - minServiceFee *big.Int, - owner common.Address, - l1Client *ethclient.Client, - settlementClient *ethclient.Client, - l1ChainID *big.Int, - settlementChainID *big.Int, -) *API { - a := &API{ - logger: logger, - mux: http.NewServeMux(), - port: port, - status: &status{}, - health: health, - bidder: bdr, - transferer: transferer, - minServiceFee: minServiceFee, - owner: owner, - l1Client: l1Client, - settlementClient: settlementClient, - l1ChainID: l1ChainID, - settlementChainID: settlementChainID, - } - - a.status.bridgedAmount.Store(big.NewInt(0)) - a.status.bidAmountSpent.Store(big.NewInt(0)) - a.status.feesAccumulated.Store(big.NewInt(0)) - - a.mux.HandleFunc("GET /health", func(w http.ResponseWriter, req *http.Request) { - err := a.health.Health() - if err != nil { - apiserver.WriteError(w, http.StatusServiceUnavailable, err) - return - } - w.WriteHeader(http.StatusOK) - _, err = fmt.Fprintf(w, "ok\n") - if err != nil { - a.logger.Error( - "failed to write response", - "error", err, - ) - } - }) - - a.mux.HandleFunc("GET /estimate", func(w http.ResponseWriter, req *http.Request) { - estimation, err := a.bidder.Estimate() - if err != nil { - apiserver.WriteError(w, http.StatusInternalServerError, err) - return - } - if err := apiserver.WriteResponse(w, http.StatusOK, struct { - Seconds int64 `json:"seconds"` - Cost string `json:"cost"` - Destination string `json:"destination"` - }{ - Seconds: estimation, - Cost: a.minServiceFee.String(), - Destination: a.owner.Hex(), - }); err != nil { - a.logger.Error("failed to write response", "error", err) - } - }) - - a.mux.HandleFunc("GET /status", func(w http.ResponseWriter, req *http.Request) { - bridgedAmount := a.status.bridgedAmount.Load() - bidAmountSpent := a.status.bidAmountSpent.Load() - feesAccumulated := a.status.feesAccumulated.Load() - - l1Balance, err := a.l1Client.BalanceAt(req.Context(), a.owner, nil) - if err != nil { - apiserver.WriteError(w, http.StatusInternalServerError, err) - return - } - - settlementBalance, err := a.settlementClient.BalanceAt(req.Context(), a.owner, nil) - if err != nil { - apiserver.WriteError(w, http.StatusInternalServerError, err) - return - } - - if err := apiserver.WriteResponse(w, http.StatusOK, struct { - BidsAttempted int64 `json:"bidsAttempted"` - BidsSucceeded int64 `json:"bidsSucceeded"` - TransfersAttempted int64 `json:"transfersAttempted"` - TransfersSucceeded int64 `json:"transfersSucceeded"` - BridgedAmount string `json:"bridgedAmount"` - BidAmountSpent string `json:"bidAmountSpent"` - FeesAccumulated string `json:"feesAccumulated"` - L1Balance string `json:"l1Balance"` - SettlementBalance string `json:"settlementBalance"` - }{ - BidsAttempted: a.status.bidsAttempted.Load(), - BidsSucceeded: a.status.bidsSucceeded.Load(), - TransfersAttempted: a.status.transfersAttempted.Load(), - TransfersSucceeded: a.status.transfersSucceeded.Load(), - BridgedAmount: bridgedAmount.String(), - BidAmountSpent: bidAmountSpent.String(), - FeesAccumulated: feesAccumulated.String(), - L1Balance: l1Balance.String(), - SettlementBalance: settlementBalance.String(), - }); err != nil { - a.logger.Error("failed to write response", "error", err) - } - }) - - a.mux.HandleFunc("POST /bid", func(w http.ResponseWriter, req *http.Request) { - b, err := apiserver.BindJSON[bid](w, req) - if err != nil { - apiserver.WriteError(w, http.StatusBadRequest, err) - return - } - - if b.RawTx == "" || b.BridgeAmount == "" { - apiserver.WriteError(w, http.StatusBadRequest, errors.New("missing fields")) - return - } - - tx, err := a.transferer.ValidateTx(b.RawTx, a.l1ChainID) - if err != nil { - apiserver.WriteError(w, http.StatusBadRequest, fmt.Errorf("invalid raw tx: %w", err)) - return - } - - bridgeAmt, ok := new(big.Int).SetString(b.BridgeAmount, 10) - if !ok { - apiserver.WriteError(w, http.StatusBadRequest, errors.New("invalid bridge amount")) - return - } - - minCost := new(big.Int).Add(bridgeAmt, a.minServiceFee) - if tx.Value().Cmp(minCost) < 0 { - diff := new(big.Int).Sub(minCost, tx.Value()) - apiserver.WriteError( - w, - http.StatusBadRequest, - fmt.Errorf("insufficient funds; short by %s", diff.String()), - ) - return - } - - fees := new(big.Int).Sub(tx.Value(), bridgeAmt) - halfFee := big.NewInt(0).Div(fees, big.NewInt(2)) - - var destAddr common.Address - if b.DestAddress == "" { - destAddr, err = a.transferer.Sender(tx) - if err != nil { - apiserver.WriteError( - w, - http.StatusBadRequest, - fmt.Errorf("failed to identify sender: %w", err), - ) - return - } - } else { - destAddr = common.HexToAddress(b.DestAddress) - } - - a.status.bidsAttempted.Add(1) - statusC, err := a.bidder.Bid( - req.Context(), - halfFee, - bridgeAmt, - // bridgeAmt, - b.RawTx, - nil, - ) - if err != nil { - apiserver.WriteError(w, http.StatusInternalServerError, err) - return - } - - for status := range statusC { - switch status.Type { - case bidder.BidStatusNoOfProviders: - a.logger.Info("no of providers", "count", status.Arg.(int)) - case bidder.BidStatusWaitSecs: - a.logger.Info("waiting for next slot", "seconds", status.Arg.(int)) - case bidder.BidStatusAttempted: - a.logger.Info("bid attempted", "block", status.Arg) - case bidder.BidStatusFailed: - apiserver.WriteError( - w, - http.StatusInternalServerError, - fmt.Errorf("bid failed: %s", status.Arg.(string)), - ) - return - } - } - - a.status.bidsSucceeded.Add(1) - - a.status.transfersAttempted.Add(1) - err = a.transferer.Transfer( - req.Context(), - destAddr, - a.settlementChainID, - bridgeAmt, - ) - if err != nil { - apiserver.WriteError(w, http.StatusInternalServerError, err) - return - } - a.status.transfersSucceeded.Add(1) - a.status.bridgedAmount.Store(new(big.Int).Add(a.status.bridgedAmount.Load(), bridgeAmt)) - a.status.bidAmountSpent.Store(new(big.Int).Add(a.status.bidAmountSpent.Load(), halfFee)) - a.status.feesAccumulated.Store(new(big.Int).Add(a.status.feesAccumulated.Load(), halfFee)) - - if err := apiserver.WriteResponse(w, http.StatusOK, struct { - Message string `json:"message"` - }{ - Message: "success", - }); err != nil { - a.logger.Error("failed to write response", "error", err) - } - }) - - return a -} - -func (a *API) Start() { - a.srv = &http.Server{ - Addr: fmt.Sprintf(":%d", a.port), - Handler: http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { - recorder := &responseStatusRecorder{ResponseWriter: w} - - start := time.Now() - a.mux.ServeHTTP(recorder, req) - a.logger.Info( - "api access", - slog.Int("http_status", recorder.status), - slog.String("http_method", req.Method), - slog.String("path", req.URL.Path), - slog.Duration("duration", time.Since(start)), - ) - }), - } - - go func() { - if err := a.srv.ListenAndServe(); err != nil { - fmt.Fprintf(os.Stderr, "error: %v\n", err) - } - }() -} - -func (a *API) Close() error { - ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) - defer cancel() - - return a.srv.Shutdown(ctx) -} - -type responseStatusRecorder struct { - http.ResponseWriter - status int -} - -func (r *responseStatusRecorder) WriteHeader(status int) { - r.status = status - r.ResponseWriter.WriteHeader(status) -} - -// Hijack implements http.Hijacker. -func (r *responseStatusRecorder) Hijack() (net.Conn, *bufio.ReadWriter, error) { - return r.ResponseWriter.(http.Hijacker).Hijack() -} - -// Flush implements http.Flusher. -func (r *responseStatusRecorder) Flush() { - r.ResponseWriter.(http.Flusher).Flush() -} - -// Push implements http.Pusher. -func (r *responseStatusRecorder) Push(target string, opts *http.PushOptions) error { - return r.ResponseWriter.(http.Pusher).Push(target, opts) -} diff --git a/tools/instant-bridge/main.go b/tools/instant-bridge/main.go deleted file mode 100644 index edd3415a9..000000000 --- a/tools/instant-bridge/main.go +++ /dev/null @@ -1,266 +0,0 @@ -package main - -import ( - "fmt" - "math/big" - "os" - "os/signal" - "slices" - "strings" - "syscall" - - "github.com/ethereum/go-ethereum/common" - "github.com/primev/mev-commit/tools/instant-bridge/service" - "github.com/primev/mev-commit/x/keysigner" - "github.com/primev/mev-commit/x/util" - "github.com/urfave/cli/v2" -) - -var ( - optionHTTPPort = &cli.IntFlag{ - Name: "http-port", - Usage: "port for the HTTP server", - EnvVars: []string{"INSTANT_BRIDGE_HTTP_PORT"}, - Value: 8080, - } - - optionKeystorePath = &cli.StringFlag{ - Name: "keystore-dir", - Usage: "directory where keystore file is stored", - EnvVars: []string{"INSTANT_BRIDGE_KEYSTORE_DIR"}, - Required: true, - } - - optionKeystorePassword = &cli.StringFlag{ - Name: "keystore-password", - Usage: "use to access keystore", - EnvVars: []string{"INSTANT_BRIDGE_KEYSTORE_PASSWORD"}, - Required: true, - } - - optionL1RPCUrls = &cli.StringSliceFlag{ - Name: "l1-rpc-urls", - Usage: "URLs for L1 RPC", - EnvVars: []string{"INSTANT_BRIDGE_L1_RPC_URLS"}, - Required: true, - } - - optionSettlementRPCUrl = &cli.StringFlag{ - Name: "settlement-rpc-url", - Usage: "URL for settlement RPC", - EnvVars: []string{"INSTANT_BRIDGE_SETTLEMENT_RPC_URL"}, - Required: true, - } - - optionBidderRPCUrl = &cli.StringFlag{ - Name: "bidder-rpc-url", - Usage: "URL for mev-commit bidder RPC", - EnvVars: []string{"INSTANT_BRIDGE_BIDDER_RPC_URL"}, - Required: true, - } - - optionL1ContractAddr = &cli.StringFlag{ - Name: "l1-contract-addr", - Usage: "address of the L1 gateway contract", - EnvVars: []string{"INSTANT_BRIDGE_L1_CONTRACT_ADDR"}, - Required: true, - } - - optionSettlementThreshold = &cli.StringFlag{ - Name: "settlement-threshold", - Usage: "Minimum threshold for settlement chain balance", - EnvVars: []string{"INSTANT_BRIDGE_SETTLEMENT_THRESHOLD"}, - Value: "5000000000000000000", // 5 ETH - } - - optionSettlementTopup = &cli.StringFlag{ - Name: "settlement-topup", - Usage: "topup for settlement", - EnvVars: []string{"INSTANT_BRIDGE_SETTLEMENT_TOPUP"}, - Value: "10000000000000000000", // 10 ETH - } - - optionAutoDepositAmount = &cli.StringFlag{ - Name: "auto-deposit-amount", - Usage: "auto deposit amount", - EnvVars: []string{"INSTANT_BRIDGE_AUTO_DEPOSIT_AMOUNT"}, - Value: "1000000000000000000", // 1 ETH - } - - optionMinServiceFee = &cli.StringFlag{ - Name: "min-service-fee", - Usage: "minimum service fee", - EnvVars: []string{"INSTANT_BRIDGE_MIN_SERVICE_FEE"}, - Value: "50000000000000000", // 0.05 ETH - } - - optionGasTipCap = &cli.StringFlag{ - Name: "gas-tip-cap", - Usage: "gas tip cap", - EnvVars: []string{"INSTANT_BRIDGE_GAS_TIP_CAP"}, - Value: "50000000", // 0.05 gWEI - } - - optionGasFeeCap = &cli.StringFlag{ - Name: "gas-fee-cap", - Usage: "gas fee cap", - EnvVars: []string{"INSTANT_BRIDGE_GAS_FEE_CAP"}, - Value: "60000000", // 0.06 gWEI - } - - optionSettlementContractAddr = &cli.StringFlag{ - Name: "settlement-contract-addr", - Usage: "address of the settlement gateway contract", - EnvVars: []string{"INSTANT_BRIDGE_SETTLEMENT_CONTRACT_ADDR"}, - Required: true, - } - - optionLogFmt = &cli.StringFlag{ - Name: "log-fmt", - Usage: "log format to use, options are 'text' or 'json'", - EnvVars: []string{"INSTANT_BRIDGE_LOG_FMT"}, - Value: "text", - Action: func(ctx *cli.Context, s string) error { - if !slices.Contains([]string{"text", "json"}, s) { - return fmt.Errorf("invalid log-fmt, expecting 'text' or 'json'") - } - return nil - }, - } - - optionLogLevel = &cli.StringFlag{ - Name: "log-level", - Usage: "log level to use, options are 'debug', 'info', 'warn', 'error'", - EnvVars: []string{"INSTANT_BRIDGE_LOG_LEVEL"}, - Value: "info", - Action: func(ctx *cli.Context, s string) error { - if !slices.Contains([]string{"debug", "info", "warn", "error"}, s) { - return fmt.Errorf("invalid log-level, expecting 'debug', 'info', 'warn', 'error'") - } - return nil - }, - } - - optionLogTags = &cli.StringFlag{ - Name: "log-tags", - Usage: "log tags is a comma-separated list of pairs that will be inserted into each log line", - EnvVars: []string{"INSTANT_BRIDGE_LOG_TAGS"}, - Action: func(ctx *cli.Context, s string) error { - for i, p := range strings.Split(s, ",") { - if len(strings.Split(p, ":")) != 2 { - return fmt.Errorf("invalid log-tags at index %d, expecting ", i) - } - } - return nil - }, - } -) - -func main() { - app := &cli.App{ - Name: "instant-bridge", - Usage: "Instant Bridge service", - Flags: []cli.Flag{ - optionHTTPPort, - optionLogFmt, - optionLogLevel, - optionLogTags, - optionKeystorePath, - optionKeystorePassword, - optionL1RPCUrls, - optionSettlementRPCUrl, - optionBidderRPCUrl, - optionL1ContractAddr, - optionSettlementThreshold, - optionSettlementTopup, - optionMinServiceFee, - optionGasTipCap, - optionGasFeeCap, - optionSettlementContractAddr, - optionAutoDepositAmount, - }, - Action: func(c *cli.Context) error { - logger, err := util.NewLogger( - c.String(optionLogLevel.Name), - c.String(optionLogFmt.Name), - c.String(optionLogTags.Name), - c.App.Writer, - ) - if err != nil { - return fmt.Errorf("failed to create logger: %w", err) - } - - minServiceFee, ok := new(big.Int).SetString(c.String(optionMinServiceFee.Name), 10) - if !ok { - return fmt.Errorf("failed to parse min-service-fee") - } - - gasTipCap, ok := new(big.Int).SetString(c.String(optionGasTipCap.Name), 10) - if !ok { - return fmt.Errorf("failed to parse gas-tip-cap") - } - - gasFeeCap, ok := new(big.Int).SetString(c.String(optionGasFeeCap.Name), 10) - if !ok { - return fmt.Errorf("failed to parse gas-fee-cap") - } - - autoDepositAmount, ok := new(big.Int).SetString(c.String(optionAutoDepositAmount.Name), 10) - if !ok { - return fmt.Errorf("failed to parse auto-deposit-amount") - } - - settlementThreshold, ok := new(big.Int).SetString(c.String(optionSettlementThreshold.Name), 10) - if !ok { - return fmt.Errorf("failed to parse settlement-threshold") - } - - settlementTopup, ok := new(big.Int).SetString(c.String(optionSettlementTopup.Name), 10) - if !ok { - return fmt.Errorf("failed to parse settlement-topup") - } - - signer, err := keysigner.NewKeystoreSigner( - c.String(optionKeystorePath.Name), - c.String(optionKeystorePassword.Name), - ) - if err != nil { - return fmt.Errorf("failed to create signer: %w", err) - } - - sigc := make(chan os.Signal, 1) - signal.Notify(sigc, syscall.SIGINT, syscall.SIGTERM) - - config := service.Config{ - HTTPPort: c.Int(optionHTTPPort.Name), - Logger: logger, - MinServiceFee: minServiceFee, - GasTipCap: gasTipCap, - GasFeeCap: gasFeeCap, - AutoDepositAmount: autoDepositAmount, - SettlementThreshold: settlementThreshold, - SettlementTopup: settlementTopup, - SettlementRPCUrl: c.String(optionSettlementRPCUrl.Name), - BidderRPC: c.String(optionBidderRPCUrl.Name), - L1RPCUrls: c.StringSlice(optionL1RPCUrls.Name), - L1ContractAddr: common.HexToAddress(c.String(optionL1ContractAddr.Name)), - SettlementContractAddr: common.HexToAddress(c.String(optionSettlementContractAddr.Name)), - Signer: signer, - } - - s, err := service.New(&config) - if err != nil { - return fmt.Errorf("failed to create service: %w", err) - } - - <-sigc - logger.Info("shutting down...") - - return s.Close() - }, - } - - if err := app.Run(os.Args); err != nil { - fmt.Fprintf(os.Stderr, "error: %v\n", err) - } -} diff --git a/tools/instant-bridge/service/service.go b/tools/instant-bridge/service/service.go deleted file mode 100644 index db33b9380..000000000 --- a/tools/instant-bridge/service/service.go +++ /dev/null @@ -1,218 +0,0 @@ -package service - -import ( - "context" - "crypto/tls" - "errors" - "io" - "log/slog" - "math/big" - "time" - - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/ethclient" - bidderapiv1 "github.com/primev/mev-commit/p2p/gen/go/bidderapi/v1" - debugapiv1 "github.com/primev/mev-commit/p2p/gen/go/debugapi/v1" - notificationsapiv1 "github.com/primev/mev-commit/p2p/gen/go/notificationsapi/v1" - "github.com/primev/mev-commit/tools/instant-bridge/api" - "github.com/primev/mev-commit/x/accountsync" - "github.com/primev/mev-commit/x/contracts/ethwrapper" - "github.com/primev/mev-commit/x/health" - "github.com/primev/mev-commit/x/keysigner" - bidder "github.com/primev/mev-commit/x/opt-in-bidder" - "github.com/primev/mev-commit/x/transfer" - "google.golang.org/grpc" - "google.golang.org/grpc/credentials" -) - -type Config struct { - Logger *slog.Logger - Signer keysigner.KeySigner - BidderRPC string - AutoDepositAmount *big.Int - L1RPCUrls []string - SettlementRPCUrl string - L1ContractAddr common.Address - SettlementContractAddr common.Address - SettlementThreshold *big.Int - SettlementTopup *big.Int - HTTPPort int - MinServiceFee *big.Int - GasTipCap *big.Int - GasFeeCap *big.Int -} - -type Service struct { - cancel context.CancelFunc - closers []io.Closer -} - -func New(config *Config) (*Service, error) { - s := &Service{} - - conn, err := grpc.NewClient( - config.BidderRPC, - grpc.WithTransportCredentials(credentials.NewTLS( - &tls.Config{InsecureSkipVerify: true}, - )), - ) - if err != nil { - return nil, err - } - - s.closers = append(s.closers, conn) - - l1RPCClient, err := ethwrapper.NewClient( - config.Logger.With("module", "ethwrapper"), - config.L1RPCUrls, - ethwrapper.EthClientWithMaxRetries(5), - ) - if err != nil { - return nil, err - } - l1ChainID, err := l1RPCClient.RawClient().ChainID(context.Background()) - if err != nil { - return nil, err - } - - settlementClient, err := ethclient.Dial(config.SettlementRPCUrl) - if err != nil { - return nil, err - } - settlementChainID, err := settlementClient.ChainID(context.Background()) - if err != nil { - return nil, err - } - - bidderCli := bidderapiv1.NewBidderClient(conn) - topologyCli := debugapiv1.NewDebugServiceClient(conn) - notificationsCli := notificationsapiv1.NewNotificationsClient(conn) - - status, err := bidderCli.DepositManagerStatus(context.Background(), &bidderapiv1.DepositManagerStatusRequest{}) - if err != nil { - return nil, err - } - if !status.Enabled { - resp, err := bidderCli.EnableDepositManager(context.Background(), &bidderapiv1.EnableDepositManagerRequest{}) - if err != nil { - return nil, err - } - if !resp.Success { - return nil, errors.New("failed to enable deposit manager") - } - } - config.Logger.Info("deposit manager enabled") - - validProviders, err := bidderCli.GetValidProviders(context.Background(), &bidderapiv1.GetValidProvidersRequest{}) - if err != nil { - return nil, err - } - if len(validProviders.ValidProviders) == 0 { - return nil, errors.New("no connected and valid providers found") - } - - targetDeposits := make([]*bidderapiv1.TargetDeposit, len(validProviders.ValidProviders)) - for i, provider := range validProviders.ValidProviders { - targetDeposits[i] = &bidderapiv1.TargetDeposit{ - Provider: provider, - TargetDeposit: config.AutoDepositAmount.String(), - } - } - - resp, err := bidderCli.SetTargetDeposits(context.Background(), &bidderapiv1.SetTargetDepositsRequest{ - TargetDeposits: targetDeposits, - }) - if err != nil { - return nil, err - } - if len(resp.SuccessfullySetDeposits) != len(targetDeposits) { - return nil, errors.New("failed to set target deposits") - } - - bridgeConfig := transfer.BridgeConfig{ - Signer: config.Signer, - L1ContractAddr: config.L1ContractAddr, - SettlementContractAddr: config.SettlementContractAddr, - L1RPCUrl: config.L1RPCUrls[0], - SettlementRPCUrl: config.SettlementRPCUrl, - } - - syncer := accountsync.NewAccountSync(config.Signer.GetAddress(), settlementClient) - bridger := transfer.NewBridger( - config.Logger.With("module", "bridger"), - syncer, - bridgeConfig, - config.SettlementThreshold, - config.SettlementTopup, - ) - - bidderClient := bidder.NewBidderClient( - config.Logger.With("module", "bidder"), - bidderCli, - topologyCli, - notificationsCli, - l1RPCClient, - ) - - ctx, cancel := context.WithCancel(context.Background()) - s.cancel = cancel - - healthChecker := health.New() - - bridgerDone := bridger.Start(ctx) - healthChecker.Register(health.CloseChannelHealthCheck("Bridger", bridgerDone)) - s.closers = append(s.closers, channelCloser(bridgerDone)) - - bidderDone := bidderClient.Start(ctx) - healthChecker.Register(health.CloseChannelHealthCheck("BidderService", bidderDone)) - s.closers = append(s.closers, channelCloser(bidderDone)) - - transferer := transfer.NewTransferer( - config.Logger.With("module", "transferer"), - settlementClient, - config.Signer, - config.GasTipCap, - config.GasFeeCap, - ) - - apiService := api.NewAPI( - config.Logger.With("module", "api"), - config.HTTPPort, - healthChecker, - bidderClient, - transferer, - config.MinServiceFee, - config.Signer.GetAddress(), - l1RPCClient.RawClient(), - settlementClient, - l1ChainID, - settlementChainID, - ) - - apiService.Start() - s.closers = append(s.closers, apiService) - - return s, nil -} - -func (s *Service) Close() error { - s.cancel() - - for _, c := range s.closers { - if err := c.Close(); err != nil { - return err - } - } - return nil -} - -type channelCloser <-chan struct{} - -func (c channelCloser) Close() error { - select { - case <-c: - case <-time.After(5 * time.Second): - return errors.New("timed out waiting for channel to close") - } - return nil -} diff --git a/x/opt-in-bidder/bidder.go b/x/opt-in-bidder/bidder.go deleted file mode 100644 index 6d4eded6e..000000000 --- a/x/opt-in-bidder/bidder.go +++ /dev/null @@ -1,398 +0,0 @@ -package optinbidder - -import ( - "context" - "errors" - "fmt" - "io" - "log/slog" - "math/big" - "sync" - "sync/atomic" - "time" - - bidderapiv1 "github.com/primev/mev-commit/p2p/gen/go/bidderapi/v1" - debugapiv1 "github.com/primev/mev-commit/p2p/gen/go/debugapi/v1" - notificationsapiv1 "github.com/primev/mev-commit/p2p/gen/go/notificationsapi/v1" - "grpc.go4.org/metadata" -) - -const ( - epochNotificationTopic = "epoch_validators_opted_in" - slotDuration = 12 * time.Second -) - -var ( - ErrNoEpochInfo = errors.New("no epoch info available") - ErrNoSlotInCurrentEpoch = errors.New("no slot available in current epoch") - ErrNoProviders = errors.New("no connected providers found") -) - -var nowFunc = time.Now - -type slotInfo struct { - slot uint64 - startTime time.Time - blsKey string -} - -type epochInfo struct { - epoch uint64 - startTime time.Time - slots []slotInfo -} - -type BlockNumberGetter interface { - BlockNumber(ctx context.Context) (uint64, error) -} - -type BidderClient struct { - logger *slog.Logger - bigWg sync.WaitGroup - bidderClient bidderapiv1.BidderClient - topologyClient debugapiv1.DebugServiceClient - notificationsClient notificationsapiv1.NotificationsClient - currentEpoch atomic.Pointer[epochInfo] - blkNumberGetter BlockNumberGetter -} - -func NewBidderClient( - logger *slog.Logger, - bidderClient bidderapiv1.BidderClient, - topologyClient debugapiv1.DebugServiceClient, - notificationsClient notificationsapiv1.NotificationsClient, - blkNumberGetter BlockNumberGetter, -) *BidderClient { - return &BidderClient{ - logger: logger, - bidderClient: bidderClient, - topologyClient: topologyClient, - notificationsClient: notificationsClient, - blkNumberGetter: blkNumberGetter, - } -} - -func (b *BidderClient) Start(ctx context.Context) <-chan struct{} { - done := make(chan struct{}) - go func() { - defer close(done) - - lastMsg := nowFunc() - RESTART: - sub, err := b.notificationsClient.Subscribe(ctx, ¬ificationsapiv1.SubscribeRequest{ - Topics: []string{epochNotificationTopic}, - }) - if err != nil { - b.logger.Error("failed to subscribe to notifications", "error", err) - return - } - - if time.Since(lastMsg) > 15*time.Minute { - b.logger.Error("no messages received for 15 minutes, closing subscription") - return - } - - for { - select { - case <-ctx.Done(): - b.logger.Info("context done") - return - default: - } - - msg, err := sub.Recv() - if err != nil { - b.logger.Error("failed to receive message", "error", err) - goto RESTART - } - - lastMsg = nowFunc() - - b.logger.Debug("received message", "msg", msg) - - if msg.Topic != epochNotificationTopic { - b.logger.Error("unexpected topic", "topic", msg.Topic) - continue - } - - epoch, err := parseEpochInfo(msg) - if err != nil { - b.logger.Error("failed to parse epoch info", "error", err, "msg", msg) - continue - } - - b.currentEpoch.Store(epoch) - b.logger.Info("current epoch info updated", "epoch", epoch.epoch) - } - }() - return done -} - -func parseEpochInfo(msg *notificationsapiv1.Notification) (*epochInfo, error) { - epochIdx := msg.Value.Fields["epoch"].GetNumberValue() - if epochIdx == 0 { - return nil, errors.New("failed to parse epoch index") - } - startTime := msg.Value.Fields["epoch_start_time"].GetNumberValue() - if startTime == 0 { - return nil, errors.New("failed to parse start time") - } - slots := msg.Value.Fields["slots"].GetListValue() - if slots == nil { - return nil, errors.New("failed to parse slots") - } - epoch := &epochInfo{ - epoch: uint64(epochIdx), - startTime: time.Unix(int64(startTime), 0), - } - baseSlot := epochIdx * 32 - for _, slot := range slots.Values { - slotIdx := slot.GetStructValue().Fields["slot"].GetNumberValue() - if slotIdx == 0 { - return nil, errors.New("failed to parse slot index") - } - if slotIdx < baseSlot || slotIdx >= baseSlot+32 { - return nil, errors.New("slot index out of range") - } - blsKey := slot.GetStructValue().Fields["bls_key"].GetStringValue() - if blsKey == "" { - return nil, errors.New("failed to parse BLS key") - } - idx := slotIdx - baseSlot - epoch.slots = append(epoch.slots, slotInfo{ - slot: uint64(slotIdx), - startTime: epoch.startTime.Add(time.Duration(idx) * slotDuration), - blsKey: blsKey, - }) - } - - return epoch, nil -} - -type BidStatusType int - -const ( - BidStatusNoOfProviders BidStatusType = iota - BidStatusWaitSecs - BidStatusAttempted - BidStatusFailed - BidStatusCancelled - BidStatusCommitment -) - -type BidStatus struct { - Type BidStatusType - Arg any -} - -type BidOpts struct { - WaitForOptIn bool - BlockNumber uint64 - RevertingTxHashes []string - DecayDuration time.Duration - Constraint *bidderapiv1.PositionConstraint - IgnoreProviders []string -} - -var defaultBidOpts = &BidOpts{ - WaitForOptIn: true, -} - -func (b *BidderClient) Bid( - ctx context.Context, - bidAmount *big.Int, - slashAmount *big.Int, - rawTx string, - opts *BidOpts, -) (chan BidStatus, error) { - if opts == nil { - opts = defaultBidOpts - } - - topo, err := b.topologyClient.GetTopology(ctx, &debugapiv1.EmptyMessage{}) - if err != nil { - b.logger.Error("failed to get topology", "error", err) - return nil, err - } - - providers := topo.Topology.Fields["connected_providers"].GetListValue() - if providers == nil || len(providers.Values) == 0 { - return nil, ErrNoProviders - } - - filteredProviders := make([]any, 0, len(providers.Values)) - for _, ip := range opts.IgnoreProviders { - for _, p := range providers.Values { - if p.GetStringValue() != ip { - filteredProviders = append(filteredProviders, p) - } - } - } - - // Channel length chosen is 3 so that sending the bid is not blocked by the first - // status message. - res := make(chan BidStatus, 3) - b.bigWg.Add(1) - go func() { - defer fmt.Println("BidderClient goroutine exiting") - defer close(res) - defer b.bigWg.Done() - - res <- BidStatus{Type: BidStatusNoOfProviders, Arg: len(filteredProviders)} - - if opts.WaitForOptIn { - nextSlot, err := b.getNextSlot() - if err != nil { - b.logger.Error("failed to get next slot", "error", err) - res <- BidStatus{Type: BidStatusFailed, Arg: err.Error()} - return - } - - bidTime := nextSlot.startTime.Add(-1 * time.Second) - wait := bidTime.Sub(nowFunc()) - res <- BidStatus{Type: BidStatusWaitSecs, Arg: int(wait.Seconds())} - - if wait > 0 { - b.logger.Info("waiting for next slot", "wait", wait) - select { - case <-time.After(wait): - case <-ctx.Done(): - res <- BidStatus{Type: BidStatusCancelled, Arg: ctx.Err().Error()} - return - } - } - } - - blkNumber := opts.BlockNumber - if blkNumber == 0 { - bNo, err := b.blkNumberGetter.BlockNumber(ctx) - if err != nil { - b.logger.Error("failed to get block number", "error", err) - res <- BidStatus{Type: BidStatusFailed, Arg: err.Error()} - return - } - blkNumber = bNo + 1 - } - - res <- BidStatus{Type: BidStatusAttempted, Arg: blkNumber} - b.logger.Info( - "attempting to send bid", - "blockNumber", blkNumber, - "bidAmount", bidAmount, - "slashAmount", slashAmount, - ) - - bidReq := &bidderapiv1.Bid{ - Amount: bidAmount.String(), - BlockNumber: int64(blkNumber), - RawTransactions: []string{rawTx}, - DecayStartTimestamp: nowFunc().Add(200 * time.Millisecond).UnixMilli(), - SlashAmount: slashAmount.String(), - RevertingTxHashes: opts.RevertingTxHashes, - } - - if opts.DecayDuration > 0 { - bidReq.DecayEndTimestamp = time.UnixMilli(bidReq.DecayStartTimestamp).Add(opts.DecayDuration).UnixMilli() - } else { - 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, - }, - }, - }, - } - } - - if len(opts.IgnoreProviders) > 0 { - mdMap := make(map[string]string) - for _, ip := range opts.IgnoreProviders { - mdMap["ignore-provider"] = ip - } - md := metadata.New(mdMap) - ctx = metadata.NewContext(ctx, md) - } - - pc, err := b.bidderClient.SendBid(ctx, bidReq) - if err != nil { - b.logger.Error("failed to send bid", "error", err) - res <- BidStatus{Type: BidStatusFailed, Arg: err.Error()} - return - } - - for { - select { - case <-ctx.Done(): - res <- BidStatus{Type: BidStatusCancelled, Arg: ctx.Err().Error()} - return - default: - } - - msg, err := pc.Recv() - if err != nil { - if errors.Is(err, io.EOF) { - return - } - if errors.Is(err, context.Canceled) { - res <- BidStatus{Type: BidStatusCancelled, Arg: err.Error()} - return - } - b.logger.Error("failed to receive commitment", "error", err) - res <- BidStatus{Type: BidStatusFailed, Arg: err.Error()} - return - } - res <- BidStatus{Type: BidStatusCommitment, Arg: msg} - } - }() - - return res, nil -} - -func (b *BidderClient) ConnectedProviders(ctx context.Context) ([]string, error) { - topo, err := b.topologyClient.GetTopology(ctx, &debugapiv1.EmptyMessage{}) - if err != nil { - b.logger.Error("failed to get topology", "error", err) - return []string{}, err - } - - providers := topo.Topology.Fields["connected_providers"].GetListValue() - if providers == nil { - return []string{}, nil - } - - prvs := make([]string, 0, len(providers.Values)) - for _, p := range providers.Values { - prvs = append(prvs, p.GetStringValue()) - } - return prvs, nil -} - -func (b *BidderClient) Estimate() (int64, error) { - nextSlot, err := b.getNextSlot() - if err != nil { - return 0, err - } - - return int64(nextSlot.startTime.Sub(nowFunc()).Seconds()), nil -} - -func (b *BidderClient) getNextSlot() (slotInfo, error) { - epochInfo := b.currentEpoch.Load() - if epochInfo == nil { - return slotInfo{}, ErrNoEpochInfo - } - - now := nowFunc() - for _, slot := range epochInfo.slots { - if now.Before(slot.startTime) { - return slot, nil - } - } - - return slotInfo{}, ErrNoSlotInCurrentEpoch -} diff --git a/x/opt-in-bidder/bidder_test.go b/x/opt-in-bidder/bidder_test.go deleted file mode 100644 index 9a8bdb81b..000000000 --- a/x/opt-in-bidder/bidder_test.go +++ /dev/null @@ -1,265 +0,0 @@ -package optinbidder_test - -import ( - "context" - "crypto/rand" - "encoding/hex" - "io" - "math/big" - "os" - "testing" - "time" - - bidderapiv1 "github.com/primev/mev-commit/p2p/gen/go/bidderapi/v1" - debugapiv1 "github.com/primev/mev-commit/p2p/gen/go/debugapi/v1" - notificationsapiv1 "github.com/primev/mev-commit/p2p/gen/go/notificationsapi/v1" - optinbidder "github.com/primev/mev-commit/x/opt-in-bidder" - "github.com/primev/mev-commit/x/util" - "google.golang.org/grpc" - "google.golang.org/protobuf/types/known/structpb" -) - -type testRPCServices struct { - bidderapiv1.BidderClient - debugapiv1.DebugServiceClient - notificationsapiv1.NotificationsClient - - notificationChan chan *notificationsapiv1.Notification - bidChan chan *bidderapiv1.Bid - commitmentChan chan *bidderapiv1.Commitment - topo *debugapiv1.TopologyResponse -} - -type testNotificationStream struct { - grpc.ClientStream - - ctx context.Context - notificationChan chan *notificationsapiv1.Notification -} - -func (t *testNotificationStream) Recv() (*notificationsapiv1.Notification, error) { - select { - case <-t.ctx.Done(): - return nil, io.EOF - case n := <-t.notificationChan: - return n, nil - } -} - -type testCommitmentStream struct { - grpc.ClientStream - - ctx context.Context - commitmentChan chan *bidderapiv1.Commitment -} - -func (t *testCommitmentStream) Recv() (*bidderapiv1.Commitment, error) { - select { - case <-t.ctx.Done(): - return nil, io.EOF - case c, more := <-t.commitmentChan: - if !more { - return nil, io.EOF - } - return c, nil - } -} - -func (t *testRPCServices) SendBid( - ctx context.Context, - in *bidderapiv1.Bid, - _ ...grpc.CallOption, -) (grpc.ServerStreamingClient[bidderapiv1.Commitment], error) { - select { - case t.bidChan <- in: - case <-ctx.Done(): - return nil, ctx.Err() - } - return &testCommitmentStream{ctx: ctx, commitmentChan: t.commitmentChan}, nil -} - -func (t *testRPCServices) GetTopology( - _ context.Context, - _ *debugapiv1.EmptyMessage, - _ ...grpc.CallOption, -) (*debugapiv1.TopologyResponse, error) { - return t.topo, nil -} - -func (t *testRPCServices) Subscribe( - ctx context.Context, - in *notificationsapiv1.SubscribeRequest, - _ ...grpc.CallOption, -) (grpc.ServerStreamingClient[notificationsapiv1.Notification], error) { - return &testNotificationStream{ctx: ctx, notificationChan: t.notificationChan}, nil -} - -type testBlockNumberGetter struct { - blockNumber uint64 -} - -func (t *testBlockNumberGetter) BlockNumber(ctx context.Context) (uint64, error) { - return t.blockNumber, nil -} - -type testTimeSetter struct { - now time.Time -} - -func (t *testTimeSetter) Now() time.Time { - return t.now -} - -func TestBidderClient(t *testing.T) { - t.Parallel() - - clock := time.Now() - timeSetter := &testTimeSetter{ - now: clock, - } - - optinbidder.SetNowFunc(timeSetter.Now) - - topoVal, err := structpb.NewStruct(map[string]interface{}{ - "connected_providers": []any{"provider1", "provider2"}, - }) - if err != nil { - t.Fatal(err) - } - // Create a new test RPC services. - rpcServices := &testRPCServices{ - notificationChan: make(chan *notificationsapiv1.Notification), - bidChan: make(chan *bidderapiv1.Bid), - commitmentChan: make(chan *bidderapiv1.Commitment), - topo: &debugapiv1.TopologyResponse{ - Topology: &structpb.Struct{}, - }, - } - - blockNumberGetter := &testBlockNumberGetter{blockNumber: 10} - bidderClient := optinbidder.NewBidderClient( - util.NewTestLogger(os.Stdout), - rpcServices, - rpcServices, - rpcServices, - blockNumberGetter, - ) - - ctx, cancel := context.WithCancel(context.Background()) - done := bidderClient.Start(ctx) - - _, err = bidderClient.Estimate() - if err != optinbidder.ErrNoEpochInfo { - t.Fatalf("expected error %v, got %v", optinbidder.ErrNoEpochInfo, err) - } - - // Send a notification. - nVal, err := structpb.NewStruct(map[string]interface{}{ - "epoch": 1, - "epoch_start_time": clock.Add(2 * time.Second).Unix(), - "slots": []any{ - map[string]interface{}{ - "slot": 33, - "start_time": clock.Add(14 * time.Second).Unix(), - "bls_key": "key2", - "opted_in": true, - }, - }, - }) - if err != nil { - t.Fatal(err) - } - - rpcServices.notificationChan <- ¬ificationsapiv1.Notification{ - Topic: "epoch_validators_opted_in", - Value: nVal, - } - - for { - if _, err := bidderClient.Estimate(); err == nil { - break - } - } - - estimate, err := bidderClient.Estimate() - if err != nil { - t.Fatal(err) - } - if estimate != 13 { - t.Fatalf("expected estimate 13, got %d", estimate) - } - - timeSetter.now = clock.Add(10 * time.Second) - - buf := make([]byte, 32) - _, _ = rand.Read(buf) - txString := hex.EncodeToString(buf) - - _, err = bidderClient.Bid(ctx, big.NewInt(1), big.NewInt(1), txString, nil) - if err == nil { - t.Fatal("expected error, got nil") - } - - rpcServices.topo = &debugapiv1.TopologyResponse{ - Topology: topoVal, - } - - statusC, err := bidderClient.Bid(ctx, big.NewInt(1), big.NewInt(1), txString, nil) - if err != nil { - t.Fatal(err) - } - - commitments := 0 -waitLoop: - for { - select { - case status, more := <-statusC: - if !more { - break waitLoop - } - switch status.Type { - case optinbidder.BidStatusNoOfProviders: - if status.Arg.(int) != 2 { - t.Fatalf("expected 2 providers, got %d", status.Arg) - } - case optinbidder.BidStatusWaitSecs: - if status.Arg.(int) != 2 { - t.Fatalf("expected 2 seconds, got %d", status.Arg) - } - case optinbidder.BidStatusAttempted: - if status.Arg.(uint64) != 11 { - t.Fatalf("expected 11, got %d", status.Arg) - } - case optinbidder.BidStatusCommitment: - if status.Arg.(*bidderapiv1.Commitment).BlockNumber != 11 { - t.Fatalf("expected block number 11, got %d", status.Arg.(*bidderapiv1.Commitment).BlockNumber) - } - commitments++ - } - case bid := <-rpcServices.bidChan: - if bid.Amount != big.NewInt(1).String() { - t.Fatalf("expected amount 1, got %s", bid.Amount) - } - if bid.BlockNumber != 11 { - t.Fatalf("expected block number 11, got %d", bid.BlockNumber) - } - if bid.RawTransactions[0] != txString { - t.Fatalf("expected raw transaction %x, got %s", buf, bid.RawTransactions[0]) - } - rpcServices.commitmentChan <- &bidderapiv1.Commitment{ - BlockNumber: 11, - } - rpcServices.commitmentChan <- &bidderapiv1.Commitment{ - BlockNumber: 11, - } - close(rpcServices.commitmentChan) - } - } - - if commitments != 2 { - t.Fatalf("expected 2 commitments, got %d", commitments) - } - - cancel() - <-done -} diff --git a/x/opt-in-bidder/export_test.go b/x/opt-in-bidder/export_test.go deleted file mode 100644 index 31674fc5f..000000000 --- a/x/opt-in-bidder/export_test.go +++ /dev/null @@ -1,7 +0,0 @@ -package optinbidder - -import "time" - -func SetNowFunc(f func() time.Time) { - nowFunc = f -} From 60d9b99961f248ce6dddf0b1aa55eda6d06cbfa3 Mon Sep 17 00:00:00 2001 From: Alok Date: Tue, 11 Nov 2025 16:20:41 +0530 Subject: [PATCH 10/16] fix: remove old instant bridge --- p2p/pkg/preconfirmation/preconfirmation.go | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/p2p/pkg/preconfirmation/preconfirmation.go b/p2p/pkg/preconfirmation/preconfirmation.go index 07bede61b..cd339602e 100644 --- a/p2p/pkg/preconfirmation/preconfirmation.go +++ b/p2p/pkg/preconfirmation/preconfirmation.go @@ -5,6 +5,7 @@ import ( "errors" "log/slog" "math/big" + "slices" "sync" "time" @@ -153,13 +154,13 @@ func (p *Preconfirmation) SendBid( ignoredProviders := md.Get("ignore-provider") for _, ip := range ignoredProviders { ignoredAddr := common.HexToAddress(ip) - filteredProviders := make([]p2p.Peer, 0, len(providers)) - for _, provider := range providers { - if provider.EthAddress != ignoredAddr { - filteredProviders = append(filteredProviders, provider) - } + idx := slices.IndexFunc(providers, func(p p2p.Peer) bool { + return p.EthAddress == ignoredAddr + }) + if idx != -1 { + p.logger.Info("ignoring provider for this bid", "provider", ignoredAddr.Hex(), "bid", bid) + providers = append(providers[:idx], providers[idx+1:]...) } - providers = filteredProviders } } From 352ddf95f4cb7b998750de9d9bb7dd48a9b87bb1 Mon Sep 17 00:00:00 2001 From: Alok Date: Tue, 11 Nov 2025 16:36:03 +0530 Subject: [PATCH 11/16] fix: remove old instant bridge --- tools/preconf-rpc/notifier/notifier.go | 4 ++++ tools/preconf-rpc/sender/sender.go | 5 +++-- tools/preconf-rpc/sender/sender_test.go | 2 +- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/tools/preconf-rpc/notifier/notifier.go b/tools/preconf-rpc/notifier/notifier.go index 4060d5d02..d96c4fda8 100644 --- a/tools/preconf-rpc/notifier/notifier.go +++ b/tools/preconf-rpc/notifier/notifier.go @@ -49,6 +49,7 @@ type Field struct { type txnInfo struct { txn *sender.Transaction noOfAttempts int + noOfBlocks int timeTaken time.Duration } @@ -295,6 +296,8 @@ func (n *Notifier) StartTransactionNotifier( Field{Title: "Type", Value: buildType(t), Short: true}, Field{Title: "Attempts", Value: fmt.Sprintf("%d", t.noOfAttempts), Short: true}, Field{Title: "Duration", Value: t.timeTaken.String(), Short: true}, + Field{Title: "Included Block", Value: fmt.Sprintf("%d", t.txn.BlockNumber), Short: true}, + Field{Title: "No. of Blocks to confirm", Value: fmt.Sprintf("%d", t.noOfBlocks), Short: true}, ) if t.txn.Constraint != nil { fields = append(fields, @@ -325,6 +328,7 @@ func (n *Notifier) StartTransactionNotifier( func (n *Notifier) NotifyTransactionStatus( txn *sender.Transaction, noOfAttempts int, + noOfBlocks int, timeTaken time.Duration, ) { n.queuedMu.Lock() diff --git a/tools/preconf-rpc/sender/sender.go b/tools/preconf-rpc/sender/sender.go index ab37bb358..1aa30822a 100644 --- a/tools/preconf-rpc/sender/sender.go +++ b/tools/preconf-rpc/sender/sender.go @@ -126,7 +126,7 @@ type txnAttempt struct { } type Notifier interface { - NotifyTransactionStatus(txn *Transaction, noOfAttempts int, timeTaken time.Duration) + NotifyTransactionStatus(txn *Transaction, noOfAttempts, noOfBlocks int, timeTaken time.Duration) } type TxSender struct { @@ -858,5 +858,6 @@ func (t *TxSender) clearBlockAttemptHistory(txn *Transaction, endTime time.Time) _ = t.txnAttemptHistory.Remove(txn.Hash()) - t.notifier.NotifyTransactionStatus(txn, totalAttempts, endTime.Sub(attempts.startTime).Round(time.Millisecond)) + timeTaken := endTime.Sub(attempts.startTime).Round(time.Millisecond) + t.notifier.NotifyTransactionStatus(txn, totalAttempts, len(attempts.attempts), timeTaken) } diff --git a/tools/preconf-rpc/sender/sender_test.go b/tools/preconf-rpc/sender/sender_test.go index b18f2143f..aa8798ff3 100644 --- a/tools/preconf-rpc/sender/sender_test.go +++ b/tools/preconf-rpc/sender/sender_test.go @@ -259,7 +259,7 @@ type mockNotifier struct { notifications []string } -func (m *mockNotifier) NotifyTransactionStatus(txn *sender.Transaction, attempts int, start time.Duration) { +func (m *mockNotifier) NotifyTransactionStatus(txn *sender.Transaction, attempts, blocks int, start time.Duration) { m.notifications = append(m.notifications, txn.Hash().Hex()) } From ce1ecefc9a18838c66b9a67b8d198a65809ef5f4 Mon Sep 17 00:00:00 2001 From: Alok Date: Tue, 11 Nov 2025 16:42:59 +0530 Subject: [PATCH 12/16] fix: lint --- tools/go.mod | 1 + tools/go.sum | 2 ++ x/go.mod | 4 +--- x/go.sum | 8 -------- 4 files changed, 4 insertions(+), 11 deletions(-) diff --git a/tools/go.mod b/tools/go.mod index c210fb0b8..2f365b2d7 100644 --- a/tools/go.mod +++ b/tools/go.mod @@ -27,6 +27,7 @@ require ( github.com/google/go-cmp v0.6.0 github.com/testcontainers/testcontainers-go v0.27.0 golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 + grpc.go4.org v0.0.0-20170609214715-11d0a25b4919 resenje.org/multex v0.2.0 ) diff --git a/tools/go.sum b/tools/go.sum index 8f8bd4347..fad1a6b3a 100644 --- a/tools/go.sum +++ b/tools/go.sum @@ -388,6 +388,8 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gotest.tools/v3 v3.5.0 h1:Ljk6PdHdOhAb5aDMWXjDLMMhph+BpztA4v1QdqEW2eY= gotest.tools/v3 v3.5.0/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU= +grpc.go4.org v0.0.0-20170609214715-11d0a25b4919 h1:tmXTu+dfa+d9Evp8NpJdgOy6+rt8/x4yG7qPBrtNfLY= +grpc.go4.org v0.0.0-20170609214715-11d0a25b4919/go.mod h1:77eQGdRu53HpSqPFJFmuJdjuHRquDANNeA4x7B8WQ9o= resenje.org/multex v0.2.0 h1:y1S8+bItGZo0lberxtQi9IhbWTpvRezhCWIFvt12VmU= resenje.org/multex v0.2.0/go.mod h1:z+E+cUHGTgpqYn+P3yFOnC92i3X7rStzSur4rjOZM9s= rsc.io/tmplfunc v0.0.3 h1:53XFQh69AfOa8Tw0Jm7t+GV7KZhOi6jzsCzTtKbMvzU= diff --git a/x/go.mod b/x/go.mod index fa3b0abc3..e5fd859e5 100644 --- a/x/go.mod +++ b/x/go.mod @@ -9,7 +9,6 @@ require ( github.com/google/go-cmp v0.6.0 github.com/primev/mev-commit/bridge/standard v0.0.1 github.com/primev/mev-commit/contracts-abi v0.0.1 - github.com/primev/mev-commit/p2p v0.0.1 github.com/prometheus/client_golang v1.19.1 github.com/stretchr/testify v1.10.0 go.opentelemetry.io/otel v1.28.0 @@ -18,11 +17,9 @@ require ( go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.28.0 go.opentelemetry.io/otel/sdk v1.28.0 google.golang.org/grpc v1.67.1 - google.golang.org/protobuf v1.34.2 ) require ( - buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.32.0-20240221180331-f05a6f4403ce.1 // indirect github.com/Microsoft/go-winio v0.6.2 // indirect github.com/StackExchange/wmi v1.2.1 // indirect github.com/beorn7/perks v1.0.1 // indirect @@ -71,6 +68,7 @@ require ( golang.org/x/text v0.24.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20240814211410-ddb44dafa142 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142 // indirect + google.golang.org/protobuf v1.34.2 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect rsc.io/tmplfunc v0.0.3 // indirect ) diff --git a/x/go.sum b/x/go.sum index 5d0110d3b..c2ab1c0db 100644 --- a/x/go.sum +++ b/x/go.sum @@ -1,5 +1,3 @@ -buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.32.0-20240221180331-f05a6f4403ce.1 h1:AmmAwHbvaeOIxDKG2+aTn5C36HjmFIMkrdTp49rp80Q= -buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.32.0-20240221180331-f05a6f4403ce.1/go.mod h1:tiTMKD8j6Pd/D2WzREoweufjzaJKHZg35f/VGcZ2v3I= github.com/DataDog/zstd v1.5.5 h1:oWf5W7GtOLgp6bciQYDmhHHjdhYkALu6S/5Ni9ZgSvQ= github.com/DataDog/zstd v1.5.5/go.mod h1:g4AWEaM3yOg3HYfnJ3YIawPnVdXJh9QME85blwSAmyw= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= @@ -80,10 +78,8 @@ github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang-jwt/jwt/v4 v4.5.1 h1:JdqV9zKUdtaa9gdPlywC3aeoEsR681PlKC+4F5gQgeo= github.com/golang-jwt/jwt/v4 v4.5.1/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= -github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/snappy v0.0.5-0.20220116011046-fa5810519dcb h1:PBC98N2aIaM3XXiurYmW7fx4GZkL8feAMVq7nEjURHk= github.com/golang/snappy v0.0.5-0.20220116011046-fa5810519dcb/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= -github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= @@ -155,7 +151,6 @@ github.com/pion/dtls/v2 v2.2.11 h1:9U/dpCYl1ySttROPWJgqWKEylUdT0fXp/xst6JwY5Ks= github.com/pion/dtls/v2 v2.2.11/go.mod h1:d9SYc9fch0CqK90mRk1dC7AkzzpwJj6u2GU3u+9pqFE= github.com/pion/logging v0.2.2 h1:M9+AIj/+pxNsDfAT64+MAVgJO0rsyLnoJKCqf//DoeY= github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms= -github.com/pion/stun v0.6.1 h1:8lp6YejULeHBF8NmV8e2787BogQhduZugh5PdhDyyN4= github.com/pion/stun/v2 v2.0.0 h1:A5+wXKLAypxQri59+tmQKVs7+l6mMM+3d+eER9ifRU0= github.com/pion/stun/v2 v2.0.0/go.mod h1:22qRSh08fSEttYUmJZGlriq9+03jtVmXNODgLccj8GQ= github.com/pion/transport/v2 v2.2.5 h1:iyi25i/21gQck4hfRhomF6SktmUQjRsRW4WJdhfc3Kc= @@ -233,15 +228,12 @@ golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0= golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU= golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY= golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/genproto/googleapis/api v0.0.0-20240814211410-ddb44dafa142 h1:wKguEg1hsxI2/L3hUYrpo1RVi48K+uTyzKqprwLXsb8= google.golang.org/genproto/googleapis/api v0.0.0-20240814211410-ddb44dafa142/go.mod h1:d6be+8HhtEtucleCbxpPW9PA9XwISACu8nvpPqF0BVo= google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142 h1:e7S5W7MGGLaSu8j3YjdezkZ+m1/Nm0uRVRMEMGk26Xs= google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU= google.golang.org/grpc v1.67.1 h1:zWnc1Vrcno+lHZCOofnIMvycFcc0QRGIzm9dhnDX68E= google.golang.org/grpc v1.67.1/go.mod h1:1gLDyUQU7CTLJI90u3nXZ9ekeghjeM7pTDZlqFNg2AA= -google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= -google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= From 1ca5dce7ab62d451b29f794fdcc314101e55ba50 Mon Sep 17 00:00:00 2001 From: Alok Date: Tue, 11 Nov 2025 20:17:41 +0530 Subject: [PATCH 13/16] fix: ctx handling --- p2p/pkg/preconfirmation/preconfirmation.go | 1 + tools/go.mod | 1 - tools/go.sum | 2 -- tools/preconf-rpc/bidder/bidder.go | 4 ++-- 4 files changed, 3 insertions(+), 5 deletions(-) diff --git a/p2p/pkg/preconfirmation/preconfirmation.go b/p2p/pkg/preconfirmation/preconfirmation.go index cd339602e..af1c30195 100644 --- a/p2p/pkg/preconfirmation/preconfirmation.go +++ b/p2p/pkg/preconfirmation/preconfirmation.go @@ -152,6 +152,7 @@ func (p *Preconfirmation) SendBid( md, ok := metadata.FromIncomingContext(ctx) if ok { ignoredProviders := md.Get("ignore-provider") + p.logger.Info("ignoring providers for this bid", "providers", ignoredProviders, "bid", bid) for _, ip := range ignoredProviders { ignoredAddr := common.HexToAddress(ip) idx := slices.IndexFunc(providers, func(p p2p.Peer) bool { diff --git a/tools/go.mod b/tools/go.mod index 2f365b2d7..c210fb0b8 100644 --- a/tools/go.mod +++ b/tools/go.mod @@ -27,7 +27,6 @@ require ( github.com/google/go-cmp v0.6.0 github.com/testcontainers/testcontainers-go v0.27.0 golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 - grpc.go4.org v0.0.0-20170609214715-11d0a25b4919 resenje.org/multex v0.2.0 ) diff --git a/tools/go.sum b/tools/go.sum index fad1a6b3a..8f8bd4347 100644 --- a/tools/go.sum +++ b/tools/go.sum @@ -388,8 +388,6 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gotest.tools/v3 v3.5.0 h1:Ljk6PdHdOhAb5aDMWXjDLMMhph+BpztA4v1QdqEW2eY= gotest.tools/v3 v3.5.0/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU= -grpc.go4.org v0.0.0-20170609214715-11d0a25b4919 h1:tmXTu+dfa+d9Evp8NpJdgOy6+rt8/x4yG7qPBrtNfLY= -grpc.go4.org v0.0.0-20170609214715-11d0a25b4919/go.mod h1:77eQGdRu53HpSqPFJFmuJdjuHRquDANNeA4x7B8WQ9o= resenje.org/multex v0.2.0 h1:y1S8+bItGZo0lberxtQi9IhbWTpvRezhCWIFvt12VmU= resenje.org/multex v0.2.0/go.mod h1:z+E+cUHGTgpqYn+P3yFOnC92i3X7rStzSur4rjOZM9s= rsc.io/tmplfunc v0.0.3 h1:53XFQh69AfOa8Tw0Jm7t+GV7KZhOi6jzsCzTtKbMvzU= diff --git a/tools/preconf-rpc/bidder/bidder.go b/tools/preconf-rpc/bidder/bidder.go index 13f6f5995..607d8ee2c 100644 --- a/tools/preconf-rpc/bidder/bidder.go +++ b/tools/preconf-rpc/bidder/bidder.go @@ -14,7 +14,7 @@ import ( bidderapiv1 "github.com/primev/mev-commit/p2p/gen/go/bidderapi/v1" debugapiv1 "github.com/primev/mev-commit/p2p/gen/go/debugapi/v1" notificationsapiv1 "github.com/primev/mev-commit/p2p/gen/go/notificationsapi/v1" - "grpc.go4.org/metadata" + "google.golang.org/grpc/metadata" ) const ( @@ -292,7 +292,7 @@ func (b *BidderClient) Bid( pairs = append(pairs, "ignore-provider", ip) } md := metadata.Pairs(pairs...) - ctx = metadata.NewContext(ctx, md) + ctx = metadata.NewOutgoingContext(ctx, md) } pc, err := b.bidderClient.SendBid(ctx, bidReq) From bcf3d961141c8bfaad1037c6b384b904a9df978b Mon Sep 17 00:00:00 2001 From: Alok Nerurkar Date: Tue, 11 Nov 2025 23:30:51 +0530 Subject: [PATCH 14/16] fix: nonce fix --- tools/preconf-rpc/blocktracker/blocktracker.go | 8 ++++++++ tools/preconf-rpc/blocktracker/blocktracker_test.go | 4 ++++ tools/preconf-rpc/handlers/handlers.go | 8 ++++++++ tools/preconf-rpc/store/store.go | 2 +- x/contracts/ethwrapper/client.go | 8 ++++++++ 5 files changed, 29 insertions(+), 1 deletion(-) diff --git a/tools/preconf-rpc/blocktracker/blocktracker.go b/tools/preconf-rpc/blocktracker/blocktracker.go index 7e344e0f0..c9287f414 100644 --- a/tools/preconf-rpc/blocktracker/blocktracker.go +++ b/tools/preconf-rpc/blocktracker/blocktracker.go @@ -18,6 +18,7 @@ import ( type EthClient interface { BlockNumber(ctx context.Context) (uint64, error) BlockByNumber(ctx context.Context, blockNumber *big.Int) (*types.Block, error) + PendingNonceAt(ctx context.Context, account common.Address) (uint64, error) } type blockTracker struct { @@ -117,6 +118,13 @@ func (b *blockTracker) LatestBlockNumber() uint64 { return b.latestBlockNo.Load() } +func (b *blockTracker) AccountNonce( + ctx context.Context, + account common.Address, +) (uint64, error) { + return b.client.PendingNonceAt(ctx, account) +} + func (b *blockTracker) NextBlockNumber() (uint64, time.Duration, error) { latestBlockNo := b.latestBlockNo.Load() block, found := b.blocks.Get(latestBlockNo) diff --git a/tools/preconf-rpc/blocktracker/blocktracker_test.go b/tools/preconf-rpc/blocktracker/blocktracker_test.go index 9f58ee114..858ce6459 100644 --- a/tools/preconf-rpc/blocktracker/blocktracker_test.go +++ b/tools/preconf-rpc/blocktracker/blocktracker_test.go @@ -37,6 +37,10 @@ func (m *mockEthClient) BlockByNumber(ctx context.Context, blockNumber *big.Int) return block, nil } +func (m *mockEthClient) PendingNonceAt(ctx context.Context, account common.Address) (uint64, error) { + return 0, nil +} + type testHasher struct { hasher hash.Hash } diff --git a/tools/preconf-rpc/handlers/handlers.go b/tools/preconf-rpc/handlers/handlers.go index 1ce0b637a..7c0acf4f6 100644 --- a/tools/preconf-rpc/handlers/handlers.go +++ b/tools/preconf-rpc/handlers/handlers.go @@ -41,6 +41,7 @@ type Store interface { type BlockTracker interface { LatestBlockNumber() uint64 + AccountNonce(ctx context.Context, account common.Address) (uint64, error) } type Sender interface { @@ -506,6 +507,13 @@ func (h *rpcMethodHandler) handleGetTxCount(ctx context.Context, params ...any) accNonce += 1 + backendNonce, err := h.blockTracker.AccountNonce(ctx, common.HexToAddress(account)) + if err == nil { + if backendNonce > accNonce { + accNonce = backendNonce + } + } + nonceJSON, err := json.Marshal(accNonce) if err != nil { h.logger.Error("Failed to marshal nonce to JSON", "error", err, "account", account) diff --git a/tools/preconf-rpc/store/store.go b/tools/preconf-rpc/store/store.go index cd7198666..60da49243 100644 --- a/tools/preconf-rpc/store/store.go +++ b/tools/preconf-rpc/store/store.go @@ -442,7 +442,7 @@ func (s *rpcstore) GetCurrentNonce(ctx context.Context, sender common.Address) u query := ` SELECT COALESCE(MAX(nonce), 0) FROM mcTransactions - WHERE sender = $1 AND (status = 'pending' OR status = 'pre-confirmed'); + WHERE sender = $1; ` row := s.db.QueryRowContext(ctx, query, sender.Hex()) var nextNonce uint64 diff --git a/x/contracts/ethwrapper/client.go b/x/contracts/ethwrapper/client.go index 814cfbf1c..0562f49f0 100644 --- a/x/contracts/ethwrapper/client.go +++ b/x/contracts/ethwrapper/client.go @@ -264,3 +264,11 @@ func (c *Client) TransactionReceipt(ctx context.Context, txHash common.Hash) (*t } return rawClient.TransactionReceipt(ctx, txHash) } + +func (c *Client) PendingNonceAt(ctx context.Context, account common.Address) (uint64, error) { + rawClient := c.RawClient() + if rawClient == nil { + return 0, fmt.Errorf("no raw client") + } + return rawClient.PendingNonceAt(ctx, account) +} From c2313b436421ff70adf45376b4d3ed8211488b59 Mon Sep 17 00:00:00 2001 From: Alok Nerurkar Date: Wed, 12 Nov 2025 00:49:20 +0530 Subject: [PATCH 15/16] fix: nonce fix --- tools/preconf-rpc/store/store.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/preconf-rpc/store/store.go b/tools/preconf-rpc/store/store.go index 60da49243..3c694f53f 100644 --- a/tools/preconf-rpc/store/store.go +++ b/tools/preconf-rpc/store/store.go @@ -442,7 +442,7 @@ func (s *rpcstore) GetCurrentNonce(ctx context.Context, sender common.Address) u query := ` SELECT COALESCE(MAX(nonce), 0) FROM mcTransactions - WHERE sender = $1; + WHERE sender = $1 AND status != 'failed'; ` row := s.db.QueryRowContext(ctx, query, sender.Hex()) var nextNonce uint64 From ea56bc46017afabdf3f82685cb03d9b68fba99cf Mon Sep 17 00:00:00 2001 From: Alok Nerurkar Date: Wed, 12 Nov 2025 01:25:20 +0530 Subject: [PATCH 16/16] fix: nits --- tools/preconf-rpc/sender/sender.go | 24 ++++++++++++++++++++++-- tools/preconf-rpc/sender/sender_test.go | 4 ++++ 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/tools/preconf-rpc/sender/sender.go b/tools/preconf-rpc/sender/sender.go index 1aa30822a..35b0174d1 100644 --- a/tools/preconf-rpc/sender/sender.go +++ b/tools/preconf-rpc/sender/sender.go @@ -104,6 +104,7 @@ type Pricer interface { type BlockTracker interface { WaitForTxnInclusion(txnHash common.Hash) chan uint64 NextBlockNumber() (uint64, time.Duration, error) + LatestBlockNumber() uint64 } type Transferer interface { @@ -523,7 +524,7 @@ BID_LOOP: } endTime := time.Now() if len(txn.commitments) > 0 { - endTime = time.UnixMilli(txn.commitments[len(txn.commitments)-1].DispatchTimestamp) + endTime = time.UnixMilli(txn.commitments[0].DispatchTimestamp) } t.clearBlockAttemptHistory(txn, endTime) break BID_LOOP @@ -681,6 +682,17 @@ func (t *TxSender) sendBid( if !isRetry { logs, err := t.simulator.Simulate(ctx, txn.Raw) if err != nil { + if t.blockTracker.LatestBlockNumber() < bidBlockNo { + logger.Warn( + "Simulation failed, but block may not be mined yet, will retry", + "error", err, + "blockNumber", bidBlockNo, + ) + return bidResult{}, &errRetry{ + err: fmt.Errorf("simulation may have failed due to unmined block: %w", err), + retryAfter: time.Second, + } + } logger.Error("Failed to simulate transaction", "error", err, "blockNumber", bidBlockNo) return bidResult{}, fmt.Errorf("failed to simulate transaction: %w", err) } @@ -690,8 +702,11 @@ func (t *TxSender) sendBid( return bidResult{}, fmt.Errorf("failed to get connected providers: %w", err) } txn.logs = logs - txn.commitments = nil txn.noOfProviders = len(providers) + // We could have already made a attempt on the previous block but the block + // update hasn't happened yet. This means that the bid might fail, but + // we should retain the previous commitments. Only clear if we get new + // commitments for the new block. } bidC, err := t.bidder.Bid( @@ -732,6 +747,11 @@ BID_LOOP: } switch bidStatus.Type { case bidder.BidStatusCommitment: + if len(txn.commitments) > 0 { + if txn.commitments[0].BlockNumber != int64(bidBlockNo) { + txn.commitments = nil // clear previous commitments for new block + } + } txn.commitments = append(txn.commitments, bidStatus.Arg.(*bidderapiv1.Commitment)) if t.fastTrack(txn.commitments, optedInSlot) && txn.Status != TxStatusPreConfirmed { txn.Status = TxStatusPreConfirmed diff --git a/tools/preconf-rpc/sender/sender_test.go b/tools/preconf-rpc/sender/sender_test.go index aa8798ff3..c9929a5c2 100644 --- a/tools/preconf-rpc/sender/sender_test.go +++ b/tools/preconf-rpc/sender/sender_test.go @@ -249,6 +249,10 @@ func (m *mockBlockTracker) NextBlockNumber() (uint64, time.Duration, error) { } } +func (m *mockBlockTracker) LatestBlockNumber() uint64 { + return 0 +} + type mockTransferer struct{} func (m *mockTransferer) Transfer(ctx context.Context, to common.Address, chainID *big.Int, amount *big.Int) error {