diff --git a/tools/preconf-rpc/blocktracker/blocktracker.go b/tools/preconf-rpc/blocktracker/blocktracker.go index 04912abc6..ebecc7b7b 100644 --- a/tools/preconf-rpc/blocktracker/blocktracker.go +++ b/tools/preconf-rpc/blocktracker/blocktracker.go @@ -63,7 +63,7 @@ func (b *blockTracker) Start(ctx context.Context) <-chan struct{} { } _ = b.blocks.Add(blockNo, block) b.latestBlockNo.Store(block.NumberU64()) - b.log.Info("New block detected", "number", block.NumberU64(), "hash", block.Hash().Hex()) + b.log.Debug("New block detected", "number", block.NumberU64(), "hash", block.Hash().Hex()) b.triggerCheck() } } diff --git a/tools/preconf-rpc/handlers/handlers.go b/tools/preconf-rpc/handlers/handlers.go index 1701d8243..17a58f4b9 100644 --- a/tools/preconf-rpc/handlers/handlers.go +++ b/tools/preconf-rpc/handlers/handlers.go @@ -22,7 +22,7 @@ type Bidder interface { } type Pricer interface { - EstimatePrice(ctx context.Context, txn *types.Transaction) (*pricer.BlockPrice, error) + EstimatePrice(ctx context.Context) (*pricer.BlockPrices, error) } type Store interface { @@ -136,10 +136,7 @@ func (h *rpcMethodHandler) RegisterMethods(server *rpcserver.JSONRPCServer) { return json.RawMessage(fmt.Sprintf(`{"timeInSecs": "%d"}`, timeToOptIn)), false, nil }) server.RegisterHandler("mevcommit_estimateDeposit", func(ctx context.Context, params ...any) (json.RawMessage, bool, error) { - blockPrice, err := h.pricer.EstimatePrice( - ctx, - types.NewTransaction(0, h.depositAddress, big.NewInt(0), 21000, big.NewInt(0), nil), - ) + blockPrices, err := h.pricer.EstimatePrice(ctx) if err != nil { h.logger.Error("Failed to estimate deposit price", "error", err) return nil, false, rpcserver.NewJSONErr( @@ -147,15 +144,9 @@ func (h *rpcMethodHandler) RegisterMethods(server *rpcserver.JSONRPCServer) { "failed to estimate deposit price", ) } - if blockPrice == nil { - h.logger.Warn("No block price estimated for deposit") - return nil, false, rpcserver.NewJSONErr( - rpcserver.CodeCustomError, - "no block price available for deposit", - ) - } + cost := getNextBlockPrice(blockPrices) result := map[string]interface{}{ - "bidAmount": blockPrice.BidAmount.String(), + "bidAmount": cost.String(), "depositAddress": h.depositAddress.Hex(), } @@ -167,14 +158,11 @@ func (h *rpcMethodHandler) RegisterMethods(server *rpcserver.JSONRPCServer) { "failed to marshal deposit estimate", ) } - h.logger.Debug("Estimated deposit price", "bidAmount", blockPrice.BidAmount, "depositAddress", h.depositAddress.Hex()) + h.logger.Debug("Estimated deposit price", "bidAmount", cost, "depositAddress", h.depositAddress.Hex()) return resultJSON, false, nil }) server.RegisterHandler("mevcommit_estimateBridge", func(ctx context.Context, params ...any) (json.RawMessage, bool, error) { - blockPrice, err := h.pricer.EstimatePrice( - ctx, - types.NewTransaction(0, h.bridgeAddress, big.NewInt(0), 21000, big.NewInt(0), nil), - ) + blockPrices, err := h.pricer.EstimatePrice(ctx) if err != nil { h.logger.Error("Failed to estimate bridge price", "error", err) return nil, false, rpcserver.NewJSONErr( @@ -182,11 +170,8 @@ func (h *rpcMethodHandler) RegisterMethods(server *rpcserver.JSONRPCServer) { "failed to estimate bridge price", ) } - if blockPrice == nil { - h.logger.Warn("No block price estimated for bridge") - return nil, true, nil // No price available, proxy - } - bridgeCost := new(big.Int).Mul(blockPrice.BidAmount, big.NewInt(2)) + cost := getNextBlockPrice(blockPrices) + bridgeCost := new(big.Int).Mul(cost, big.NewInt(2)) result := map[string]interface{}{ "bidAmount": bridgeCost.String(), "bridgeAddress": h.bridgeAddress.Hex(), @@ -200,11 +185,26 @@ func (h *rpcMethodHandler) RegisterMethods(server *rpcserver.JSONRPCServer) { "failed to marshal bridge estimate", ) } - h.logger.Debug("Estimated bridge price", "bidAmount", blockPrice.BidAmount, "bridgeAddress", h.bridgeAddress.Hex()) + h.logger.Debug("Estimated bridge price", "bidAmount", bridgeCost, "bridgeAddress", h.bridgeAddress.Hex()) return resultJSON, false, nil }) } +func getNextBlockPrice(blockPrices *pricer.BlockPrices) *big.Int { + for _, price := range blockPrices.Prices { + if price.BlockNumber == blockPrices.CurrentBlockNumber+1 { + for _, estimate := range price.EstimatedPrices { + if estimate.Confidence == 99 { + priceInWei := estimate.PriorityFeePerGasGwei * 1e9 + return new(big.Int).Mul(new(big.Int).SetUint64(uint64(priceInWei)), big.NewInt(21000)) + } + } + } + } + + return big.NewInt(0) // Return zero if no suitable estimate is found +} + func (h *rpcMethodHandler) handleGetBlockByHash( ctx context.Context, params ...any, diff --git a/tools/preconf-rpc/main.go b/tools/preconf-rpc/main.go index 02297b762..90f378ba5 100644 --- a/tools/preconf-rpc/main.go +++ b/tools/preconf-rpc/main.go @@ -176,6 +176,13 @@ var ( }, } + optionBlocknativeAPIKey = &cli.StringFlag{ + Name: "blocknative-api-key", + Usage: "Blocknative API key for transaction pricing", + EnvVars: []string{"PRECONF_RPC_BLOCKNATIVE_API_KEY"}, + Value: "", + } + optionLogFmt = &cli.StringFlag{ Name: "log-fmt", Usage: "log format to use, options are 'text' or 'json'", @@ -246,6 +253,7 @@ func main() { optionAutoDepositAmount, optionDepositAddress, optionBridgeAddress, + optionBlocknativeAPIKey, }, Action: func(c *cli.Context) error { logger, err := util.NewLogger( @@ -316,6 +324,7 @@ func main() { Signer: signer, DepositAddress: common.HexToAddress(c.String(optionDepositAddress.Name)), BridgeAddress: common.HexToAddress(c.String(optionBridgeAddress.Name)), + PricerAPIKey: c.String(optionBlocknativeAPIKey.Name), } s, err := service.New(&config) diff --git a/tools/preconf-rpc/pricer/pricer.go b/tools/preconf-rpc/pricer/pricer.go index 8c05cd619..892036340 100644 --- a/tools/preconf-rpc/pricer/pricer.go +++ b/tools/preconf-rpc/pricer/pricer.go @@ -5,34 +5,39 @@ import ( "encoding/json" "errors" "io" - "math/big" "net/http" "time" - - "github.com/ethereum/go-ethereum/core/types" ) var apiURL = "https://api.blocknative.com/gasprices/blockprices?chainid=1" -type blockPrice struct { - CurrentBlockNumber int64 `json:"currentBlockNumber"` - BlockPrices []struct { - BlockNumber int64 `json:"blockNumber"` - EstimatedPrices []struct { - Confidence int `json:"confidence"` - PriorityFeePerGas float64 `json:"maxPriorityFeePerGas"` - } - } +type EstimatedPrice struct { + Confidence int `json:"confidence"` + PriorityFeePerGasGwei float64 `json:"maxPriorityFeePerGas"` } type BlockPrice struct { - BlockNumber int64 - BidAmount *big.Int + BlockNumber int64 `json:"blockNumber"` + EstimatedPrices []EstimatedPrice `json:"estimatedPrices"` +} + +type BlockPrices struct { + MsSinceLastBlock int64 `json:"msSinceLastBlock"` + CurrentBlockNumber int64 `json:"currentBlockNumber"` + Prices []BlockPrice `json:"blockPrices"` +} + +type BidPricer struct { + apiKey string } -type BidPricer struct{} +func NewPricer(apiKey string) *BidPricer { + return &BidPricer{ + apiKey: apiKey, + } +} -func (b *BidPricer) EstimatePrice(ctx context.Context, txn *types.Transaction) (*BlockPrice, error) { +func (b *BidPricer) EstimatePrice(ctx context.Context) (*BlockPrices, error) { client := &http.Client{ Timeout: 10 * time.Second, } @@ -42,6 +47,10 @@ func (b *BidPricer) EstimatePrice(ctx context.Context, txn *types.Transaction) ( return nil, err } + if b.apiKey != "" { + req.Header.Set("Authorization", b.apiKey) + } + resp, err := client.Do(req) if err != nil { return nil, err @@ -60,30 +69,14 @@ func (b *BidPricer) EstimatePrice(ctx context.Context, txn *types.Transaction) ( return nil, err } - var bp blockPrice - if err := json.Unmarshal(respBuf, &bp); err != nil { + bp := new(BlockPrices) + if err := json.Unmarshal(respBuf, bp); err != nil { return nil, err } - if len(bp.BlockPrices) == 0 { + if len(bp.Prices) == 0 { return nil, errors.New("no block prices available") } - for _, price := range bp.BlockPrices { - if price.BlockNumber == bp.CurrentBlockNumber+1 { - for _, p := range price.EstimatedPrices { - if p.Confidence == 99 { // Assuming we want the 99% confidence price - // Convert the priority fee from Gwei to Wei - // 1 Gwei = 1e9 Wei - priorityFee := p.PriorityFeePerGas * 1e9 - bidAmount := big.NewInt(0).Mul(big.NewInt(int64(priorityFee)), big.NewInt(int64(txn.Gas()))) - return &BlockPrice{BlockNumber: price.BlockNumber, BidAmount: bidAmount}, nil - } - } - } - } - - // If we reach here, it means we didn't find a suitable price. - // This could happen if the API response format changes or if no 99% confidence price is available. - return nil, errors.New("no suitable price found for the next block") + return bp, nil } diff --git a/tools/preconf-rpc/pricer/pricer_test.go b/tools/preconf-rpc/pricer/pricer_test.go index b84d54ad0..fd897e6c4 100644 --- a/tools/preconf-rpc/pricer/pricer_test.go +++ b/tools/preconf-rpc/pricer/pricer_test.go @@ -2,41 +2,46 @@ package pricer_test import ( "context" - "math/big" "testing" - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/core/types" "github.com/primev/mev-commit/tools/preconf-rpc/pricer" ) func TestEstimatePrice(t *testing.T) { t.Parallel() - bp := pricer.BidPricer{} + bp := pricer.NewPricer("") ctx := context.Background() - txn := types.NewTransaction( - 0, - common.HexToAddress("0x1234567890123456789012345678901234567890"), - big.NewInt(1000000000), // 1 Gwei - 21000, // gas limit - big.NewInt(1000000000), // gas price - nil, // no data - ) - - price, err := bp.EstimatePrice(ctx, txn) + + prices, err := bp.EstimatePrice(ctx) if err != nil { t.Fatalf("failed to estimate price: %v", err) } + if prices.CurrentBlockNumber == 0 { + t.Error("expected non-zero current block number") + } + + if len(prices.Prices) == 0 { + t.Error("expected at least one block price") + } + + price := prices.Prices[0] + if price.BlockNumber == 0 { - t.Error("expected non-zero block number in estimated price") + t.Error("expected non-zero block number in price") + } + + if len(price.EstimatedPrices) == 0 { + t.Error("expected at least one estimated price") } - if price.BidAmount.Cmp(big.NewInt(0)) <= 0 { - t.Error("expected estimated price to be greater than zero") + for _, estPrice := range price.EstimatedPrices { + if estPrice.PriorityFeePerGasGwei <= 0 { + t.Errorf("expected positive priority fee per gas, got %f", estPrice.PriorityFeePerGasGwei) + } } - t.Logf("Estimated price: %s at block %d", price.BidAmount.String(), price.BlockNumber) + t.Logf("Estimated prices: %v", prices) } diff --git a/tools/preconf-rpc/sender/sender.go b/tools/preconf-rpc/sender/sender.go index 9049a223b..bd2a9fdb9 100644 --- a/tools/preconf-rpc/sender/sender.go +++ b/tools/preconf-rpc/sender/sender.go @@ -8,9 +8,11 @@ import ( "math/big" "strings" "sync" + "time" "github.com/ethereum/go-ethereum/common" "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" "github.com/primev/mev-commit/tools/preconf-rpc/pricer" optinbidder "github.com/primev/mev-commit/x/opt-in-bidder" @@ -35,7 +37,11 @@ const ( ) const ( - blockTime = 12 // seconds, typical Ethereum block time + blockTime = 12 // seconds, typical Ethereum block time + bidTimeout = 3 * time.Second // timeout for bid operations + defaultConfidence = 90 // default confidence level for the next block + confidenceSecondAttempt = 95 // confidence level for the second attempt + confidenceSubsequentAttempts = 99 // confidence level for subsequent attempts ) var ( @@ -79,7 +85,7 @@ type Bidder interface { } type Pricer interface { - EstimatePrice(ctx context.Context, txn *types.Transaction) (*pricer.BlockPrice, error) + EstimatePrice(ctx context.Context) (*pricer.BlockPrices, error) } type BlockTracker interface { @@ -90,6 +96,17 @@ type Transferer interface { Transfer(ctx context.Context, to common.Address, chainID *big.Int, amount *big.Int) error } +type blockAttempt struct { + blockNumber int64 + attempts int +} + +type txnAttempt struct { + txnHash common.Hash + startTime time.Time + attempts []*blockAttempt +} + type TxSender struct { logger *slog.Logger store Store @@ -105,6 +122,7 @@ type TxSender struct { inflightTxns map[common.Hash]struct{} inflightAccount map[common.Address]struct{} inflightMu sync.Mutex + txnAttemptHistory *lru.Cache[common.Hash, *txnAttempt] } func NewTxSender( @@ -115,7 +133,13 @@ func NewTxSender( transferer Transferer, settlementChainId *big.Int, logger *slog.Logger, -) *TxSender { +) (*TxSender, error) { + txnAttemptHistory, err := lru.New[common.Hash, *txnAttempt](1000) + if err != nil { + logger.Error("Failed to create transaction attempt history cache", "error", err) + return nil, fmt.Errorf("failed to create transaction attempt history cache: %w", err) + } + return &TxSender{ store: st, bidder: bidder, @@ -128,7 +152,8 @@ func NewTxSender( trigger: make(chan struct{}, 1), inflightTxns: make(map[common.Hash]struct{}), inflightAccount: make(map[common.Address]struct{}), - } + txnAttemptHistory: txnAttemptHistory, + }, nil } func validateTransaction(tx *Transaction) error { @@ -294,6 +319,20 @@ BID_LOOP: result, err = t.sendBid(ctx, txn) switch { case err != nil: + if retryErr, ok := err.(*errRetry); ok { + t.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 + } + continue + } return err case result.optedInSlot: if result.noOfProviders == len(result.commitments) { @@ -309,11 +348,30 @@ BID_LOOP: "blockNumber", result.blockNumber, "bidAmount", result.bidAmount.String(), ) + t.clearBlockAttemptHistory(txn.Hash()) break BID_LOOP } default: } + if result.noOfProviders > len(result.commitments) { + t.logger.Warn( + "Not all builders committed to the bid", + "noOfProviders", result.noOfProviders, + "noOfCommitments", len(result.commitments), + "sender", txn.Sender.Hex(), + "type", txn.Type, + "blockNumber", result.blockNumber, + "bidAmount", result.bidAmount.String(), + ) + blockTimeUsed := time.Since(result.startTime).Milliseconds() + result.msSinceLastBlock + if blockTimeUsed < (blockTime*1000 - 1000) { + // If not all builders committed, we will retry the bid process + // immediately if we have atleast 1 second 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) @@ -331,6 +389,7 @@ BID_LOOP: "blockNumber", result.blockNumber, "bidAmount", result.bidAmount.String(), ) + t.clearBlockAttemptHistory(txn.Hash()) break BID_LOOP } } @@ -362,12 +421,23 @@ BID_LOOP: return nil } +type errRetry struct { + err error + retryAfter time.Duration +} + +func (e *errRetry) Error() string { + return fmt.Sprintf("retry after %s: %v", e.retryAfter, e.err) +} + type bidResult struct { - noOfProviders int - blockNumber uint64 - optedInSlot bool - bidAmount *big.Int - commitments []*bidderapiv1.Commitment + startTime time.Time + msSinceLastBlock int64 + noOfProviders int + blockNumber uint64 + optedInSlot bool + bidAmount *big.Int + commitments []*bidderapiv1.Commitment } func (t *TxSender) sendBid( @@ -385,35 +455,50 @@ func (t *TxSender) sendBid( optedInSlot := timeToOptIn <= blockTime - price, err := t.pricer.EstimatePrice(ctx, txn.Transaction) + start := time.Now() + + prices, err := t.pricer.EstimatePrice(ctx) if err != nil { t.logger.Error("Failed to estimate transaction price", "error", err) - return bidResult{}, fmt.Errorf("failed to estimate transaction price: %w", err) + return bidResult{}, &errRetry{ + err: fmt.Errorf("failed to estimate transaction price: %w", err), + retryAfter: time.Second, + } + } + + cost, blockNo, err := t.calculatePriceForNextBlock(txn, prices) + if err != nil { + t.logger.Error("Failed to calculate price for next block", "error", err) + return bidResult{}, &errRetry{ + err: fmt.Errorf("failed to calculate price: %w", err), + retryAfter: time.Second, + } } + slashAmount := big.NewInt(0) switch txn.Type { case TxTypeRegular: - if !t.store.HasBalance(ctx, txn.Sender, price.BidAmount) { + if !t.store.HasBalance(ctx, txn.Sender, cost) { t.logger.Error("Insufficient balance for sender", "sender", txn.Sender.Hex()) return bidResult{}, fmt.Errorf("insufficient balance for sender: %s", txn.Sender.Hex()) } case TxTypeDeposit: - if txn.Value().Cmp(price.BidAmount) < 0 { + if txn.Value().Cmp(cost) < 0 { t.logger.Error( "Deposit amount is less than price of deposit", "sender", txn.Sender.Hex(), "deposit", txn.Value().String(), - "price", price.BidAmount.String(), + "price", cost.String(), ) return bidResult{}, fmt.Errorf( "deposit amount is less than price of deposit: %s, deposit: %s, price: %s", txn.Sender.Hex(), txn.Value().String(), - price.BidAmount.String(), + cost.String(), ) } case TxTypeInstantBridge: - costOfBridge := new(big.Int).Mul(price.BidAmount, big.NewInt(2)) // 2x the price for instant bridge + costOfBridge := new(big.Int).Mul(cost, big.NewInt(2)) // 2x the price for instant bridge if txn.Value().Cmp(costOfBridge) < 0 { t.logger.Error( "Instant bridge amount is less than price of bridge", @@ -428,17 +513,22 @@ func (t *TxSender) sendBid( costOfBridge.String(), ) } + slashAmount = new(big.Int).Set(txn.Value()) } + cctx, cancel := context.WithTimeout(ctx, bidTimeout) + defer cancel() + bidC, err := t.bidder.Bid( - ctx, - price.BidAmount, - big.NewInt(0), + cctx, + cost, + slashAmount, strings.TrimPrefix(txn.Raw, "0x"), &optinbidder.BidOpts{ WaitForOptIn: false, - BlockNumber: uint64(price.BlockNumber), + BlockNumber: uint64(blockNo), RevertingTxHashes: []string{txn.Hash().Hex()}, + DecayDuration: bidTimeout, }, ) if err != nil { @@ -447,8 +537,10 @@ func (t *TxSender) sendBid( } result := bidResult{ - commitments: make([]*bidderapiv1.Commitment, 0), - bidAmount: price.BidAmount, + commitments: make([]*bidderapiv1.Commitment, 0), + bidAmount: cost, + startTime: start, + msSinceLastBlock: prices.MsSinceLastBlock, } BID_LOOP: for { @@ -488,3 +580,76 @@ BID_LOOP: result.optedInSlot = optedInSlot return result, nil } + +func (t *TxSender) calculatePriceForNextBlock(txn *Transaction, prices *pricer.BlockPrices) (*big.Int, int64, error) { + attempts, found := t.txnAttemptHistory.Get(txn.Hash()) + if !found { + attempts = &txnAttempt{ + txnHash: txn.Hash(), + startTime: time.Now(), + } + } + + // default confidence level for the next block + confidence := defaultConfidence + + for _, attempt := range attempts.attempts { + if attempt.blockNumber == prices.CurrentBlockNumber+1 { + attempt.attempts++ + switch { + case attempt.attempts == 2: + confidence = confidenceSecondAttempt + case attempt.attempts > 2: + confidence = confidenceSubsequentAttempts + } + } + } + // If this is the first attempt for the next block, we add it with confidence 90 + if confidence == defaultConfidence { + attempts.attempts = append(attempts.attempts, &blockAttempt{ + blockNumber: prices.CurrentBlockNumber + 1, + attempts: 1, + }) + } + + _ = t.txnAttemptHistory.Add(txn.Hash(), attempts) + + for _, price := range prices.Prices { + if price.BlockNumber == prices.CurrentBlockNumber+1 { + for _, estimatedPrice := range price.EstimatedPrices { + if estimatedPrice.Confidence == confidence { + // the gwei value is in float, so we need to convert it to wei before multiplying with gas limit + priceInWei := estimatedPrice.PriorityFeePerGasGwei * 1e9 // Convert Gwei to Wei + return new(big.Int).Mul(big.NewInt(int64(priceInWei)), big.NewInt(int64(txn.Gas()))), price.BlockNumber, nil + } + } + } + } + + return nil, 0, fmt.Errorf( + "no estimated price found for block %d with confidence %d", prices.CurrentBlockNumber+1, confidence, + ) +} + +func (t *TxSender) clearBlockAttemptHistory(txnHash common.Hash) { + attempts, found := t.txnAttemptHistory.Get(txnHash) + if !found { + return + } + + totalAttempts := 0 + for _, attempt := range attempts.attempts { + totalAttempts += attempt.attempts + } + + t.logger.Info( + "Clearing block attempt history for transaction", + "hash", txnHash.Hex(), + "blockAttempts", len(attempts.attempts), + "startTime", attempts.startTime.Format(time.RFC3339), + "startBlockNumber", attempts.attempts[0].blockNumber, + "totalAttempts", totalAttempts, + ) + + _ = t.txnAttemptHistory.Remove(txnHash) +} diff --git a/tools/preconf-rpc/sender/sender_test.go b/tools/preconf-rpc/sender/sender_test.go index ab31b5fc5..07716fa28 100644 --- a/tools/preconf-rpc/sender/sender_test.go +++ b/tools/preconf-rpc/sender/sender_test.go @@ -176,21 +176,28 @@ func (m *mockBidder) Bid( } type mockPricer struct { - in chan *types.Transaction - out chan *pricer.BlockPrice + out chan *pricer.BlockPrices + errOut chan error } func (m *mockPricer) EstimatePrice( ctx context.Context, - txn *types.Transaction, -) (*pricer.BlockPrice, error) { - m.in <- txn +) (*pricer.BlockPrices, error) { select { - case price := <-m.out: - if price == nil { + case err := <-m.errOut: + if err != nil { + return nil, err + } + default: + // No error, continue + } + + select { + case prices := <-m.out: + if prices == nil { return nil, errors.New("nil price returned") } - return price, nil + return prices, nil case <-ctx.Done(): return nil, ctx.Err() } @@ -230,8 +237,8 @@ func TestSender(t *testing.T) { st := newMockStore() testPricer := &mockPricer{ - in: make(chan *types.Transaction, 10), - out: make(chan *pricer.BlockPrice, 10), + out: make(chan *pricer.BlockPrices, 10), + errOut: make(chan error, 1), } bidder := &mockBidder{ optinEstimate: make(chan int64, 10), @@ -243,7 +250,7 @@ func TestSender(t *testing.T) { out: make(chan bool, 10), } - sndr := sender.NewTxSender( + sndr, err := sender.NewTxSender( st, bidder, testPricer, @@ -252,6 +259,9 @@ func TestSender(t *testing.T) { big.NewInt(1), // Settlement chain ID util.NewTestLogger(os.Stdout), ) + if err != nil { + t.Fatalf("failed to create sender: %v", err) + } ctx, cancel := context.WithCancel(context.Background()) @@ -271,7 +281,7 @@ func TestSender(t *testing.T) { Raw: "0x1234567890123456789012345678901234567890", } - if err := st.AddBalance(ctx, tx1.Sender, big.NewInt(1000)); err != nil { + if err := st.AddBalance(ctx, tx1.Sender, big.NewInt(5e18)); err != nil { t.Fatalf("failed to add balance: %v", err) } @@ -280,16 +290,36 @@ func TestSender(t *testing.T) { } // Simulate opted in block + bidder.optinEstimate <- 2 + + // simulate error and ensure retry happens + testPricer.errOut <- errors.New("simulated error for testing") + bidder.optinEstimate <- 1 // Simulate a price estimate - op := <-testPricer.in - if op.Hash().Hex() != tx1.Hash().Hex() { - t.Fatalf("expected transaction hash %s, got %s", tx1.Hash().Hex(), op.Hash().Hex()) - } - testPricer.out <- &pricer.BlockPrice{ - BlockNumber: 1, - BidAmount: big.NewInt(100), + testPricer.out <- &pricer.BlockPrices{ + CurrentBlockNumber: 0, + MsSinceLastBlock: 1000, + Prices: []pricer.BlockPrice{ + { + BlockNumber: 1, + EstimatedPrices: []pricer.EstimatedPrice{ + { + Confidence: 90, + PriorityFeePerGasGwei: 1.0, + }, + { + Confidence: 95, + PriorityFeePerGasGwei: 1.5, + }, + { + Confidence: 99, + PriorityFeePerGasGwei: 2.0, + }, + }, + }, + }, } // Simulate a bid response @@ -346,7 +376,7 @@ func TestSender(t *testing.T) { Transaction: types.NewTransaction( 2, common.HexToAddress("0x1234567890123456789012345678901234567890"), - big.NewInt(1000), + big.NewInt(1e18), 21000, big.NewInt(1), nil, @@ -364,13 +394,74 @@ func TestSender(t *testing.T) { bidder.optinEstimate <- 20 // Simulate a price estimate - op = <-testPricer.in - if op.Hash().Hex() != tx2.Hash().Hex() { - t.Fatalf("expected transaction hash %s, got %s", tx2.Hash().Hex(), op.Hash().Hex()) + testPricer.out <- &pricer.BlockPrices{ + CurrentBlockNumber: 1, + MsSinceLastBlock: 1000, + Prices: []pricer.BlockPrice{ + { + BlockNumber: 2, + EstimatedPrices: []pricer.EstimatedPrice{ + { + Confidence: 90, + PriorityFeePerGasGwei: 1.0, + }, + { + Confidence: 95, + PriorityFeePerGasGwei: 1.5, + }, + { + Confidence: 99, + PriorityFeePerGasGwei: 2.0, + }, + }, + }, + }, + } + + // Simulate a bid response + bidOp = <-bidder.in + if bidOp.rawTx != tx2.Raw[2:] { + t.Fatalf("expected raw transaction %s, got %s", tx1.Raw, bidOp.rawTx) } - testPricer.out <- &pricer.BlockPrice{ - BlockNumber: 2, - BidAmount: big.NewInt(100), + resC = make(chan optinbidder.BidStatus, 3) + resC <- optinbidder.BidStatus{ + Type: optinbidder.BidStatusNoOfProviders, + Arg: 1, + } + resC <- optinbidder.BidStatus{ + Type: optinbidder.BidStatusAttempted, + Arg: uint64(2), + } + // Simulate retry due to incomplete commitments + close(resC) + bidder.out <- resC + + // Simulate non opted in block + bidder.optinEstimate <- 18 + + // Simulate a price estimate + testPricer.out <- &pricer.BlockPrices{ + CurrentBlockNumber: 1, + MsSinceLastBlock: 1000, + Prices: []pricer.BlockPrice{ + { + BlockNumber: 2, + EstimatedPrices: []pricer.EstimatedPrice{ + { + Confidence: 90, + PriorityFeePerGasGwei: 1.0, + }, + { + Confidence: 95, + PriorityFeePerGasGwei: 1.5, + }, + { + Confidence: 99, + PriorityFeePerGasGwei: 2.0, + }, + }, + }, + }, } // Simulate a bid response diff --git a/tools/preconf-rpc/service/service.go b/tools/preconf-rpc/service/service.go index 0585795d1..f07a28273 100644 --- a/tools/preconf-rpc/service/service.go +++ b/tools/preconf-rpc/service/service.go @@ -56,6 +56,7 @@ type Config struct { HTTPPort int GasTipCap *big.Int GasFeeCap *big.Int + PricerAPIKey string } type Service struct { @@ -173,7 +174,7 @@ func New(config *Config) (*Service, error) { config.Logger.With("module", "rpcserver"), ) - bidpricer := &pricer.BidPricer{} + bidpricer := pricer.NewPricer(config.PricerAPIKey) db, err := initDB(config) if err != nil { @@ -196,7 +197,7 @@ func New(config *Config) (*Service, error) { blockTrackerDone := blockTracker.Start(ctx) healthChecker.Register(health.CloseChannelHealthCheck("BlockTracker", blockTrackerDone)) - sndr := sender.NewTxSender( + sndr, err := sender.NewTxSender( rpcstore, bidderClient, bidpricer, @@ -205,6 +206,9 @@ func New(config *Config) (*Service, error) { settlementChainID, config.Logger.With("module", "txsender"), ) + if err != nil { + return nil, fmt.Errorf("failed to create transaction sender: %w", err) + } senderDone := sndr.Start(ctx) healthChecker.Register(health.CloseChannelHealthCheck("TxSender", senderDone)) diff --git a/x/opt-in-bidder/bidder.go b/x/opt-in-bidder/bidder.go index ad33a8cd8..6f6ab9012 100644 --- a/x/opt-in-bidder/bidder.go +++ b/x/opt-in-bidder/bidder.go @@ -188,6 +188,7 @@ type BidOpts struct { WaitForOptIn bool BlockNumber uint64 RevertingTxHashes []string + DecayDuration time.Duration } var defaultBidOpts = &BidOpts{ @@ -269,15 +270,22 @@ func (b *BidderClient) Bid( "slashAmount", slashAmount, ) - pc, err := b.bidderClient.SendBid(ctx, &bidderapiv1.Bid{ + bidReq := &bidderapiv1.Bid{ Amount: bidAmount.String(), BlockNumber: int64(blkNumber), RawTransactions: []string{rawTx}, DecayStartTimestamp: nowFunc().Add(100 * time.Millisecond).UnixMilli(), - DecayEndTimestamp: nowFunc().Add(12 * time.Second).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() + } + + 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()} @@ -305,10 +313,8 @@ func (b *BidderClient) Bid( res <- BidStatus{Type: BidStatusFailed, Arg: err.Error()} return } - res <- BidStatus{Type: BidStatusCommitment, Arg: msg} } - }() return res, nil