diff --git a/oracle/cmd/main.go b/oracle/cmd/main.go index 6acfed2ae..019cdae3a 100644 --- a/oracle/cmd/main.go +++ b/oracle/cmd/main.go @@ -256,6 +256,13 @@ var ( "https://bloxroute.regulated.blxrbdn.com", ), }) + + optionBidOptionsSlashEnabled = altsrc.NewBoolFlag(&cli.BoolFlag{ + Name: "bid-options-slash-enabled", + Usage: "Enable slashing based on bid options (position constraints, shutterised bids)", + EnvVars: []string{"MEV_ORACLE_BID_OPTIONS_SLASH_ENABLED"}, + Value: false, + }) ) func main() { @@ -288,6 +295,7 @@ func main() { optionGasTipCap, optionGasFeeCap, optionRelayUrls, + optionBidOptionsSlashEnabled, } app := &cli.App{ Name: "mev-oracle", @@ -408,6 +416,7 @@ func launchOracleWithConfig(c *cli.Context) error { DefaultGasTipCap: gasTipCap, DefaultGasFeeCap: gasFeeCap, RelayUrls: c.StringSlice(optionRelayUrls.Name), + BidOptionsSlashEnabled: c.Bool(optionBidOptionsSlashEnabled.Name), }) if err != nil { return fmt.Errorf("failed starting node: %w", err) diff --git a/oracle/pkg/node/node.go b/oracle/pkg/node/node.go index b60531391..812b85c70 100644 --- a/oracle/pkg/node/node.go +++ b/oracle/pkg/node/node.go @@ -63,6 +63,7 @@ type Options struct { DefaultGasLimit uint64 DefaultGasTipCap *big.Int DefaultGasFeeCap *big.Int + BidOptionsSlashEnabled bool } type Node struct { @@ -269,6 +270,7 @@ func NewNode(opts *Options) (*Node, error) { evtMgr, oracleTransactorSession, txmonitor.NewEVMHelperWithLogger(rawClient, nd.logger, contracts), + opts.BidOptionsSlashEnabled, ) if err != nil { nd.logger.Error("failed to instantiate updater", "error", err) diff --git a/oracle/pkg/updater/updater.go b/oracle/pkg/updater/updater.go index 33070d2be..f40099e0e 100644 --- a/oracle/pkg/updater/updater.go +++ b/oracle/pkg/updater/updater.go @@ -84,15 +84,16 @@ type EVMClient interface { } type Updater struct { - logger *slog.Logger - l1Client EVMClient - winnerRegister WinnerRegister - oracle Oracle - evtMgr events.EventManager - l1BlockCache *lru.Cache[uint64, map[string]TxMetadata] - openedCmts chan *preconf.PreconfmanagerOpenedCommitmentStored - metrics *metrics - receiptBatcher txmonitor.BatchReceiptGetter + logger *slog.Logger + l1Client EVMClient + winnerRegister WinnerRegister + oracle Oracle + evtMgr events.EventManager + l1BlockCache *lru.Cache[uint64, map[string]TxMetadata] + openedCmts chan *preconf.PreconfmanagerOpenedCommitmentStored + metrics *metrics + receiptBatcher txmonitor.BatchReceiptGetter + bidOptionsSlashEnabled bool } func NewUpdater( @@ -102,21 +103,23 @@ func NewUpdater( evtMgr events.EventManager, oracle Oracle, receiptBatcher txmonitor.BatchReceiptGetter, + bidOptionsSlashEnabled bool, ) (*Updater, error) { l1BlockCache, err := lru.New[uint64, map[string]TxMetadata](1024) if err != nil { return nil, fmt.Errorf("failed to create L1 block cache: %w", err) } return &Updater{ - logger: logger, - l1Client: l1Client, - l1BlockCache: l1BlockCache, - winnerRegister: winnerRegister, - evtMgr: evtMgr, - oracle: oracle, - receiptBatcher: receiptBatcher, - metrics: newMetrics(), - openedCmts: make(chan *preconf.PreconfmanagerOpenedCommitmentStored), + logger: logger, + l1Client: l1Client, + l1BlockCache: l1BlockCache, + winnerRegister: winnerRegister, + evtMgr: evtMgr, + oracle: oracle, + receiptBatcher: receiptBatcher, + metrics: newMetrics(), + openedCmts: make(chan *preconf.PreconfmanagerOpenedCommitmentStored), + bidOptionsSlashEnabled: bidOptionsSlashEnabled, }, nil } @@ -309,15 +312,18 @@ func (u *Updater) handleOpenedCommitment( revertableTxnsMap[txn] = true } - opts := new(bidderapiv1.BidOptions) - if update.BidOptions != nil { - if err := proto.Unmarshal(update.BidOptions, opts); err != nil { - u.logger.Error( - "failed to unmarshal bid options", - "commitmentIdx", common.Bytes2Hex(update.CommitmentIndex[:]), - "error", err, - ) - return err + var opts *bidderapiv1.BidOptions + if u.bidOptionsSlashEnabled { + opts = new(bidderapiv1.BidOptions) + if update.BidOptions != nil { + if err := proto.Unmarshal(update.BidOptions, opts); err != nil { + u.logger.Error( + "failed to unmarshal bid options", + "commitmentIdx", common.Bytes2Hex(update.CommitmentIndex[:]), + "error", err, + ) + return err + } } } @@ -339,16 +345,18 @@ func (u *Updater) handleOpenedCommitment( "revertible", revertableTxnsMap[commitmentTxnHashes[i]], ) - for _, opt := range opts.Options { - if sOpt := opt.GetShutterisedBidOption(); sOpt != nil { - u.logger.Info( - "shutterised bid option present, skipping slash", - "commitmentIdx", common.Bytes2Hex(update.CommitmentIndex[:]), - "txnHash", update.TxnHash, - "blockNumber", update.BlockNumber, - "shutter option", sOpt, - ) - return nil + if u.bidOptionsSlashEnabled { + for _, opt := range opts.Options { + if sOpt := opt.GetShutterisedBidOption(); sOpt != nil { + u.logger.Info( + "shutterised bid option present, skipping slash", + "commitmentIdx", common.Bytes2Hex(update.CommitmentIndex[:]), + "txnHash", update.TxnHash, + "blockNumber", update.BlockNumber, + "shutter option", sOpt, + ) + return nil + } } } @@ -362,25 +370,27 @@ func (u *Updater) handleOpenedCommitment( ) } - for idx, opt := range opts.Options { - if opt.GetPositionConstraint() != nil { - if checkPositionConstraintSatisfied(opt.GetPositionConstraint(), txnDetails, txns) { - u.logger.Debug( - "positional constraint satisfied", - "commitmentIdx", common.Bytes2Hex(update.CommitmentIndex[:]), - "txnHash", update.TxnHash, - "blockNumber", update.BlockNumber, - "constraint", opt.GetPositionConstraint(), - ) - // Remove the satisfied constraint - opts.Options = append(opts.Options[:idx], opts.Options[idx+1:]...) - break + if u.bidOptionsSlashEnabled { + for idx, opt := range opts.Options { + if opt.GetPositionConstraint() != nil { + if checkPositionConstraintSatisfied(opt.GetPositionConstraint(), txnDetails, txns) { + u.logger.Debug( + "positional constraint satisfied", + "commitmentIdx", common.Bytes2Hex(update.CommitmentIndex[:]), + "txnHash", update.TxnHash, + "blockNumber", update.BlockNumber, + "constraint", opt.GetPositionConstraint(), + ) + // Remove the satisfied constraint + opts.Options = append(opts.Options[:idx], opts.Options[idx+1:]...) + break + } } } } } - if len(opts.Options) > 0 { + if u.bidOptionsSlashEnabled && len(opts.Options) > 0 { u.logger.Info( "not all positional constraints satisfied", "commitmentIdx", common.Bytes2Hex(update.CommitmentIndex[:]), diff --git a/oracle/pkg/updater/updater_test.go b/oracle/pkg/updater/updater_test.go index e161b35f9..c524cf2c2 100644 --- a/oracle/pkg/updater/updater_test.go +++ b/oracle/pkg/updater/updater_test.go @@ -214,6 +214,7 @@ func TestUpdater(t *testing.T) { evtMgr, oracle, &testBatcher{}, + false, ) if err != nil { t.Fatal(err) @@ -423,6 +424,7 @@ func TestUpdaterRevertedTxns(t *testing.T) { evtMgr, oracle, testBatcher, + false, ) if err != nil { t.Fatal(err) @@ -632,6 +634,7 @@ func TestUpdaterRevertedTxnsWithRevertingHashes(t *testing.T) { evtMgr, oracle, testBatcher, + false, ) if err != nil { t.Fatal(err) @@ -809,6 +812,7 @@ func TestUpdaterBundlesFailure(t *testing.T) { evtMgr, oracle, &testBatcher{}, + false, ) if err != nil { t.Fatal(err) @@ -994,6 +998,7 @@ func TestUpdaterIgnoreCommitments(t *testing.T) { evtMgr, oracle, &testBatcher{}, + false, ) if err != nil { t.Fatal(err) @@ -1086,6 +1091,7 @@ func TestComputeResidualAfterDecay(t *testing.T) { nil, nil, nil, + false, ) if err != nil { // The current NewUpdater only returns error on cache creation failure, unlikely here. @@ -1574,18 +1580,6 @@ func TestBidOptions(t *testing.T) { commitments = append(commitments, commitment) } - register := &testWinnerRegister{ - winners: []testWinner{ - { - blockNum: 5, - winner: updater.Winner{ - Winner: builderAddr.Bytes(), - }, - }, - }, - settlements: make(chan testSettlement, 1), - } - body := &types.Body{Transactions: txns, Uncles: nil} l1Client := &testEVMClient{ @@ -1618,93 +1612,128 @@ func TestBidOptions(t *testing.T) { &pcABI, ) - oracle := &testOracle{ - commitments: make(chan processedCommitment, 1), - } + for _, tc := range []struct { + name string + bidOptionsSlashEnabled bool + }{ + {name: "enabled", bidOptionsSlashEnabled: true}, + {name: "disabled", bidOptionsSlashEnabled: false}, + } { + t.Run(tc.name, func(t *testing.T) { + register := &testWinnerRegister{ + winners: []testWinner{ + { + blockNum: 5, + winner: updater.Winner{ + Winner: builderAddr.Bytes(), + }, + }, + }, + settlements: make(chan testSettlement, 1), + } - updtr, err := updater.NewUpdater( - slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug})), - l1Client, - register, - evtMgr, - oracle, - &testBatcher{}, - ) - if err != nil { - t.Fatal(err) - } + oracle := &testOracle{ + commitments: make(chan processedCommitment, 1), + } - ctx, cancel := context.WithCancel(context.Background()) - done := updtr.Start(ctx) + updtr, err := updater.NewUpdater( + slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug})), + l1Client, + register, + evtMgr, + oracle, + &testBatcher{}, + tc.bidOptionsSlashEnabled, + ) + if err != nil { + t.Fatal(err) + } - for idx, c := range commitments { - if err := publishOpenedCommitment(evtMgr, &pcABI, c); err != nil { - t.Fatal(err) - } + ctx, cancel := context.WithCancel(context.Background()) + done := updtr.Start(ctx) - if c.Committer.Cmp(otherBuilderAddr) == 0 { - continue - } + for idx, c := range commitments { + if err := publishOpenedCommitment(evtMgr, &pcABI, c); err != nil { + t.Fatal(err) + } - select { - case <-time.After(5 * time.Second): - t.Fatal("timeout") - case commitment := <-oracle.commitments: - if !bytes.Equal(commitment.commitmentIdx[:], c.CommitmentIndex[:]) { - t.Fatal("wrong commitment index") - } - if commitment.blockNum.Cmp(big.NewInt(5)) != 0 { - t.Fatal("wrong block number") - } - if commitment.builder != c.Committer { - t.Fatal("wrong builder") - } - if idx%2 == 0 && commitment.isSlash { - t.Fatal("wrong isSlash") - } - if (idx%2 == 1 && idx < 10) && !commitment.isSlash { - t.Fatal("wrong isSlash") - } - if commitment.residualDecay.Cmp(big.NewInt(50*updater.PRECISION)) != 0 { - t.Fatal("wrong residual decay") - } - } + if c.Committer.Cmp(otherBuilderAddr) == 0 { + continue + } - select { - case <-time.After(5 * time.Second): - t.Fatal("timeout") - case settlement := <-register.settlements: - if !bytes.Equal(settlement.commitmentIdx, c.CommitmentIndex[:]) { - t.Fatal("wrong commitment index") - } - if settlement.txHash != c.TxnHash { - t.Fatal("wrong txn hash") - } - if settlement.blockNum != 5 { - t.Fatal("wrong block number") - } - if !bytes.Equal(settlement.builder, c.Committer.Bytes()) { - t.Fatal("wrong builder") - } - if settlement.amount.Uint64() != 10 { - t.Fatal("wrong amount") - } - if idx%2 == 0 && settlement.settlementType != updater.SettlementTypeReward { - t.Fatal("wrong settlement type") - } - if (idx%2 == 1 && idx < 10) && settlement.settlementType != updater.SettlementTypeSlash { - t.Fatal("wrong settlement type") - } - if settlement.decayPercentage != 50*updater.PRECISION { - t.Fatal("wrong decay percentage") + select { + case <-time.After(5 * time.Second): + t.Fatal("timeout") + case commitment := <-oracle.commitments: + if !bytes.Equal(commitment.commitmentIdx[:], c.CommitmentIndex[:]) { + t.Fatal("wrong commitment index") + } + if commitment.blockNum.Cmp(big.NewInt(5)) != 0 { + t.Fatal("wrong block number") + } + if commitment.builder != c.Committer { + t.Fatal("wrong builder") + } + if tc.bidOptionsSlashEnabled { + if idx%2 == 0 && commitment.isSlash { + t.Fatal("wrong isSlash") + } + if (idx%2 == 1 && idx < 10) && !commitment.isSlash { + t.Fatal("wrong isSlash") + } + } else { + if commitment.isSlash { + t.Fatalf("expected no slash when bid options disabled, idx=%d", idx) + } + } + if commitment.residualDecay.Cmp(big.NewInt(50*updater.PRECISION)) != 0 { + t.Fatal("wrong residual decay") + } + } + + select { + case <-time.After(5 * time.Second): + t.Fatal("timeout") + case settlement := <-register.settlements: + if !bytes.Equal(settlement.commitmentIdx, c.CommitmentIndex[:]) { + t.Fatal("wrong commitment index") + } + if settlement.txHash != c.TxnHash { + t.Fatal("wrong txn hash") + } + if settlement.blockNum != 5 { + t.Fatal("wrong block number") + } + if !bytes.Equal(settlement.builder, c.Committer.Bytes()) { + t.Fatal("wrong builder") + } + if settlement.amount.Uint64() != 10 { + t.Fatal("wrong amount") + } + if tc.bidOptionsSlashEnabled { + if idx%2 == 0 && settlement.settlementType != updater.SettlementTypeReward { + t.Fatal("wrong settlement type") + } + if (idx%2 == 1 && idx < 10) && settlement.settlementType != updater.SettlementTypeSlash { + t.Fatal("wrong settlement type") + } + } else { + if settlement.settlementType != updater.SettlementTypeReward { + t.Fatalf("expected reward when bid options disabled, got %s, idx=%d", settlement.settlementType, idx) + } + } + if settlement.decayPercentage != 50*updater.PRECISION { + t.Fatal("wrong decay percentage") + } + } } - } - } - cancel() - select { - case <-done: - case <-time.After(5 * time.Second): - t.Fatal("timeout") + cancel() + select { + case <-done: + case <-time.After(5 * time.Second): + t.Fatal("timeout") + } + }) } }