From 6621a5c89a92e7593f702e4c82e69d1215b2ca59 Mon Sep 17 00:00:00 2001
From: David <david@taiko.xyz>
Date: Tue, 7 Mar 2023 20:00:27 +0800
Subject: [PATCH] feat(proposer): new flag to propose empty blocks (#175)

---
 cmd/flags/proposer.go     |  6 +++++
 proposer/config.go        | 50 ++++++++++++++++++++--------------
 proposer/proposer.go      | 56 ++++++++++++++++++++++++++++++++++-----
 proposer/proposer_test.go |  6 ++++-
 testutils/helper.go       |  7 ++---
 testutils/interfaces.go   |  1 +
 version/version.go        |  2 +-
 7 files changed, 95 insertions(+), 33 deletions(-)

diff --git a/cmd/flags/proposer.go b/cmd/flags/proposer.go
index e22beec65..fad1412c9 100644
--- a/cmd/flags/proposer.go
+++ b/cmd/flags/proposer.go
@@ -47,6 +47,11 @@ var (
 		Value:    "Comma separated accounts to treat as locals (priority inclusion)",
 		Category: proposerCategory,
 	}
+	ProposeEmptyBlocksInterval = &cli.StringFlag{
+		Name:     "proposeEmptyBlockInterval",
+		Usage:    "Time interval to propose empty blocks",
+		Category: proposerCategory,
+	}
 )
 
 // All proposer flags.
@@ -58,4 +63,5 @@ var ProposerFlags = MergeFlags(CommonFlags, []cli.Flag{
 	ShufflePoolContent,
 	CommitSlot,
 	TxPoolLocals,
+	ProposeEmptyBlocksInterval,
 })
diff --git a/proposer/config.go b/proposer/config.go
index ef6db2ab6..d98858743 100644
--- a/proposer/config.go
+++ b/proposer/config.go
@@ -14,16 +14,17 @@ import (
 
 // Config contains all configurations to initialize a Taiko proposer.
 type Config struct {
-	L1Endpoint              string
-	L2Endpoint              string
-	TaikoL1Address          common.Address
-	TaikoL2Address          common.Address
-	L1ProposerPrivKey       *ecdsa.PrivateKey
-	L2SuggestedFeeRecipient common.Address
-	ProposeInterval         *time.Duration
-	ShufflePoolContent      bool
-	CommitSlot              uint64
-	LocalAddresses          []common.Address
+	L1Endpoint                 string
+	L2Endpoint                 string
+	TaikoL1Address             common.Address
+	TaikoL2Address             common.Address
+	L1ProposerPrivKey          *ecdsa.PrivateKey
+	L2SuggestedFeeRecipient    common.Address
+	ProposeInterval            *time.Duration
+	ShufflePoolContent         bool
+	CommitSlot                 uint64
+	LocalAddresses             []common.Address
+	ProposeEmptyBlocksInterval *time.Duration
 }
 
 // NewConfigFromCliContext initializes a Config instance from
@@ -45,6 +46,14 @@ func NewConfigFromCliContext(c *cli.Context) (*Config, error) {
 		}
 		proposingInterval = &interval
 	}
+	var proposeEmptyBlocksInterval *time.Duration
+	if c.IsSet(flags.ProposeEmptyBlocksInterval.Name) {
+		interval, err := time.ParseDuration(c.String(flags.ProposeEmptyBlocksInterval.Name))
+		if err != nil {
+			return nil, fmt.Errorf("invalid proposing empty blocks interval: %w", err)
+		}
+		proposeEmptyBlocksInterval = &interval
+	}
 
 	l2SuggestedFeeRecipient := c.String(flags.L2SuggestedFeeRecipient.Name)
 	if !common.IsHexAddress(l2SuggestedFeeRecipient) {
@@ -63,15 +72,16 @@ func NewConfigFromCliContext(c *cli.Context) (*Config, error) {
 	}
 
 	return &Config{
-		L1Endpoint:              c.String(flags.L1WSEndpoint.Name),
-		L2Endpoint:              c.String(flags.L2HTTPEndpoint.Name),
-		TaikoL1Address:          common.HexToAddress(c.String(flags.TaikoL1Address.Name)),
-		TaikoL2Address:          common.HexToAddress(c.String(flags.TaikoL2Address.Name)),
-		L1ProposerPrivKey:       l1ProposerPrivKey,
-		L2SuggestedFeeRecipient: common.HexToAddress(l2SuggestedFeeRecipient),
-		ProposeInterval:         proposingInterval,
-		ShufflePoolContent:      c.Bool(flags.ShufflePoolContent.Name),
-		CommitSlot:              c.Uint64(flags.CommitSlot.Name),
-		LocalAddresses:          localAddresses,
+		L1Endpoint:                 c.String(flags.L1WSEndpoint.Name),
+		L2Endpoint:                 c.String(flags.L2HTTPEndpoint.Name),
+		TaikoL1Address:             common.HexToAddress(c.String(flags.TaikoL1Address.Name)),
+		TaikoL2Address:             common.HexToAddress(c.String(flags.TaikoL2Address.Name)),
+		L1ProposerPrivKey:          l1ProposerPrivKey,
+		L2SuggestedFeeRecipient:    common.HexToAddress(l2SuggestedFeeRecipient),
+		ProposeInterval:            proposingInterval,
+		ShufflePoolContent:         c.Bool(flags.ShufflePoolContent.Name),
+		CommitSlot:                 c.Uint64(flags.CommitSlot.Name),
+		LocalAddresses:             localAddresses,
+		ProposeEmptyBlocksInterval: proposeEmptyBlocksInterval,
 	}, nil
 }
