From a8a6369df34ef8269669b49922a795ad07374fe1 Mon Sep 17 00:00:00 2001 From: Tristan Wilson Date: Wed, 13 Nov 2024 11:50:44 +0100 Subject: [PATCH 1/3] Add early timeboost submission grace period This allows the winner of the next round to submit their txs for that round during a short grace period at the end of the current round (2s by default). The call to timeboost_sendExpressLaneTransaction will not return, and the tx will not be sequenced, until the next round starts. This allows winners of the auction to take advantage of the full round without having to spam transactions around the round tickover time to ensure their transaction is included as soon as possible. --- cmd/nitro/nitro.go | 4 +- execution/gethexec/express_lane_service.go | 10 +++- .../gethexec/express_lane_service_test.go | 56 +++++++++++++++++-- execution/gethexec/sequencer.go | 13 ++++- system_tests/timeboost_test.go | 2 +- 5 files changed, 75 insertions(+), 10 deletions(-) diff --git a/cmd/nitro/nitro.go b/cmd/nitro/nitro.go index 228868db4a..f60979a8d3 100644 --- a/cmd/nitro/nitro.go +++ b/cmd/nitro/nitro.go @@ -661,7 +661,9 @@ func mainImpl() int { execNode.Sequencer.StartExpressLane( ctx, common.HexToAddress(execNodeConfig.Sequencer.Timeboost.AuctionContractAddress), - common.HexToAddress(execNodeConfig.Sequencer.Timeboost.AuctioneerAddress)) + common.HexToAddress(execNodeConfig.Sequencer.Timeboost.AuctioneerAddress), + execNodeConfig.Sequencer.Timeboost.EarlySubmissionGrace, + ) } err = nil diff --git a/execution/gethexec/express_lane_service.go b/execution/gethexec/express_lane_service.go index 33b7c7f18a..1a72863346 100644 --- a/execution/gethexec/express_lane_service.go +++ b/execution/gethexec/express_lane_service.go @@ -43,6 +43,7 @@ type expressLaneService struct { initialTimestamp time.Time roundDuration time.Duration auctionClosing time.Duration + earlySubmissionGrace time.Duration chainConfig *params.ChainConfig logs chan []*types.Log seqClient *ethclient.Client @@ -56,6 +57,7 @@ func newExpressLaneService( auctionContractAddr common.Address, sequencerClient *ethclient.Client, bc *core.BlockChain, + earlySubmissionGrace time.Duration, ) (*expressLaneService, error) { chainConfig := bc.Config() auctionContract, err := express_lane_auctiongen.NewExpressLaneAuction(auctionContractAddr, sequencerClient) @@ -90,6 +92,7 @@ pending: chainConfig: chainConfig, initialTimestamp: initialTimestamp, auctionClosing: auctionClosingDuration, + earlySubmissionGrace: earlySubmissionGrace, roundControl: lru.NewCache[uint64, *expressLaneControl](8), // Keep 8 rounds cached. auctionContractAddr: auctionContractAddr, roundDuration: roundDuration, @@ -295,7 +298,12 @@ func (es *expressLaneService) validateExpressLaneTx(msg *timeboost.ExpressLaneSu } currentRound := timeboost.CurrentRound(es.initialTimestamp, es.roundDuration) if msg.Round != currentRound { - return errors.Wrapf(timeboost.ErrBadRoundNumber, "express lane tx round %d does not match current round %d", msg.Round, currentRound) + if msg.Round == currentRound+1 && + timeboost.TimeTilNextRound(es.initialTimestamp, es.roundDuration) <= es.earlySubmissionGrace { + time.Sleep(timeboost.TimeTilNextRound(es.initialTimestamp, es.roundDuration)) + } else { + return errors.Wrapf(timeboost.ErrBadRoundNumber, "express lane tx round %d does not match current round %d", msg.Round, currentRound) + } } if !es.currentRoundHasController() { return timeboost.ErrNoOnchainController diff --git a/execution/gethexec/express_lane_service_test.go b/execution/gethexec/express_lane_service_test.go index 0c4116046f..70c05f48f5 100644 --- a/execution/gethexec/express_lane_service_test.go +++ b/execution/gethexec/express_lane_service_test.go @@ -22,7 +22,7 @@ import ( "github.com/stretchr/testify/require" ) -var testPriv *ecdsa.PrivateKey +var testPriv, testPriv2 *ecdsa.PrivateKey func init() { privKey, err := crypto.HexToECDSA("93be75cc4df7acbb636b6abe6de2c0446235ac1dc7da9f290a70d83f088b486d") @@ -30,6 +30,11 @@ func init() { panic(err) } testPriv = privKey + privKey2, err := crypto.HexToECDSA("93be75cc4df7acbb636b6abe6de2c0446235ac1dc7da9f290a70d83f088b486e") + if err != nil { + panic(err) + } + testPriv2 = privKey2 } func Test_expressLaneService_validateExpressLaneTx(t *testing.T) { @@ -193,7 +198,7 @@ func Test_expressLaneService_validateExpressLaneTx(t *testing.T) { control: expressLaneControl{ controller: common.Address{'b'}, }, - sub: buildValidSubmission(t, common.HexToAddress("0x2Aef36410182881a4b13664a1E079762D7F716e6"), testPriv), + sub: buildValidSubmission(t, common.HexToAddress("0x2Aef36410182881a4b13664a1E079762D7F716e6"), testPriv, 0), expectedErr: timeboost.ErrNotExpressLaneController, }, { @@ -210,7 +215,7 @@ func Test_expressLaneService_validateExpressLaneTx(t *testing.T) { control: expressLaneControl{ controller: crypto.PubkeyToAddress(testPriv.PublicKey), }, - sub: buildValidSubmission(t, common.HexToAddress("0x2Aef36410182881a4b13664a1E079762D7F716e6"), testPriv), + sub: buildValidSubmission(t, common.HexToAddress("0x2Aef36410182881a4b13664a1E079762D7F716e6"), testPriv, 0), valid: true, }, } @@ -231,6 +236,46 @@ 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, + earlySubmissionGrace: time.Second * 2, + chainConfig: ¶ms.ChainConfig{ + ChainID: big.NewInt(1), + }, + roundControl: lru.NewCache[uint64, *expressLaneControl](8), + } + es.roundControl.Add(0, &expressLaneControl{ + controller: crypto.PubkeyToAddress(testPriv.PublicKey), + }) + es.roundControl.Add(1, &expressLaneControl{ + controller: crypto.PubkeyToAddress(testPriv2.PublicKey), + }) + + sub1 := buildValidSubmission(t, auctionContractAddr, testPriv, 0) + err := es.validateExpressLaneTx(sub1) + require.NoError(t, err) + + // Send req for next round + sub2 := buildValidSubmission(t, auctionContractAddr, testPriv2, 1) + err = es.validateExpressLaneTx(sub2) + require.ErrorIs(t, err, timeboost.ErrBadRoundNumber) + + // Sleep til 2 seconds before grace + time.Sleep(time.Second * 6) + err = es.validateExpressLaneTx(sub2) + require.ErrorIs(t, err, timeboost.ErrBadRoundNumber) + + // Send req for next round within grace period + time.Sleep(time.Second * 2) + err = es.validateExpressLaneTx(sub2) + require.NoError(t, err) +} + type stubPublisher struct { publishFn func(parentCtx context.Context, tx *types.Transaction, options *arbitrum_types.ConditionalOptions, delay bool) error } @@ -461,7 +506,7 @@ func Benchmark_expressLaneService_validateExpressLaneTx(b *testing.B) { sequence: 1, controller: addr, }) - sub := buildValidSubmission(b, common.HexToAddress("0x2Aef36410182881a4b13664a1E079762D7F716e6"), testPriv) + sub := buildValidSubmission(b, common.HexToAddress("0x2Aef36410182881a4b13664a1E079762D7F716e6"), testPriv, 0) b.StartTimer() for i := 0; i < b.N; i++ { err := es.validateExpressLaneTx(sub) @@ -510,13 +555,14 @@ func buildValidSubmission( t testing.TB, auctionContractAddr common.Address, privKey *ecdsa.PrivateKey, + round uint64, ) *timeboost.ExpressLaneSubmission { b := &timeboost.ExpressLaneSubmission{ ChainId: big.NewInt(1), AuctionContractAddress: auctionContractAddr, Transaction: types.NewTransaction(0, common.Address{}, big.NewInt(0), 0, big.NewInt(0), nil), Signature: make([]byte, 65), - Round: 0, + Round: round, } data, err := b.ToMessageBytes() require.NoError(t, err) diff --git a/execution/gethexec/sequencer.go b/execution/gethexec/sequencer.go index b46a959f07..a3d50ae44a 100644 --- a/execution/gethexec/sequencer.go +++ b/execution/gethexec/sequencer.go @@ -90,6 +90,7 @@ type TimeboostConfig struct { AuctioneerAddress string `koanf:"auctioneer-address"` ExpressLaneAdvantage time.Duration `koanf:"express-lane-advantage"` SequencerHTTPEndpoint string `koanf:"sequencer-http-endpoint"` + EarlySubmissionGrace time.Duration `koanf:"early-submission-grace"` } var DefaultTimeboostConfig = TimeboostConfig{ @@ -98,6 +99,7 @@ var DefaultTimeboostConfig = TimeboostConfig{ AuctioneerAddress: "", ExpressLaneAdvantage: time.Millisecond * 200, SequencerHTTPEndpoint: "http://localhost:8547", + EarlySubmissionGrace: time.Second * 2, } func (c *SequencerConfig) Validate() error { @@ -191,6 +193,7 @@ func TimeboostAddOptions(prefix string, f *flag.FlagSet) { f.String(prefix+".auctioneer-address", DefaultTimeboostConfig.AuctioneerAddress, "Address of the Timeboost Autonomous Auctioneer") f.Duration(prefix+".express-lane-advantage", DefaultTimeboostConfig.ExpressLaneAdvantage, "specify the express lane advantage") f.String(prefix+".sequencer-http-endpoint", DefaultTimeboostConfig.SequencerHTTPEndpoint, "this sequencer's http endpoint") + f.Duration(prefix+".early-submission-grace", DefaultTimeboostConfig.EarlySubmissionGrace, "period of time before the next round where submissions for the next round will be queued") } type txQueueItem struct { @@ -482,7 +485,7 @@ func (s *Sequencer) publishTransactionImpl(parentCtx context.Context, tx *types. } if s.config().Timeboost.Enable && s.expressLaneService != nil { - if isExpressLaneController && s.expressLaneService.currentRoundHasController() { + if !isExpressLaneController && s.expressLaneService.currentRoundHasController() { time.Sleep(s.config().Timeboost.ExpressLaneAdvantage) } } @@ -1242,7 +1245,12 @@ func (s *Sequencer) Start(ctxIn context.Context) error { return nil } -func (s *Sequencer) StartExpressLane(ctx context.Context, auctionContractAddr common.Address, auctioneerAddr common.Address) { +func (s *Sequencer) StartExpressLane( + ctx context.Context, + auctionContractAddr common.Address, + auctioneerAddr common.Address, + earlySubmissionGrace time.Duration, +) { if !s.config().Timeboost.Enable { log.Crit("Timeboost is not enabled, but StartExpressLane was called") } @@ -1257,6 +1265,7 @@ func (s *Sequencer) StartExpressLane(ctx context.Context, auctionContractAddr co auctionContractAddr, seqClient, s.execEngine.bc, + earlySubmissionGrace, ) if err != nil { log.Crit("Failed to create express lane service", "err", err, "auctionContractAddr", auctionContractAddr) diff --git a/system_tests/timeboost_test.go b/system_tests/timeboost_test.go index 2b8db0a9c9..9552eb0c61 100644 --- a/system_tests/timeboost_test.go +++ b/system_tests/timeboost_test.go @@ -414,7 +414,7 @@ func setupExpressLaneAuction( // This is hacky- we are manually starting the ExpressLaneService here instead of letting it be started // by the sequencer. This is due to needing to deploy the auction contract first. builderSeq.execConfig.Sequencer.Timeboost.Enable = true - builderSeq.L2.ExecNode.Sequencer.StartExpressLane(ctx, proxyAddr, seqInfo.GetAddress("AuctionContract")) + builderSeq.L2.ExecNode.Sequencer.StartExpressLane(ctx, proxyAddr, seqInfo.GetAddress("AuctionContract"), gethexec.DefaultTimeboostConfig.EarlySubmissionGrace) t.Log("Started express lane service in sequencer") // Set up an autonomous auction contract service that runs in the background in this test. From 2bea5ca021b1b171cee33a34745700d21c14c34a Mon Sep 17 00:00:00 2001 From: Tristan Wilson Date: Wed, 20 Nov 2024 14:57:18 -0800 Subject: [PATCH 2/3] Loop for round check --- execution/gethexec/express_lane_service.go | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/execution/gethexec/express_lane_service.go b/execution/gethexec/express_lane_service.go index 26b2389689..8d794b6002 100644 --- a/execution/gethexec/express_lane_service.go +++ b/execution/gethexec/express_lane_service.go @@ -369,8 +369,13 @@ func (es *expressLaneService) validateExpressLaneTx(msg *timeboost.ExpressLaneSu if msg.AuctionContractAddress != es.auctionContractAddr { return errors.Wrapf(timeboost.ErrWrongAuctionContract, "msg auction contract address %s does not match sequencer auction contract address %s", msg.AuctionContractAddress, es.auctionContractAddr) } - currentRound := timeboost.CurrentRound(es.initialTimestamp, es.roundDuration) - if msg.Round != currentRound { + + for { + currentRound := timeboost.CurrentRound(es.initialTimestamp, es.roundDuration) + if msg.Round == currentRound { + break + } + if msg.Round == currentRound+1 && timeboost.TimeTilNextRound(es.initialTimestamp, es.roundDuration) <= es.earlySubmissionGrace { time.Sleep(timeboost.TimeTilNextRound(es.initialTimestamp, es.roundDuration)) From bcabeaadeb0a7a07dd1b1ade56a1458ddabc12c0 Mon Sep 17 00:00:00 2001 From: Tristan Wilson Date: Thu, 21 Nov 2024 11:19:46 -0800 Subject: [PATCH 3/3] Fix race condition around checking current round --- execution/gethexec/express_lane_service.go | 8 ++++++-- timeboost/ticker.go | 16 ++++++++++++++-- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/execution/gethexec/express_lane_service.go b/execution/gethexec/express_lane_service.go index 91e12d43a9..8eed250022 100644 --- a/execution/gethexec/express_lane_service.go +++ b/execution/gethexec/express_lane_service.go @@ -372,6 +372,7 @@ func (es *expressLaneService) validateExpressLaneTx(msg *timeboost.ExpressLaneSu return errors.Wrapf(timeboost.ErrWrongAuctionContract, "msg auction contract address %s does not match sequencer auction contract address %s", msg.AuctionContractAddress, es.auctionContractAddr) } + currentTime := time.Now() for { currentRound := timeboost.CurrentRound(es.initialTimestamp, es.roundDuration) if msg.Round == currentRound { @@ -379,8 +380,11 @@ func (es *expressLaneService) validateExpressLaneTx(msg *timeboost.ExpressLaneSu } if msg.Round == currentRound+1 && - timeboost.TimeTilNextRound(es.initialTimestamp, es.roundDuration) <= es.earlySubmissionGrace { - time.Sleep(timeboost.TimeTilNextRound(es.initialTimestamp, es.roundDuration)) + 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)) } else { return errors.Wrapf(timeboost.ErrBadRoundNumber, "express lane tx round %d does not match current round %d", msg.Round, currentRound) } diff --git a/timeboost/ticker.go b/timeboost/ticker.go index fa8d14c9dd..12bd728de9 100644 --- a/timeboost/ticker.go +++ b/timeboost/ticker.go @@ -49,10 +49,15 @@ 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](time.Since(initialRoundTimestamp) / roundDuration) + return arbmath.SaturatingUCast[uint64](currentTime.Sub(initialRoundTimestamp) / roundDuration) } func isAuctionRoundClosed( @@ -81,7 +86,14 @@ func timeIntoRound( func TimeTilNextRound( initialTimestamp time.Time, roundDuration time.Duration) time.Duration { - currentRoundNum := CurrentRound(initialTimestamp, roundDuration) + 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) }