diff --git a/execution/gethexec/express_lane_service.go b/execution/gethexec/express_lane_service.go index 534d553384..c981160814 100644 --- a/execution/gethexec/express_lane_service.go +++ b/execution/gethexec/express_lane_service.go @@ -30,7 +30,6 @@ import ( "github.com/offchainlabs/nitro/solgen/go/express_lane_auctiongen" "github.com/offchainlabs/nitro/timeboost" - "github.com/offchainlabs/nitro/util/arbmath" "github.com/offchainlabs/nitro/util/stopwaiter" ) @@ -49,9 +48,7 @@ type expressLaneService struct { transactionPublisher transactionPublisher auctionContractAddr common.Address apiBackend *arbitrum.APIBackend - initialTimestamp time.Time - roundDuration time.Duration - auctionClosing time.Duration + roundTimingInfo timeboost.RoundTimingInfo earlySubmissionGrace time.Duration chainConfig *params.ChainConfig logs chan []*types.Log @@ -143,8 +140,7 @@ func newExpressLaneService( retries := 0 pending: - var roundTimingInfo timeboost.RoundTimingInfo - roundTimingInfo, err = auctionContract.RoundTimingInfo(&bind.CallOpts{}) + rawRoundTimingInfo, err := auctionContract.RoundTimingInfo(&bind.CallOpts{}) if err != nil { const maxRetries = 5 if errors.Is(err, bind.ErrNoCode) && retries < maxRetries { @@ -156,23 +152,20 @@ pending: } return nil, err } - if err = roundTimingInfo.Validate(nil); err != nil { + roundTimingInfo, err := timeboost.NewRoundTimingInfo(rawRoundTimingInfo) + if err != nil { return nil, err } - initialTimestamp := time.Unix(roundTimingInfo.OffsetTimestamp, 0) - roundDuration := arbmath.SaturatingCast[time.Duration](roundTimingInfo.RoundDurationSeconds) * time.Second - auctionClosingDuration := arbmath.SaturatingCast[time.Duration](roundTimingInfo.AuctionClosingSeconds) * time.Second + return &expressLaneService{ transactionPublisher: transactionPublisher, auctionContract: auctionContract, apiBackend: apiBackend, chainConfig: chainConfig, - initialTimestamp: initialTimestamp, - auctionClosing: auctionClosingDuration, + roundTimingInfo: *roundTimingInfo, earlySubmissionGrace: earlySubmissionGrace, roundControl: lru.NewCache[uint64, *expressLaneControl](8), // Keep 8 rounds cached. auctionContractAddr: auctionContractAddr, - roundDuration: roundDuration, logs: make(chan []*types.Log, 10_000), messagesBySequenceNumber: make(map[uint64]*timeboost.ExpressLaneSubmission), }, nil @@ -184,7 +177,7 @@ func (es *expressLaneService) Start(ctxIn context.Context) { // Log every new express lane auction round. es.LaunchThread(func(ctx context.Context) { log.Info("Watching for new express lane rounds") - waitTime := timeboost.TimeTilNextRound(es.initialTimestamp, es.roundDuration) + waitTime := es.roundTimingInfo.TimeTilNextRound() // Wait until the next round starts select { case <-ctx.Done(): @@ -193,7 +186,7 @@ func (es *expressLaneService) Start(ctxIn context.Context) { // First tick happened, now set up regular ticks } - ticker := time.NewTicker(es.roundDuration) + ticker := time.NewTicker(es.roundTimingInfo.Round) defer ticker.Stop() for { @@ -201,7 +194,9 @@ func (es *expressLaneService) Start(ctxIn context.Context) { case <-ctx.Done(): return case t := <-ticker.C: - round := timeboost.CurrentRound(es.initialTimestamp, es.roundDuration) + round := es.roundTimingInfo.RoundNumber() + // TODO (BUG?) is there a race here where messages for a new round can come + // in before this tick has been processed? log.Info( "New express lane auction round", "round", round, @@ -318,25 +313,13 @@ func (es *expressLaneService) Start(ctxIn context.Context) { } func (es *expressLaneService) currentRoundHasController() bool { - currRound := timeboost.CurrentRound(es.initialTimestamp, es.roundDuration) - control, ok := es.roundControl.Get(currRound) + control, ok := es.roundControl.Get(es.roundTimingInfo.RoundNumber()) if !ok { return false } return control.controller != (common.Address{}) } -func (es *expressLaneService) isWithinAuctionCloseWindow(arrivalTime time.Time) bool { - // Calculate the next round start time - elapsedTime := arrivalTime.Sub(es.initialTimestamp) - elapsedRounds := elapsedTime / es.roundDuration - nextRoundStart := es.initialTimestamp.Add((elapsedRounds + 1) * es.roundDuration) - // Calculate the time to the next round - timeToNextRound := nextRoundStart.Sub(arrivalTime) - // Check if the arrival timestamp is within AUCTION_CLOSING_DURATION of TIME_TO_NEXT_ROUND - return timeToNextRound <= es.auctionClosing -} - // Sequence express lane submission skips validation of the express lane message itself, // as the core validator logic is handled in `validateExpressLaneTx“ func (es *expressLaneService) sequenceExpressLaneSubmission( @@ -374,14 +357,14 @@ func (es *expressLaneService) sequenceExpressLaneSubmission( if !exists { break } + delete(es.messagesBySequenceNumber, nextMsg.SequenceNumber) if err := es.transactionPublisher.PublishTimeboostedTransaction( ctx, nextMsg.Transaction, msg.Options, ); err != nil { - // If the tx failed, clear it from the sequence map. - delete(es.messagesBySequenceNumber, msg.SequenceNumber) - return err + // If the tx fails we return an error with all the necessary info for the controller to successfully try again + return fmt.Errorf("express lane transaction of sequence number: %d and transaction hash: %v, failed with an error: %w", nextMsg.SequenceNumber, nextMsg.Transaction.Hash(), err) } // Increase the global round sequence number. control.sequence += 1 @@ -402,18 +385,16 @@ func (es *expressLaneService) validateExpressLaneTx(msg *timeboost.ExpressLaneSu } for { - currentRound := timeboost.CurrentRound(es.initialTimestamp, es.roundDuration) + currentRound := es.roundTimingInfo.RoundNumber() if msg.Round == currentRound { break } - currentTime := time.Now() - if msg.Round == currentRound+1 && - timeboost.TimeTilNextRoundAfterTimestamp(es.initialTimestamp, currentTime, es.roundDuration) <= es.earlySubmissionGrace { - // If it becomes the next round in between checking the currentRound - // above, and here, then this will be a negative duration which is - // treated as time.Sleep(0), which is fine. - time.Sleep(timeboost.TimeTilNextRoundAfterTimestamp(es.initialTimestamp, currentTime, es.roundDuration)) + timeTilNextRound := es.roundTimingInfo.TimeTilNextRound() + // We allow txs to come in for the next round if it is close enough to that round, + // but we sleep until the round starts. + if msg.Round == currentRound+1 && timeTilNextRound <= es.earlySubmissionGrace { + time.Sleep(timeTilNextRound) } else { return errors.Wrapf(timeboost.ErrBadRoundNumber, "express lane tx round %d does not match current round %d", msg.Round, currentRound) } diff --git a/execution/gethexec/express_lane_service_test.go b/execution/gethexec/express_lane_service_test.go index 2afbfa2d6e..0c69c341a0 100644 --- a/execution/gethexec/express_lane_service_test.go +++ b/execution/gethexec/express_lane_service_test.go @@ -39,6 +39,15 @@ func init() { testPriv2 = privKey2 } +func defaultTestRoundTimingInfo(offset time.Time) timeboost.RoundTimingInfo { + return timeboost.RoundTimingInfo{ + Offset: offset, + Round: time.Minute, + AuctionClosing: time.Second * 15, + ReserveSubmission: time.Second * 15, + } +} + func Test_expressLaneService_validateExpressLaneTx(t *testing.T) { tests := []struct { name string @@ -110,6 +119,7 @@ func Test_expressLaneService_validateExpressLaneTx(t *testing.T) { name: "no onchain controller", es: &expressLaneService{ auctionContractAddr: common.Address{'a'}, + roundTimingInfo: defaultTestRoundTimingInfo(time.Now()), chainConfig: ¶ms.ChainConfig{ ChainID: big.NewInt(1), }, @@ -127,8 +137,7 @@ func Test_expressLaneService_validateExpressLaneTx(t *testing.T) { name: "bad round number", es: &expressLaneService{ auctionContractAddr: common.Address{'a'}, - initialTimestamp: time.Now(), - roundDuration: time.Minute, + roundTimingInfo: defaultTestRoundTimingInfo(time.Now()), chainConfig: ¶ms.ChainConfig{ ChainID: big.NewInt(1), }, @@ -150,8 +159,7 @@ func Test_expressLaneService_validateExpressLaneTx(t *testing.T) { name: "malformed signature", es: &expressLaneService{ auctionContractAddr: common.Address{'a'}, - initialTimestamp: time.Now(), - roundDuration: time.Minute, + roundTimingInfo: defaultTestRoundTimingInfo(time.Now()), chainConfig: ¶ms.ChainConfig{ ChainID: big.NewInt(1), }, @@ -173,8 +181,7 @@ func Test_expressLaneService_validateExpressLaneTx(t *testing.T) { name: "wrong signature", es: &expressLaneService{ auctionContractAddr: common.HexToAddress("0x2Aef36410182881a4b13664a1E079762D7F716e6"), - initialTimestamp: time.Now(), - roundDuration: time.Minute, + roundTimingInfo: defaultTestRoundTimingInfo(time.Now()), chainConfig: ¶ms.ChainConfig{ ChainID: big.NewInt(1), }, @@ -190,8 +197,7 @@ func Test_expressLaneService_validateExpressLaneTx(t *testing.T) { name: "not express lane controller", es: &expressLaneService{ auctionContractAddr: common.HexToAddress("0x2Aef36410182881a4b13664a1E079762D7F716e6"), - initialTimestamp: time.Now(), - roundDuration: time.Minute, + roundTimingInfo: defaultTestRoundTimingInfo(time.Now()), chainConfig: ¶ms.ChainConfig{ ChainID: big.NewInt(1), }, @@ -207,8 +213,7 @@ func Test_expressLaneService_validateExpressLaneTx(t *testing.T) { name: "OK", es: &expressLaneService{ auctionContractAddr: common.HexToAddress("0x2Aef36410182881a4b13664a1E079762D7F716e6"), - initialTimestamp: time.Now(), - roundDuration: time.Minute, + roundTimingInfo: defaultTestRoundTimingInfo(time.Now()), chainConfig: ¶ms.ChainConfig{ ChainID: big.NewInt(1), }, @@ -241,10 +246,12 @@ func Test_expressLaneService_validateExpressLaneTx(t *testing.T) { func Test_expressLaneService_validateExpressLaneTx_gracePeriod(t *testing.T) { auctionContractAddr := common.HexToAddress("0x2Aef36410182881a4b13664a1E079762D7F716e6") es := &expressLaneService{ - auctionContractAddr: auctionContractAddr, - initialTimestamp: time.Now(), - roundDuration: time.Second * 10, - auctionClosing: time.Second * 5, + auctionContractAddr: auctionContractAddr, + roundTimingInfo: timeboost.RoundTimingInfo{ + Offset: time.Now(), + Round: time.Second * 10, + AuctionClosing: time.Second * 5, + }, earlySubmissionGrace: time.Second * 2, chainConfig: ¶ms.ChainConfig{ ChainID: big.NewInt(1), @@ -439,16 +446,10 @@ func Test_expressLaneService_sequenceExpressLaneSubmission_erroredTx(t *testing. require.Equal(t, []uint64{1, 2, 3}, stubPublisher.publishedTxOrder) } +// TODO this test is just for RoundTimingInfo func TestIsWithinAuctionCloseWindow(t *testing.T) { initialTimestamp := time.Date(2024, 8, 8, 15, 0, 0, 0, time.UTC) - roundDuration := 1 * time.Minute - auctionClosing := 15 * time.Second - - es := &expressLaneService{ - initialTimestamp: initialTimestamp, - roundDuration: roundDuration, - auctionClosing: auctionClosing, - } + roundTimingInfo := defaultTestRoundTimingInfo(initialTimestamp) tests := []struct { name string @@ -484,9 +485,9 @@ func TestIsWithinAuctionCloseWindow(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - actual := es.isWithinAuctionCloseWindow(tt.arrivalTime) + actual := roundTimingInfo.IsWithinAuctionCloseWindow(tt.arrivalTime) if actual != tt.expectedBool { - t.Errorf("isWithinAuctionCloseWindow(%v) = %v; want %v", tt.arrivalTime, actual, tt.expectedBool) + t.Errorf("IsWithinAuctionCloseWindow(%v) = %v; want %v", tt.arrivalTime, actual, tt.expectedBool) } }) } @@ -497,8 +498,7 @@ func Benchmark_expressLaneService_validateExpressLaneTx(b *testing.B) { addr := crypto.PubkeyToAddress(testPriv.PublicKey) es := &expressLaneService{ auctionContractAddr: common.HexToAddress("0x2Aef36410182881a4b13664a1E079762D7F716e6"), - initialTimestamp: time.Now(), - roundDuration: time.Minute, + roundTimingInfo: defaultTestRoundTimingInfo(time.Now()), roundControl: lru.NewCache[uint64, *expressLaneControl](8), chainConfig: ¶ms.ChainConfig{ ChainID: big.NewInt(1), diff --git a/execution/gethexec/sequencer.go b/execution/gethexec/sequencer.go index e91ea5a421..98d6d1d480 100644 --- a/execution/gethexec/sequencer.go +++ b/execution/gethexec/sequencer.go @@ -594,7 +594,7 @@ func (s *Sequencer) PublishAuctionResolutionTransaction(ctx context.Context, tx if sender != auctioneerAddr { return fmt.Errorf("sender %#x is not the auctioneer address %#x", sender, auctioneerAddr) } - if !s.expressLaneService.isWithinAuctionCloseWindow(arrivalTime) { + if !s.expressLaneService.roundTimingInfo.IsWithinAuctionCloseWindow(arrivalTime) { return fmt.Errorf("transaction arrival time not within auction closure window: %v", arrivalTime) } txBytes, err := tx.MarshalBinary() diff --git a/system_tests/timeboost_test.go b/system_tests/timeboost_test.go index a23ba19d71..e8b9b57175 100644 --- a/system_tests/timeboost_test.go +++ b/system_tests/timeboost_test.go @@ -57,7 +57,9 @@ func TestExpressLaneControlTransfer(t *testing.T) { auctionContract, err := express_lane_auctiongen.NewExpressLaneAuction(auctionContractAddr, seqClient) Require(t, err) - info, err := auctionContract.RoundTimingInfo(&bind.CallOpts{}) + rawRoundTimingInfo, err := auctionContract.RoundTimingInfo(&bind.CallOpts{}) + Require(t, err) + roundTimingInfo, err := timeboost.NewRoundTimingInfo(rawRoundTimingInfo) Require(t, err) // Prepare clients that can submit txs to the sequencer via the express lane. @@ -70,8 +72,7 @@ func TestExpressLaneControlTransfer(t *testing.T) { expressLaneClient := newExpressLaneClient( priv, chainId, - time.Unix(info.OffsetTimestamp, 0), - arbmath.SaturatingCast[time.Duration](info.RoundDurationSeconds)*time.Second, + *roundTimingInfo, auctionContractAddr, seqDial, ) @@ -85,13 +86,13 @@ func TestExpressLaneControlTransfer(t *testing.T) { // Bob will win the auction and become controller for next round placeBidsAndDecideWinner(t, ctx, seqClient, seqInfo, auctionContract, "Bob", "Alice", bobBidderClient, aliceBidderClient, roundDuration) - waitTillNextRound(roundDuration) + time.Sleep(roundTimingInfo.TimeTilNextRound()) // Check that Bob's tx gets priority since he's the controller verifyControllerAdvantage(t, ctx, seqClient, bobExpressLaneClient, seqInfo, "Bob", "Alice") // Transfer express lane control from Bob to Alice - currRound := timeboost.CurrentRound(time.Unix(info.OffsetTimestamp, 0), roundDuration) + currRound := roundTimingInfo.RoundNumber() duringRoundTransferTx, err := auctionContract.ExpressLaneAuctionTransactor.TransferExpressLaneController(&bobOpts, currRound, seqInfo.Accounts["Alice"].Address) Require(t, err) err = bobExpressLaneClient.SendTransaction(ctx, duringRoundTransferTx) @@ -107,7 +108,7 @@ func TestExpressLaneControlTransfer(t *testing.T) { // Alice now transfers control to bob before her round begins winnerRound := currRound + 1 - currRound = timeboost.CurrentRound(time.Unix(info.OffsetTimestamp, 0), roundDuration) + currRound = roundTimingInfo.RoundNumber() if currRound >= winnerRound { t.Fatalf("next round already began, try running the test again. Current round: %d, Winner Round: %d", currRound, winnerRound) } @@ -156,11 +157,13 @@ func TestSequencerFeed_ExpressLaneAuction_ExpressLaneTxsHaveAdvantage(t *testing auctionContract, err := express_lane_auctiongen.NewExpressLaneAuction(auctionContractAddr, seqClient) Require(t, err) - info, err := auctionContract.RoundTimingInfo(&bind.CallOpts{}) + rawRoundTimingInfo, err := auctionContract.RoundTimingInfo(&bind.CallOpts{}) + Require(t, err) + roundTimingInfo, err := timeboost.NewRoundTimingInfo(rawRoundTimingInfo) Require(t, err) placeBidsAndDecideWinner(t, ctx, seqClient, seqInfo, auctionContract, "Bob", "Alice", bobBidderClient, aliceBidderClient, roundDuration) - waitTillNextRound(roundDuration) + time.Sleep(roundTimingInfo.TimeTilNextRound()) chainId, err := seqClient.ChainID(ctx) Require(t, err) @@ -172,8 +175,7 @@ func TestSequencerFeed_ExpressLaneAuction_ExpressLaneTxsHaveAdvantage(t *testing expressLaneClient := newExpressLaneClient( bobPriv, chainId, - time.Unix(info.OffsetTimestamp, 0), - arbmath.SaturatingCast[time.Duration](info.RoundDurationSeconds)*time.Second, + *roundTimingInfo, auctionContractAddr, seqDial, ) @@ -198,11 +200,15 @@ func TestSequencerFeed_ExpressLaneAuction_InnerPayloadNoncesAreRespected(t *test auctionContract, err := express_lane_auctiongen.NewExpressLaneAuction(auctionContractAddr, seqClient) Require(t, err) - info, err := auctionContract.RoundTimingInfo(&bind.CallOpts{}) + rawRoundTimingInfo, err := auctionContract.RoundTimingInfo(&bind.CallOpts{}) + Require(t, err) + roundTimingInfo, err := timeboost.NewRoundTimingInfo(rawRoundTimingInfo) + Require(t, err) + Require(t, err) placeBidsAndDecideWinner(t, ctx, seqClient, seqInfo, auctionContract, "Bob", "Alice", bobBidderClient, aliceBidderClient, roundDuration) - waitTillNextRound(roundDuration) + time.Sleep(roundTimingInfo.TimeTilNextRound()) // Prepare a client that can submit txs to the sequencer via the express lane. bobPriv := seqInfo.Accounts["Bob"].PrivateKey @@ -213,8 +219,7 @@ func TestSequencerFeed_ExpressLaneAuction_InnerPayloadNoncesAreRespected(t *test expressLaneClient := newExpressLaneClient( bobPriv, chainId, - time.Unix(int64(info.OffsetTimestamp), 0), - arbmath.SaturatingCast[time.Duration](info.RoundDurationSeconds)*time.Second, + *roundTimingInfo, auctionContractAddr, seqDial, ) @@ -299,9 +304,12 @@ func TestSequencerFeed_ExpressLaneAuction_InnerPayloadNoncesAreRespected(t *test func placeBidsAndDecideWinner(t *testing.T, ctx context.Context, seqClient *ethclient.Client, seqInfo *BlockchainTestInfo, auctionContract *express_lane_auctiongen.ExpressLaneAuction, winner, loser string, winnerBidderClient, loserBidderClient *timeboost.BidderClient, roundDuration time.Duration) { t.Helper() - info, err := auctionContract.RoundTimingInfo(&bind.CallOpts{}) + rawRoundTimingInfo, err := auctionContract.RoundTimingInfo(&bind.CallOpts{}) + Require(t, err) + roundTimingInfo, err := timeboost.NewRoundTimingInfo(rawRoundTimingInfo) Require(t, err) - currRound := timeboost.CurrentRound(time.Unix(int64(info.OffsetTimestamp), 0), roundDuration) + currRound := roundTimingInfo.RoundNumber() + // We are now in the bidding round, both issue their bids. winner will win t.Logf("%s and %s now submitting their bids at %v", winner, loser, time.Now()) winnerBid, err := winnerBidderClient.Bid(ctx, big.NewInt(2), seqInfo.GetAddress(winner)) @@ -397,12 +405,6 @@ func verifyControllerAdvantage(t *testing.T, ctx context.Context, seqClient *eth } } -func waitTillNextRound(roundDuration time.Duration) { - now := time.Now() - waitTime := roundDuration - time.Duration(now.Second())*time.Second - time.Duration(now.Nanosecond()) - time.Sleep(waitTime) -} - func setupExpressLaneAuction( t *testing.T, dbDirPath string, @@ -692,10 +694,12 @@ func setupExpressLaneAuction( Require(t, bob.Deposit(ctx, big.NewInt(30))) // Wait until the next timeboost round + a few milliseconds. - now = time.Now() - waitTime = roundDuration - time.Duration(now.Second())*time.Second - time.Duration(now.Nanosecond()) t.Logf("Alice and Bob are now deposited into the autonomous auction contract, waiting %v for bidding round..., timestamp %v", waitTime, time.Now()) - time.Sleep(waitTime) + rawRoundTimingInfo, err := auctionContract.RoundTimingInfo(&bind.CallOpts{}) + Require(t, err) + roundTimingInfo, err := timeboost.NewRoundTimingInfo(rawRoundTimingInfo) + Require(t, err) + time.Sleep(roundTimingInfo.TimeTilNextRound()) t.Logf("Reached the bidding round at %v", time.Now()) time.Sleep(time.Second * 5) return seqNode, seqClient, seqInfo, proxyAddr, alice, bob, roundDuration, cleanupSeq @@ -746,31 +750,28 @@ func awaitAuctionResolved( type expressLaneClient struct { stopwaiter.StopWaiter sync.Mutex - privKey *ecdsa.PrivateKey - chainId *big.Int - initialRoundTimestamp time.Time - roundDuration time.Duration - auctionContractAddr common.Address - client *rpc.Client - sequence uint64 + privKey *ecdsa.PrivateKey + chainId *big.Int + roundTimingInfo timeboost.RoundTimingInfo + auctionContractAddr common.Address + client *rpc.Client + sequence uint64 } func newExpressLaneClient( privKey *ecdsa.PrivateKey, chainId *big.Int, - initialRoundTimestamp time.Time, - roundDuration time.Duration, + roundTimingInfo timeboost.RoundTimingInfo, auctionContractAddr common.Address, client *rpc.Client, ) *expressLaneClient { return &expressLaneClient{ - privKey: privKey, - chainId: chainId, - initialRoundTimestamp: initialRoundTimestamp, - roundDuration: roundDuration, - auctionContractAddr: auctionContractAddr, - client: client, - sequence: 0, + privKey: privKey, + chainId: chainId, + roundTimingInfo: roundTimingInfo, + auctionContractAddr: auctionContractAddr, + client: client, + sequence: 0, } } @@ -787,7 +788,7 @@ func (elc *expressLaneClient) SendTransaction(ctx context.Context, transaction * } msg := &timeboost.JsonExpressLaneSubmission{ ChainId: (*hexutil.Big)(elc.chainId), - Round: hexutil.Uint64(timeboost.CurrentRound(elc.initialRoundTimestamp, elc.roundDuration)), + Round: hexutil.Uint64(elc.roundTimingInfo.RoundNumber()), AuctionContractAddress: elc.auctionContractAddr, Transaction: encodedTx, SequenceNumber: hexutil.Uint64(elc.sequence), diff --git a/timeboost/auctioneer.go b/timeboost/auctioneer.go index 33bb101455..1c66161252 100644 --- a/timeboost/auctioneer.go +++ b/timeboost/auctioneer.go @@ -29,7 +29,6 @@ import ( "github.com/offchainlabs/nitro/cmd/util" "github.com/offchainlabs/nitro/pubsub" "github.com/offchainlabs/nitro/solgen/go/express_lane_auctiongen" - "github.com/offchainlabs/nitro/util/arbmath" "github.com/offchainlabs/nitro/util/redisutil" "github.com/offchainlabs/nitro/util/stopwaiter" ) @@ -114,9 +113,7 @@ type AuctioneerServer struct { auctionContractAddr common.Address bidsReceiver chan *JsonValidatedBid bidCache *bidCache - initialRoundTimestamp time.Time - auctionClosingDuration time.Duration - roundDuration time.Duration + roundTimingInfo RoundTimingInfo streamTimeout time.Duration auctionResolutionWaitTime time.Duration database *SqliteDatabase @@ -189,17 +186,17 @@ func NewAuctioneerServer(ctx context.Context, configFetcher AuctioneerServerConf if err != nil { return nil, err } - var roundTimingInfo RoundTimingInfo - roundTimingInfo, err = auctionContract.RoundTimingInfo(&bind.CallOpts{}) + rawRoundTimingInfo, err := auctionContract.RoundTimingInfo(&bind.CallOpts{}) if err != nil { return nil, err } - if err = roundTimingInfo.Validate(&cfg.AuctionResolutionWaitTime); err != nil { + roundTimingInfo, err := NewRoundTimingInfo(rawRoundTimingInfo) + if err != nil { + return nil, err + } + if err = roundTimingInfo.ValidateResolutionWaitTime(cfg.AuctionResolutionWaitTime); err != nil { return nil, err } - auctionClosingDuration := arbmath.SaturatingCast[time.Duration](roundTimingInfo.AuctionClosingSeconds) * time.Second - initialTimestamp := time.Unix(roundTimingInfo.OffsetTimestamp, 0) - roundDuration := arbmath.SaturatingCast[time.Duration](roundTimingInfo.RoundDurationSeconds) * time.Second return &AuctioneerServer{ txOpts: txOpts, sequencerRpc: client, @@ -211,9 +208,7 @@ func NewAuctioneerServer(ctx context.Context, configFetcher AuctioneerServerConf auctionContractAddr: auctionContractAddr, bidsReceiver: make(chan *JsonValidatedBid, 100_000), // TODO(Terence): Is 100k enough? Make this configurable? bidCache: newBidCache(), - initialRoundTimestamp: initialTimestamp, - auctionClosingDuration: auctionClosingDuration, - roundDuration: roundDuration, + roundTimingInfo: *roundTimingInfo, auctionResolutionWaitTime: cfg.AuctionResolutionWaitTime, }, nil } @@ -306,8 +301,8 @@ func (a *AuctioneerServer) Start(ctx_in context.Context) { // Auction resolution thread. a.StopWaiter.LaunchThread(func(ctx context.Context) { - ticker := newAuctionCloseTicker(a.roundDuration, a.auctionClosingDuration) - go ticker.start() + ticker := newRoundTicker(a.roundTimingInfo) + go ticker.tickAtAuctionClose() for { select { case <-ctx.Done(): @@ -328,7 +323,7 @@ func (a *AuctioneerServer) Start(ctx_in context.Context) { // Resolves the auction by calling the smart contract with the top two bids. func (a *AuctioneerServer) resolveAuction(ctx context.Context) error { - upcomingRound := CurrentRound(a.initialRoundTimestamp, a.roundDuration) + 1 + upcomingRound := a.roundTimingInfo.RoundNumber() + 1 result := a.bidCache.topTwoBids() first := result.firstPlace second := result.secondPlace @@ -376,8 +371,7 @@ func (a *AuctioneerServer) resolveAuction(ctx context.Context) error { return err } - currentRound := CurrentRound(a.initialRoundTimestamp, a.roundDuration) - roundEndTime := a.initialRoundTimestamp.Add(arbmath.SaturatingCast[time.Duration](currentRound) * a.roundDuration) + roundEndTime := a.roundTimingInfo.TimeOfNextRound() retryInterval := 1 * time.Second if err := retryUntil(ctx, func() error { diff --git a/timeboost/bid_validator.go b/timeboost/bid_validator.go index f99b4c89e3..fda5bcf58d 100644 --- a/timeboost/bid_validator.go +++ b/timeboost/bid_validator.go @@ -21,7 +21,6 @@ import ( "github.com/offchainlabs/nitro/pubsub" "github.com/offchainlabs/nitro/solgen/go/express_lane_auctiongen" - "github.com/offchainlabs/nitro/util/arbmath" "github.com/offchainlabs/nitro/util/redisutil" "github.com/offchainlabs/nitro/util/stopwaiter" ) @@ -71,10 +70,7 @@ type BidValidator struct { auctionContractAddr common.Address auctionContractDomainSeparator [32]byte bidsReceiver chan *Bid - initialRoundTimestamp time.Time - roundDuration time.Duration - auctionClosingDuration time.Duration - reserveSubmissionDuration time.Duration + roundTimingInfo RoundTimingInfo reservePriceLock sync.RWMutex reservePrice *big.Int bidsPerSenderInRound map[common.Address]uint8 @@ -112,18 +108,14 @@ func NewBidValidator( if err != nil { return nil, err } - var roundTimingInfo RoundTimingInfo - roundTimingInfo, err = auctionContract.RoundTimingInfo(&bind.CallOpts{}) + rawRoundTimingInfo, err := auctionContract.RoundTimingInfo(&bind.CallOpts{}) if err != nil { return nil, err } - if err = roundTimingInfo.Validate(nil); err != nil { + roundTimingInfo, err := NewRoundTimingInfo(rawRoundTimingInfo) + if err != nil { return nil, err } - initialTimestamp := time.Unix(int64(roundTimingInfo.OffsetTimestamp), 0) - roundDuration := arbmath.SaturatingCast[time.Duration](roundTimingInfo.RoundDurationSeconds) * time.Second - auctionClosingDuration := arbmath.SaturatingCast[time.Duration](roundTimingInfo.AuctionClosingSeconds) * time.Second - reserveSubmissionDuration := arbmath.SaturatingCast[time.Duration](roundTimingInfo.ReserveSubmissionSeconds) * time.Second reservePrice, err := auctionContract.ReservePrice(&bind.CallOpts{}) if err != nil { @@ -146,10 +138,7 @@ func NewBidValidator( auctionContractAddr: auctionContractAddr, auctionContractDomainSeparator: domainSeparator, bidsReceiver: make(chan *Bid, 10_000), - initialRoundTimestamp: initialTimestamp, - roundDuration: roundDuration, - auctionClosingDuration: auctionClosingDuration, - reserveSubmissionDuration: reserveSubmissionDuration, + roundTimingInfo: *roundTimingInfo, reservePrice: reservePrice, domainValue: domainValue, bidsPerSenderInRound: make(map[common.Address]uint8), @@ -207,10 +196,10 @@ func (bv *BidValidator) Start(ctx_in context.Context) { // Thread to set reserve price and clear per-round map of bid count per account. bv.StopWaiter.LaunchThread(func(ctx context.Context) { - reservePriceTicker := newAuctionCloseTicker(bv.roundDuration, bv.auctionClosingDuration+bv.reserveSubmissionDuration) - go reservePriceTicker.start() - auctionCloseTicker := newAuctionCloseTicker(bv.roundDuration, bv.auctionClosingDuration) - go auctionCloseTicker.start() + reservePriceTicker := newRoundTicker(bv.roundTimingInfo) + go reservePriceTicker.tickAtReserveSubmissionDeadline() + auctionCloseTicker := newRoundTicker(bv.roundTimingInfo) + go auctionCloseTicker.tickAtAuctionClose() for { select { @@ -306,18 +295,13 @@ func (bv *BidValidator) validateBid( } // Check if the bid is intended for upcoming round. - upcomingRound := CurrentRound(bv.initialRoundTimestamp, bv.roundDuration) + 1 + upcomingRound := bv.roundTimingInfo.RoundNumber() + 1 if bid.Round != upcomingRound { return nil, errors.Wrapf(ErrBadRoundNumber, "wanted %d, got %d", upcomingRound, bid.Round) } // Check if the auction is closed. - if isAuctionRoundClosed( - time.Now(), - bv.initialRoundTimestamp, - bv.roundDuration, - bv.auctionClosingDuration, - ) { + if bv.roundTimingInfo.isAuctionRoundClosed() { return nil, errors.Wrap(ErrBadRoundNumber, "auction is closed") } diff --git a/timeboost/bid_validator_test.go b/timeboost/bid_validator_test.go index 2d8c0b9918..80ddc481a5 100644 --- a/timeboost/bid_validator_test.go +++ b/timeboost/bid_validator_test.go @@ -101,11 +101,13 @@ func TestBidValidator_validateBid(t *testing.T) { for _, tt := range tests { bv := BidValidator{ - chainId: big.NewInt(1), - initialRoundTimestamp: time.Now().Add(-time.Second * 3), + chainId: big.NewInt(1), + roundTimingInfo: RoundTimingInfo{ + Offset: time.Now().Add(-time.Second * 3), + Round: 10 * time.Second, + AuctionClosing: 5 * time.Second, + }, reservePrice: big.NewInt(2), - roundDuration: 10 * time.Second, - auctionClosingDuration: 5 * time.Second, auctionContract: setup.expressLaneAuction, auctionContractAddr: setup.expressLaneAuctionAddr, bidsPerSenderInRound: make(map[common.Address]uint8), @@ -129,11 +131,13 @@ func TestBidValidator_validateBid_perRoundBidLimitReached(t *testing.T) { } auctionContractAddr := common.Address{'a'} bv := BidValidator{ - chainId: big.NewInt(1), - initialRoundTimestamp: time.Now().Add(-time.Second), + chainId: big.NewInt(1), + roundTimingInfo: RoundTimingInfo{ + Offset: time.Now().Add(-time.Second), + Round: time.Minute, + AuctionClosing: 45 * time.Second, + }, reservePrice: big.NewInt(2), - roundDuration: time.Minute, - auctionClosingDuration: 45 * time.Second, bidsPerSenderInRound: make(map[common.Address]uint8), maxBidsPerSenderInRound: 5, auctionContractAddr: auctionContractAddr, diff --git a/timeboost/bidder_client.go b/timeboost/bidder_client.go index db64d8b784..66c69991f4 100644 --- a/timeboost/bidder_client.go +++ b/timeboost/bidder_client.go @@ -4,7 +4,6 @@ import ( "context" "fmt" "math/big" - "time" "github.com/pkg/errors" "github.com/spf13/pflag" @@ -20,7 +19,6 @@ import ( "github.com/offchainlabs/nitro/cmd/util" "github.com/offchainlabs/nitro/solgen/go/express_lane_auctiongen" "github.com/offchainlabs/nitro/timeboost/bindings" - "github.com/offchainlabs/nitro/util/arbmath" "github.com/offchainlabs/nitro/util/containers" "github.com/offchainlabs/nitro/util/signature" "github.com/offchainlabs/nitro/util/stopwaiter" @@ -67,8 +65,7 @@ type BidderClient struct { auctionContract *express_lane_auctiongen.ExpressLaneAuction biddingTokenContract *bindings.MockERC20 auctioneerClient *rpc.Client - initialRoundTimestamp time.Time - roundDuration time.Duration + roundTimingInfo RoundTimingInfo domainValue []byte } @@ -96,18 +93,16 @@ func NewBidderClient( if err != nil { return nil, err } - var roundTimingInfo RoundTimingInfo - roundTimingInfo, err = auctionContract.RoundTimingInfo(&bind.CallOpts{ + rawRoundTimingInfo, err := auctionContract.RoundTimingInfo(&bind.CallOpts{ Context: ctx, }) if err != nil { return nil, err } - if err = roundTimingInfo.Validate(nil); err != nil { + roundTimingInfo, err := NewRoundTimingInfo(rawRoundTimingInfo) + if err != nil { return nil, err } - initialTimestamp := time.Unix(int64(roundTimingInfo.OffsetTimestamp), 0) - roundDuration := arbmath.SaturatingCast[time.Duration](roundTimingInfo.RoundDurationSeconds) * time.Second txOpts, signer, err := util.OpenWallet("bidder-client", &cfg.Wallet, chainId) if err != nil { return nil, errors.Wrap(err, "opening wallet") @@ -138,8 +133,7 @@ func NewBidderClient( auctionContract: auctionContract, biddingTokenContract: biddingTokenContract, auctioneerClient: bidValidatorClient, - initialRoundTimestamp: initialTimestamp, - roundDuration: roundDuration, + roundTimingInfo: *roundTimingInfo, domainValue: domainValue, }, nil } @@ -205,7 +199,7 @@ func (bd *BidderClient) Bid( ChainId: bd.chainId, ExpressLaneController: expressLaneController, AuctionContractAddress: bd.auctionContractAddress, - Round: CurrentRound(bd.initialRoundTimestamp, bd.roundDuration) + 1, + Round: bd.roundTimingInfo.RoundNumber() + 1, Amount: amount, } bidHash, err := newBid.ToEIP712Hash(domainSeparator) diff --git a/timeboost/roundtiminginfo.go b/timeboost/roundtiminginfo.go index 74ceab4364..d511f116c6 100644 --- a/timeboost/roundtiminginfo.go +++ b/timeboost/roundtiminginfo.go @@ -7,21 +7,13 @@ import ( "fmt" "time" + "github.com/offchainlabs/nitro/solgen/go/express_lane_auctiongen" "github.com/offchainlabs/nitro/util/arbmath" ) -// Solgen solidity bindings don't give names to return structs, give it a name for convenience. -type RoundTimingInfo struct { - OffsetTimestamp int64 - RoundDurationSeconds uint64 - AuctionClosingSeconds uint64 - ReserveSubmissionSeconds uint64 -} - -// Validate the RoundTimingInfo fields. -// resolutionWaitTime is an additional parameter passed into the auctioneer that it -// needs to validate against the other fields. -func (c *RoundTimingInfo) Validate(resolutionWaitTime *time.Duration) error { +// Validate the express_lane_auctiongen.RoundTimingInfo fields. +// Returns errors in terms of the solidity field names to ease debugging. +func validateRoundTimingInfo(c *express_lane_auctiongen.RoundTimingInfo) error { roundDuration := arbmath.SaturatingCast[time.Duration](c.RoundDurationSeconds) * time.Second auctionClosing := arbmath.SaturatingCast[time.Duration](c.AuctionClosingSeconds) * time.Second reserveSubmission := arbmath.SaturatingCast[time.Duration](c.ReserveSubmissionSeconds) * time.Second @@ -49,14 +41,93 @@ func (c *RoundTimingInfo) Validate(resolutionWaitTime *time.Duration) error { combinedClosingTime/time.Second) } - // Validate resolution wait time if provided - if resolutionWaitTime != nil { - // Resolution wait time shouldn't be more than 50% of auction closing time - if *resolutionWaitTime > auctionClosing/2 { - return fmt.Errorf("resolution wait time (%v) must not exceed 50%% of auction closing time (%v)", - *resolutionWaitTime, auctionClosing) - } + return nil +} + +// RoundTimingInfo holds the information from the Solidity type of the same name, +// validated and converted into higher level time types, with helpful methods +// for calculating round number, if a round is closed, and time til close. +type RoundTimingInfo struct { + Offset time.Time + Round time.Duration + AuctionClosing time.Duration + ReserveSubmission time.Duration +} + +// Convert from solgen bindings to domain type +func NewRoundTimingInfo(c express_lane_auctiongen.RoundTimingInfo) (*RoundTimingInfo, error) { + if err := validateRoundTimingInfo(&c); err != nil { + return nil, err } + return &RoundTimingInfo{ + Offset: time.Unix(c.OffsetTimestamp, 0), + Round: arbmath.SaturatingCast[time.Duration](c.RoundDurationSeconds) * time.Second, + AuctionClosing: arbmath.SaturatingCast[time.Duration](c.AuctionClosingSeconds) * time.Second, + ReserveSubmission: arbmath.SaturatingCast[time.Duration](c.ReserveSubmissionSeconds) * time.Second, + }, nil +} + +// resolutionWaitTime is an additional parameter that the Auctioneer +// needs to validate against other timing fields. +func (info *RoundTimingInfo) ValidateResolutionWaitTime(resolutionWaitTime time.Duration) error { + // Resolution wait time shouldn't be more than 50% of auction closing time + if resolutionWaitTime > info.AuctionClosing/2 { + return fmt.Errorf("resolution wait time (%v) must not exceed 50%% of auction closing time (%v)", + resolutionWaitTime, info.AuctionClosing) + } return nil } + +// RoundNumber returns the round number as of now. +func (info *RoundTimingInfo) RoundNumber() uint64 { + return info.RoundNumberAt(time.Now()) +} + +// RoundNumberAt returns the round number as of some timestamp. +func (info *RoundTimingInfo) RoundNumberAt(currentTime time.Time) uint64 { + return arbmath.SaturatingUCast[uint64](currentTime.Sub(info.Offset) / info.Round) + // info.Round has already been validated to be nonzero during construction. +} + +// TimeTilNextRound returns the time til the next round as of now. +func (info *RoundTimingInfo) TimeTilNextRound() time.Duration { + return info.TimeTilNextRoundAt(time.Now()) +} + +// TimeTilNextRoundAt returns the time til the next round, +// where the next round is determined from the timestamp passed in. +func (info *RoundTimingInfo) TimeTilNextRoundAt(currentTime time.Time) time.Duration { + return time.Until(info.TimeOfNextRoundAt(currentTime)) +} + +func (info *RoundTimingInfo) TimeOfNextRound() time.Time { + return info.TimeOfNextRoundAt(time.Now()) +} + +func (info *RoundTimingInfo) TimeOfNextRoundAt(currentTime time.Time) time.Time { + roundNum := info.RoundNumberAt(currentTime) + return info.Offset.Add(info.Round * arbmath.SaturatingCast[time.Duration](roundNum+1)) +} + +func (info *RoundTimingInfo) durationIntoRound(timestamp time.Time) time.Duration { + secondsSinceOffset := uint64(timestamp.Sub(info.Offset).Seconds()) + roundDurationSeconds := uint64(info.Round.Seconds()) + return arbmath.SaturatingCast[time.Duration](secondsSinceOffset % roundDurationSeconds) +} + +func (info *RoundTimingInfo) isAuctionRoundClosed() bool { + return info.isAuctionRoundClosedAt(time.Now()) +} + +func (info *RoundTimingInfo) isAuctionRoundClosedAt(currentTime time.Time) bool { + if currentTime.Before(info.Offset) { + return false + } + + return info.durationIntoRound(currentTime)*time.Second >= info.Round-info.AuctionClosing +} + +func (info *RoundTimingInfo) IsWithinAuctionCloseWindow(timestamp time.Time) bool { + return info.TimeTilNextRoundAt(timestamp) <= info.AuctionClosing +} diff --git a/timeboost/ticker.go b/timeboost/ticker.go index 12bd728de9..f9bfc18ed4 100644 --- a/timeboost/ticker.go +++ b/timeboost/ticker.go @@ -2,43 +2,39 @@ package timeboost import ( "time" - - "github.com/offchainlabs/nitro/util/arbmath" ) -type auctionCloseTicker struct { - c chan time.Time - done chan bool - roundDuration time.Duration - auctionClosingDuration time.Duration +type roundTicker struct { + c chan time.Time + done chan bool + roundTimingInfo RoundTimingInfo } -func newAuctionCloseTicker(roundDuration, auctionClosingDuration time.Duration) *auctionCloseTicker { - return &auctionCloseTicker{ - c: make(chan time.Time, 1), - done: make(chan bool), - roundDuration: roundDuration, - auctionClosingDuration: auctionClosingDuration, +func newRoundTicker(roundTimingInfo RoundTimingInfo) *roundTicker { + return &roundTicker{ + c: make(chan time.Time, 1), + done: make(chan bool), + roundTimingInfo: roundTimingInfo, } } -func (t *auctionCloseTicker) start() { +func (t *roundTicker) tickAtAuctionClose() { + t.start(t.roundTimingInfo.AuctionClosing) +} + +func (t *roundTicker) tickAtReserveSubmissionDeadline() { + t.start(t.roundTimingInfo.AuctionClosing + t.roundTimingInfo.ReserveSubmission) +} + +func (t *roundTicker) start(timeBeforeRoundStart time.Duration) { for { - now := time.Now() - // Calculate the start of the next round - startOfNextRound := now.Truncate(t.roundDuration).Add(t.roundDuration) - // Subtract AUCTION_CLOSING_SECONDS seconds to get the tick time - nextTickTime := startOfNextRound.Add(-t.auctionClosingDuration) - // Ensure we are not setting a past tick time - if nextTickTime.Before(now) { - // If the calculated tick time is in the past, move to the next interval - nextTickTime = nextTickTime.Add(t.roundDuration) + nextTick := t.roundTimingInfo.TimeTilNextRound() - timeBeforeRoundStart + if nextTick < 0 { + nextTick += t.roundTimingInfo.Round } - // Calculate how long to wait until the next tick - waitTime := nextTickTime.Sub(now) select { - case <-time.After(waitTime): + case <-time.After(nextTick): t.c <- time.Now() case <-t.done: close(t.c) @@ -46,54 +42,3 @@ func (t *auctionCloseTicker) start() { } } } - -// CurrentRound returns the current round number. -func CurrentRound(initialRoundTimestamp time.Time, roundDuration time.Duration) uint64 { - return RoundAtTimestamp(initialRoundTimestamp, time.Now(), roundDuration) -} - -// CurrentRound returns the round number as of some timestamp. -func RoundAtTimestamp(initialRoundTimestamp time.Time, currentTime time.Time, roundDuration time.Duration) uint64 { - if roundDuration == 0 { - return 0 - } - return arbmath.SaturatingUCast[uint64](currentTime.Sub(initialRoundTimestamp) / roundDuration) -} - -func isAuctionRoundClosed( - timestamp time.Time, - initialTimestamp time.Time, - roundDuration time.Duration, - auctionClosingDuration time.Duration, -) bool { - if timestamp.Before(initialTimestamp) { - return false - } - timeInRound := timeIntoRound(timestamp, initialTimestamp, roundDuration) - return arbmath.SaturatingCast[time.Duration](timeInRound)*time.Second >= roundDuration-auctionClosingDuration -} - -func timeIntoRound( - timestamp time.Time, - initialTimestamp time.Time, - roundDuration time.Duration, -) uint64 { - secondsSinceOffset := uint64(timestamp.Sub(initialTimestamp).Seconds()) - roundDurationSeconds := uint64(roundDuration.Seconds()) - return secondsSinceOffset % roundDurationSeconds -} - -func TimeTilNextRound( - initialTimestamp time.Time, - roundDuration time.Duration) time.Duration { - return TimeTilNextRoundAfterTimestamp(initialTimestamp, time.Now(), roundDuration) -} - -func TimeTilNextRoundAfterTimestamp( - initialTimestamp time.Time, - currentTime time.Time, - roundDuration time.Duration) time.Duration { - currentRoundNum := RoundAtTimestamp(initialTimestamp, currentTime, roundDuration) - nextRoundStart := initialTimestamp.Add(roundDuration * arbmath.SaturatingCast[time.Duration](currentRoundNum+1)) - return time.Until(nextRoundStart) -} diff --git a/timeboost/ticker_test.go b/timeboost/ticker_test.go index b1ee996bc0..f284ba56ae 100644 --- a/timeboost/ticker_test.go +++ b/timeboost/ticker_test.go @@ -9,32 +9,34 @@ import ( func Test_auctionClosed(t *testing.T) { t.Parallel() - roundDuration := time.Minute - auctionClosingDuration := time.Second * 15 - now := time.Now() - waitTime := roundDuration - time.Duration(now.Second())*time.Second - time.Duration(now.Nanosecond()) - initialTimestamp := now.Add(waitTime) + roundTimingInfo := RoundTimingInfo{ + Offset: time.Now(), + Round: time.Minute, + AuctionClosing: time.Second * 15, + } + + initialTimestamp := time.Now() // We should not have closed the round yet, and the time into the round should be less than a second. - isClosed := isAuctionRoundClosed(initialTimestamp, initialTimestamp, roundDuration, auctionClosingDuration) + isClosed := roundTimingInfo.isAuctionRoundClosedAt(initialTimestamp) require.False(t, isClosed) // Wait right before auction closure (before the 45 second mark). - timestamp := initialTimestamp.Add((roundDuration - auctionClosingDuration) - time.Second) - isClosed = isAuctionRoundClosed(timestamp, initialTimestamp, roundDuration, auctionClosingDuration) + timestamp := initialTimestamp.Add((roundTimingInfo.Round - roundTimingInfo.AuctionClosing) - time.Second) + isClosed = roundTimingInfo.isAuctionRoundClosedAt(timestamp) require.False(t, isClosed) // Wait a second more and the auction should be closed. - timestamp = initialTimestamp.Add(roundDuration - auctionClosingDuration) - isClosed = isAuctionRoundClosed(timestamp, initialTimestamp, roundDuration, auctionClosingDuration) + timestamp = initialTimestamp.Add(roundTimingInfo.Round - roundTimingInfo.AuctionClosing) + isClosed = roundTimingInfo.isAuctionRoundClosedAt(timestamp) require.True(t, isClosed) // Future timestamp should also be closed, until we reach the new round - for i := float64(0); i < auctionClosingDuration.Seconds(); i++ { - timestamp = initialTimestamp.Add((roundDuration - auctionClosingDuration) + time.Second*time.Duration(i)) - isClosed = isAuctionRoundClosed(timestamp, initialTimestamp, roundDuration, auctionClosingDuration) + for i := float64(0); i < roundTimingInfo.AuctionClosing.Seconds(); i++ { + timestamp = initialTimestamp.Add((roundTimingInfo.Round - roundTimingInfo.AuctionClosing) + time.Second*time.Duration(i)) + isClosed = roundTimingInfo.isAuctionRoundClosedAt(timestamp) require.True(t, isClosed) } - isClosed = isAuctionRoundClosed(initialTimestamp.Add(roundDuration), initialTimestamp, roundDuration, auctionClosingDuration) + isClosed = roundTimingInfo.isAuctionRoundClosedAt(initialTimestamp.Add(roundTimingInfo.Round)) require.False(t, isClosed) }