Skip to content

Commit

Permalink
Merge pull request #2971 from OffchainLabs/fix-auction-tiebreaking
Browse files Browse the repository at this point in the history
Fix auction resolution during a tie
  • Loading branch information
joshuacolvin0 authored Feb 24, 2025
2 parents 22bb9ff + 56bcd32 commit d5b5334
Show file tree
Hide file tree
Showing 4 changed files with 171 additions and 59 deletions.
114 changes: 114 additions & 0 deletions system_tests/timeboost_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,120 @@ import (
"github.com/offchainlabs/nitro/util/testhelpers"
)

func TestAuctionResolutionDuringATie(t *testing.T) {
testAuctionResolutionDuringATie(t, false)
}

func TestAuctionResolutionDuringATieMultipleRuns(t *testing.T) {
t.Skip("This test is skipped in CI as it might probably take too long to complete")
testAuctionResolutionDuringATie(t, true)
}

func testAuctionResolutionDuringATie(t *testing.T, multiRuns bool) {
t.Parallel()
ctx, cancel := context.WithCancel(context.Background())
defer cancel()

tmpDir, err := os.MkdirTemp("", "*")
require.NoError(t, err)
t.Cleanup(func() {
require.NoError(t, os.RemoveAll(tmpDir))
})

auctionContractAddr, aliceBidderClient, bobBidderClient, _, builderSeq, cleanupSeq, _, _ := setupExpressLaneAuction(t, tmpDir, ctx, 0)
_, seqClient, seqInfo := builderSeq.L2.ConsensusNode, builderSeq.L2.Client, builderSeq.L2Info
defer cleanupSeq()

auctionContract, err := express_lane_auctiongen.NewExpressLaneAuction(auctionContractAddr, seqClient)
Require(t, err)
rawRoundTimingInfo, err := auctionContract.RoundTimingInfo(&bind.CallOpts{})
Require(t, err)
roundTimingInfo, err := timeboost.NewRoundTimingInfo(rawRoundTimingInfo)
Require(t, err)
domainSeparator, err := auctionContract.DomainSeparator(&bind.CallOpts{Context: ctx})
Require(t, err)

aliceAddr := seqInfo.GetAddress("Alice")
bobAddr := seqInfo.GetAddress("Bob")
var aliceHasWon, bobHasWon bool

for {
// For the next round, we will send equal bids and verify we get the correct winner
t.Logf("Alice and Bob now submitting their equal bids at %v", time.Now())
aliceBid, err := aliceBidderClient.Bid(ctx, big.NewInt(1), aliceAddr)
Require(t, err)
bobBid, err := bobBidderClient.Bid(ctx, big.NewInt(1), bobAddr)
Require(t, err)
t.Logf("Alice bid %+v", aliceBid)
t.Logf("Bob bid %+v", bobBid)

// Check if bidHash from ToEIP712Hash matches with the calculation in auction contract
matchBidHash := func(bid *timeboost.Bid) {
expectedBidHash, err := auctionContract.GetBidHash(&bind.CallOpts{}, bid.Round, bid.ExpressLaneController, bid.Amount)
Require(t, err)
bidHash, err := bid.ToEIP712Hash(domainSeparator)
Require(t, err)
if !bytes.Equal(expectedBidHash[:], bidHash.Bytes()) {
t.Fatalf("bid hash mismatch with contract. Want: %v, Got: %v", expectedBidHash, bidHash.Bytes())
}
}
matchBidHash(aliceBid)
matchBidHash(bobBid)

// Subscribe to auction resolutions and wait for a winner
winnerAddr, _ := awaitAuctionResolved(t, ctx, seqClient, auctionContract)

// Get expected Winner on the GO side
toValidatedBid := func(bidder common.Address, bid *timeboost.Bid) *timeboost.ValidatedBid {
return &timeboost.ValidatedBid{
ExpressLaneController: bid.ExpressLaneController,
Amount: bid.Amount,
Signature: bid.Signature,
ChainId: bid.ChainId,
AuctionContractAddress: bid.AuctionContractAddress,
Round: bid.Round,
Bidder: bidder,
}

}

var expectedWinner common.Address
aliceBigIntHash := toValidatedBid(aliceAddr, aliceBid).BigIntHash(domainSeparator)
BobBigIntHash := toValidatedBid(bobAddr, bobBid).BigIntHash(domainSeparator)
if aliceBigIntHash.Cmp(BobBigIntHash) > 0 {
expectedWinner = aliceAddr
} else if aliceBigIntHash.Cmp(BobBigIntHash) < 0 {
expectedWinner = bobAddr
}

// If tie can't be broken by BigIntHash, then whoever is picked first is the winner- auction contract will agree with that as well
if (expectedWinner != common.Address{}) {
// Verify that the winner on the GO side is the same on the contract side
if expectedWinner != winnerAddr {
t.Fatalf("Unexpected auction winner in case of a tie. Want: %s, Got: %s", expectedWinner, winnerAddr)
}
}

if !multiRuns {
break
}

if winnerAddr == aliceAddr {
aliceHasWon = true
} else if winnerAddr == bobAddr {
bobHasWon = true
} else {
t.Fatalf("Unexpected winner of the auction round: %s", winnerAddr)
}

// Both bidders winning a tie has been tested
if aliceHasWon && bobHasWon {
break
}
time.Sleep(roundTimingInfo.TimeTilNextRound())
}
}

