From cd2620486c8cd8e1c4f293036e4afc3dabec46cb Mon Sep 17 00:00:00 2001 From: maskpp Date: Fri, 5 Apr 2024 10:57:16 +0800 Subject: [PATCH] feat(proposer): add more tests for propsoer (#686) --- driver/chain_syncer/calldata/syncer.go | 2 +- driver/txlist_fetcher/blob.go | 8 +- internal/testutils/helper.go | 52 ++++++++ proposer/proposer_test.go | 172 +++++++++++++++++++++---- 4 files changed, 207 insertions(+), 27 deletions(-) diff --git a/driver/chain_syncer/calldata/syncer.go b/driver/chain_syncer/calldata/syncer.go index 36480e9a6..7fa5ec4f8 100644 --- a/driver/chain_syncer/calldata/syncer.go +++ b/driver/chain_syncer/calldata/syncer.go @@ -239,7 +239,7 @@ func (s *Syncer) onBlockProposed( // Decode transactions list. var txListDecoder txlistfetcher.TxListFetcher if event.Meta.BlobUsed { - txListDecoder = txlistfetcher.NewBlobTxListFetcher(s.rpc) + txListDecoder = txlistfetcher.NewBlobTxListFetcher(s.rpc.L1Beacon) } else { txListDecoder = new(txlistfetcher.CalldataFetcher) } diff --git a/driver/txlist_fetcher/blob.go b/driver/txlist_fetcher/blob.go index 4acc4d5d7..9cb0897e9 100644 --- a/driver/txlist_fetcher/blob.go +++ b/driver/txlist_fetcher/blob.go @@ -16,12 +16,12 @@ import ( // BlobFetcher is responsible for fetching the txList blob from the L1 block sidecar. type BlobFetcher struct { - rpc *rpc.Client + l1Beacon *rpc.BeaconClient } // NewBlobTxListFetcher creates a new BlobFetcher instance based on the given rpc client. -func NewBlobTxListFetcher(rpc *rpc.Client) *BlobFetcher { - return &BlobFetcher{rpc} +func NewBlobTxListFetcher(l1Beacon *rpc.BeaconClient) *BlobFetcher { + return &BlobFetcher{l1Beacon} } // Fetch implements the TxListFetcher interface. @@ -35,7 +35,7 @@ func (d *BlobFetcher) Fetch( } // Fetch the L1 block sidecars. - sidecars, err := d.rpc.L1Beacon.GetBlobs(ctx, meta.Timestamp) + sidecars, err := d.l1Beacon.GetBlobs(ctx, meta.Timestamp) if err != nil { return nil, err } diff --git a/internal/testutils/helper.go b/internal/testutils/helper.go index a24687c77..6f6418498 100644 --- a/internal/testutils/helper.go +++ b/internal/testutils/helper.go @@ -6,12 +6,14 @@ import ( "crypto/rand" "errors" "fmt" + "math/big" "net/http" "net/url" "os" "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/common/hexutil" "github.com/ethereum/go-ethereum/core/types" @@ -21,6 +23,7 @@ import ( "github.com/phayes/freeport" "github.com/taikoxyz/taiko-client/bindings" + "github.com/taikoxyz/taiko-client/pkg/rpc" "github.com/taikoxyz/taiko-client/prover/server" ) @@ -305,3 +308,52 @@ func LocalRandomProverEndpoint() *url.URL { func SignatureFromRSV(r, s string, v byte) []byte { return append(append(hexutil.MustDecode(r), hexutil.MustDecode(s)...), v) } + +// SendDynamicFeeTx sends a dynamic transaction, used for tests. +func SendDynamicFeeTx( + client *rpc.EthClient, + priv *ecdsa.PrivateKey, + to *common.Address, + value *big.Int, + data []byte, +) (*types.Transaction, error) { + head, err := client.HeaderByNumber(context.Background(), nil) + if err != nil { + return nil, err + } + + auth, err := bind.NewKeyedTransactorWithChainID(priv, client.ChainID) + if err != nil { + return nil, err + } + + nonce, err := client.PendingNonceAt(context.Background(), auth.From) + if err != nil { + return nil, err + } + + gasTipCap, err := client.SuggestGasTipCap(context.Background()) + if err != nil { + return nil, err + } + + tx, err := auth.Signer(auth.From, types.NewTx(&types.DynamicFeeTx{ + To: to, + Nonce: nonce, + Value: value, + GasTipCap: gasTipCap, + GasFeeCap: new(big.Int).Add( + gasTipCap, + new(big.Int).Mul(head.BaseFee, big.NewInt(2)), + ), + Gas: 2100_000, + Data: data, + })) + if err != nil { + return nil, err + } + if err = client.SendTransaction(context.Background(), tx); err != nil { + return nil, err + } + return tx, nil +} diff --git a/proposer/proposer_test.go b/proposer/proposer_test.go index 84c2e5a72..62d72c31e 100644 --- a/proposer/proposer_test.go +++ b/proposer/proposer_test.go @@ -2,25 +2,34 @@ package proposer import ( "context" - "math/big" "os" "testing" "time" "github.com/ethereum-optimism/optimism/op-service/txmgr" "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/math" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/log" + "github.com/ethereum/go-ethereum/miner" + "github.com/ethereum/go-ethereum/rlp" "github.com/stretchr/testify/suite" "github.com/taikoxyz/taiko-client/bindings" + "github.com/taikoxyz/taiko-client/driver/chain_syncer/beaconsync" + "github.com/taikoxyz/taiko-client/driver/chain_syncer/calldata" + "github.com/taikoxyz/taiko-client/driver/state" + txlistfetcher "github.com/taikoxyz/taiko-client/driver/txlist_fetcher" "github.com/taikoxyz/taiko-client/internal/testutils" + "github.com/taikoxyz/taiko-client/internal/utils" "github.com/taikoxyz/taiko-client/pkg/jwt" "github.com/taikoxyz/taiko-client/pkg/rpc" ) type ProposerTestSuite struct { testutils.ClientTestSuite + s *calldata.Syncer p *Proposer cancel context.CancelFunc } @@ -28,6 +37,19 @@ type ProposerTestSuite struct { func (s *ProposerTestSuite) SetupTest() { s.ClientTestSuite.SetupTest() + state2, err := state.New(context.Background(), s.RPCClient) + s.Nil(err) + + syncer, err := calldata.NewSyncer( + context.Background(), + s.RPCClient, + state2, + beaconsync.NewSyncProgressTracker(s.RPCClient.L2, 1*time.Hour), + 0, + ) + s.Nil(err) + s.s = syncer + l1ProposerPrivKey, err := crypto.ToECDSA(common.FromHex(os.Getenv("L1_PROPOSER_PRIVATE_KEY"))) s.Nil(err) @@ -82,6 +104,132 @@ func (s *ProposerTestSuite) SetupTest() { s.cancel = cancel } +func parseTxs(client *rpc.Client, event *bindings.TaikoL1ClientBlockProposed) (types.Transactions, error) { + tx, err := client.L1.TransactionInBlock(context.Background(), event.Raw.BlockHash, event.Raw.TxIndex) + if err != nil { + return nil, err + } + + // Decode transactions list. + var txListDecoder txlistfetcher.TxListFetcher + if event.Meta.BlobUsed { + txListDecoder = txlistfetcher.NewBlobTxListFetcher(client.L1Beacon) + } else { + txListDecoder = new(txlistfetcher.CalldataFetcher) + } + txListBytes, err := txListDecoder.Fetch(context.Background(), tx, &event.Meta) + if err != nil { + return nil, err + } + + txListBytes, err = utils.Decompress(txListBytes) + if err != nil { + return nil, err + } + + var txs types.Transactions + return txs, rlp.DecodeBytes(txListBytes, &txs) +} + +func (s *ProposerTestSuite) getLatestProposedTxs( + n int, + timeout time.Duration, +) (<-chan []types.Transactions, error) { + sink := make(chan *bindings.TaikoL1ClientBlockProposed) + sub, err := s.p.rpc.TaikoL1.WatchBlockProposed(nil, sink, nil, nil) + if err != nil { + return nil, err + } + + var resCh = make(chan []types.Transactions, 1) + go func() { + defer sub.Unsubscribe() + + txLst := make([]types.Transactions, 0, n) + tick := time.After(timeout) + for len(txLst) < cap(txLst) { + select { + case event := <-sink: + txs, err := parseTxs(s.RPCClient, event) + if err != nil { + log.Error("failed to parse txs", "err", err) + } + txLst = append(txLst, txs) + case <-tick: + return + } + } + resCh <- txLst + }() + + return resCh, nil +} + +func (s *ProposerTestSuite) TestProposeOpNoEmptyBlock() { + defer s.Nil(s.s.ProcessL1Blocks(context.Background())) + + p := s.p + + batchSize := 100 + + var err error + for i := 0; i < batchSize; i++ { + to := common.BytesToAddress(testutils.RandomBytes(32)) + _, err = testutils.SendDynamicFeeTx(s.RPCClient.L2, s.TestAddrPrivKey, &to, nil, nil) + s.Nil(err) + } + + var preBuiltTxList []*miner.PreBuiltTxList + for i := 0; i < 3 && len(preBuiltTxList) == 0; i++ { + preBuiltTxList, err = s.RPCClient.GetPoolContent( + context.Background(), + p.proposerAddress, + p.protocolConfigs.BlockMaxGasLimit, + rpc.BlockMaxTxListBytes, + p.LocalAddresses, + p.MaxProposedTxListsPerEpoch, + ) + time.Sleep(time.Second) + } + s.Nil(err) + s.Equal(true, len(preBuiltTxList) > 0) + + txsCh, err := s.getLatestProposedTxs(len(preBuiltTxList), time.Minute) + s.Nil(err) + + var ( + blockMinGasLimit uint64 = math.MaxUint64 + blockMinTxListBytes uint64 = math.MaxUint64 + txLists = make([]types.Transactions, 0, len(preBuiltTxList)) + ) + for _, txs := range preBuiltTxList { + if txs.EstimatedGasUsed <= blockMinGasLimit { + blockMinGasLimit = txs.EstimatedGasUsed + } else { + break + } + if txs.BytesLength <= blockMinTxListBytes { + blockMinTxListBytes = txs.BytesLength + } else { + break + } + txLists = append(txLists, txs.TxList) + } + + // Start proposer + p.LocalAddressesOnly = false + p.MinGasUsed = blockMinGasLimit + p.MinTxListBytes = blockMinTxListBytes + p.ProposeInterval = time.Second + p.MinProposingInternal = time.Minute + s.Nil(p.ProposeOp(context.Background())) + + txs := <-txsCh + for i := 0; i < len(txLists); i++ { + s.Equal(txLists[i].Len(), txs[i].Len()) + } +} + func (s *ProposerTestSuite) TestName() { s.Equal("proposer", s.p.Name()) } @@ -97,29 +245,9 @@ func (s *ProposerTestSuite) TestProposeOp() { close(sink) }() - nonce, err := s.p.rpc.L2.PendingNonceAt(context.Background(), s.TestAddr) - s.Nil(err) - - parent, err := s.p.rpc.L2.BlockByNumber(context.Background(), nil) - s.Nil(err) - - baseFeeInfo, err := s.p.rpc.TaikoL2.GetBasefee(nil, 1, uint32(parent.GasUsed())) - s.Nil(err) - to := common.BytesToAddress(testutils.RandomBytes(32)) - tx := types.NewTx(&types.DynamicFeeTx{ - ChainID: s.RPCClient.L2.ChainID, - Nonce: nonce, - GasTipCap: common.Big0, - GasFeeCap: new(big.Int).SetUint64(baseFeeInfo.Basefee.Uint64() * 2), - Gas: 21000, - To: &to, - Value: common.Big1, - }) - - signedTx, err := types.SignTx(tx, types.LatestSignerForChainID(s.p.rpc.L2.ChainID), s.TestAddrPrivKey) + _, err = testutils.SendDynamicFeeTx(s.p.rpc.L2, s.TestAddrPrivKey, &to, common.Big1, nil) s.Nil(err) - s.Nil(s.p.rpc.L2.SendTransaction(context.Background(), signedTx)) s.Nil(s.p.ProposeOp(context.Background()))