From 30f4f57f07438670c2bb7749f8fdff580342eff6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C3=A9ter=20Garamv=C3=B6lgyi?= Date: Wed, 15 Feb 2023 08:13:03 +0100 Subject: [PATCH] feat: add block transaction count limit (#216) * feat: add block transaction count limit * add tests * nit --- consensus/errors.go | 3 ++ core/block_validator.go | 3 ++ core/blockchain_test.go | 61 +++++++++++++++++++++++++++++++++++++++++ miner/worker.go | 5 ++++ params/config.go | 14 ++++++++-- 5 files changed, 83 insertions(+), 3 deletions(-) diff --git a/consensus/errors.go b/consensus/errors.go index ac5242fb54..a3c8f737f0 100644 --- a/consensus/errors.go +++ b/consensus/errors.go @@ -34,4 +34,7 @@ var ( // ErrInvalidNumber is returned if a block's number doesn't equal its parent's // plus one. ErrInvalidNumber = errors.New("invalid block number") + + // ErrInvalidTxCount is returned if a block contains too many transactions. + ErrInvalidTxCount = errors.New("invalid transaction count") ) diff --git a/core/block_validator.go b/core/block_validator.go index 856abe1cf9..01786e02ce 100644 --- a/core/block_validator.go +++ b/core/block_validator.go @@ -54,6 +54,9 @@ func (v *BlockValidator) ValidateBody(block *types.Block) error { if v.bc.HasBlockAndState(block.Hash(), block.NumberU64()) { return ErrKnownBlock } + if !v.config.IsValidTxCount(len(block.Transactions())) { + return consensus.ErrInvalidTxCount + } // Header validity is known at this point, check the uncles and transactions header := block.Header() if err := v.engine.VerifyUncles(v.bc, block); err != nil { diff --git a/core/blockchain_test.go b/core/blockchain_test.go index d6c6f7739f..46c3726346 100644 --- a/core/blockchain_test.go +++ b/core/blockchain_test.go @@ -3177,3 +3177,64 @@ func TestFeeVault(t *testing.T) { t.Fatalf("fee vault balance incorrect: expected %d, got %d", expected, actual) } } + +// TestTransactionCountLimit tests that the chain reject blocks with too many transactions. +func TestTransactionCountLimit(t *testing.T) { + // Create config that allows at most 1 transaction per block + config := params.TestChainConfig + config.MaxTxPerBlock = new(int) + *config.MaxTxPerBlock = 1 + + var ( + engine = ethash.NewFaker() + db = rawdb.NewMemoryDatabase() + key, _ = crypto.HexToECDSA("b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291") + address = crypto.PubkeyToAddress(key.PublicKey) + funds = big.NewInt(1000000000000000) + gspec = &Genesis{Config: config, Alloc: GenesisAlloc{address: {Balance: funds}}} + genesis = gspec.MustCommit(db) + ) + + addTx := func(b *BlockGen) { + tx := types.NewTransaction(b.TxNonce(address), address, big.NewInt(0), 50000, b.header.BaseFee, nil) + signed, _ := types.SignTx(tx, types.HomesteadSigner{}, key) + b.AddTx(signed) + } + + // Initialize blockchain + blockchain, err := NewBlockChain(db, nil, config, engine, vm.Config{}, nil, nil) + if err != nil { + t.Fatalf("failed to create new chain manager: %v", err) + } + defer blockchain.Stop() + + // Insert empty block + block1, _ := GenerateChain(config, genesis, ethash.NewFaker(), db, 1, func(i int, b *BlockGen) { + // empty + }) + + if _, err := blockchain.InsertChain(block1); err != nil { + t.Fatalf("failed to insert chain: %v", err) + } + + // Insert block with 1 transaction + block2, _ := GenerateChain(config, genesis, ethash.NewFaker(), db, 1, func(i int, b *BlockGen) { + addTx(b) + }) + + if _, err := blockchain.InsertChain(block2); err != nil { + t.Fatalf("failed to insert chain: %v", err) + } + + // Insert block with 2 transactions + block3, _ := GenerateChain(config, genesis, ethash.NewFaker(), db, 1, func(i int, b *BlockGen) { + addTx(b) + addTx(b) + }) + + _, err = blockchain.InsertChain(block3) + + if !errors.Is(err, consensus.ErrInvalidTxCount) { + t.Fatalf("error mismatch: have: %v, want: %v", err, consensus.ErrInvalidTxCount) + } +} diff --git a/miner/worker.go b/miner/worker.go index 08b13c0d5d..ff6df549fd 100644 --- a/miner/worker.go +++ b/miner/worker.go @@ -818,6 +818,11 @@ func (w *worker) commitTransactions(txs *types.TransactionsByPriceAndNonce, coin } return atomic.LoadInt32(interrupt) == commitInterruptNewHead } + // If we have collected enough transactions then we're done + if !w.chainConfig.IsValidTxCount(w.current.tcount + 1) { + log.Trace("Transaction count limit reached", "have", w.current.tcount, "want", w.chainConfig.MaxTxPerBlock) + break + } // If we don't have enough gas for any further transactions then we're done if w.current.gasPool.Gas() < params.TxGas { log.Trace("Not enough gas for further transactions", "have", w.current.gasPool, "want", params.TxGas) diff --git a/params/config.go b/params/config.go index ce9b3f533e..65a0ae8536 100644 --- a/params/config.go +++ b/params/config.go @@ -258,16 +258,16 @@ var ( // // This configuration is intentionally not using keyed fields to force anyone // adding flags to the config to also have to set these fields. - AllEthashProtocolChanges = &ChainConfig{big.NewInt(1337), big.NewInt(0), nil, false, big.NewInt(0), common.Hash{}, big.NewInt(0), big.NewInt(0), big.NewInt(0), big.NewInt(0), big.NewInt(0), big.NewInt(0), big.NewInt(0), big.NewInt(0), big.NewInt(0), big.NewInt(0), nil, new(EthashConfig), nil, false, nil, true, true} + AllEthashProtocolChanges = &ChainConfig{big.NewInt(1337), big.NewInt(0), nil, false, big.NewInt(0), common.Hash{}, big.NewInt(0), big.NewInt(0), big.NewInt(0), big.NewInt(0), big.NewInt(0), big.NewInt(0), big.NewInt(0), big.NewInt(0), big.NewInt(0), big.NewInt(0), nil, new(EthashConfig), nil, false, nil, true, true, nil} // AllCliqueProtocolChanges contains every protocol change (EIPs) introduced // and accepted by the Ethereum core developers into the Clique consensus. // // This configuration is intentionally not using keyed fields to force anyone // adding flags to the config to also have to set these fields. - AllCliqueProtocolChanges = &ChainConfig{big.NewInt(1337), big.NewInt(0), nil, false, big.NewInt(0), common.Hash{}, big.NewInt(0), big.NewInt(0), big.NewInt(0), big.NewInt(0), big.NewInt(0), big.NewInt(0), big.NewInt(0), big.NewInt(0), big.NewInt(0), nil, nil, nil, &CliqueConfig{Period: 0, Epoch: 30000}, false, nil, true, true} + AllCliqueProtocolChanges = &ChainConfig{big.NewInt(1337), big.NewInt(0), nil, false, big.NewInt(0), common.Hash{}, big.NewInt(0), big.NewInt(0), big.NewInt(0), big.NewInt(0), big.NewInt(0), big.NewInt(0), big.NewInt(0), big.NewInt(0), big.NewInt(0), nil, nil, nil, &CliqueConfig{Period: 0, Epoch: 30000}, false, nil, true, true, nil} - TestChainConfig = &ChainConfig{big.NewInt(1), big.NewInt(0), nil, false, big.NewInt(0), common.Hash{}, big.NewInt(0), big.NewInt(0), big.NewInt(0), big.NewInt(0), big.NewInt(0), big.NewInt(0), big.NewInt(0), big.NewInt(0), big.NewInt(0), big.NewInt(0), nil, new(EthashConfig), nil, false, &common.Address{123}, true, true} + TestChainConfig = &ChainConfig{big.NewInt(1), big.NewInt(0), nil, false, big.NewInt(0), common.Hash{}, big.NewInt(0), big.NewInt(0), big.NewInt(0), big.NewInt(0), big.NewInt(0), big.NewInt(0), big.NewInt(0), big.NewInt(0), big.NewInt(0), big.NewInt(0), nil, new(EthashConfig), nil, false, &common.Address{123}, true, true, nil} TestRules = TestChainConfig.Rules(new(big.Int)) ) @@ -367,6 +367,9 @@ type ChainConfig struct { // Scroll genesis extension: enable EIP-1559 in tx pool, EnableEIP2718 should be true too. EnableEIP1559 bool `json:"enableEIP1559,omitempty"` + + // Scroll genesis extension: Maximum number of transactions per block [optional] + MaxTxPerBlock *int `json:"maxTxPerBlock,omitempty"` } // EthashConfig is the consensus engine configs for proof-of-work based sealing. @@ -494,6 +497,11 @@ func (c *ChainConfig) IsTerminalPoWBlock(parentTotalDiff *big.Int, totalDiff *bi return parentTotalDiff.Cmp(c.TerminalTotalDifficulty) < 0 && totalDiff.Cmp(c.TerminalTotalDifficulty) >= 0 } +// IsValidTxCount returns whether the given block's transaction count is below the limit. +func (c *ChainConfig) IsValidTxCount(count int) bool { + return c.MaxTxPerBlock == nil || count <= *c.MaxTxPerBlock +} + // CheckCompatible checks whether scheduled fork transitions have been imported // with a mismatching chain configuration. func (c *ChainConfig) CheckCompatible(newcfg *ChainConfig, height uint64) *ConfigCompatError {