func TestExpressLaneTxsHandlingDuringSequencerSwapDueToPriorities(t *testing.T) {
testTxsHandlingDuringSequencerSwap(t, false)
}
Expand Down
60 changes: 34 additions & 26 deletions timeboost/auctioneer.go
Original file line number Diff line number Diff line change
Expand Up @@ -106,19 +106,20 @@ func AuctioneerServerConfigAddOptions(prefix string, f *pflag.FlagSet) {
// It is responsible for receiving bids, validating them, and resolving auctions.
type AuctioneerServer struct {
stopwaiter.StopWaiter
consumer *pubsub.Consumer[*JsonValidatedBid, error]
txOpts *bind.TransactOpts
chainId *big.Int
endpointManager SequencerEndpointManager
auctionContract *express_lane_auctiongen.ExpressLaneAuction
auctionContractAddr common.Address
bidsReceiver chan *JsonValidatedBid
bidCache *bidCache
roundTimingInfo RoundTimingInfo
streamTimeout time.Duration
auctionResolutionWaitTime time.Duration
database *SqliteDatabase
s3StorageService *S3StorageService
consumer *pubsub.Consumer[*JsonValidatedBid, error]
txOpts *bind.TransactOpts
chainId *big.Int
endpointManager SequencerEndpointManager
auctionContract *express_lane_auctiongen.ExpressLaneAuction
auctionContractAddr common.Address
auctionContractDomainSeparator [32]byte
bidsReceiver chan *JsonValidatedBid
bidCache *bidCache
roundTimingInfo RoundTimingInfo
streamTimeout time.Duration
auctionResolutionWaitTime time.Duration
database *SqliteDatabase
s3StorageService *S3StorageService
}

// NewAuctioneerServer creates a new autonomous auctioneer struct.
Expand Down Expand Up @@ -183,6 +184,12 @@ func NewAuctioneerServer(ctx context.Context, configFetcher AuctioneerServerConf
if err != nil {
return nil, err
}
domainSeparator, err := auctionContract.DomainSeparator(&bind.CallOpts{
Context: ctx,
})
if err != nil {
return nil, err
}
rawRoundTimingInfo, err := auctionContract.RoundTimingInfo(&bind.CallOpts{})
if err != nil {
return nil, err
Expand All @@ -195,18 +202,19 @@ func NewAuctioneerServer(ctx context.Context, configFetcher AuctioneerServerConf
return nil, err
}
return &AuctioneerServer{
txOpts: txOpts,
endpointManager: endpointManager,
chainId: chainId,
database: database,
s3StorageService: s3StorageService,
consumer: c,
auctionContract: auctionContract,
auctionContractAddr: auctionContractAddr,
bidsReceiver: make(chan *JsonValidatedBid, 100_000), // TODO(Terence): Is 100k enough? Make this configurable?
bidCache: newBidCache(),
roundTimingInfo: *roundTimingInfo,
auctionResolutionWaitTime: cfg.AuctionResolutionWaitTime,
txOpts: txOpts,
endpointManager: endpointManager,
chainId: chainId,
database: database,
s3StorageService: s3StorageService,
consumer: c,
auctionContract: auctionContract,
auctionContractAddr: auctionContractAddr,
auctionContractDomainSeparator: domainSeparator,
bidsReceiver: make(chan *JsonValidatedBid, 100_000), // TODO(Terence): Is 100k enough? Make this configurable?
bidCache: newBidCache(domainSeparator),
roundTimingInfo: *roundTimingInfo,
auctionResolutionWaitTime: cfg.AuctionResolutionWaitTime,
}, nil
}

Expand Down Expand Up @@ -316,7 +324,7 @@ func (a *AuctioneerServer) Start(ctx_in context.Context) {
log.Error("Could not resolve auction for round", "error", err)
}
// Clear the bid cache.
a.bidCache = newBidCache()
a.bidCache = newBidCache(a.auctionContractDomainSeparator)
}
}
})
Expand Down
10 changes: 6 additions & 4 deletions timeboost/bid_cache.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,15 @@ import (
)

