diff --git a/tools/preconf-rpc/backrunner/backrunner.go b/tools/preconf-rpc/backrunner/backrunner.go index af16b96ba..ff7db529f 100644 --- a/tools/preconf-rpc/backrunner/backrunner.go +++ b/tools/preconf-rpc/backrunner/backrunner.go @@ -287,6 +287,8 @@ func (b *backrunner) checkRewards(ctx context.Context, start int64) error { if !ok { continue } + // 90% to user, 10% to platform + amount = new(big.Int).Div(new(big.Int).Mul(amount, big.NewInt(90)), big.NewInt(100)) if updated, err := b.store.UpdateSwapReward(ctx, amount, record.BundleHashes); err != nil { return fmt.Errorf("updating backrun reward: %w", err) } else if updated { diff --git a/tools/preconf-rpc/backrunner/backrunner_test.go b/tools/preconf-rpc/backrunner/backrunner_test.go index dda085f98..12f3c861c 100644 --- a/tools/preconf-rpc/backrunner/backrunner_test.go +++ b/tools/preconf-rpc/backrunner/backrunner_test.go @@ -237,6 +237,7 @@ func TestBackrun(t *testing.T) { for { if reward, exists := st.GetReward(common.HexToHash("0xa3d8155e77cc46237e007e7a1274ca277209c47f27bae4405c74f01bb14673ec")); exists { expectedReward := big.NewInt(111444335163840) + expectedReward = new(big.Int).Div(new(big.Int).Mul(expectedReward, big.NewInt(90)), big.NewInt(100)) // 90% to user if reward.Cmp(expectedReward) != 0 { t.Fatalf("unexpected reward: got %v, want %v", reward, expectedReward) } diff --git a/tools/preconf-rpc/blocktracker/blocktracker.go b/tools/preconf-rpc/blocktracker/blocktracker.go index ea22d08b6..ae4ce9fc5 100644 --- a/tools/preconf-rpc/blocktracker/blocktracker.go +++ b/tools/preconf-rpc/blocktracker/blocktracker.go @@ -13,6 +13,7 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" lru "github.com/hashicorp/golang-lru/v2" + "github.com/primev/mev-commit/x/contracts/txmonitor" "golang.org/x/sync/errgroup" ) @@ -22,6 +23,15 @@ type EthClient interface { PendingNonceAt(ctx context.Context, account common.Address) (uint64, error) } +type BatchReceiptGetter interface { + BatchReceipts(ctx context.Context, txHashes []common.Hash) ([]txmonitor.Result, error) +} + +type ReceiptStore interface { + GetReceipt(ctx context.Context, txHash common.Hash) (*types.Receipt, error) + StoreReceipt(ctx context.Context, receipt *types.Receipt) error +} + type LatestBlockInfo struct { Number uint64 Time int64 @@ -33,24 +43,28 @@ type blockTracker struct { latestBlockInfo atomic.Pointer[LatestBlockInfo] blocks *lru.Cache[uint64, *types.Block] client EthClient + receiptGetter BatchReceiptGetter + receiptStore ReceiptStore log *slog.Logger txnToCheckMu sync.Mutex txnsToCheck map[common.Hash]chan uint64 newBlockChan chan uint64 } -func NewBlockTracker(client EthClient, log *slog.Logger) (*blockTracker, error) { +func NewBlockTracker(client EthClient, receiptGetter BatchReceiptGetter, receiptStore ReceiptStore, log *slog.Logger) (*blockTracker, error) { cache, err := lru.New[uint64, *types.Block](1000) if err != nil { log.Error("Failed to create LRU cache", "error", err) return nil, err } return &blockTracker{ - blocks: cache, - client: client, - log: log, - txnsToCheck: make(map[common.Hash]chan uint64), - newBlockChan: make(chan uint64, 1), + blocks: cache, + client: client, + receiptGetter: receiptGetter, + receiptStore: receiptStore, + log: log, + txnsToCheck: make(map[common.Hash]chan uint64), + newBlockChan: make(chan uint64, 1), }, nil } @@ -141,6 +155,21 @@ func (b *blockTracker) Start(ctx context.Context) <-chan struct{} { delete(b.txnsToCheck, txHash) } b.txnToCheckMu.Unlock() + receipts, err := b.receiptGetter.BatchReceipts(egCtx, txnsToClear) + if err != nil { + b.log.Error("Failed to get batch receipts", "error", err) + continue + } + for _, res := range receipts { + if res.Err != nil { + b.log.Error("Failed to get receipt for txn", "txHash", res.Receipt.TxHash.Hex(), "error", res.Err) + continue + } + err = b.receiptStore.StoreReceipt(egCtx, res.Receipt) + if err != nil { + b.log.Error("Failed to store receipt", "txHash", res.Receipt.TxHash.Hex(), "error", err) + } + } } } }) diff --git a/tools/preconf-rpc/blocktracker/blocktracker_test.go b/tools/preconf-rpc/blocktracker/blocktracker_test.go index d2d134471..ff06f1e33 100644 --- a/tools/preconf-rpc/blocktracker/blocktracker_test.go +++ b/tools/preconf-rpc/blocktracker/blocktracker_test.go @@ -2,6 +2,7 @@ package blocktracker_test import ( "context" + "errors" "hash" "math/big" "os" @@ -12,6 +13,7 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" "github.com/primev/mev-commit/tools/preconf-rpc/blocktracker" + "github.com/primev/mev-commit/x/contracts/txmonitor" "github.com/primev/mev-commit/x/util" "golang.org/x/crypto/sha3" ) @@ -58,6 +60,40 @@ func (m *mockEthClient) PendingNonceAt(ctx context.Context, account common.Addre return 0, nil } +type mockBatchReceiptGetter struct { + receipts map[common.Hash]*types.Receipt +} + +func (m *mockBatchReceiptGetter) BatchReceipts(ctx context.Context, txHashes []common.Hash) ([]txmonitor.Result, error) { + receipts := make([]txmonitor.Result, len(txHashes)) + for i := range txHashes { + receipt, exists := m.receipts[txHashes[i]] + if exists { + receipts[i] = txmonitor.Result{Receipt: receipt, Err: nil} + } else { + receipts[i] = txmonitor.Result{Receipt: nil, Err: errors.New("receipt not found")} + } + } + return receipts, nil +} + +type mockReceiptStore struct { + receipts map[common.Hash]*types.Receipt +} + +func (m *mockReceiptStore) GetReceipt(ctx context.Context, txHash common.Hash) (*types.Receipt, error) { + receipt, exists := m.receipts[txHash] + if !exists { + return nil, errors.New("receipt not found") + } + return receipt, nil +} + +func (m *mockReceiptStore) StoreReceipt(ctx context.Context, receipt *types.Receipt) error { + m.receipts[receipt.TxHash] = receipt + return nil +} + type testHasher struct { hasher hash.Hash } @@ -128,7 +164,29 @@ func TestBlockTracker(t *testing.T) { }, } - tracker, err := blocktracker.NewBlockTracker(client, util.NewTestLogger(os.Stdout)) + receiptGetter := &mockBatchReceiptGetter{ + receipts: map[common.Hash]*types.Receipt{ + tx1.Hash(): { + TxHash: tx1.Hash(), + BlockNumber: big.NewInt(100), + }, + tx2.Hash(): { + TxHash: tx2.Hash(), + BlockNumber: big.NewInt(100), + }, + tx3.Hash(): { + TxHash: tx3.Hash(), + BlockNumber: big.NewInt(101), + }, + tx4.Hash(): nil, // Simulate missing receipt + }, + } + + receiptStore := &mockReceiptStore{ + receipts: make(map[common.Hash]*types.Receipt), + } + + tracker, err := blocktracker.NewBlockTracker(client, receiptGetter, receiptStore, util.NewTestLogger(os.Stdout)) if err != nil { t.Fatalf("Failed to create block tracker: %v", err) } diff --git a/tools/preconf-rpc/handlers/handlers.go b/tools/preconf-rpc/handlers/handlers.go index a5dc80a85..b2f510029 100644 --- a/tools/preconf-rpc/handlers/handlers.go +++ b/tools/preconf-rpc/handlers/handlers.go @@ -41,6 +41,8 @@ type Store interface { HasBalance(ctx context.Context, account common.Address, amount *big.Int) bool AlreadySubsidized(ctx context.Context, account common.Address) bool AddSubsidy(ctx context.Context, account common.Address, amount *big.Int) error + GetReceipt(ctx context.Context, txHash common.Hash) (*types.Receipt, error) + StoreReceipt(ctx context.Context, receipt *types.Receipt) error } type BlockTracker interface { @@ -583,13 +585,30 @@ func (h *rpcMethodHandler) handleGetTxReceipt(ctx context.Context, params ...any txHash := common.HexToHash(txHashStr) + receipt, err := h.store.GetReceipt(ctx, txHash) + if err == nil && receipt != nil { + receiptJSON, err := json.Marshal(receipt) + if err != nil { + h.logger.Error("Failed to marshal receipt to JSON", "error", err, "txHash", txHash) + return nil, false, rpcserver.NewJSONErr( + rpcserver.CodeCustomError, + "failed to marshal receipt", + ) + } + return receiptJSON, false, nil + } + txn, err := h.store.GetTransactionByHash(ctx, txHash) if err != nil { return nil, true, nil } - if txn.Status != sender.TxStatusFailed && txn.Status != sender.TxStatusPreConfirmed { + switch txn.Status { + case sender.TxStatusPending, sender.TxStatusConfirmed: + // go to RPC proxy return nil, true, nil + case sender.TxStatusPreConfirmed, sender.TxStatusFailed: + // continue processing } logs, err := h.store.GetTransactionLogs(ctx, txHash) diff --git a/tools/preconf-rpc/points/points.go b/tools/preconf-rpc/points/points.go index 4910f6dbf..66fe95c73 100644 --- a/tools/preconf-rpc/points/points.go +++ b/tools/preconf-rpc/points/points.go @@ -14,7 +14,8 @@ import ( "github.com/ethereum/go-ethereum/common" ) -const weiPerPoint = 33_333_333_333_333 +// 0.00001 ETH in wei +const weiPerPoint = 10000000000000 type pointsTracker struct { apiURL string diff --git a/tools/preconf-rpc/service/service.go b/tools/preconf-rpc/service/service.go index 7ade9b74a..888b6db9a 100644 --- a/tools/preconf-rpc/service/service.go +++ b/tools/preconf-rpc/service/service.go @@ -17,6 +17,7 @@ import ( "sync" "time" + "github.com/ethereum/go-ethereum/accounts/abi" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/ethclient" _ "github.com/lib/pq" @@ -36,6 +37,7 @@ import ( "github.com/primev/mev-commit/tools/preconf-rpc/sim" "github.com/primev/mev-commit/tools/preconf-rpc/store" "github.com/primev/mev-commit/x/accountsync" + "github.com/primev/mev-commit/x/contracts/txmonitor" "github.com/primev/mev-commit/x/health" "github.com/primev/mev-commit/x/keysigner" "github.com/primev/mev-commit/x/transfer" @@ -246,6 +248,8 @@ func New(config *Config) (*Service, error) { blockTracker, err := blocktracker.NewBlockTracker( l1RPCClient, + txmonitor.NewEVMHelperWithLogger(l1RPCClient, config.Logger.With("module", "evmhelper"), map[common.Address]*abi.ABI{}), + rpcstore, config.Logger.With("module", "blocktracker"), ) if err != nil { @@ -712,7 +716,8 @@ func initDB(opts *Config) (db *sql.DB, err error) { db.SetMaxOpenConns(50) db.SetMaxIdleConns(25) - db.SetConnMaxLifetime(1 * time.Hour) + db.SetConnMaxLifetime(2 * time.Hour) + db.SetConnMaxIdleTime(15 * time.Minute) return db, err } diff --git a/tools/preconf-rpc/store/store.go b/tools/preconf-rpc/store/store.go index 01774f3e3..5af6fa826 100644 --- a/tools/preconf-rpc/store/store.go +++ b/tools/preconf-rpc/store/store.go @@ -87,6 +87,13 @@ CREATE TABLE IF NOT EXISTS settlementInfo ( FOREIGN KEY (transaction_hash) REFERENCES mcTransactions (hash) ON DELETE CASCADE );` +var receipts = ` +CREATE TABLE IF NOT EXISTS receipts ( + transaction_hash TEXT PRIMARY KEY, + receipt_data BYTEA, + FOREIGN KEY (transaction_hash) REFERENCES mcTransactions (hash) ON DELETE CASCADE +);` + type rpcstore struct { db *sql.DB } @@ -100,6 +107,7 @@ func New(db *sql.DB) (*rpcstore, error) { simulationLogs, swapInfo, settlementInfo, + receipts, } { _, err := db.Exec(table) if err != nil { @@ -974,8 +982,9 @@ func (r *rpcstore) UpdateSettlementPayment( INSERT INTO settlementInfo (transaction_hash, payment, refund) VALUES ($1, $2, $3) ON CONFLICT (transaction_hash) - DO UPDATE SET payment = EXCLUDED.payment, - refund = EXCLUDED.refund; + DO UPDATE SET + payment = COALESCE(settlementInfo.payment, 0) + COALESCE(EXCLUDED.payment, 0), + refund = COALESCE(settlementInfo.refund, 0) + COALESCE(EXCLUDED.refund, 0); ` _, err = dbtx.ExecContext( @@ -989,9 +998,8 @@ func (r *rpcstore) UpdateSettlementPayment( return fmt.Errorf("failed to update settlement payment for txn %s: %w", txnHash.Hex(), err) } - switch { - case payment != nil && payment.Cmp(big.NewInt(0)) > 0: - // deduct user balance, set to 0 on underflow + if payment != nil && payment.Cmp(big.NewInt(0)) > 0 { + // deduct user balance if _, err := dbtx.ExecContext( ctx, `UPDATE balances b @@ -1007,23 +1015,6 @@ func (r *rpcstore) UpdateSettlementPayment( ); err != nil { return fmt.Errorf("failed to deduct user balance for txn %s: %w", txnHash.Hex(), err) } - case refund != nil && refund.Cmp(big.NewInt(0)) > 0: - // add refund to user balance - if _, err := dbtx.ExecContext( - ctx, - `UPDATE balances b - SET balance = b.balance + $1::numeric - FROM ( - SELECT DISTINCT sender - FROM mcTransactions - WHERE hash = $2 - ) t - WHERE b.account = t.sender;`, - refund.String(), - txnHash.Hex(), - ); err != nil { - return fmt.Errorf("failed to add refund to user balance for txn %s: %w", txnHash.Hex(), err) - } } if err := dbtx.Commit(); err != nil { @@ -1032,3 +1023,60 @@ func (r *rpcstore) UpdateSettlementPayment( return nil } + +func (r *rpcstore) StoreReceipt( + ctx context.Context, + receipt *types.Receipt, +) error { + receiptData, err := json.Marshal(receipt) + if err != nil { + return fmt.Errorf("failed to marshal receipt for txn %s: %w", receipt.TxHash.Hex(), err) + } + + query := ` + INSERT INTO receipts (transaction_hash, receipt_data) + VALUES ($1, $2) + ON CONFLICT (transaction_hash) + DO UPDATE SET receipt_data = EXCLUDED.receipt_data; + ` + + _, err = r.db.ExecContext( + ctx, + query, + receipt.TxHash.Hex(), + receiptData, + ) + if err != nil { + return fmt.Errorf("failed to store receipt for txn %s: %w", receipt.TxHash.Hex(), err) + } + + return nil +} + +func (r *rpcstore) GetReceipt( + ctx context.Context, + txnHash common.Hash, +) (*types.Receipt, error) { + query := ` + SELECT receipt_data + FROM receipts + WHERE transaction_hash = $1; + ` + + row := r.db.QueryRowContext(ctx, query, txnHash.Hex()) + var receiptData []byte + err := row.Scan(&receiptData) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + return nil, fmt.Errorf("receipt not found for transaction %s: %w", txnHash.Hex(), ErrNotFound) + } + return nil, fmt.Errorf("failed to get receipt for transaction %s: %w", txnHash.Hex(), err) + } + + receipt := new(types.Receipt) + if err := json.Unmarshal(receiptData, receipt); err != nil { + return nil, fmt.Errorf("failed to unmarshal receipt for transaction %s: %w", txnHash.Hex(), err) + } + + return receipt, nil +} diff --git a/tools/preconf-rpc/store/store_test.go b/tools/preconf-rpc/store/store_test.go index 733e55351..31963d5fd 100644 --- a/tools/preconf-rpc/store/store_test.go +++ b/tools/preconf-rpc/store/store_test.go @@ -462,4 +462,35 @@ func TestStore(t *testing.T) { t.Errorf("expected user deposit and bridge count 0, got %d and %d", userTxns.DepositCount, userTxns.BridgeCount) } }) + + t.Run("Receipt", func(t *testing.T) { + receipt := &types.Receipt{ + Type: types.LegacyTxType, + PostState: []uint8{}, + Status: types.ReceiptStatusSuccessful, + CumulativeGasUsed: 21000, + Bloom: types.Bloom{}, + Logs: txn1Logs, + TxHash: txn1.Hash(), + ContractAddress: common.Address{}, // usually zero unless contract creation + GasUsed: 21000, + BlockHash: common.HexToHash("0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef"), + BlockNumber: big.NewInt(1), + TransactionIndex: 0, + } + + err := st.StoreReceipt(context.Background(), receipt) + if err != nil { + t.Fatalf("failed to store receipt: %v", err) + } + + retrievedReceipt, err := st.GetReceipt(context.Background(), txn1.Hash()) + if err != nil { + t.Fatalf("failed to get receipt: %v", err) + } + + if diff := cmp.Diff(receipt, retrievedReceipt, cmpopts.IgnoreUnexported(types.Receipt{}, big.Int{})); diff != "" { + t.Fatalf("receipt mismatch (-want +got):\n%s", diff) + } + }) }