Skip to content

Commit

Permalink
Merge pull request #2841 from OffchainLabs/express-lane-timeboost-ref…
Browse files Browse the repository at this point in the history
…actor-fix-time

Refactor RoundTimingInfo, fix auctionCloseTicker
  • Loading branch information
Tristan-Wilson authored Dec 18, 2024
2 parents 89c0f53 + 3f3aba0 commit bbc703a
Show file tree
Hide file tree
Showing 11 changed files with 242 additions and 264 deletions.
55 changes: 18 additions & 37 deletions execution/gethexec/express_lane_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)

Expand All @@ -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
Expand Down Expand Up @@ -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 {
Expand All @@ -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
Expand All @@ -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():
Expand All @@ -193,15 +186,17 @@ 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 {
select {
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,
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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)
}
Expand Down
52 changes: 26 additions & 26 deletions execution/gethexec/express_lane_service_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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: &params.ChainConfig{
ChainID: big.NewInt(1),
},
Expand All @@ -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: &params.ChainConfig{
ChainID: big.NewInt(1),
},
Expand All @@ -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: &params.ChainConfig{
ChainID: big.NewInt(1),
},
Expand All @@ -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: &params.ChainConfig{
ChainID: big.NewInt(1),
},
Expand All @@ -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: &params.ChainConfig{
ChainID: big.NewInt(1),
},
Expand All @@ -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: &params.ChainConfig{
ChainID: big.NewInt(1),
},
Expand Down Expand Up @@ -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: &params.ChainConfig{
ChainID: big.NewInt(1),
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
}
})
}
Expand All @@ -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: &params.ChainConfig{
ChainID: big.NewInt(1),
Expand Down
2 changes: 1 addition & 1 deletion execution/gethexec/sequencer.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
53 changes: 28 additions & 25 deletions system_tests/timeboost_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,9 @@ 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)
bobPriv := seqInfo.Accounts["Bob"].PrivateKey

Expand All @@ -69,8 +71,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,
)
Expand Down Expand Up @@ -149,7 +150,11 @@ 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)
bobPriv := seqInfo.Accounts["Bob"].PrivateKey

Expand All @@ -159,8 +164,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,
)
Expand Down Expand Up @@ -520,7 +524,9 @@ func setupExpressLaneAuction(
bob.Start(ctx)

// Wait until the initial round.
info, err := auctionContract.RoundTimingInfo(&bind.CallOpts{})
rawRoundTimingInfo, err := auctionContract.RoundTimingInfo(&bind.CallOpts{})
Require(t, err)
roundTimingInfo, err := timeboost.NewRoundTimingInfo(rawRoundTimingInfo)
Require(t, err)
timeToWait := time.Until(initialTimestampUnix)
t.Logf("Waiting until the initial round %v and %v, current time %v", timeToWait, initialTimestampUnix, time.Now())
Expand Down Expand Up @@ -561,7 +567,7 @@ func setupExpressLaneAuction(
waitTime = roundDuration - time.Duration(now.Second())*time.Second - time.Duration(now.Nanosecond())
time.Sleep(waitTime)

currRound := timeboost.CurrentRound(time.Unix(int64(info.OffsetTimestamp), 0), roundDuration)
currRound := roundTimingInfo.RoundNumber()
t.Log("curr round", currRound)
if currRound != winnerRound {
now = time.Now()
Expand Down Expand Up @@ -633,31 +639,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,
}
}

Expand All @@ -674,7 +677,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),
Expand Down
Loading

0 comments on commit bbc703a

Please sign in to comment.