type bidCache struct {
auctionContractDomainSeparator [32]byte
sync.RWMutex
bidsByExpressLaneControllerAddr map[common.Address]*ValidatedBid
}

func newBidCache() *bidCache {
func newBidCache(auctionContractDomainSeparator [32]byte) *bidCache {
return &bidCache{
bidsByExpressLaneControllerAddr: make(map[common.Address]*ValidatedBid),
auctionContractDomainSeparator: auctionContractDomainSeparator,
}
}

Expand Down Expand Up @@ -50,16 +52,16 @@ func (bc *bidCache) topTwoBids() *auctionResult {
result.secondPlace = result.firstPlace
result.firstPlace = bid
} else if bid.Amount.Cmp(result.firstPlace.Amount) == 0 {
if bid.bigIntHash().Cmp(result.firstPlace.bigIntHash()) > 0 {
if bid.BigIntHash(bc.auctionContractDomainSeparator).Cmp(result.firstPlace.BigIntHash(bc.auctionContractDomainSeparator)) > 0 {
result.secondPlace = result.firstPlace
result.firstPlace = bid
} else if result.secondPlace == nil || bid.bigIntHash().Cmp(result.secondPlace.bigIntHash()) > 0 {
} else if result.secondPlace == nil || bid.BigIntHash(bc.auctionContractDomainSeparator).Cmp(result.secondPlace.BigIntHash(bc.auctionContractDomainSeparator)) > 0 {
result.secondPlace = bid
}
} else if result.secondPlace == nil || bid.Amount.Cmp(result.secondPlace.Amount) > 0 {
result.secondPlace = bid
} else if bid.Amount.Cmp(result.secondPlace.Amount) == 0 {
if bid.bigIntHash().Cmp(result.secondPlace.bigIntHash()) > 0 {
if bid.BigIntHash(bc.auctionContractDomainSeparator).Cmp(result.secondPlace.BigIntHash(bc.auctionContractDomainSeparator)) > 0 {
result.secondPlace = bid
}
}
Expand Down
46 changes: 17 additions & 29 deletions timeboost/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,14 +83,15 @@ type JsonBid struct {
}

type ValidatedBid struct {
ExpressLaneController common.Address
Amount *big.Int
Signature []byte
// For tie breaking
ChainId *big.Int
AuctionContractAddress common.Address
Round uint64
Bidder common.Address
Signature []byte

// For tie breaking
Bidder common.Address
ExpressLaneController common.Address
Round uint64
Amount *big.Int
}

// BigIntHash returns the hash of the bidder and bidBytes in the form of a big.Int.
Expand All @@ -100,31 +101,18 @@ type ValidatedBid struct {
//
// This is only used for breaking ties amongst equivalent bids and not used for
// Bid signing, which uses EIP 712 as the hashing scheme.
func (v *ValidatedBid) bigIntHash() *big.Int {
bidBytes := v.bidBytes()
func (v *ValidatedBid) BigIntHash(domainSeparator [32]byte) *big.Int {
bid := &Bid{
ExpressLaneController: v.ExpressLaneController,
Round: v.Round,
Amount: v.Amount,
}
// Since ToEIP712Hash is deterministic, this error can be ignored here, as the bidvalidator
// would have previously validated it when calculating bidHash
bidHash, _ := bid.ToEIP712Hash(domainSeparator)
bidder := v.Bidder.Bytes()

return new(big.Int).SetBytes(crypto.Keccak256Hash(bidder, bidBytes).Bytes())
}

// bidBytes returns the byte representation equivalent to the Solidity implementation of
//
// abi.encodePacked(BID_DOMAIN, block.chainid, address(this), _round, _amount, _expressLaneController)
func (v *ValidatedBid) bidBytes() []byte {
var buffer bytes.Buffer

buffer.Write(domainValue)
buffer.Write(v.ChainId.Bytes())
buffer.Write(v.AuctionContractAddress.Bytes())

roundBytes := make([]byte, 8)
binary.BigEndian.PutUint64(roundBytes, v.Round)
buffer.Write(roundBytes)

buffer.Write(v.Amount.Bytes())
buffer.Write(v.ExpressLaneController.Bytes())

return buffer.Bytes()
return new(big.Int).SetBytes(crypto.Keccak256Hash(bidder, bidHash.Bytes()).Bytes())
}

func (v *ValidatedBid) ToJson() *JsonValidatedBid {
Expand Down

0 comments on commit d5b5334

Please sign in to comment.