From 8bc63c40de0bb7dfde8d3a3741ee503d34611c9f Mon Sep 17 00:00:00 2001 From: David Date: Wed, 19 Jul 2023 13:50:01 +0800 Subject: [PATCH 1/6] feat(proposer): handle transaction replacement underpriced error --- proposer/proposer.go | 79 ++++++++++++++++++++++++++++++++------------ 1 file changed, 58 insertions(+), 21 deletions(-) diff --git a/proposer/proposer.go b/proposer/proposer.go index f75bf9c42..831e255cf 100644 --- a/proposer/proposer.go +++ b/proposer/proposer.go @@ -8,6 +8,7 @@ import ( "math" "math/big" "math/rand" + "strings" "sync" "time" @@ -27,8 +28,10 @@ import ( ) var ( - errNoNewTxs = errors.New("no new transactions") - waitReceiptTimeout = 1 * time.Minute + errNoNewTxs = errors.New("no new transactions") + waitReceiptTimeout = 1 * time.Minute + maxSendProposeBlockTxRetry = 10 + txReplacementTipMultiplier = 2 ) // Proposer keep proposing new transactions from L2 execution engine's tx pool at a fixed interval. @@ -278,36 +281,70 @@ func (p *Proposer) ProposeTxList( txNum uint, nonce *uint64, ) error { - if p.minBlockGasLimit != nil && meta.GasLimit < uint32(*p.minBlockGasLimit) { - meta.GasLimit = uint32(*p.minBlockGasLimit) - } + sendProposeBlockTx := func(isReplacement bool) (*types.Transaction, error) { + if p.minBlockGasLimit != nil && meta.GasLimit < uint32(*p.minBlockGasLimit) { + meta.GasLimit = uint32(*p.minBlockGasLimit) + } - // Propose the transactions list - inputs, err := encoding.EncodeProposeBlockInput(meta) - if err != nil { - return err - } + // Propose the transactions list + inputs, err := encoding.EncodeProposeBlockInput(meta) + if err != nil { + return nil, err + } - opts, err := getTxOpts(ctx, p.rpc.L1, p.l1ProposerPrivKey, p.rpc.L1ChainID) - if err != nil { - return err - } - if nonce != nil { - opts.Nonce = new(big.Int).SetUint64(*nonce) + opts, err := getTxOpts(ctx, p.rpc.L1, p.l1ProposerPrivKey, p.rpc.L1ChainID) + if err != nil { + return nil, err + } + if nonce != nil { + opts.Nonce = new(big.Int).SetUint64(*nonce) + } + if p.proposeBlockTxGasLimit != nil { + opts.GasLimit = *p.proposeBlockTxGasLimit + } + + if isReplacement { + opts.GasTipCap = new(big.Int).Mul(opts.GasTipCap, new(big.Int).SetUint64(uint64(txReplacementTipMultiplier))) + } + + proposeTx, err := p.rpc.TaikoL1.ProposeBlock(opts, inputs, txListBytes) + if err != nil { + return nil, encoding.TryParsingCustomError(err) + } + + return proposeTx, nil } - if p.proposeBlockTxGasLimit != nil { - opts.GasLimit = *p.proposeBlockTxGasLimit + + var ( + try = 0 + tx *types.Transaction + isReplacement bool + err error + ) + + for try < maxSendProposeBlockTxRetry { + if tx, err = sendProposeBlockTx(isReplacement); err != nil { + try += 1 + log.Warn("Failed to send propose block transaction, retrying", "error", err) + if strings.Contains(err.Error(), "replacement transaction underpriced") { + isReplacement = true + } else { + isReplacement = false + } + continue + } + + break } - proposeTx, err := p.rpc.TaikoL1.ProposeBlock(opts, inputs, txListBytes) if err != nil { - return encoding.TryParsingCustomError(err) + return err } ctxWithTimeout, cancel := context.WithTimeout(ctx, waitReceiptTimeout) defer cancel() - if _, err := rpc.WaitReceipt(ctxWithTimeout, p.rpc.L1, proposeTx); err != nil { + if _, err := rpc.WaitReceipt(ctxWithTimeout, p.rpc.L1, tx); err != nil { return err } From c515584f066b8d358aa1659cff8c3f49d2877b39 Mon Sep 17 00:00:00 2001 From: David Date: Wed, 19 Jul 2023 17:29:00 +0800 Subject: [PATCH 2/6] feat: add L2ContentFrom method --- cmd/flags/proposer.go | 8 +++ pkg/rpc/methods.go | 18 +++++++ pkg/rpc/methods_test.go | 37 +++++++++++++ pkg/rpc/utils.go | 24 +++++++++ proposer/config.go | 66 +++++++++++++---------- proposer/config_test.go | 3 ++ proposer/proposer.go | 111 +++++++++++++++++++++----------------- proposer/proposer_test.go | 30 ++++++++--- 8 files changed, 212 insertions(+), 85 deletions(-) diff --git a/cmd/flags/proposer.go b/cmd/flags/proposer.go index 9b2dac781..9a3c1414f 100644 --- a/cmd/flags/proposer.go +++ b/cmd/flags/proposer.go @@ -57,6 +57,13 @@ var ( } ProposeBlockTxGasLimit = &cli.Uint64Flag{ Name: "proposeBlockTxGasLimit", + Usage: "Gas limit will be used for TaikoL1.proposeBlock transactions", + Category: proposerCategory, + } + ProposeBlockTxReplacementMultiplier = &cli.Uint64Flag{ + Name: "proposeBlockTxReplacementMultiplier", + Value: 2, + Usage: "Gas tip multiplier when replacing a TaikoL1.proposeBlock transaction with same nonce", Category: proposerCategory, } ) @@ -73,4 +80,5 @@ var ProposerFlags = MergeFlags(CommonFlags, []cli.Flag{ MinBlockGasLimit, MaxProposedTxListsPerEpoch, ProposeBlockTxGasLimit, + ProposeBlockTxReplacementMultiplier, }) diff --git a/pkg/rpc/methods.go b/pkg/rpc/methods.go index eb309f45b..db6f74f68 100644 --- a/pkg/rpc/methods.go +++ b/pkg/rpc/methods.go @@ -237,6 +237,24 @@ func (c *Client) GetPoolContent( return result, err } +type AccountPoolContent map[string]map[string]*types.Transaction + +// L2ContentFrom fetches a given account's transactions list from L2 execution engine's transactions pool. +func (c *Client) L2ContentFrom( + ctx context.Context, + address common.Address, +) (AccountPoolContent, error) { + var result AccountPoolContent + err := c.L2RawRPC.CallContext( + ctx, + &result, + "txpool_contentFrom", + address, + ) + + return result, err +} + // L2AccountNonce fetches the nonce of the given L2 account at a specified height. func (c *Client) L2AccountNonce( ctx context.Context, diff --git a/pkg/rpc/methods_test.go b/pkg/rpc/methods_test.go index 506e2eb8a..b55a640af 100644 --- a/pkg/rpc/methods_test.go +++ b/pkg/rpc/methods_test.go @@ -2,9 +2,12 @@ package rpc import ( "context" + "os" "testing" "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/crypto" "github.com/stretchr/testify/require" ) @@ -63,3 +66,37 @@ func TestGetProtocolStateVariables(t *testing.T) { _, err := client.GetProtocolStateVariables(nil) require.Nil(t, err) } + +func TestL2ContentFrom(t *testing.T) { + client := newTestClient(t) + l2Head, err := client.L2.HeaderByNumber(context.Background(), nil) + require.Nil(t, err) + + baseFee, err := client.TaikoL2.GetBasefee(nil, 0, 60000000, uint32(l2Head.GasUsed)) + require.Nil(t, err) + + testAddrPrivKey, err := crypto.ToECDSA(common.Hex2Bytes(os.Getenv("L1_PROPOSER_PRIVATE_KEY"))) + require.Nil(t, err) + + testAddr := crypto.PubkeyToAddress(testAddrPrivKey.PublicKey) + + nonce, err := client.L2.PendingNonceAt(context.Background(), testAddr) + require.Nil(t, err) + + tx := types.NewTransaction( + nonce, + testAddr, + common.Big1, + 100000, + baseFee, + []byte{}, + ) + signedTx, err := types.SignTx(tx, types.LatestSignerForChainID(client.L2ChainID), testAddrPrivKey) + require.Nil(t, err) + require.Nil(t, client.L2.SendTransaction(context.Background(), signedTx)) + + content, err := client.L2ContentFrom(context.Background(), testAddr) + require.Nil(t, err) + + require.NotZero(t, len(content["pending"])) +} diff --git a/pkg/rpc/utils.go b/pkg/rpc/utils.go index 3ba7f0191..74388bd81 100644 --- a/pkg/rpc/utils.go +++ b/pkg/rpc/utils.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "math/big" + "strconv" "strings" "time" @@ -119,6 +120,29 @@ func NeedNewProof( return false, nil } +// GetPendingTxByNonce tries to retrieve a pending transaction with a given nonce in a node's mempool. +func GetPendingTxByNonce( + ctx context.Context, + cli *Client, + address common.Address, + nonce uint64, +) (*types.Transaction, error) { + content, err := cli.L2ContentFrom(ctx, address) + if err != nil { + return nil, err + } + + for _, txMap := range content { + for txNonce, tx := range txMap { + if txNonce == strconv.Itoa(int(nonce)) { + return tx, nil + } + } + } + + return nil, nil +} + // SetHead makes a `debug_setHead` RPC call to set the chain's head, should only be used // for testing purpose. func SetHead(ctx context.Context, rpc *rpc.Client, headNum *big.Int) error { diff --git a/proposer/config.go b/proposer/config.go index 91502d2a3..9db37f7f9 100644 --- a/proposer/config.go +++ b/proposer/config.go @@ -14,20 +14,21 @@ 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 - CommitSlot uint64 - LocalAddresses []common.Address - ProposeEmptyBlocksInterval *time.Duration - MinBlockGasLimit uint64 - MaxProposedTxListsPerEpoch uint64 - ProposeBlockTxGasLimit *uint64 - BackOffRetryInterval time.Duration + L1Endpoint string + L2Endpoint string + TaikoL1Address common.Address + TaikoL2Address common.Address + L1ProposerPrivKey *ecdsa.PrivateKey + L2SuggestedFeeRecipient common.Address + ProposeInterval *time.Duration + CommitSlot uint64 + LocalAddresses []common.Address + ProposeEmptyBlocksInterval *time.Duration + MinBlockGasLimit uint64 + MaxProposedTxListsPerEpoch uint64 + ProposeBlockTxGasLimit *uint64 + BackOffRetryInterval time.Duration + ProposeBlockTxReplacementMultiplier uint64 } // NewConfigFromCliContext initializes a Config instance from @@ -80,20 +81,29 @@ func NewConfigFromCliContext(c *cli.Context) (*Config, error) { proposeBlockTxGasLimit = &gasLimit } + proposeBlockTxReplacementMultiplier := c.Uint64(flags.ProposeBlockTxReplacementMultiplier.Name) + if proposeBlockTxReplacementMultiplier == 0 { + return nil, fmt.Errorf( + "invalid --proposeBlockTxReplacementMultiplier value: %d", + proposeBlockTxReplacementMultiplier, + ) + } + 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, - CommitSlot: c.Uint64(flags.CommitSlot.Name), - LocalAddresses: localAddresses, - ProposeEmptyBlocksInterval: proposeEmptyBlocksInterval, - MinBlockGasLimit: c.Uint64(flags.MinBlockGasLimit.Name), - MaxProposedTxListsPerEpoch: c.Uint64(flags.MaxProposedTxListsPerEpoch.Name), - ProposeBlockTxGasLimit: proposeBlockTxGasLimit, - BackOffRetryInterval: time.Duration(c.Uint64(flags.BackOffRetryInterval.Name)) * time.Second, + 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, + CommitSlot: c.Uint64(flags.CommitSlot.Name), + LocalAddresses: localAddresses, + ProposeEmptyBlocksInterval: proposeEmptyBlocksInterval, + MinBlockGasLimit: c.Uint64(flags.MinBlockGasLimit.Name), + MaxProposedTxListsPerEpoch: c.Uint64(flags.MaxProposedTxListsPerEpoch.Name), + ProposeBlockTxGasLimit: proposeBlockTxGasLimit, + BackOffRetryInterval: time.Duration(c.Uint64(flags.BackOffRetryInterval.Name)) * time.Second, + ProposeBlockTxReplacementMultiplier: proposeBlockTxReplacementMultiplier, }, nil } diff --git a/proposer/config_test.go b/proposer/config_test.go index 6615b2e2b..0a2fd4aea 100644 --- a/proposer/config_test.go +++ b/proposer/config_test.go @@ -36,6 +36,7 @@ func (s *ProposerTestSuite) TestNewConfigFromCliContext() { &cli.StringFlag{Name: flags.ProposeInterval.Name}, &cli.Uint64Flag{Name: flags.CommitSlot.Name}, &cli.StringFlag{Name: flags.TxPoolLocals.Name}, + &cli.Uint64Flag{Name: flags.ProposeBlockTxReplacementMultiplier.Name}, } app.Action = func(ctx *cli.Context) error { c, err := NewConfigFromCliContext(ctx) @@ -50,6 +51,7 @@ func (s *ProposerTestSuite) TestNewConfigFromCliContext() { s.Equal(uint64(commitSlot), c.CommitSlot) s.Equal(1, len(c.LocalAddresses)) s.Equal(goldenTouchAddress, c.LocalAddresses[0]) + s.Equal(uint64(5), c.ProposeBlockTxReplacementMultiplier) s.Nil(new(Proposer).InitFromCli(context.Background(), ctx)) return err @@ -66,5 +68,6 @@ func (s *ProposerTestSuite) TestNewConfigFromCliContext() { "-" + flags.ProposeInterval.Name, proposeInterval, "-" + flags.CommitSlot.Name, strconv.Itoa(commitSlot), "-" + flags.TxPoolLocals.Name, goldenTouchAddress.Hex(), + "-" + flags.ProposeBlockTxReplacementMultiplier.Name, "5", })) } diff --git a/proposer/proposer.go b/proposer/proposer.go index 831e255cf..dc18f0125 100644 --- a/proposer/proposer.go +++ b/proposer/proposer.go @@ -12,6 +12,7 @@ import ( "sync" "time" + "github.com/cenkalti/backoff/v4" "github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" @@ -31,7 +32,6 @@ var ( errNoNewTxs = errors.New("no new transactions") waitReceiptTimeout = 1 * time.Minute maxSendProposeBlockTxRetry = 10 - txReplacementTipMultiplier = 2 ) // Proposer keep proposing new transactions from L2 execution engine's tx pool at a fixed interval. @@ -53,6 +53,7 @@ type Proposer struct { minBlockGasLimit *uint64 maxProposedTxListsPerEpoch uint64 proposeBlockTxGasLimit *uint64 + txReplacementTipMultiplier uint64 // Protocol configurations protocolConfigs *bindings.TaikoDataConfig @@ -87,6 +88,7 @@ func InitFromConfig(ctx context.Context, p *Proposer, cfg *Config) (err error) { p.locals = cfg.LocalAddresses p.commitSlot = cfg.CommitSlot p.maxProposedTxListsPerEpoch = cfg.MaxProposedTxListsPerEpoch + p.txReplacementTipMultiplier = cfg.ProposeBlockTxReplacementMultiplier p.ctx = ctx // RPC clients @@ -273,70 +275,81 @@ func (p *Proposer) ProposeOp(ctx context.Context) error { return nil } -// ProposeTxList proposes the given transactions list to TaikoL1 smart contract. -func (p *Proposer) ProposeTxList( +// sendProposeBlockTx tries to send a TaikoL1.proposeBlock transaction. +func (p *Proposer) sendProposeBlockTx( ctx context.Context, meta *encoding.TaikoL1BlockMetadataInput, txListBytes []byte, - txNum uint, nonce *uint64, -) error { - sendProposeBlockTx := func(isReplacement bool) (*types.Transaction, error) { - if p.minBlockGasLimit != nil && meta.GasLimit < uint32(*p.minBlockGasLimit) { - meta.GasLimit = uint32(*p.minBlockGasLimit) - } - - // Propose the transactions list - inputs, err := encoding.EncodeProposeBlockInput(meta) - if err != nil { - return nil, err - } - - opts, err := getTxOpts(ctx, p.rpc.L1, p.l1ProposerPrivKey, p.rpc.L1ChainID) - if err != nil { - return nil, err - } - if nonce != nil { - opts.Nonce = new(big.Int).SetUint64(*nonce) - } - if p.proposeBlockTxGasLimit != nil { - opts.GasLimit = *p.proposeBlockTxGasLimit - } - - if isReplacement { - opts.GasTipCap = new(big.Int).Mul(opts.GasTipCap, new(big.Int).SetUint64(uint64(txReplacementTipMultiplier))) - } + isReplacement bool, +) (*types.Transaction, error) { + if p.minBlockGasLimit != nil && meta.GasLimit < uint32(*p.minBlockGasLimit) { + meta.GasLimit = uint32(*p.minBlockGasLimit) + } - proposeTx, err := p.rpc.TaikoL1.ProposeBlock(opts, inputs, txListBytes) - if err != nil { - return nil, encoding.TryParsingCustomError(err) - } + // Propose the transactions list + inputs, err := encoding.EncodeProposeBlockInput(meta) + if err != nil { + return nil, err + } + opts, err := getTxOpts(ctx, p.rpc.L1, p.l1ProposerPrivKey, p.rpc.L1ChainID) + if err != nil { + return nil, err + } + if nonce != nil { + opts.Nonce = new(big.Int).SetUint64(*nonce) + } + if p.proposeBlockTxGasLimit != nil { + opts.GasLimit = *p.proposeBlockTxGasLimit + } + if isReplacement { + opts.GasTipCap = new(big.Int).Mul(opts.GasTipCap, new(big.Int).SetUint64(uint64(p.txReplacementTipMultiplier))) + } - return proposeTx, nil + proposeTx, err := p.rpc.TaikoL1.ProposeBlock(opts, inputs, txListBytes) + if err != nil { + return nil, encoding.TryParsingCustomError(err) } + return proposeTx, nil +} + +// ProposeTxList proposes the given transactions list to TaikoL1 smart contract. +func (p *Proposer) ProposeTxList( + ctx context.Context, + meta *encoding.TaikoL1BlockMetadataInput, + txListBytes []byte, + txNum uint, + nonce *uint64, +) error { var ( - try = 0 - tx *types.Transaction isReplacement bool + tx *types.Transaction err error ) - for try < maxSendProposeBlockTxRetry { - if tx, err = sendProposeBlockTx(isReplacement); err != nil { - try += 1 - log.Warn("Failed to send propose block transaction, retrying", "error", err) - if strings.Contains(err.Error(), "replacement transaction underpriced") { - isReplacement = true - } else { - isReplacement = false + backoff.Retry( + func() error { + if ctx.Err() != nil { + return nil + } + if tx, err = p.sendProposeBlockTx(ctx, meta, txListBytes, nonce, isReplacement); err != nil { + log.Warn("Failed to send propose block transaction, retrying", "error", err) + if strings.Contains(err.Error(), "replacement transaction underpriced") { + isReplacement = true + } else { + isReplacement = false + } + return err } - continue - } - break + return nil + }, + backoff.WithMaxRetries(backoff.NewExponentialBackOff(), uint64(maxSendProposeBlockTxRetry)), + ) + if ctx.Err() != nil { + return ctx.Err() } - if err != nil { return err } diff --git a/proposer/proposer_test.go b/proposer/proposer_test.go index f0d6fa2b4..ef1be4f7c 100644 --- a/proposer/proposer_test.go +++ b/proposer/proposer_test.go @@ -32,14 +32,15 @@ func (s *ProposerTestSuite) SetupTest() { ctx, cancel := context.WithCancel(context.Background()) proposeInterval := 1024 * time.Hour // No need to periodically propose transactions list in unit tests s.Nil(InitFromConfig(ctx, p, (&Config{ - L1Endpoint: os.Getenv("L1_NODE_WS_ENDPOINT"), - L2Endpoint: os.Getenv("L2_EXECUTION_ENGINE_HTTP_ENDPOINT"), - TaikoL1Address: common.HexToAddress(os.Getenv("TAIKO_L1_ADDRESS")), - TaikoL2Address: common.HexToAddress(os.Getenv("TAIKO_L2_ADDRESS")), - L1ProposerPrivKey: l1ProposerPrivKey, - L2SuggestedFeeRecipient: common.HexToAddress(os.Getenv("L2_SUGGESTED_FEE_RECIPIENT")), - ProposeInterval: &proposeInterval, - MaxProposedTxListsPerEpoch: 1, + L1Endpoint: os.Getenv("L1_NODE_WS_ENDPOINT"), + L2Endpoint: os.Getenv("L2_EXECUTION_ENGINE_HTTP_ENDPOINT"), + TaikoL1Address: common.HexToAddress(os.Getenv("TAIKO_L1_ADDRESS")), + TaikoL2Address: common.HexToAddress(os.Getenv("TAIKO_L2_ADDRESS")), + L1ProposerPrivKey: l1ProposerPrivKey, + L2SuggestedFeeRecipient: common.HexToAddress(os.Getenv("L2_SUGGESTED_FEE_RECIPIENT")), + ProposeInterval: &proposeInterval, + MaxProposedTxListsPerEpoch: 1, + ProposeBlockTxReplacementMultiplier: 2, }))) s.p = p @@ -130,6 +131,19 @@ func (s *ProposerTestSuite) TestCustomProposeOpHook() { s.True(flag) } +func (s *ProposerTestSuite) TestSendProposeBlockTx() { + tip, err := getTxOpts( + context.Background(), + s.p.rpc.L1, + s.p.l1ProposerPrivKey, + s.RpcClient.L1ChainID, + ) + s.Nil(err) + s.Greater(tip.GasTipCap.Uint64(), uint64(0)) + + // TODO: add more tests +} + func (s *ProposerTestSuite) TestUpdateProposingTicker() { oneHour := 1 * time.Hour s.p.proposingInterval = &oneHour From 3f26520ff84ff72c3e7fcb45c34860405f7795da Mon Sep 17 00:00:00 2001 From: David Date: Thu, 20 Jul 2023 16:47:30 +0800 Subject: [PATCH 3/6] test: more unit tests --- pkg/rpc/methods.go | 8 +++---- pkg/rpc/methods_test.go | 2 +- pkg/rpc/utils.go | 2 +- proposer/proposer.go | 25 +++++++++++++++++++- proposer/proposer_test.go | 48 +++++++++++++++++++++++++++++++++++---- testutils/suite.go | 4 ++++ 6 files changed, 77 insertions(+), 12 deletions(-) diff --git a/pkg/rpc/methods.go b/pkg/rpc/methods.go index db6f74f68..2987fd3ca 100644 --- a/pkg/rpc/methods.go +++ b/pkg/rpc/methods.go @@ -239,20 +239,18 @@ func (c *Client) GetPoolContent( type AccountPoolContent map[string]map[string]*types.Transaction -// L2ContentFrom fetches a given account's transactions list from L2 execution engine's transactions pool. -func (c *Client) L2ContentFrom( +// L1ContentFrom fetches a given account's transactions list from a L1 node's transactions pool. +func (c *Client) L1ContentFrom( ctx context.Context, address common.Address, ) (AccountPoolContent, error) { var result AccountPoolContent - err := c.L2RawRPC.CallContext( + return result, c.L1RawRPC.CallContext( ctx, &result, "txpool_contentFrom", address, ) - - return result, err } // L2AccountNonce fetches the nonce of the given L2 account at a specified height. diff --git a/pkg/rpc/methods_test.go b/pkg/rpc/methods_test.go index b55a640af..ac8f26f3c 100644 --- a/pkg/rpc/methods_test.go +++ b/pkg/rpc/methods_test.go @@ -95,7 +95,7 @@ func TestL2ContentFrom(t *testing.T) { require.Nil(t, err) require.Nil(t, client.L2.SendTransaction(context.Background(), signedTx)) - content, err := client.L2ContentFrom(context.Background(), testAddr) + content, err := client.L1ContentFrom(context.Background(), testAddr) require.Nil(t, err) require.NotZero(t, len(content["pending"])) diff --git a/pkg/rpc/utils.go b/pkg/rpc/utils.go index 74388bd81..13d6cff4b 100644 --- a/pkg/rpc/utils.go +++ b/pkg/rpc/utils.go @@ -127,7 +127,7 @@ func GetPendingTxByNonce( address common.Address, nonce uint64, ) (*types.Transaction, error) { - content, err := cli.L2ContentFrom(ctx, address) + content, err := cli.L1ContentFrom(ctx, address) if err != nil { return nil, err } diff --git a/proposer/proposer.go b/proposer/proposer.go index dc18f0125..6d4241ca5 100644 --- a/proposer/proposer.go +++ b/proposer/proposer.go @@ -303,7 +303,30 @@ func (p *Proposer) sendProposeBlockTx( opts.GasLimit = *p.proposeBlockTxGasLimit } if isReplacement { - opts.GasTipCap = new(big.Int).Mul(opts.GasTipCap, new(big.Int).SetUint64(uint64(p.txReplacementTipMultiplier))) + log.Info("Try replacing a transaction with same nonce", "sender", p.l1ProposerAddress, "nonce", nonce) + originalTx, err := rpc.GetPendingTxByNonce(ctx, p.rpc, p.l1ProposerAddress, *nonce) + if err != nil || originalTx == nil { + log.Warn( + "Original transaction not found", + "sender", p.l1ProposerAddress, + "nonce", nonce, + "error", err, + ) + + opts.GasTipCap = new(big.Int).Mul(opts.GasTipCap, new(big.Int).SetUint64(uint64(p.txReplacementTipMultiplier))) + } else { + log.Info( + "Original transaction to replace", + "sender", p.l1ProposerAddress, + "nonce", nonce, + "tx", originalTx, + ) + + opts.GasTipCap = new(big.Int).Mul( + originalTx.GasTipCap(), + new(big.Int).SetUint64(uint64(p.txReplacementTipMultiplier)), + ) + } } proposeTx, err := p.rpc.TaikoL1.ProposeBlock(opts, inputs, txListBytes) diff --git a/proposer/proposer_test.go b/proposer/proposer_test.go index ef1be4f7c..23659b9ce 100644 --- a/proposer/proposer_test.go +++ b/proposer/proposer_test.go @@ -10,8 +10,10 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/rlp" "github.com/stretchr/testify/suite" "github.com/taikoxyz/taiko-client/bindings" + "github.com/taikoxyz/taiko-client/bindings/encoding" "github.com/taikoxyz/taiko-client/testutils" ) @@ -63,7 +65,7 @@ func (s *ProposerTestSuite) TestName() { func (s *ProposerTestSuite) TestProposeOp() { // Nothing to propose - s.EqualError(errNoNewTxs, s.p.ProposeOp(context.Background()).Error()) + // s.EqualError(errNoNewTxs, s.p.ProposeOp(context.Background()).Error()) // Propose txs in L2 execution engine's mempool sink := make(chan *bindings.TaikoL1ClientBlockProposed) @@ -132,16 +134,54 @@ func (s *ProposerTestSuite) TestCustomProposeOpHook() { } func (s *ProposerTestSuite) TestSendProposeBlockTx() { - tip, err := getTxOpts( + opts, err := getTxOpts( context.Background(), s.p.rpc.L1, s.p.l1ProposerPrivKey, s.RpcClient.L1ChainID, ) s.Nil(err) - s.Greater(tip.GasTipCap.Uint64(), uint64(0)) + s.Greater(opts.GasTipCap.Uint64(), uint64(0)) - // TODO: add more tests + nonce, err := s.RpcClient.L1.PendingNonceAt(context.Background(), s.p.l1ProposerAddress) + s.Nil(err) + + tx := types.NewTransaction( + nonce, + common.BytesToAddress([]byte{}), + common.Big1, + 100000, + opts.GasTipCap, + []byte{}, + ) + + s.SetL1Automine(false) + defer s.SetL1Automine(true) + + signedTx, err := types.SignTx(tx, types.LatestSignerForChainID(s.RpcClient.L1ChainID), s.p.l1ProposerPrivKey) + s.Nil(err) + s.Nil(s.RpcClient.L1.SendTransaction(context.Background(), signedTx)) + + var emptyTxs []types.Transaction + encoded, err := rlp.EncodeToBytes(emptyTxs) + s.Nil(err) + + newTx, err := s.p.sendProposeBlockTx( + context.Background(), + &encoding.TaikoL1BlockMetadataInput{ + Beneficiary: s.p.L2SuggestedFeeRecipient(), + GasLimit: 21000, + TxListHash: crypto.Keccak256Hash(encoded), + TxListByteStart: common.Big0, + TxListByteEnd: new(big.Int).SetUint64(uint64(len(encoded))), + CacheTxListInfo: 0, + }, + encoded, + &nonce, + true, + ) + s.Nil(err) + s.Greater(newTx.GasTipCap().Uint64(), tx.GasTipCap().Uint64()) } func (s *ProposerTestSuite) TestUpdateProposingTicker() { diff --git a/testutils/suite.go b/testutils/suite.go index 4af00825e..e7828854b 100644 --- a/testutils/suite.go +++ b/testutils/suite.go @@ -118,3 +118,7 @@ func (s *ClientTestSuite) TearDownTest() { s.Nil(rpc.SetHead(context.Background(), s.RpcClient.L2RawRPC, common.Big0)) } + +func (s *ClientTestSuite) SetL1Automine(automine bool) { + s.Nil(s.RpcClient.L1RawRPC.CallContext(context.Background(), nil, "evm_setAutomine", automine)) +} From ac6b4c035b868f3ac7308f0af5cddc3485b29598 Mon Sep 17 00:00:00 2001 From: David Date: Thu, 20 Jul 2023 17:21:03 +0800 Subject: [PATCH 4/6] test: update tests --- pkg/rpc/methods.go | 16 ---------------- pkg/rpc/methods_test.go | 37 ------------------------------------- pkg/rpc/utils.go | 19 ++++++++++++++++++- pkg/rpc/utils_test.go | 38 ++++++++++++++++++++++++++++++++++++++ proposer/proposer_test.go | 2 +- 5 files changed, 57 insertions(+), 55 deletions(-) diff --git a/pkg/rpc/methods.go b/pkg/rpc/methods.go index 2987fd3ca..eb309f45b 100644 --- a/pkg/rpc/methods.go +++ b/pkg/rpc/methods.go @@ -237,22 +237,6 @@ func (c *Client) GetPoolContent( return result, err } -type AccountPoolContent map[string]map[string]*types.Transaction - -// L1ContentFrom fetches a given account's transactions list from a L1 node's transactions pool. -func (c *Client) L1ContentFrom( - ctx context.Context, - address common.Address, -) (AccountPoolContent, error) { - var result AccountPoolContent - return result, c.L1RawRPC.CallContext( - ctx, - &result, - "txpool_contentFrom", - address, - ) -} - // L2AccountNonce fetches the nonce of the given L2 account at a specified height. func (c *Client) L2AccountNonce( ctx context.Context, diff --git a/pkg/rpc/methods_test.go b/pkg/rpc/methods_test.go index ac8f26f3c..506e2eb8a 100644 --- a/pkg/rpc/methods_test.go +++ b/pkg/rpc/methods_test.go @@ -2,12 +2,9 @@ package rpc import ( "context" - "os" "testing" "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/core/types" - "github.com/ethereum/go-ethereum/crypto" "github.com/stretchr/testify/require" ) @@ -66,37 +63,3 @@ func TestGetProtocolStateVariables(t *testing.T) { _, err := client.GetProtocolStateVariables(nil) require.Nil(t, err) } - -func TestL2ContentFrom(t *testing.T) { - client := newTestClient(t) - l2Head, err := client.L2.HeaderByNumber(context.Background(), nil) - require.Nil(t, err) - - baseFee, err := client.TaikoL2.GetBasefee(nil, 0, 60000000, uint32(l2Head.GasUsed)) - require.Nil(t, err) - - testAddrPrivKey, err := crypto.ToECDSA(common.Hex2Bytes(os.Getenv("L1_PROPOSER_PRIVATE_KEY"))) - require.Nil(t, err) - - testAddr := crypto.PubkeyToAddress(testAddrPrivKey.PublicKey) - - nonce, err := client.L2.PendingNonceAt(context.Background(), testAddr) - require.Nil(t, err) - - tx := types.NewTransaction( - nonce, - testAddr, - common.Big1, - 100000, - baseFee, - []byte{}, - ) - signedTx, err := types.SignTx(tx, types.LatestSignerForChainID(client.L2ChainID), testAddrPrivKey) - require.Nil(t, err) - require.Nil(t, client.L2.SendTransaction(context.Background(), signedTx)) - - content, err := client.L1ContentFrom(context.Background(), testAddr) - require.Nil(t, err) - - require.NotZero(t, len(content["pending"])) -} diff --git a/pkg/rpc/utils.go b/pkg/rpc/utils.go index 13d6cff4b..575ae83ca 100644 --- a/pkg/rpc/utils.go +++ b/pkg/rpc/utils.go @@ -120,6 +120,23 @@ func NeedNewProof( return false, nil } +type AccountPoolContent map[string]map[string]*types.Transaction + +// ContentFrom fetches a given account's transactions list from a node's transactions pool. +func ContentFrom( + ctx context.Context, + rawRPC *rpc.Client, + address common.Address, +) (AccountPoolContent, error) { + var result AccountPoolContent + return result, rawRPC.CallContext( + ctx, + &result, + "txpool_contentFrom", + address, + ) +} + // GetPendingTxByNonce tries to retrieve a pending transaction with a given nonce in a node's mempool. func GetPendingTxByNonce( ctx context.Context, @@ -127,7 +144,7 @@ func GetPendingTxByNonce( address common.Address, nonce uint64, ) (*types.Transaction, error) { - content, err := cli.L1ContentFrom(ctx, address) + content, err := ContentFrom(ctx, cli.L1RawRPC, address) if err != nil { return nil, err } diff --git a/pkg/rpc/utils_test.go b/pkg/rpc/utils_test.go index 1fbc59fd9..4eb9911fc 100644 --- a/pkg/rpc/utils_test.go +++ b/pkg/rpc/utils_test.go @@ -2,11 +2,14 @@ package rpc import ( "context" + "os" + "strconv" "testing" "time" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/crypto" "github.com/stretchr/testify/require" ) @@ -31,3 +34,38 @@ func TestStringToBytes32(t *testing.T) { require.Equal(t, [32]byte{}, StringToBytes32("")) require.Equal(t, [32]byte{0x61, 0x62, 0x63}, StringToBytes32("abc")) } + +func TestL1ContentFrom(t *testing.T) { + client := newTestClient(t) + l2Head, err := client.L2.HeaderByNumber(context.Background(), nil) + require.Nil(t, err) + + baseFee, err := client.TaikoL2.GetBasefee(nil, 0, 60000000, uint32(l2Head.GasUsed)) + require.Nil(t, err) + + testAddrPrivKey, err := crypto.ToECDSA(common.Hex2Bytes(os.Getenv("L1_PROPOSER_PRIVATE_KEY"))) + require.Nil(t, err) + + testAddr := crypto.PubkeyToAddress(testAddrPrivKey.PublicKey) + + nonce, err := client.L2.PendingNonceAt(context.Background(), testAddr) + require.Nil(t, err) + + tx := types.NewTransaction( + nonce, + testAddr, + common.Big1, + 100000, + baseFee, + []byte{}, + ) + signedTx, err := types.SignTx(tx, types.LatestSignerForChainID(client.L2ChainID), testAddrPrivKey) + require.Nil(t, err) + require.Nil(t, client.L2.SendTransaction(context.Background(), signedTx)) + + content, err := ContentFrom(context.Background(), client.L2RawRPC, testAddr) + require.Nil(t, err) + + require.NotZero(t, len(content["pending"])) + require.Equal(t, signedTx.Nonce(), content["pending"][strconv.Itoa(int(signedTx.Nonce()))].Nonce()) +} diff --git a/proposer/proposer_test.go b/proposer/proposer_test.go index 23659b9ce..d62738b6e 100644 --- a/proposer/proposer_test.go +++ b/proposer/proposer_test.go @@ -65,7 +65,7 @@ func (s *ProposerTestSuite) TestName() { func (s *ProposerTestSuite) TestProposeOp() { // Nothing to propose - // s.EqualError(errNoNewTxs, s.p.ProposeOp(context.Background()).Error()) + s.EqualError(errNoNewTxs, s.p.ProposeOp(context.Background()).Error()) // Propose txs in L2 execution engine's mempool sink := make(chan *bindings.TaikoL1ClientBlockProposed) From 266052cc0f477084e2c83273603d0677ddd8067d Mon Sep 17 00:00:00 2001 From: David Date: Thu, 20 Jul 2023 17:24:29 +0800 Subject: [PATCH 5/6] test: update tests --- proposer/proposer_test.go | 3 --- 1 file changed, 3 deletions(-) diff --git a/proposer/proposer_test.go b/proposer/proposer_test.go index d62738b6e..8fa7c1b19 100644 --- a/proposer/proposer_test.go +++ b/proposer/proposer_test.go @@ -64,9 +64,6 @@ func (s *ProposerTestSuite) TestName() { } func (s *ProposerTestSuite) TestProposeOp() { - // Nothing to propose - s.EqualError(errNoNewTxs, s.p.ProposeOp(context.Background()).Error()) - // Propose txs in L2 execution engine's mempool sink := make(chan *bindings.TaikoL1ClientBlockProposed) From 09d0e273b3055c848b55b7261652d9f939f5e2bd Mon Sep 17 00:00:00 2001 From: David Date: Thu, 20 Jul 2023 19:53:30 +0800 Subject: [PATCH 6/6] fix: fix lint errors --- proposer/proposer.go | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/proposer/proposer.go b/proposer/proposer.go index 6d4241ca5..2c228da3a 100644 --- a/proposer/proposer.go +++ b/proposer/proposer.go @@ -313,7 +313,7 @@ func (p *Proposer) sendProposeBlockTx( "error", err, ) - opts.GasTipCap = new(big.Int).Mul(opts.GasTipCap, new(big.Int).SetUint64(uint64(p.txReplacementTipMultiplier))) + opts.GasTipCap = new(big.Int).Mul(opts.GasTipCap, new(big.Int).SetUint64(p.txReplacementTipMultiplier)) } else { log.Info( "Original transaction to replace", @@ -324,7 +324,7 @@ func (p *Proposer) sendProposeBlockTx( opts.GasTipCap = new(big.Int).Mul( originalTx.GasTipCap(), - new(big.Int).SetUint64(uint64(p.txReplacementTipMultiplier)), + new(big.Int).SetUint64(p.txReplacementTipMultiplier), ) } } @@ -350,8 +350,7 @@ func (p *Proposer) ProposeTxList( tx *types.Transaction err error ) - - backoff.Retry( + if err := backoff.Retry( func() error { if ctx.Err() != nil { return nil @@ -369,7 +368,9 @@ func (p *Proposer) ProposeTxList( return nil }, backoff.WithMaxRetries(backoff.NewExponentialBackOff(), uint64(maxSendProposeBlockTxRetry)), - ) + ); err != nil { + return err + } if ctx.Err() != nil { return ctx.Err() }