diff --git a/proposer/proposer.go b/proposer/proposer.go
index 8406c3c17..52094e049 100644
--- a/proposer/proposer.go
+++ b/proposer/proposer.go
@@ -3,6 +3,7 @@ package proposer
 import (
 	"context"
 	"crypto/ecdsa"
+	"errors"
 	"fmt"
 	"math/big"
 	"math/rand"
@@ -23,6 +24,10 @@ import (
 	"github.com/urfave/cli/v2"
 )
 
+var (
+	errNoNewTxs = errors.New("no new transactions")
+)
+
 // Proposer keep proposing new transactions from L2 execution engine's tx pool at a fixed interval.
 type Proposer struct {
 	// RPC clients
@@ -33,10 +38,11 @@ type Proposer struct {
 	l2SuggestedFeeRecipient common.Address
 
 	// Proposing configurations
-	proposingInterval *time.Duration
-	proposingTimer    *time.Timer
-	commitSlot        uint64
-	locals            []common.Address
+	proposingInterval          *time.Duration
+	proposeEmptyBlocksInterval *time.Duration
+	proposingTimer             *time.Timer
+	commitSlot                 uint64
+	locals                     []common.Address
 
 	// Protocol configurations
 	protocolConfigs *bindings.TaikoDataConfig
@@ -64,6 +70,7 @@ func InitFromConfig(ctx context.Context, p *Proposer, cfg *Config) (err error) {
 	p.l1ProposerPrivKey = cfg.L1ProposerPrivKey
 	p.l2SuggestedFeeRecipient = cfg.L2SuggestedFeeRecipient
 	p.proposingInterval = cfg.ProposeInterval
+	p.proposeEmptyBlocksInterval = cfg.ProposeEmptyBlocksInterval
 	p.wg = sync.WaitGroup{}
 	p.locals = cfg.LocalAddresses
 	p.commitSlot = cfg.CommitSlot
@@ -105,6 +112,7 @@ func (p *Proposer) eventLoop() {
 		p.wg.Done()
 	}()
 
+	var lastNonEmptyBlockProposedAt = time.Now()
 	for {
 		p.updateProposingTicker()
 
@@ -115,9 +123,27 @@ func (p *Proposer) eventLoop() {
 			metrics.ProposerProposeEpochCounter.Inc(1)
 
 			if err := p.ProposeOp(p.ctx); err != nil {
-				log.Error("Proposing operation error", "error", err)
+				if !errors.Is(err, errNoNewTxs) {
+					log.Error("Proposing operation error", "error", err)
+					continue
+				}
+
+				if p.proposeEmptyBlocksInterval != nil {
+					if time.Now().Before(lastNonEmptyBlockProposedAt.Add(*p.proposeEmptyBlocksInterval)) {
+						continue
+					}
+
+					if err := p.ProposeEmptyBlockOp(p.ctx); err != nil {
+						log.Error("Proposing an empty block operation error", "error", err)
+					}
+
+					lastNonEmptyBlockProposedAt = time.Now()
+				}
+
 				continue
 			}
+
+			lastNonEmptyBlockProposedAt = time.Now()
 		}
 	}
 }
@@ -154,7 +180,11 @@ func (p *Proposer) ProposeOp(ctx context.Context) error {
 		return fmt.Errorf("failed to fetch transaction pool content: %w", err)
 	}
 
-	log.Info("Transactions count", "count", len(txLists))
+	log.Info("Transactions lists count", "count", len(txLists))
+
+	if len(txLists) == 0 {
+		return errNoNewTxs
+	}
 
 	var commitTxListResQueue []*commitTxListRes
 	for i, txs := range txLists {
@@ -300,6 +330,20 @@ func (p *Proposer) ProposeTxList(
 	return nil
 }
 
+// ProposeEmptyBlockOp performs a proposing one empty block operation.
+func (p *Proposer) ProposeEmptyBlockOp(ctx context.Context) error {
+	meta, commitTx, err := p.CommitTxList(ctx, []byte{}, 21000, 0)
+	if err != nil {
+		return fmt.Errorf("failed to commit an empty block: %w", err)
+	}
+
+	if err := p.ProposeTxList(ctx, meta, commitTx, []byte{}, 0); err != nil {
+		return fmt.Errorf("failed to propose an empty block: %w", err)
+	}
+
+	return nil
+}
+
 // updateProposingTicker updates the internal proposing timer.
 func (p *Proposer) updateProposingTicker() {
 	if p.proposingTimer != nil {
diff --git a/proposer/proposer_test.go b/proposer/proposer_test.go
index 098a1d12a..3fe177011 100644
--- a/proposer/proposer_test.go
+++ b/proposer/proposer_test.go
@@ -62,7 +62,7 @@ func (s *ProposerTestSuite) TestName() {
 
 func (s *ProposerTestSuite) TestProposeOp() {
 	// Nothing to propose
-	s.Nil(s.p.ProposeOp(context.Background()))
+	s.EqualError(errNoNewTxs, s.p.ProposeOp(context.Background()).Error())
 
 	// Propose txs in L2 execution engine's mempool
 	sink := make(chan *bindings.TaikoL1ClientBlockProposed)
@@ -103,6 +103,10 @@ func (s *ProposerTestSuite) TestProposeOp() {
 	s.Equal(types.ReceiptStatusSuccessful, receipt.Status)
 }
 
+func (s *ProposerTestSuite) TestProposeEmptyBlockOp() {
+	s.Nil(s.p.ProposeEmptyBlockOp(context.Background()))
+}
+
 func (s *ProposerTestSuite) TestCommitTxList() {
 	txListBytes := testutils.RandomBytes(1024)
 	gasLimit := uint64(102400)
diff --git a/testutils/helper.go b/testutils/helper.go
index 1056f4d01..bc151a51a 100644
--- a/testutils/helper.go
+++ b/testutils/helper.go
@@ -49,17 +49,14 @@ func ProposeAndInsertEmptyBlocks(
 	}()
 
 	// Zero byte txList
-	meta, commitTx, err := proposer.CommitTxList(context.Background(), []byte{}, 1024, 0)
-	s.Nil(err)
-
-	s.Nil(proposer.ProposeTxList(context.Background(), meta, commitTx, []byte{}, 0))
+	s.Nil(proposer.ProposeEmptyBlockOp(context.Background()))
 
 	// RLP encoded empty list
 	var emptyTxs []types.Transaction
 	encoded, err := rlp.EncodeToBytes(emptyTxs)
 	s.Nil(err)
 
-	meta, commitTx, err = proposer.CommitTxList(context.Background(), encoded, 1024, 0)
+	meta, commitTx, err := proposer.CommitTxList(context.Background(), encoded, 1024, 0)
 	s.Nil(err)
 
 	s.Nil(proposer.ProposeTxList(context.Background(), meta, commitTx, encoded, 0))
diff --git a/testutils/interfaces.go b/testutils/interfaces.go
index 77455f70c..140f05d43 100644
--- a/testutils/interfaces.go
+++ b/testutils/interfaces.go
@@ -15,6 +15,7 @@ type CalldataSyncer interface {
 type Proposer interface {
 	utils.SubcommandApplication
 	ProposeOp(ctx context.Context) error
+	ProposeEmptyBlockOp(ctx context.Context) error
 	CommitTxList(ctx context.Context, txListBytes []byte, gasLimit uint64, splittedIdx int) (
 		*bindings.TaikoDataBlockMetadata,
 		*types.Transaction,
diff --git a/version/version.go b/version/version.go
index 2cccb6e9d..5759df03d 100644
--- a/version/version.go
+++ b/version/version.go
@@ -2,7 +2,7 @@ package version
 
 // Version info.
 var (
-	Version = "0.4.0"
+	Version = "0.5.0"
 	Meta    = "dev"
 )