Skip to content

Commit bec9ad0

Browse files
authored
fix(da): increase backoff for mempool errors (#1535)
## Overview This PR increases the backoff duration for mempool errors, avoiding the scenario where a temporary mempool error e.g due to congestion can exhaust all retries. Fixes #1522 ## Checklist - [ ] New and updated code has appropriate documentation - [ ] New and updated code has new and/or updated testing - [ ] Required CI checks are passing - [ ] Visual proof for any user facing features like CLI or documentation updates - [ ] Linked issues closed with keywords <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **New Features** - Introduced a default expiration time for mempool transactions to improve transaction management. - Enhanced block submission with handling for `maxBlobSize` and `gasPrice`, offering better flexibility and efficiency. - Added a new flag in `rollkit start` command for setting DA gas price multiplier, facilitating more robust transaction retries. - **Improvements** - Extended the submission timeout period from 60 to 90 seconds, allowing for more reliable block submissions under network delays. - Enriched error messaging and introduced new status codes for block submission to improve debugging and operational transparency. - **Refactor** - Replaced `MockDA` struct with `mock.MockDA` in tests for a more streamlined testing approach. - Implemented a mock `DA` interface in `da/mock/mock.go`, enhancing testability and development efficiency. - **Tests** - Updated various test functions to accommodate new parameters and improved error handling, ensuring the reliability of changes. - **Documentation** - Documented the addition of the DA gas price multiplier flag in `rollkit start` command usage. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
1 parent a5c1173 commit bec9ad0

File tree

13 files changed

+238
-90
lines changed

13 files changed

+238
-90
lines changed

block/manager.go

Lines changed: 39 additions & 1 deletion
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+
// defaultMempoolTTL is the number of blocks until transaction is dropped from mempool
39+
const defaultMempoolTTL = 25
40+
3841
// maxSubmitAttempts defines how many times Rollkit will re-try to publish block to DA layer.
3942
// This is temporary solution. It will be removed in future versions.
4043
const maxSubmitAttempts = 30
@@ -164,6 +167,11 @@ func NewManager(
164167
conf.BlockTime = defaultBlockTime
165168
}
166169

170+
if conf.DAMempoolTTL == 0 {
171+
logger.Info("Using default mempool ttl", "MempoolTTL", defaultMempoolTTL)
172+
conf.DAMempoolTTL = defaultMempoolTTL
173+
}
174+
167175
proposerAddress, err := getAddress(proposerKey)
168176
if err != nil {
169177
return nil, err
@@ -835,8 +843,15 @@ func (m *Manager) submitBlocksToDA(ctx context.Context) error {
835843
blocksToSubmit := m.pendingBlocks.getPendingBlocks()
836844
numSubmittedBlocks := 0
837845
attempt := 0
846+
maxBlobSize, err := m.dalc.DA.MaxBlobSize(ctx)
847+
if err != nil {
848+
return err
849+
}
850+
initialMaxBlobSize := maxBlobSize
851+
initialGasPrice := m.dalc.GasPrice
852+
gasPrice := m.dalc.GasPrice
838853
for ctx.Err() == nil && !submittedAllBlocks && attempt < maxSubmitAttempts {
839-
res := m.dalc.SubmitBlocks(ctx, blocksToSubmit)
854+
res := m.dalc.SubmitBlocks(ctx, blocksToSubmit, maxBlobSize, gasPrice)
840855
switch res.Code {
841856
case da.StatusSuccess:
842857
m.logger.Info("successfully submitted Rollkit blocks to DA layer", "daHeight", res.DAHeight, "count", res.SubmittedCount)
@@ -850,6 +865,29 @@ func (m *Manager) submitBlocksToDA(ctx context.Context) error {
850865
}
851866
m.pendingBlocks.removeSubmittedBlocks(submittedBlocks)
852867
blocksToSubmit = notSubmittedBlocks
868+
// reset submission options when successful
869+
// scale back gasPrice gradually
870+
backoff = initialBackoff
871+
maxBlobSize = initialMaxBlobSize
872+
gasPrice = gasPrice / m.dalc.GasMultiplier
873+
if gasPrice < initialGasPrice {
874+
gasPrice = initialGasPrice
875+
}
876+
m.logger.Debug("resetting DA layer submission options", "backoff", backoff, "gasPrice", gasPrice, "maxBlobSize", maxBlobSize)
877+
case da.StatusNotIncludedInBlock, da.StatusAlreadyInMempool:
878+
m.logger.Error("DA layer submission failed", "error", res.Message, "attempt", attempt)
879+
backoff = m.conf.DABlockTime * time.Duration(m.conf.DAMempoolTTL)
880+
if m.dalc.GasMultiplier != -1 && gasPrice != -1 {
881+
gasPrice = gasPrice * m.dalc.GasMultiplier
882+
}
883+
m.logger.Info("retrying DA layer submission with", "backoff", backoff, "gasPrice", gasPrice, "maxBlobSize", maxBlobSize)
884+
time.Sleep(backoff)
885+
backoff = m.exponentialBackoff(backoff)
886+
case da.StatusTooBig:
887+
m.logger.Error("DA layer submission failed", "error", res.Message, "attempt", attempt)
888+
maxBlobSize = maxBlobSize / 4
889+
time.Sleep(backoff)
890+
backoff = m.exponentialBackoff(backoff)
853891
default:
854892
m.logger.Error("DA layer submission failed", "error", res.Message, "attempt", attempt)
855893
time.Sleep(backoff)

block/manager_test.go

Lines changed: 48 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,30 @@
11
package block
22

33
import (
4+
"bytes"
45
"context"
56
"testing"
7+
"time"
68

79
cmtypes "github.com/cometbft/cometbft/types"
810
"github.com/stretchr/testify/assert"
911
"github.com/stretchr/testify/require"
1012

13+
goDA "github.com/rollkit/go-da"
1114
goDATest "github.com/rollkit/go-da/test"
1215

1316
"github.com/rollkit/rollkit/da"
17+
"github.com/rollkit/rollkit/da/mock"
1418
"github.com/rollkit/rollkit/store"
1519
test "github.com/rollkit/rollkit/test/log"
1620
"github.com/rollkit/rollkit/types"
1721
)
1822

1923
// Returns a minimalistic block manager
20-
func getManager(t *testing.T) *Manager {
24+
func getManager(t *testing.T, backend goDA.DA) *Manager {
2125
logger := test.NewFileLoggerCustom(t, test.TempLogFileName(t, t.Name()))
2226
return &Manager{
23-
dalc: &da.DAClient{DA: goDATest.NewDummyDA(), GasPrice: -1, Logger: logger},
27+
dalc: &da.DAClient{DA: backend, GasPrice: -1, GasMultiplier: -1, Logger: logger},
2428
blockCache: NewBlockCache(),
2529
logger: logger,
2630
}
@@ -125,11 +129,52 @@ func TestIsDAIncluded(t *testing.T) {
125129
require.True(m.IsDAIncluded(hash))
126130
}
127131

132+
func TestSubmitBlocksToMockDA(t *testing.T) {
133+
ctx := context.Background()
134+
135+
mockDA := &mock.MockDA{}
136+
m := getManager(t, mockDA)
137+
m.conf.DABlockTime = time.Millisecond
138+
m.conf.DAMempoolTTL = 1
139+
m.dalc.GasPrice = 1.0
140+
m.dalc.GasMultiplier = 1.2
141+
142+
t.Run("handle_tx_already_in_mempool", func(t *testing.T) {
143+
var blobs [][]byte
144+
block := types.GetRandomBlock(1, 5)
145+
blob, err := block.MarshalBinary()
146+
147+
require.NoError(t, err)
148+
blobs = append(blobs, blob)
149+
// Set up the mock to
150+
// * throw timeout waiting for tx to be included exactly once
151+
// * wait for tx to drop from mempool exactly DABlockTime * DAMempoolTTL seconds
152+
// * retry with a higher gas price
153+
// * successfully submit
154+
mockDA.On("MaxBlobSize").Return(uint64(12345), nil)
155+
mockDA.
156+
On("Submit", blobs, 1.0, []byte(nil)).
157+
Return([][]byte{}, da.ErrTxTimedout).Once()
158+
mockDA.
159+
On("Submit", blobs, 1.0*1.2, []byte(nil)).
160+
Return([][]byte{}, da.ErrTxAlreadyInMempool).Times(int(m.conf.DAMempoolTTL))
161+
mockDA.
162+
On("Submit", blobs, 1.0*1.2*1.2, []byte(nil)).
163+
Return([][]byte{bytes.Repeat([]byte{0x00}, 8)}, nil)
164+
165+
m.pendingBlocks = NewPendingBlocks()
166+
m.pendingBlocks.addPendingBlock(block)
167+
err = m.submitBlocksToDA(ctx)
168+
require.NoError(t, err)
169+
mockDA.AssertExpectations(t)
170+
})
171+
}
172+
128173
func TestSubmitBlocksToDA(t *testing.T) {
129174
require := require.New(t)
130175
ctx := context.Background()
131176

132-
m := getManager(t)
177+
m := getManager(t, goDATest.NewDummyDA())
133178

134179
maxDABlobSizeLimit, err := m.dalc.DA.MaxBlobSize(ctx)
135180
require.NoError(err)

cmd/rollkit/docs/rollkit_start.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ rollkit start [flags]
3232
--rollkit.block_time duration block time (for aggregator mode) (default 1s)
3333
--rollkit.da_address string DA address (host:port) (default ":26650")
3434
--rollkit.da_block_time duration DA chain block time (for syncing) (default 15s)
35+
--rollkit.da_gas_multiplier float DA gas price multiplier for retrying blob transactions (default -1)
3536
--rollkit.da_gas_price float DA gas price for blob transactions (default -1)
3637
--rollkit.da_namespace string DA namespace to submit blob transactions
3738
--rollkit.da_start_height uint starting DA block height (for syncing)

config/config.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ const (
2020
FlagDABlockTime = "rollkit.da_block_time"
2121
// FlagDAGasPrice is a flag for specifying the data availability layer gas price
2222
FlagDAGasPrice = "rollkit.da_gas_price"
23+
// FlagDAGasMultiplier is a flag for specifying the data availability layer gas price retry multiplier
24+
FlagDAGasMultiplier = "rollkit.da_gas_multiplier"
2325
// FlagDAStartHeight is a flag for specifying the data availability layer start height
2426
FlagDAStartHeight = "rollkit.da_start_height"
2527
// FlagDANamespace is a flag for specifying the DA namespace ID
@@ -48,6 +50,7 @@ type NodeConfig struct {
4850
LazyAggregator bool `mapstructure:"lazy_aggregator"`
4951
Instrumentation *cmcfg.InstrumentationConfig `mapstructure:"instrumentation"`
5052
DAGasPrice float64 `mapstructure:"da_gas_price"`
53+
DAGasMultiplier float64 `mapstructure:"da_gas_multiplier"`
5154

5255
// CLI flags
5356
DANamespace string `mapstructure:"da_namespace"`
@@ -66,6 +69,8 @@ type BlockManagerConfig struct {
6669
DABlockTime time.Duration `mapstructure:"da_block_time"`
6770
// DAStartHeight allows skipping first DAStartHeight-1 blocks when querying for blocks.
6871
DAStartHeight uint64 `mapstructure:"da_start_height"`
72+
// DAMempoolTTL is the number of DA blocks until transaction is dropped from the mempool.
73+
DAMempoolTTL uint64 `mapstructure:"da_mempool_ttl"`
6974
}
7075

7176
// GetNodeConfig translates Tendermint's configuration into Rollkit configuration.
@@ -102,6 +107,7 @@ func (nc *NodeConfig) GetViperConfig(v *viper.Viper) error {
102107
nc.Aggregator = v.GetBool(FlagAggregator)
103108
nc.DAAddress = v.GetString(FlagDAAddress)
104109
nc.DAGasPrice = v.GetFloat64(FlagDAGasPrice)
110+
nc.DAGasMultiplier = v.GetFloat64(FlagDAGasMultiplier)
105111
nc.DANamespace = v.GetString(FlagDANamespace)
106112
nc.DAStartHeight = v.GetUint64(FlagDAStartHeight)
107113
nc.DABlockTime = v.GetDuration(FlagDABlockTime)
@@ -124,6 +130,7 @@ func AddFlags(cmd *cobra.Command) {
124130
cmd.Flags().Duration(FlagBlockTime, def.BlockTime, "block time (for aggregator mode)")
125131
cmd.Flags().Duration(FlagDABlockTime, def.DABlockTime, "DA chain block time (for syncing)")
126132
cmd.Flags().Float64(FlagDAGasPrice, def.DAGasPrice, "DA gas price for blob transactions")
133+
cmd.Flags().Float64(FlagDAGasMultiplier, def.DAGasMultiplier, "DA gas price multiplier for retrying blob transactions")
127134
cmd.Flags().Uint64(FlagDAStartHeight, def.DAStartHeight, "starting DA block height (for syncing)")
128135
cmd.Flags().String(FlagDANamespace, def.DANamespace, "DA namespace to submit blob transactions")
129136
cmd.Flags().Bool(FlagLight, def.Light, "run light client")

config/defaults.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,10 @@ var DefaultNodeConfig = NodeConfig{
2626
BlockTime: 1 * time.Second,
2727
DABlockTime: 15 * time.Second,
2828
},
29-
DAAddress: ":26650",
30-
DAGasPrice: -1,
31-
Light: false,
29+
DAAddress: ":26650",
30+
DAGasPrice: -1,
31+
DAGasMultiplier: -1,
32+
Light: false,
3233
HeaderConfig: HeaderConfig{
3334
TrustedHash: "",
3435
},

da/da.go

Lines changed: 41 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"encoding/binary"
66
"errors"
77
"fmt"
8+
"strings"
89
"time"
910

1011
"github.com/gogo/protobuf/proto"
@@ -29,6 +30,21 @@ var (
2930

3031
// ErrBlobSizeOverLimit is used to indicate that the blob size is over limit
3132
ErrBlobSizeOverLimit = errors.New("blob: over size limit")
33+
34+
// ErrTxTimedout is the error message returned by the DA when mempool is congested
35+
ErrTxTimedout = errors.New("timed out waiting for tx to be included in a block")
36+
37+
// ErrTxAlreadyInMempool is the error message returned by the DA when tx is already in mempool
38+
ErrTxAlreadyInMempool = errors.New("tx already in mempool")
39+
40+
// ErrTxIncorrectAccountSequence is the error message returned by the DA when tx has incorrect sequence
41+
ErrTxIncorrectAccountSequence = errors.New("incorrect account sequence")
42+
43+
// ErrTxSizeTooBig is the error message returned by the DA when tx size is too big
44+
ErrTxSizeTooBig = errors.New("tx size is too big")
45+
46+
// ErrContextDeadline is the error message returned by the DA when context deadline exceeds
47+
ErrContextDeadline = errors.New("context deadline")
3248
)
3349

3450
// StatusCode is a type for DA layer return status.
@@ -42,6 +58,10 @@ const (
4258
StatusUnknown StatusCode = iota
4359
StatusSuccess
4460
StatusNotFound
61+
StatusNotIncludedInBlock
62+
StatusAlreadyInMempool
63+
StatusTooBig
64+
StatusContextDeadline
4565
StatusError
4666
)
4767

@@ -75,25 +95,17 @@ type ResultRetrieveBlocks struct {
7595

7696
// DAClient is a new DA implementation.
7797
type DAClient struct {
78-
DA goDA.DA
79-
Namespace goDA.Namespace
80-
GasPrice float64
81-
Logger log.Logger
98+
DA goDA.DA
99+
GasPrice float64
100+
GasMultiplier float64
101+
Namespace goDA.Namespace
102+
Logger log.Logger
82103
}
83104

84105
// SubmitBlocks submits blocks to DA.
85-
func (dac *DAClient) SubmitBlocks(ctx context.Context, blocks []*types.Block) ResultSubmitBlocks {
106+
func (dac *DAClient) SubmitBlocks(ctx context.Context, blocks []*types.Block, maxBlobSize uint64, gasPrice float64) ResultSubmitBlocks {
86107
var blobs [][]byte
87108
var blobSize uint64
88-
maxBlobSize, err := dac.DA.MaxBlobSize(ctx)
89-
if err != nil {
90-
return ResultSubmitBlocks{
91-
BaseResult: BaseResult{
92-
Code: StatusError,
93-
Message: "unable to get DA max blob size",
94-
},
95-
}
96-
}
97109
var submitted uint64
98110
for i := range blocks {
99111
blob, err := blocks[i].MarshalBinary()
@@ -123,11 +135,24 @@ func (dac *DAClient) SubmitBlocks(ctx context.Context, blocks []*types.Block) Re
123135
}
124136
ctx, cancel := context.WithTimeout(ctx, submitTimeout)
125137
defer cancel()
126-
ids, err := dac.DA.Submit(ctx, blobs, dac.GasPrice, dac.Namespace)
138+
ids, err := dac.DA.Submit(ctx, blobs, gasPrice, dac.Namespace)
127139
if err != nil {
140+
status := StatusError
141+
switch {
142+
case strings.Contains(err.Error(), ErrTxTimedout.Error()):
143+
status = StatusNotIncludedInBlock
144+
case strings.Contains(err.Error(), ErrTxAlreadyInMempool.Error()):
145+
status = StatusAlreadyInMempool
146+
case strings.Contains(err.Error(), ErrTxIncorrectAccountSequence.Error()):
147+
status = StatusAlreadyInMempool
148+
case strings.Contains(err.Error(), ErrTxSizeTooBig.Error()):
149+
status = StatusTooBig
150+
case strings.Contains(err.Error(), ErrContextDeadline.Error()):
151+
status = StatusContextDeadline
152+
}
128153
return ResultSubmitBlocks{
129154
BaseResult: BaseResult{
130-
Code: StatusError,
155+
Code: status,
131156
Message: "failed to submit blocks: " + err.Error(),
132157
},
133158
}

0 commit comments

Comments
 (0)