Skip to content

Commit dd06975

Browse files
authored
feat(block): add lazy block time as additional block production trigger in lazy aggregator mode (#1643)
<!-- Please read and fill out this form before submitting your PR. Please make sure you have reviewed our contributors guide before submitting your first PR. NOTE: PR titles should follow semantic commits: https://www.conventionalcommits.org/en/v1.0.0/ --> ## Overview <!-- Please provide an explanation of the PR, including the appropriate context, background, goal, and rationale. If there is an issue with this information, please provide a tl;dr and link the issue. Ex: Closes #<issue number> --> One problem that chains in lazy aggregator mode have is that there is no way externally to see if the chain is still live without sending new transactions. The lazy block time adds an additional trigger that can produce a block during long periods of inactivity. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **New Features** - Introduced a new configuration option `LazyBlockTime` to customize the time interval for block production in lazy mode when no transactions are present. - **Enhancements** - Improved block production efficiency in lazy aggregation mode by implementing new timers and sleep interval calculations. - **Testing** - Added comprehensive tests to ensure the correct calculation of sleep durations in various scenarios for block production. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
1 parent 423c41c commit dd06975

6 files changed

Lines changed: 125 additions & 22 deletions

File tree

block/block-manager.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ Block manager configuration options:
6363
|BlockTime|time.Duration|time interval used for block production and block retrieval from block store ([`defaultBlockTime`][defaultBlockTime])|
6464
|DABlockTime|time.Duration|time interval used for both block publication to DA network and block retrieval from DA network ([`defaultDABlockTime`][defaultDABlockTime])|
6565
|DAStartHeight|uint64|block retrieval from DA network starts from this height|
66+
|LazyBlockTime|time.Duration|time interval used for block production in lazy aggregator mode even when there are no transactions ([`defaultLazyBlockTime`][defaultLazyBlockTime])|
6667

6768
### Block Production
6869

@@ -181,6 +182,7 @@ See [tutorial] for running a multi-node network with both sequencer and non-sequ
181182
[maxSubmitAttempts]: https://github.com/rollkit/rollkit/blob/main/block/manager.go#L39
182183
[defaultBlockTime]: https://github.com/rollkit/rollkit/blob/main/block/manager.go#L35
183184
[defaultDABlockTime]: https://github.com/rollkit/rollkit/blob/main/block/manager.go#L32
185+
[defaultLazyBlockTime]: https://github.com/rollkit/rollkit/blob/main/block/manager.go#L38
184186
[initialBackoff]: https://github.com/rollkit/rollkit/blob/main/block/manager.go#L48
185187
[go-header]: https://github.com/celestiaorg/go-header
186188
[block-sync]: https://github.com/rollkit/rollkit/blob/main/block/block_sync.go

block/manager.go

Lines changed: 72 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,9 @@ const defaultDABlockTime = 15 * time.Second
3535
// defaultBlockTime is used only if BlockTime is not configured for manager
3636
const defaultBlockTime = 1 * time.Second
3737

38+
// defaultLazyBlockTime is used only if LazyBlockTime is not configured for manager
39+
const defaultLazyBlockTime = 60 * time.Second
40+
3841
// defaultMempoolTTL is the number of blocks until transaction is dropped from mempool
3942
const defaultMempoolTTL = 25
4043

@@ -185,6 +188,11 @@ func NewManager(
185188
conf.BlockTime = defaultBlockTime
186189
}
187190

191+
if conf.LazyBlockTime == 0 {
192+
logger.Info("Using default lazy block time", "LazyBlockTime", defaultLazyBlockTime)
193+
conf.LazyBlockTime = defaultLazyBlockTime
194+
}
195+
188196
if conf.DAMempoolTTL == 0 {
189197
logger.Info("Using default mempool ttl", "MempoolTTL", defaultMempoolTTL)
190198
conf.DAMempoolTTL = defaultMempoolTTL
@@ -332,31 +340,65 @@ func (m *Manager) AggregationLoop(ctx context.Context, lazy bool) {
332340
time.Sleep(delay)
333341
}
334342

335-
timer := time.NewTimer(0)
336-
defer timer.Stop()
343+
// blockTimer is used to signal when to build a block based on the
344+
// rollup block time. A timer is used so that the time to build a block
345+
// can be taken into account.
346+
blockTimer := time.NewTimer(0)
347+
defer blockTimer.Stop()
337348

338-
// Lazy Aggregator mode
349+
// Lazy Aggregator mode.
350+
// In Lazy Aggregator mode, blocks are built only when there are
351+
// transactions or every LazyBlockTime.
339352
if lazy {
353+
// start is used to track the start time of the block production period
354+
var start time.Time
355+
356+
// defaultSleep is used when coming out of a period of inactivity,
357+
// to try and pull in more transactions in the first block
358+
defaultSleep := time.Second
359+
360+
// lazyTimer is used to signal when a block should be built in
361+
// lazy mode to signal that the chain is still live during long
362+
// periods of inactivity.
363+
lazyTimer := time.NewTimer(0)
364+
defer lazyTimer.Stop()
340365
for {
341366
select {
342367
case <-ctx.Done():
343368
return
344-
// the buildBlock channel is signalled when Txns become available
369+
// the txsAvailable channel is signalled when Txns become available
345370
// in the mempool, or after transactions remain in the mempool after
346371
// building a block.
347372
case _, ok := <-m.txsAvailable:
348373
if ok && !m.buildingBlock {
374+
// set the buildingBlock flag to prevent multiple calls to reset the timer
349375
m.buildingBlock = true
350-
timer.Reset(1 * time.Second)
351-
}
352-
case <-timer.C:
353-
// build a block with all the transactions received in the last 1 second
354-
err := m.publishBlock(ctx)
355-
if err != nil && ctx.Err() == nil {
356-
m.logger.Error("error while publishing block", "error", err)
376+
// Reset the block timer based on the block time and the default sleep.
377+
// The default sleep is used to give time for transactions to accumulate
378+
// if we are coming out of a period of inactivity. If we had recently
379+
// produced a block (i.e. within the block time) then we will sleep for
380+
// the remaining time within the block time interval.
381+
blockTimer.Reset(getRemainingSleep(start, m.conf.BlockTime, defaultSleep))
382+
357383
}
358-
m.buildingBlock = false
384+
continue
385+
case <-lazyTimer.C:
386+
case <-blockTimer.C:
359387
}
388+
// Define the start time for the block production period
389+
start = time.Now()
390+
err := m.publishBlock(ctx)
391+
if err != nil && ctx.Err() == nil {
392+
m.logger.Error("error while publishing block", "error", err)
393+
}
394+
// unset the buildingBlocks flag
395+
m.buildingBlock = false
396+
// Reset the lazyTimer to produce a block even if there
397+
// are no transactions as a way to signal that the chain
398+
// is still live. Default sleep is set to 0 because care
399+
// about producing blocks on time vs giving time for
400+
// transactions to accumulate.
401+
lazyTimer.Reset(getRemainingSleep(start, m.conf.LazyBlockTime, 0))
360402
}
361403
}
362404

@@ -365,14 +407,18 @@ func (m *Manager) AggregationLoop(ctx context.Context, lazy bool) {
365407
select {
366408
case <-ctx.Done():
367409
return
368-
case <-timer.C:
410+
case <-blockTimer.C:
369411
}
370412
start := time.Now()
371413
err := m.publishBlock(ctx)
372414
if err != nil && ctx.Err() == nil {
373415
m.logger.Error("error while publishing block", "error", err)
374416
}
375-
timer.Reset(m.getRemainingSleep(start))
417+
// Reset the blockTimer to signal the next block production
418+
// period based on the block time. Default sleep is set to 0
419+
// because care about producing blocks on time vs giving time
420+
// for transactions to accumulate.
421+
blockTimer.Reset(getRemainingSleep(start, m.conf.BlockTime, 0))
376422
}
377423
}
378424

@@ -687,11 +733,18 @@ func (m *Manager) fetchBlock(ctx context.Context, daHeight uint64) (da.ResultRet
687733
return blockRes, err
688734
}
689735

690-
func (m *Manager) getRemainingSleep(start time.Time) time.Duration {
691-
publishingDuration := time.Since(start)
692-
sleepDuration := m.conf.BlockTime - publishingDuration
693-
if sleepDuration < 0 {
694-
sleepDuration = 0
736+
// getRemainingSleep calculates the remaining sleep time based on an interval
737+
// and a start time.
738+
func getRemainingSleep(start time.Time, interval, defaultSleep time.Duration) time.Duration {
739+
// Initialize the sleep duration to the default sleep duration to cover
740+
// the case where more time has past than the interval duration.
741+
sleepDuration := defaultSleep
742+
// Calculate the time elapsed since the start time.
743+
elapse := time.Since(start)
744+
// If less time has elapsed than the interval duration, calculate the
745+
// remaining time to sleep.
746+
if elapse < interval {
747+
sleepDuration = interval - elapse
695748
}
696749
return sleepDuration
697750
}

block/manager_test.go

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -479,3 +479,43 @@ func Test_publishBlock_ManagerNotProposer(t *testing.T) {
479479
err := m.publishBlock(context.Background())
480480
require.ErrorIs(err, ErrNotProposer)
481481
}
482+
483+
func TestGetRemainingSleep(t *testing.T) {
484+
now := time.Now()
485+
interval := 10 * time.Second
486+
defaultSleep := time.Second
487+
488+
// start height over the interval in the past
489+
farPastStart := now.Add(-interval - 1)
490+
491+
// Table test in case we need to easily extend this in the future.
492+
var tests = []struct {
493+
name string
494+
start time.Time
495+
interval time.Duration
496+
defaultSleep time.Duration
497+
expectedSleep time.Duration
498+
}{
499+
{"nil case", time.Time{}, 0, 0, 0},
500+
{"start over the interval in the past", farPastStart, interval, defaultSleep, defaultSleep},
501+
}
502+
503+
for _, test := range tests {
504+
assert.Equalf(t, test.expectedSleep, getRemainingSleep(test.start, test.interval, test.defaultSleep), "test case: %s", test.name)
505+
506+
}
507+
508+
// Custom handler for start is within the interval in the past since
509+
// getRemainingSleep uses time.Since which results in a
510+
// non-deterministic result. But we know that it should be less than
511+
// timeInThePast and greater than defaultSleep based on the test setup.
512+
timeInThePast := interval / 2
513+
514+
// start height within the interval in the past
515+
pastStart := now.Add(-timeInThePast)
516+
517+
sleep := getRemainingSleep(pastStart, interval, defaultSleep)
518+
assert.True(t, sleep <= timeInThePast)
519+
assert.True(t, sleep > defaultSleep)
520+
521+
}

config/config.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,9 @@ type BlockManagerConfig struct {
7979
// MaxPendingBlocks defines limit of blocks pending DA submission. 0 means no limit.
8080
// When limit is reached, aggregator pauses block production.
8181
MaxPendingBlocks uint64 `mapstructure:"max_pending_blocks"`
82+
// LazyBlockTime defines how often new blocks are produced in lazy mode
83+
// even if there are no transactions
84+
LazyBlockTime time.Duration `mapstructure:"lazy_block_time"`
8285
}
8386

8487
// GetNodeConfig translates Tendermint's configuration into Rollkit configuration.

config/defaults.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,9 @@ var DefaultNodeConfig = NodeConfig{
2323
Aggregator: false,
2424
LazyAggregator: false,
2525
BlockManagerConfig: BlockManagerConfig{
26-
BlockTime: 1 * time.Second,
27-
DABlockTime: 15 * time.Second,
26+
BlockTime: 1 * time.Second,
27+
DABlockTime: 15 * time.Second,
28+
LazyBlockTime: 60 * time.Second,
2829
},
2930
DAAddress: "http://localhost:26658",
3031
DAGasPrice: -1,

node/full_node_integration_test.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -181,7 +181,8 @@ func TestLazyAggregator(t *testing.T) {
181181
// the blocktime too short. in future, we can add a configuration
182182
// in go-header syncer initialization to not rely on blocktime, but the
183183
// config variable
184-
BlockTime: 1 * time.Second,
184+
BlockTime: 1 * time.Second,
185+
LazyBlockTime: 5 * time.Second,
185186
}
186187
ctx, cancel := context.WithCancel(context.Background())
187188
defer cancel()
@@ -212,6 +213,9 @@ func TestLazyAggregator(t *testing.T) {
212213
assert.NoError(err)
213214

214215
require.NoError(waitForAtLeastNBlocks(node, 4, Header))
216+
217+
// LazyBlockTime should trigger another block even without transactions
218+
require.NoError(waitForAtLeastNBlocks(node, 5, Header))
215219
}
216220

217221
// TestFastDASync verifies that nodes can sync DA blocks faster than the DA block time

0 commit comments

Comments
 (0)