From df625db36dee5dd02be767206a2cdbfef590edd8 Mon Sep 17 00:00:00 2001 From: Lucca Martins Date: Wed, 14 Jan 2026 19:04:43 -0300 Subject: [PATCH 01/14] remove base fee validation --- .gitignore | 5 +- consensus/misc/eip1559/eip1559.go | 20 +- consensus/misc/eip1559/eip1559_test.go | 475 ++++++++++++++++++++++++- params/config.go | 55 +++ params/protocol_params.go | 10 +- 5 files changed, 552 insertions(+), 13 deletions(-) diff --git a/.gitignore b/.gitignore index 83f06b0c22..892b80b06a 100644 --- a/.gitignore +++ b/.gitignore @@ -60,4 +60,7 @@ cmd/ethkey/ethkey cmd/evm/evm cmd/geth/geth cmd/rlpdump/rlpdump -cmd/workload/workload \ No newline at end of file +cmd/workload/workload + +# Claude AI assistant context +.claude.md \ No newline at end of file diff --git a/consensus/misc/eip1559/eip1559.go b/consensus/misc/eip1559/eip1559.go index f94b0a2ad2..0b11ae51f9 100644 --- a/consensus/misc/eip1559/eip1559.go +++ b/consensus/misc/eip1559/eip1559.go @@ -29,7 +29,7 @@ import ( // VerifyEIP1559Header verifies some header attributes which were changed in EIP-1559, // - gas limit check -// - basefee check +// - basefee check (pre-Dandeli only; after Dandeli HF base fee validation is removed to allow dynamic setting) func VerifyEIP1559Header(config *params.ChainConfig, parent, header *types.Header) error { // Verify that the gas limit remains within allowed bounds parentGasLimit := parent.GasLimit @@ -43,7 +43,17 @@ func VerifyEIP1559Header(config *params.ChainConfig, parent, header *types.Heade if header.BaseFee == nil { return errors.New("header is missing baseFee") } - // Verify the baseFee is correct based on the parent header. + + // After Dandeli hard fork, base fee validation is removed to allow dynamic configuration. + // This enables validators to set base fees according to new consensus rules without + // strict protocol enforcement, supporting more flexible fee markets. + // Pre-Dandeli blocks still require strict base fee validation for consensus safety. + if config.Bor != nil && config.Bor.IsDandeli(header.Number) { + // Post-Dandeli: Skip base fee validation + return nil + } + + // Pre-Dandeli: Verify the baseFee is correct based on the parent header expectedBaseFee := CalcBaseFee(config, parent) if header.BaseFee.Cmp(expectedBaseFee) != 0 { return fmt.Errorf("invalid baseFee: have %s, want %s, parentBaseFee %s, parentGasUsed %d", @@ -102,10 +112,12 @@ func CalcBaseFee(config *params.ChainConfig, parent *types.Header) *big.Int { // calcParentGasTarget calculates the target gas based on parent block gas limit. Earlier // it was derived by `ElasticityMultiplier` as it had an integer multiplier value. Post -// dandeli HF, a percentage value will be used to calculate the gas target. +// Dandeli HF, a percentage value is used to calculate the gas target (validated with fallback to default). func calcParentGasTarget(config *params.ChainConfig, parent *types.Header) uint64 { if config.Bor != nil && config.Bor.IsDandeli(parent.Number) { - return parent.GasLimit * params.TargetGasPercentagePostDandeli / 100 + // Use helper function which validates and provides defaults + targetPercentage := config.Bor.GetTargetGasPercentage(parent.Number) + return parent.GasLimit * targetPercentage / 100 } return parent.GasLimit / config.ElasticityMultiplier() } diff --git a/consensus/misc/eip1559/eip1559_test.go b/consensus/misc/eip1559/eip1559_test.go index ca9fe8e4e2..ac7de2abdf 100644 --- a/consensus/misc/eip1559/eip1559_test.go +++ b/consensus/misc/eip1559/eip1559_test.go @@ -29,6 +29,36 @@ import ( // copyConfig does a _shallow_ copy of a given config. Safe to set new values, but // do not use e.g. SetInt() on the numbers. For testing only func copyConfig(original *params.ChainConfig) *params.ChainConfig { + // Deep copy of Bor config to prevent race conditions in parallel tests + var borCopy *params.BorConfig + if original.Bor != nil { + borCopy = ¶ms.BorConfig{ + Period: original.Bor.Period, + ProducerDelay: original.Bor.ProducerDelay, + Sprint: original.Bor.Sprint, + BackupMultiplier: original.Bor.BackupMultiplier, + ValidatorContract: original.Bor.ValidatorContract, + StateReceiverContract: original.Bor.StateReceiverContract, + OverrideStateSyncRecords: original.Bor.OverrideStateSyncRecords, + OverrideStateSyncRecordsInRange: original.Bor.OverrideStateSyncRecordsInRange, + BlockAlloc: original.Bor.BlockAlloc, + BurntContract: original.Bor.BurntContract, + Coinbase: original.Bor.Coinbase, + SkipValidatorByteCheck: original.Bor.SkipValidatorByteCheck, + TargetGasPercentage: original.Bor.TargetGasPercentage, + BaseFeeChangeDenominator: original.Bor.BaseFeeChangeDenominator, + JaipurBlock: original.Bor.JaipurBlock, + DelhiBlock: original.Bor.DelhiBlock, + IndoreBlock: original.Bor.IndoreBlock, + StateSyncConfirmationDelay: original.Bor.StateSyncConfirmationDelay, + AhmedabadBlock: original.Bor.AhmedabadBlock, + BhilaiBlock: original.Bor.BhilaiBlock, + RioBlock: original.Bor.RioBlock, + MadhugiriBlock: original.Bor.MadhugiriBlock, + MadhugiriProBlock: original.Bor.MadhugiriProBlock, + DandeliBlock: original.Bor.DandeliBlock, + } + } return ¶ms.ChainConfig{ ChainID: original.ChainID, HomesteadBlock: original.HomesteadBlock, @@ -47,7 +77,7 @@ func copyConfig(original *params.ChainConfig) *params.ChainConfig { TerminalTotalDifficulty: original.TerminalTotalDifficulty, Ethash: original.Ethash, Clique: original.Clique, - Bor: original.Bor, + Bor: borCopy, } } @@ -383,3 +413,446 @@ func TestCalcBaseFeeDandeli(t *testing.T) { ) } } + +// TestDynamicTargetGasPercentage verifies that the TargetGasPercentage parameter +// can be dynamically set after Dandeli HF and affects base fee calculations correctly +func TestDynamicTargetGasPercentage(t *testing.T) { + t.Parallel() + + testConfig := copyConfig(config()) + testConfig.Bor.BhilaiBlock = big.NewInt(8) + testConfig.Bor.DandeliBlock = big.NewInt(20) + + // Test with 70% target gas percentage + targetGasPercentage70 := uint64(70) + testConfig.Bor.TargetGasPercentage = &targetGasPercentage70 + + gasLimit := uint64(60_000_000) + initialBaseFee := int64(params.InitialBaseFee) + + t.Run("70% target gas percentage", func(t *testing.T) { + // When gas used equals 70% of gas limit, base fee should stay the same + block := &types.Header{ + Number: big.NewInt(20), + GasLimit: gasLimit, + GasUsed: 42_000_000, // 70% of 60M + BaseFee: big.NewInt(initialBaseFee), + } + baseFee := CalcBaseFee(testConfig, block).Uint64() + require.Equal(t, uint64(initialBaseFee), baseFee, "base fee should remain unchanged at target") + + // When gas used is below target (50%), base fee should decrease + block.GasUsed = 30_000_000 // 50% of 60M + baseFee = CalcBaseFee(testConfig, block).Uint64() + expectedBaseFee := simpleBaseFeeCalculator(initialBaseFee, gasLimit, block.GasUsed, targetGasPercentage70) + require.Equal(t, expectedBaseFee, baseFee, "base fee should decrease when below target") + require.Less(t, baseFee, uint64(initialBaseFee), "base fee should be less than initial") + + // When gas used is above target (90%), base fee should increase + block.GasUsed = 54_000_000 // 90% of 60M + baseFee = CalcBaseFee(testConfig, block).Uint64() + expectedBaseFee = simpleBaseFeeCalculator(initialBaseFee, gasLimit, block.GasUsed, targetGasPercentage70) + require.Equal(t, expectedBaseFee, baseFee, "base fee should increase when above target") + require.Greater(t, baseFee, uint64(initialBaseFee), "base fee should be greater than initial") + }) + + // Change target gas percentage to 50% + targetGasPercentage50 := uint64(50) + testConfig.Bor.TargetGasPercentage = &targetGasPercentage50 + + t.Run("50% target gas percentage - same run different value", func(t *testing.T) { + // When gas used equals 50% of gas limit, base fee should stay the same + block := &types.Header{ + Number: big.NewInt(21), + GasLimit: gasLimit, + GasUsed: 30_000_000, // 50% of 60M + BaseFee: big.NewInt(initialBaseFee), + } + baseFee := CalcBaseFee(testConfig, block).Uint64() + require.Equal(t, uint64(initialBaseFee), baseFee, "base fee should remain unchanged at new target") + + // When gas used is below new target (40%), base fee should decrease + block.GasUsed = 24_000_000 // 40% of 60M + baseFee = CalcBaseFee(testConfig, block).Uint64() + expectedBaseFee := simpleBaseFeeCalculator(initialBaseFee, gasLimit, block.GasUsed, targetGasPercentage50) + require.Equal(t, expectedBaseFee, baseFee, "base fee should decrease when below new target") + require.Less(t, baseFee, uint64(initialBaseFee), "base fee should be less than initial") + + // When gas used is above new target (70%), base fee should increase + block.GasUsed = 42_000_000 // 70% of 60M + baseFee = CalcBaseFee(testConfig, block).Uint64() + expectedBaseFee = simpleBaseFeeCalculator(initialBaseFee, gasLimit, block.GasUsed, targetGasPercentage50) + require.Equal(t, expectedBaseFee, baseFee, "base fee should increase when above new target") + require.Greater(t, baseFee, uint64(initialBaseFee), "base fee should be greater than initial") + }) + + t.Run("nil target gas percentage falls back to default", func(t *testing.T) { + testConfig.Bor.TargetGasPercentage = nil + block := &types.Header{ + Number: big.NewInt(22), + GasLimit: gasLimit, + GasUsed: 39_000_000, // 65% of 60M (default is 65%) + BaseFee: big.NewInt(initialBaseFee), + } + baseFee := CalcBaseFee(testConfig, block).Uint64() + require.Equal(t, uint64(initialBaseFee), baseFee, "base fee should remain unchanged at default target") + }) +} + +// TestDynamicBaseFeeChangeDenominator verifies that the BaseFeeChangeDenominator parameter +// can be dynamically set after Dandeli HF and affects the rate of base fee change correctly +func TestDynamicBaseFeeChangeDenominator(t *testing.T) { + t.Parallel() + + testConfig := copyConfig(config()) + testConfig.Bor.BhilaiBlock = big.NewInt(8) + testConfig.Bor.DandeliBlock = big.NewInt(20) + + gasLimit := uint64(60_000_000) + initialBaseFee := int64(params.InitialBaseFee) + targetGasPercentage := uint64(params.TargetGasPercentagePostDandeli) + + // Test with denominator of 32 (slower changes) + denominator32 := uint64(32) + testConfig.Bor.BaseFeeChangeDenominator = &denominator32 + + t.Run("denominator 32 - slower base fee changes", func(t *testing.T) { + block := &types.Header{ + Number: big.NewInt(20), + GasLimit: gasLimit, + GasUsed: 50_000_000, // Above target + BaseFee: big.NewInt(initialBaseFee), + } + + baseFee := CalcBaseFee(testConfig, block).Uint64() + + // Calculate expected with custom denominator + target := gasLimit * targetGasPercentage / 100 + gasUsedDelta := block.GasUsed - target + expectedIncrease := new(big.Int).Mul(big.NewInt(initialBaseFee), big.NewInt(int64(gasUsedDelta))) + expectedIncrease.Div(expectedIncrease, big.NewInt(int64(target))) + expectedIncrease.Div(expectedIncrease, big.NewInt(int64(denominator32))) + expectedBaseFee := new(big.Int).Add(big.NewInt(initialBaseFee), expectedIncrease).Uint64() + + require.Equal(t, expectedBaseFee, baseFee, "base fee should change according to denominator 32") + }) + + // Change denominator to 16 (faster changes) in the same run + denominator16 := uint64(16) + testConfig.Bor.BaseFeeChangeDenominator = &denominator16 + + t.Run("denominator 16 - faster base fee changes - same run different value", func(t *testing.T) { + block := &types.Header{ + Number: big.NewInt(21), + GasLimit: gasLimit, + GasUsed: 50_000_000, // Same gas used as before + BaseFee: big.NewInt(initialBaseFee), + } + + baseFee := CalcBaseFee(testConfig, block).Uint64() + + // Calculate expected with new denominator (should result in larger change) + target := gasLimit * targetGasPercentage / 100 + gasUsedDelta := block.GasUsed - target + expectedIncrease := new(big.Int).Mul(big.NewInt(initialBaseFee), big.NewInt(int64(gasUsedDelta))) + expectedIncrease.Div(expectedIncrease, big.NewInt(int64(target))) + expectedIncrease.Div(expectedIncrease, big.NewInt(int64(denominator16))) + expectedBaseFee := new(big.Int).Add(big.NewInt(initialBaseFee), expectedIncrease).Uint64() + + require.Equal(t, expectedBaseFee, baseFee, "base fee should change more with denominator 16") + }) + + t.Run("nil denominator falls back to Bhilai default (64)", func(t *testing.T) { + testConfig.Bor.BaseFeeChangeDenominator = nil + block := &types.Header{ + Number: big.NewInt(22), + GasLimit: gasLimit, + GasUsed: 50_000_000, + BaseFee: big.NewInt(initialBaseFee), + } + + baseFee := CalcBaseFee(testConfig, block).Uint64() + + // Should use Bhilai denominator (64) + target := gasLimit * targetGasPercentage / 100 + gasUsedDelta := block.GasUsed - target + expectedIncrease := new(big.Int).Mul(big.NewInt(initialBaseFee), big.NewInt(int64(gasUsedDelta))) + expectedIncrease.Div(expectedIncrease, big.NewInt(int64(target))) + expectedIncrease.Div(expectedIncrease, big.NewInt(int64(params.BaseFeeChangeDenominatorPostBhilai))) + expectedBaseFee := new(big.Int).Add(big.NewInt(initialBaseFee), expectedIncrease).Uint64() + + require.Equal(t, expectedBaseFee, baseFee, "base fee should use Bhilai default denominator") + }) +} + +// TestVerifyEIP1559HeaderNoBaseFeeValidation tests that after removing the base fee validation, +// headers with any base fee are accepted (as long as it's not nil) +func TestVerifyEIP1559HeaderNoBaseFeeValidation(t *testing.T) { + t.Parallel() + + testConfig := copyConfig(config()) + testConfig.Bor.DandeliBlock = big.NewInt(20) + + parent := &types.Header{ + Number: big.NewInt(20), + GasLimit: 30_000_000, + GasUsed: 15_000_000, + BaseFee: big.NewInt(1_000_000_000), + } + + t.Run("accepts arbitrary base fee after validation removal", func(t *testing.T) { + // Header with a base fee that doesn't match the calculated value + // This should be accepted since we removed the validation + header := &types.Header{ + Number: big.NewInt(21), + GasLimit: 30_000_000, + GasUsed: 20_000_000, + BaseFee: big.NewInt(5_000_000_000), // Arbitrary value + } + + err := VerifyEIP1559Header(testConfig, parent, header) + require.NoError(t, err, "should accept header with arbitrary base fee") + }) + + t.Run("accepts base fee different from calculated", func(t *testing.T) { + calculatedBaseFee := CalcBaseFee(testConfig, parent) + + // Use a completely different base fee + differentBaseFee := new(big.Int).Mul(calculatedBaseFee, big.NewInt(10)) + + header := &types.Header{ + Number: big.NewInt(21), + GasLimit: 30_000_000, + GasUsed: 20_000_000, + BaseFee: differentBaseFee, + } + + err := VerifyEIP1559Header(testConfig, parent, header) + require.NoError(t, err, "should accept header with base fee different from calculated") + }) + + t.Run("rejects nil base fee", func(t *testing.T) { + header := &types.Header{ + Number: big.NewInt(21), + GasLimit: 30_000_000, + GasUsed: 20_000_000, + BaseFee: nil, // Nil base fee should still be rejected + } + + err := VerifyEIP1559Header(testConfig, parent, header) + require.Error(t, err, "should reject header with nil base fee") + require.Contains(t, err.Error(), "baseFee", "error should mention baseFee") + }) + + t.Run("accepts zero base fee", func(t *testing.T) { + header := &types.Header{ + Number: big.NewInt(21), + GasLimit: 30_000_000, + GasUsed: 20_000_000, + BaseFee: big.NewInt(0), // Zero is valid + } + + err := VerifyEIP1559Header(testConfig, parent, header) + require.NoError(t, err, "should accept header with zero base fee") + }) +} + +// TestInvalidTargetGasPercentage tests that invalid TargetGasPercentage values +// fall back to defaults and don't cause panics +func TestInvalidTargetGasPercentage(t *testing.T) { + t.Parallel() + + testConfig := copyConfig(config()) + testConfig.Bor.BhilaiBlock = big.NewInt(8) + testConfig.Bor.DandeliBlock = big.NewInt(20) + + gasLimit := uint64(60_000_000) + initialBaseFee := int64(params.InitialBaseFee) + + t.Run("zero target gas percentage falls back to default", func(t *testing.T) { + zeroValue := uint64(0) + testConfig.Bor.TargetGasPercentage = &zeroValue + + block := &types.Header{ + Number: big.NewInt(20), + GasLimit: gasLimit, + GasUsed: 39_000_000, // 65% of 60M (default target) + BaseFee: big.NewInt(initialBaseFee), + } + + // Should not panic and should use default (65%) + baseFee := CalcBaseFee(testConfig, block).Uint64() + require.Equal(t, uint64(initialBaseFee), baseFee, "should use default target and base fee unchanged") + }) + + t.Run("target gas percentage > 100 falls back to default", func(t *testing.T) { + invalidValue := uint64(150) + testConfig.Bor.TargetGasPercentage = &invalidValue + + block := &types.Header{ + Number: big.NewInt(21), + GasLimit: gasLimit, + GasUsed: 39_000_000, // 65% of 60M (default target) + BaseFee: big.NewInt(initialBaseFee), + } + + // Should not panic and should use default (65%) + baseFee := CalcBaseFee(testConfig, block).Uint64() + require.Equal(t, uint64(initialBaseFee), baseFee, "should use default target and base fee unchanged") + }) + + t.Run("valid edge case: 1% target gas percentage", func(t *testing.T) { + onePercent := uint64(1) + testConfig.Bor.TargetGasPercentage = &onePercent + + block := &types.Header{ + Number: big.NewInt(22), + GasLimit: gasLimit, + GasUsed: 600_000, // 1% of 60M + BaseFee: big.NewInt(initialBaseFee), + } + + // Should work with 1% target + baseFee := CalcBaseFee(testConfig, block).Uint64() + require.Equal(t, uint64(initialBaseFee), baseFee, "should accept 1% as valid target") + }) + + t.Run("valid edge case: 100% target gas percentage", func(t *testing.T) { + hundredPercent := uint64(100) + testConfig.Bor.TargetGasPercentage = &hundredPercent + + block := &types.Header{ + Number: big.NewInt(23), + GasLimit: gasLimit, + GasUsed: gasLimit, // 100% of gas limit + BaseFee: big.NewInt(initialBaseFee), + } + + // Should work with 100% target + baseFee := CalcBaseFee(testConfig, block).Uint64() + require.Equal(t, uint64(initialBaseFee), baseFee, "should accept 100% as valid target") + }) +} + +// TestInvalidBaseFeeChangeDenominator tests that invalid BaseFeeChangeDenominator values +// fall back to defaults and don't cause panics (especially division by zero) +func TestInvalidBaseFeeChangeDenominator(t *testing.T) { + t.Parallel() + + testConfig := copyConfig(config()) + testConfig.Bor.BhilaiBlock = big.NewInt(8) + testConfig.Bor.DandeliBlock = big.NewInt(20) + + gasLimit := uint64(60_000_000) + initialBaseFee := int64(params.InitialBaseFee) + + t.Run("zero denominator falls back to default", func(t *testing.T) { + zeroDenom := uint64(0) + testConfig.Bor.BaseFeeChangeDenominator = &zeroDenom + + block := &types.Header{ + Number: big.NewInt(20), + GasLimit: gasLimit, + GasUsed: 50_000_000, // Above target + BaseFee: big.NewInt(initialBaseFee), + } + + // Should not panic (no division by zero) and use Bhilai default (64) + baseFee := CalcBaseFee(testConfig, block) + require.NotNil(t, baseFee, "should calculate base fee without panic") + + // Verify it used default denominator (64) by checking the change is small + target := gasLimit * params.TargetGasPercentagePostDandeli / 100 + gasUsedDelta := block.GasUsed - target + expectedIncrease := new(big.Int).Mul(big.NewInt(initialBaseFee), big.NewInt(int64(gasUsedDelta))) + expectedIncrease.Div(expectedIncrease, big.NewInt(int64(target))) + expectedIncrease.Div(expectedIncrease, big.NewInt(int64(params.BaseFeeChangeDenominatorPostBhilai))) + expectedBaseFee := new(big.Int).Add(big.NewInt(initialBaseFee), expectedIncrease) + + require.Equal(t, expectedBaseFee.Uint64(), baseFee.Uint64(), "should use Bhilai default denominator (64)") + }) + + t.Run("valid edge case: denominator = 1 (extreme volatility)", func(t *testing.T) { + extremeDenom := uint64(1) + testConfig.Bor.BaseFeeChangeDenominator = &extremeDenom + + block := &types.Header{ + Number: big.NewInt(21), + GasLimit: gasLimit, + GasUsed: 50_000_000, + BaseFee: big.NewInt(initialBaseFee), + } + + // Should work but produce large changes + baseFee := CalcBaseFee(testConfig, block) + require.NotNil(t, baseFee, "should handle denominator of 1") + require.Greater(t, baseFee.Uint64(), uint64(initialBaseFee), "base fee should increase significantly with denominator 1") + }) +} + +// TestBaseFeeValidationPreDandeli tests that base fee validation still works before Dandeli HF +func TestBaseFeeValidationPreDandeli(t *testing.T) { + t.Parallel() + + testConfig := copyConfig(config()) + testConfig.Bor.DandeliBlock = big.NewInt(20) + + parent := &types.Header{ + Number: big.NewInt(10), // Pre-Dandeli + GasLimit: 30_000_000, + GasUsed: 15_000_000, + BaseFee: big.NewInt(1_000_000_000), + } + + t.Run("pre-Dandeli: rejects incorrect base fee", func(t *testing.T) { + calculatedBaseFee := CalcBaseFee(testConfig, parent) + incorrectBaseFee := new(big.Int).Mul(calculatedBaseFee, big.NewInt(2)) + + header := &types.Header{ + Number: big.NewInt(11), + GasLimit: 30_000_000, + GasUsed: 20_000_000, + BaseFee: incorrectBaseFee, // Wrong base fee + } + + err := VerifyEIP1559Header(testConfig, parent, header) + require.Error(t, err, "should reject incorrect base fee pre-Dandeli") + require.Contains(t, err.Error(), "invalid baseFee", "error should mention invalid baseFee") + }) + + t.Run("pre-Dandeli: accepts correct base fee", func(t *testing.T) { + calculatedBaseFee := CalcBaseFee(testConfig, parent) + + header := &types.Header{ + Number: big.NewInt(11), + GasLimit: 30_000_000, + GasUsed: 20_000_000, + BaseFee: calculatedBaseFee, // Correct base fee + } + + err := VerifyEIP1559Header(testConfig, parent, header) + require.NoError(t, err, "should accept correct base fee pre-Dandeli") + }) + + t.Run("post-Dandeli: accepts any base fee", func(t *testing.T) { + parent := &types.Header{ + Number: big.NewInt(20), // Post-Dandeli + GasLimit: 30_000_000, + GasUsed: 15_000_000, + BaseFee: big.NewInt(1_000_000_000), + } + + calculatedBaseFee := CalcBaseFee(testConfig, parent) + arbitraryBaseFee := new(big.Int).Mul(calculatedBaseFee, big.NewInt(5)) + + header := &types.Header{ + Number: big.NewInt(21), + GasLimit: 30_000_000, + GasUsed: 20_000_000, + BaseFee: arbitraryBaseFee, // Arbitrary base fee + } + + err := VerifyEIP1559Header(testConfig, parent, header) + require.NoError(t, err, "should accept arbitrary base fee post-Dandeli") + }) +} diff --git a/params/config.go b/params/config.go index 199d138e68..964881dbab 100644 --- a/params/config.go +++ b/params/config.go @@ -25,6 +25,7 @@ import ( "strconv" "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/params/forks" ) @@ -881,6 +882,8 @@ type BorConfig struct { BurntContract map[string]string `json:"burntContract"` // governance contract where the token will be sent to and burnt in london fork Coinbase map[string]string `json:"coinbase"` // coinbase address SkipValidatorByteCheck []uint64 `json:"skipValidatorByteCheck"` // skip validator byte check + TargetGasPercentage *uint64 `json:"targetGasPercentage"` // Post-Dandeli: target gas as % of gas limit (1-100, default 65). All validators must use same value. + BaseFeeChangeDenominator *uint64 `json:"baseFeeChangeDenominator"` // Post-Dandeli: base fee change rate (must be >0, default 64). All validators must use same value. JaipurBlock *big.Int `json:"jaipurBlock"` // Jaipur switch block (nil = no fork, 0 = already on jaipur) DelhiBlock *big.Int `json:"delhiBlock"` // Delhi switch block (nil = no fork, 0 = already on delhi) IndoreBlock *big.Int `json:"indoreBlock"` // Indore switch block (nil = no fork, 0 = already on indore) @@ -954,6 +957,58 @@ func (c *BorConfig) IsDandeli(number *big.Int) bool { return isBlockForked(c.DandeliBlock, number) } +// GetTargetGasPercentage returns the target gas percentage for gas limit calculation. +// After Dandeli hard fork, this value can be dynamically configured. It validates the +// configured value and falls back to defaults if invalid or nil. +// Valid range: 1-100 (percentage). +func (c *BorConfig) GetTargetGasPercentage(number *big.Int) uint64 { + // Only applies after Dandeli + if !c.IsDandeli(number) { + return 0 // Caller should use ElasticityMultiplier for pre-Dandeli + } + + // If custom value is set, validate it + if c.TargetGasPercentage != nil { + val := *c.TargetGasPercentage + // Validate: must be between 1 and 100 + if val > 0 && val <= 100 { + return val + } + // Invalid value - log error and fall back to default + log.Error("Invalid TargetGasPercentage in BorConfig, falling back to default", + "configured", val, + "validRange", "1-100") + } + + // Default for post-Dandeli + return TargetGasPercentagePostDandeli +} + +// GetBaseFeeChangeDenominator returns the base fee change denominator. +// After Dandeli hard fork, this value can be dynamically configured. It validates the +// configured value and falls back to hard fork based defaults if invalid or nil. +func (c *BorConfig) GetBaseFeeChangeDenominator(number *big.Int) uint64 { + // If Dandeli is active and custom value is set, validate and use it + if c.IsDandeli(number) && c.BaseFeeChangeDenominator != nil { + val := *c.BaseFeeChangeDenominator + // Validate: must be non-zero to prevent division by zero + if val > 0 { + return val + } + // Invalid value - log error and fall back to default + log.Error("Invalid BaseFeeChangeDenominator in BorConfig (must be > 0), falling back to default", + "configured", val) + } + + // Fall back to hard fork based values + if c.IsBhilai(number) { + return BaseFeeChangeDenominatorPostBhilai + } else if c.IsDelhi(number) { + return BaseFeeChangeDenominatorPostDelhi + } + return BaseFeeChangeDenominatorPreDelhi +} + // // TODO: modify this function once the block number is finalized // func (c *BorConfig) IsNapoli(number *big.Int) bool { // if c.NapoliBlock != nil { diff --git a/params/protocol_params.go b/params/protocol_params.go index f2d4091ae1..b6da1c35d0 100644 --- a/params/protocol_params.go +++ b/params/protocol_params.go @@ -241,17 +241,13 @@ var ( // - Pre-Delhi: 8 (default) // - Post-Delhi: 16 // - Post-Bhilai: 64 +// - Post-Dandeli: Configurable via BorConfig.BaseFeeChangeDenominator (validated, falls back to Bhilai default if invalid) // If borConfig is nil, returns the default value of 8. func BaseFeeChangeDenominator(borConfig *BorConfig, number *big.Int) uint64 { // Handle cases where bor consensus isn't available to avoid panic if borConfig == nil { return DefaultBaseFeeChangeDenominator } - if borConfig.IsBhilai(number) { - return BaseFeeChangeDenominatorPostBhilai - } else if borConfig.IsDelhi(number) { - return BaseFeeChangeDenominatorPostDelhi - } else { - return BaseFeeChangeDenominatorPreDelhi - } + // Use the helper function which includes validation + return borConfig.GetBaseFeeChangeDenominator(number) } From cab29b1a65264fc973140cda5f4c30163b05ebb0 Mon Sep 17 00:00:00 2001 From: Lucca Martins Date: Thu, 15 Jan 2026 08:46:42 -0300 Subject: [PATCH 02/14] moving flags to miner setup --- internal/cli/server/config.go | 26 ++++++++++++++++++++++++++ internal/cli/server/flags.go | 12 ++++++++++++ params/config.go | 16 ++++++++++------ 3 files changed, 48 insertions(+), 6 deletions(-) diff --git a/internal/cli/server/config.go b/internal/cli/server/config.go index dc3fbe5e3e..b40fe5509d 100644 --- a/internal/cli/server/config.go +++ b/internal/cli/server/config.go @@ -396,6 +396,12 @@ type SealerConfig struct { // BlockTime is the block time defined by the miner. Needs to be larger or equal to the consensus block time. If not set (default = 0), the miner will use the consensus block time. BlockTime time.Duration `hcl:"-,optional" toml:"-"` BlockTimeRaw string `hcl:"blocktime,optional" toml:"blocktime,optional"` + + // TargetGasPercentage is the target gas as percentage of gas limit (1-100, default 65) for post-Dandeli blocks + TargetGasPercentage *uint64 `hcl:"target-gas-percentage,optional" toml:"target-gas-percentage,optional"` + + // BaseFeeChangeDenominator is the base fee change rate (must be >0, default 64) for post-Dandeli blocks + BaseFeeChangeDenominator *uint64 `hcl:"base-fee-change-denominator,optional" toml:"base-fee-change-denominator,optional"` } type JsonRPCConfig struct { @@ -1136,6 +1142,16 @@ func (c *Config) buildEth(stack *node.Node, accountManager *accounts.Manager) (* } } + // Set runtime miner gas parameters in BorConfig (if not in developer mode and Bor chain) + if !c.Developer.Enabled && n.Genesis != nil && n.Genesis.Config != nil && n.Genesis.Config.Bor != nil { + if c.Sealer.TargetGasPercentage != nil { + n.Genesis.Config.Bor.TargetGasPercentage = c.Sealer.TargetGasPercentage + } + if c.Sealer.BaseFeeChangeDenominator != nil { + n.Genesis.Config.Bor.BaseFeeChangeDenominator = c.Sealer.BaseFeeChangeDenominator + } + } + // unlock accounts if len(c.Accounts.Unlock) > 0 { if !stack.Config().InsecureUnlockAllowed && stack.Config().ExtRPCEnabled() { @@ -1202,6 +1218,16 @@ func (c *Config) buildEth(stack *node.Node, accountManager *accounts.Manager) (* n.NetworkId = c.chain.NetworkId n.Genesis = c.chain.Genesis + // Set runtime miner gas parameters in BorConfig for developer mode + if n.Genesis != nil && n.Genesis.Config != nil && n.Genesis.Config.Bor != nil { + if c.Sealer.TargetGasPercentage != nil { + n.Genesis.Config.Bor.TargetGasPercentage = c.Sealer.TargetGasPercentage + } + if c.Sealer.BaseFeeChangeDenominator != nil { + n.Genesis.Config.Bor.BaseFeeChangeDenominator = c.Sealer.BaseFeeChangeDenominator + } + } + // Update cache c.Cache.Cache = 1024 diff --git a/internal/cli/server/flags.go b/internal/cli/server/flags.go index db210f3ecc..34d57b2197 100644 --- a/internal/cli/server/flags.go +++ b/internal/cli/server/flags.go @@ -378,6 +378,18 @@ func (c *Command) Flags(config *Config) *flagset.Flagset { Default: c.cliConfig.Sealer.BlockTime, Group: "Sealer", }) + f.Uint64Flag(&flagset.Uint64Flag{ + Name: "miner.target-gas-percentage", + Usage: "Target gas as percentage of gas limit (1-100, default 65) for post-Dandeli blocks", + Value: c.cliConfig.Sealer.TargetGasPercentage, + Group: "Sealer", + }) + f.Uint64Flag(&flagset.Uint64Flag{ + Name: "miner.base-fee-change-denominator", + Usage: "Base fee change rate denominator (must be >0, default 64) for post-Dandeli blocks", + Value: c.cliConfig.Sealer.BaseFeeChangeDenominator, + Group: "Sealer", + }) // ethstats f.StringFlag(&flagset.StringFlag{ diff --git a/params/config.go b/params/config.go index 964881dbab..7ff2073429 100644 --- a/params/config.go +++ b/params/config.go @@ -882,8 +882,12 @@ type BorConfig struct { BurntContract map[string]string `json:"burntContract"` // governance contract where the token will be sent to and burnt in london fork Coinbase map[string]string `json:"coinbase"` // coinbase address SkipValidatorByteCheck []uint64 `json:"skipValidatorByteCheck"` // skip validator byte check - TargetGasPercentage *uint64 `json:"targetGasPercentage"` // Post-Dandeli: target gas as % of gas limit (1-100, default 65). All validators must use same value. - BaseFeeChangeDenominator *uint64 `json:"baseFeeChangeDenominator"` // Post-Dandeli: base fee change rate (must be >0, default 64). All validators must use same value. + + // Runtime miner configuration (set via sealer/miner CLI flags, not from genesis JSON) + // These affect consensus gas pricing but are configurable per-node for operational flexibility + TargetGasPercentage *uint64 `json:"-"` // Post-Dandeli: target gas as % of gas limit (1-100, default 65). Set via --miner.target-gas-percentage + BaseFeeChangeDenominator *uint64 `json:"-"` // Post-Dandeli: base fee change rate (must be >0, default 64). Set via --miner.base-fee-change-denominator + JaipurBlock *big.Int `json:"jaipurBlock"` // Jaipur switch block (nil = no fork, 0 = already on jaipur) DelhiBlock *big.Int `json:"delhiBlock"` // Delhi switch block (nil = no fork, 0 = already on delhi) IndoreBlock *big.Int `json:"indoreBlock"` // Indore switch block (nil = no fork, 0 = already on indore) @@ -958,8 +962,8 @@ func (c *BorConfig) IsDandeli(number *big.Int) bool { } // GetTargetGasPercentage returns the target gas percentage for gas limit calculation. -// After Dandeli hard fork, this value can be dynamically configured. It validates the -// configured value and falls back to defaults if invalid or nil. +// After Dandeli hard fork, this value can be configured via CLI flags (stored in BorConfig at runtime). +// It validates the configured value and falls back to defaults if invalid or nil. // Valid range: 1-100 (percentage). func (c *BorConfig) GetTargetGasPercentage(number *big.Int) uint64 { // Only applies after Dandeli @@ -985,8 +989,8 @@ func (c *BorConfig) GetTargetGasPercentage(number *big.Int) uint64 { } // GetBaseFeeChangeDenominator returns the base fee change denominator. -// After Dandeli hard fork, this value can be dynamically configured. It validates the -// configured value and falls back to hard fork based defaults if invalid or nil. +// After Dandeli hard fork, this value can be configured via CLI flags (stored in BorConfig at runtime). +// It validates the configured value and falls back to hard fork based defaults if invalid or nil. func (c *BorConfig) GetBaseFeeChangeDenominator(number *big.Int) uint64 { // If Dandeli is active and custom value is set, validate and use it if c.IsDandeli(number) && c.BaseFeeChangeDenominator != nil { From 016bff6f5ddec43cde6c906c71ee16564a8d7d6e Mon Sep 17 00:00:00 2001 From: Lucca Martins Date: Thu, 15 Jan 2026 16:58:15 -0300 Subject: [PATCH 03/14] set values properly --- docs/cli/server.md | 14 ++++++++++ internal/cli/server/config.go | 52 +++++++++++++++++++---------------- internal/cli/server/flags.go | 18 ++++++------ 3 files changed, 52 insertions(+), 32 deletions(-) diff --git a/docs/cli/server.md b/docs/cli/server.md index 439dca0431..fff600f578 100644 --- a/docs/cli/server.md +++ b/docs/cli/server.md @@ -314,6 +314,20 @@ The ```bor server``` command runs the Bor client. - ```miner.recommit```: The time interval for miner to re-create mining work (default: 2m5s) +- ```miner.enableDynamicGasLimit```: Enable dynamic gas limit adjustment based on base fee (default: false) + +- ```miner.gasLimitMin```: Minimum gas limit when dynamic gas limit is enabled (default: 0) + +- ```miner.gasLimitMax```: Maximum gas limit when dynamic gas limit is enabled (default: 0) + +- ```miner.targetBaseFee```: Target base fee in wei for dynamic gas limit (e.g., 30000000000 for 30 gwei) (default: 0) + +- ```miner.baseFeeBuffer```: Buffer around target base fee in wei (no adjustment when within buffer) (default: 0) + +- ```miner.targetGasPercentage```: Target gas as percentage of gas limit (1-100, default 65) for post-Dandeli blocks (default: 0) + +- ```miner.baseFeeChangeDenominator```: Base fee change rate denominator (must be >0, default 64) for post-Dandeli blocks (default: 0) + ### Telemetry Options - ```metrics```: Enable metrics collection and reporting (default: false) diff --git a/internal/cli/server/config.go b/internal/cli/server/config.go index 72ef9d05fe..97f59d97dc 100644 --- a/internal/cli/server/config.go +++ b/internal/cli/server/config.go @@ -405,10 +405,10 @@ type SealerConfig struct { BlockTimeRaw string `hcl:"blocktime,optional" toml:"blocktime,optional"` // TargetGasPercentage is the target gas as percentage of gas limit (1-100, default 65) for post-Dandeli blocks - TargetGasPercentage *uint64 `hcl:"target-gas-percentage,optional" toml:"target-gas-percentage,optional"` + TargetGasPercentage uint64 `hcl:"target-gas-percentage,optional" toml:"target-gas-percentage,optional"` // BaseFeeChangeDenominator is the base fee change rate (must be >0, default 64) for post-Dandeli blocks - BaseFeeChangeDenominator *uint64 `hcl:"base-fee-change-denominator,optional" toml:"base-fee-change-denominator,optional"` + BaseFeeChangeDenominator uint64 `hcl:"base-fee-change-denominator,optional" toml:"base-fee-change-denominator,optional"` } type JsonRPCConfig struct { @@ -791,20 +791,22 @@ func DefaultConfig() *Config { LifeTime: 3 * time.Hour, }, Sealer: &SealerConfig{ - Enabled: false, - AllowGasTipOverride: false, - Etherbase: "", - GasCeil: miner.DefaultConfig.GasCeil, - EnableDynamicGasLimit: miner.DefaultConfig.EnableDynamicGasLimit, - GasLimitMin: miner.DefaultConfig.GasLimitMin, - GasLimitMax: miner.DefaultConfig.GasLimitMax, - TargetBaseFee: miner.DefaultConfig.TargetBaseFee, - BaseFeeBuffer: miner.DefaultConfig.BaseFeeBuffer, - GasPrice: big.NewInt(params.BorDefaultMinerGasPrice), // bor's default - ExtraData: "", - Recommit: 125 * time.Second, - CommitInterruptFlag: true, - BlockTime: 0, + Enabled: false, + AllowGasTipOverride: false, + Etherbase: "", + GasCeil: miner.DefaultConfig.GasCeil, + EnableDynamicGasLimit: miner.DefaultConfig.EnableDynamicGasLimit, + GasLimitMin: miner.DefaultConfig.GasLimitMin, + GasLimitMax: miner.DefaultConfig.GasLimitMax, + TargetBaseFee: miner.DefaultConfig.TargetBaseFee, + BaseFeeBuffer: miner.DefaultConfig.BaseFeeBuffer, + GasPrice: big.NewInt(params.BorDefaultMinerGasPrice), // bor's default + ExtraData: "", + Recommit: 125 * time.Second, + CommitInterruptFlag: true, + BlockTime: 0, + TargetGasPercentage: 0, // Initialize to 0, will be set from CLI or remain 0 (meaning use default) + BaseFeeChangeDenominator: 0, // Initialize to 0, will be set from CLI or remain 0 (meaning use default) }, Gpo: &GpoConfig{ Blocks: 20, @@ -1176,11 +1178,12 @@ func (c *Config) buildEth(stack *node.Node, accountManager *accounts.Manager) (* // Set runtime miner gas parameters in BorConfig (if not in developer mode and Bor chain) if !c.Developer.Enabled && n.Genesis != nil && n.Genesis.Config != nil && n.Genesis.Config.Bor != nil { - if c.Sealer.TargetGasPercentage != nil { - n.Genesis.Config.Bor.TargetGasPercentage = c.Sealer.TargetGasPercentage + // Only set if non-zero (0 means not set via CLI, use defaults from consensus) + if c.Sealer.TargetGasPercentage > 0 { + n.Genesis.Config.Bor.TargetGasPercentage = &c.Sealer.TargetGasPercentage } - if c.Sealer.BaseFeeChangeDenominator != nil { - n.Genesis.Config.Bor.BaseFeeChangeDenominator = c.Sealer.BaseFeeChangeDenominator + if c.Sealer.BaseFeeChangeDenominator > 0 { + n.Genesis.Config.Bor.BaseFeeChangeDenominator = &c.Sealer.BaseFeeChangeDenominator } } @@ -1252,11 +1255,12 @@ func (c *Config) buildEth(stack *node.Node, accountManager *accounts.Manager) (* // Set runtime miner gas parameters in BorConfig for developer mode if n.Genesis != nil && n.Genesis.Config != nil && n.Genesis.Config.Bor != nil { - if c.Sealer.TargetGasPercentage != nil { - n.Genesis.Config.Bor.TargetGasPercentage = c.Sealer.TargetGasPercentage + // Only set if non-zero (0 means not set via CLI, use defaults from consensus) + if c.Sealer.TargetGasPercentage > 0 { + n.Genesis.Config.Bor.TargetGasPercentage = &c.Sealer.TargetGasPercentage } - if c.Sealer.BaseFeeChangeDenominator != nil { - n.Genesis.Config.Bor.BaseFeeChangeDenominator = c.Sealer.BaseFeeChangeDenominator + if c.Sealer.BaseFeeChangeDenominator > 0 { + n.Genesis.Config.Bor.BaseFeeChangeDenominator = &c.Sealer.BaseFeeChangeDenominator } } diff --git a/internal/cli/server/flags.go b/internal/cli/server/flags.go index 3464a827d4..f60f96f50e 100644 --- a/internal/cli/server/flags.go +++ b/internal/cli/server/flags.go @@ -414,16 +414,18 @@ func (c *Command) Flags(config *Config) *flagset.Flagset { Group: "Sealer", }) f.Uint64Flag(&flagset.Uint64Flag{ - Name: "miner.target-gas-percentage", - Usage: "Target gas as percentage of gas limit (1-100, default 65) for post-Dandeli blocks", - Value: c.cliConfig.Sealer.TargetGasPercentage, - Group: "Sealer", + Name: "miner.targetGasPercentage", + Usage: "Target gas as percentage of gas limit (1-100, default 65) for post-Dandeli blocks", + Value: &c.cliConfig.Sealer.TargetGasPercentage, + Default: c.cliConfig.Sealer.TargetGasPercentage, + Group: "Sealer", }) f.Uint64Flag(&flagset.Uint64Flag{ - Name: "miner.base-fee-change-denominator", - Usage: "Base fee change rate denominator (must be >0, default 64) for post-Dandeli blocks", - Value: c.cliConfig.Sealer.BaseFeeChangeDenominator, - Group: "Sealer", + Name: "miner.baseFeeChangeDenominator", + Usage: "Base fee change rate denominator (must be >0, default 64) for post-Dandeli blocks", + Value: &c.cliConfig.Sealer.BaseFeeChangeDenominator, + Default: c.cliConfig.Sealer.BaseFeeChangeDenominator, + Group: "Sealer", }) // ethstats From 05fa4a7588e232a069090f9165e4a61ec5454b1b Mon Sep 17 00:00:00 2001 From: Lucca Martins Date: Thu, 15 Jan 2026 18:56:16 -0300 Subject: [PATCH 04/14] proper format --- params/config.go | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/params/config.go b/params/config.go index f205db6558..d533d6eb40 100644 --- a/params/config.go +++ b/params/config.go @@ -880,25 +880,25 @@ type BorConfig struct { OverrideStateSyncRecords map[string]int `json:"overrideStateSyncRecords"` // override state records count OverrideStateSyncRecordsInRange []BlockRangeOverride `json:"overrideStateSyncRecordsInRange"` // override state records count in a given block range BlockAlloc map[string]interface{} `json:"blockAlloc"` - BurntContract map[string]string `json:"burntContract"` // governance contract where the token will be sent to and burnt in london fork - Coinbase map[string]string `json:"coinbase"` // coinbase address - SkipValidatorByteCheck []uint64 `json:"skipValidatorByteCheck"` // skip validator byte check + BurntContract map[string]string `json:"burntContract"` // governance contract where the token will be sent to and burnt in london fork + Coinbase map[string]string `json:"coinbase"` // coinbase address + SkipValidatorByteCheck []uint64 `json:"skipValidatorByteCheck"` // skip validator byte check // Runtime miner configuration (set via sealer/miner CLI flags, not from genesis JSON) // These affect consensus gas pricing but are configurable per-node for operational flexibility - TargetGasPercentage *uint64 `json:"-"` // Post-Dandeli: target gas as % of gas limit (1-100, default 65). Set via --miner.target-gas-percentage - BaseFeeChangeDenominator *uint64 `json:"-"` // Post-Dandeli: base fee change rate (must be >0, default 64). Set via --miner.base-fee-change-denominator - - JaipurBlock *big.Int `json:"jaipurBlock"` // Jaipur switch block (nil = no fork, 0 = already on jaipur) - DelhiBlock *big.Int `json:"delhiBlock"` // Delhi switch block (nil = no fork, 0 = already on delhi) - IndoreBlock *big.Int `json:"indoreBlock"` // Indore switch block (nil = no fork, 0 = already on indore) - StateSyncConfirmationDelay map[string]uint64 `json:"stateSyncConfirmationDelay"` // StateSync Confirmation Delay, in seconds, to calculate `to` - AhmedabadBlock *big.Int `json:"ahmedabadBlock"` // Ahmedabad switch block (nil = no fork, 0 = already on ahmedabad) - BhilaiBlock *big.Int `json:"bhilaiBlock"` // Bhilai switch block (nil = no fork, 0 = already on bhilai) - RioBlock *big.Int `json:"rioBlock"` // Rio switch block (nil = no fork, 0 = already on rio) - MadhugiriBlock *big.Int `json:"madhugiriBlock"` // Madhugiri switch block (nil = no fork, 0 = already on madhugiri) - MadhugiriProBlock *big.Int `json:"madhugiriProBlock"` // MadhugiriPro switch block (nil = no fork, 0 = already on madhugiriPro) - DandeliBlock *big.Int `json:"dandeliBlock"` // Dandeli switch block (nil = no fork, 0 = already on dandeli) + TargetGasPercentage *uint64 `json:"-"` // Post-Dandeli: target gas as % of gas limit (1-100, default 65). Set via --miner.target-gas-percentage + BaseFeeChangeDenominator *uint64 `json:"-"` // Post-Dandeli: base fee change rate (must be >0, default 64). Set via --miner.base-fee-change-denominator + + JaipurBlock *big.Int `json:"jaipurBlock"` // Jaipur switch block (nil = no fork, 0 = already on jaipur) + DelhiBlock *big.Int `json:"delhiBlock"` // Delhi switch block (nil = no fork, 0 = already on delhi) + IndoreBlock *big.Int `json:"indoreBlock"` // Indore switch block (nil = no fork, 0 = already on indore) + StateSyncConfirmationDelay map[string]uint64 `json:"stateSyncConfirmationDelay"` // StateSync Confirmation Delay, in seconds, to calculate `to` + AhmedabadBlock *big.Int `json:"ahmedabadBlock"` // Ahmedabad switch block (nil = no fork, 0 = already on ahmedabad) + BhilaiBlock *big.Int `json:"bhilaiBlock"` // Bhilai switch block (nil = no fork, 0 = already on bhilai) + RioBlock *big.Int `json:"rioBlock"` // Rio switch block (nil = no fork, 0 = already on rio) + MadhugiriBlock *big.Int `json:"madhugiriBlock"` // Madhugiri switch block (nil = no fork, 0 = already on madhugiri) + MadhugiriProBlock *big.Int `json:"madhugiriProBlock"` // MadhugiriPro switch block (nil = no fork, 0 = already on madhugiriPro) + DandeliBlock *big.Int `json:"dandeliBlock"` // Dandeli switch block (nil = no fork, 0 = already on dandeli) } // String implements the stringer interface, returning the consensus engine details. From 867ebb4addac905000c4bf7904d09915f40077b4 Mon Sep 17 00:00:00 2001 From: Lucca Martins Date: Mon, 26 Jan 2026 09:46:00 -0300 Subject: [PATCH 05/14] claude feedback and coverage --- internal/cli/server/config.go | 3 + internal/cli/server/config_test.go | 133 ++++++++++++++ params/config_test.go | 277 +++++++++++++++++++++++++++++ 3 files changed, 413 insertions(+) diff --git a/internal/cli/server/config.go b/internal/cli/server/config.go index 3d99108943..52e8f9db81 100644 --- a/internal/cli/server/config.go +++ b/internal/cli/server/config.go @@ -1213,6 +1213,9 @@ func (c *Config) buildEth(stack *node.Node, accountManager *accounts.Manager) (* if !c.Developer.Enabled && n.Genesis != nil && n.Genesis.Config != nil && n.Genesis.Config.Bor != nil { // Only set if non-zero (0 means not set via CLI, use defaults from consensus) if c.Sealer.TargetGasPercentage > 0 { + if c.Sealer.TargetGasPercentage > 100 { + return nil, fmt.Errorf("miner.targetGasPercentage must be between 1-100, got %d", c.Sealer.TargetGasPercentage) + } n.Genesis.Config.Bor.TargetGasPercentage = &c.Sealer.TargetGasPercentage } if c.Sealer.BaseFeeChangeDenominator > 0 { diff --git a/internal/cli/server/config_test.go b/internal/cli/server/config_test.go index 059cdec864..604f8d22b0 100644 --- a/internal/cli/server/config_test.go +++ b/internal/cli/server/config_test.go @@ -158,3 +158,136 @@ func TestConfigStateScheme(t *testing.T) { _, err = config.buildEth(nil, nil) assert.NoError(t, err) } + +func TestSealerTargetGasPercentageConfig(t *testing.T) { + t.Run("Valid custom value sets BorConfig", func(t *testing.T) { + testCases := []uint64{1, 50, 65, 100} + + for _, targetVal := range testCases { + config := DefaultConfig() + config.Sealer.TargetGasPercentage = targetVal + + assert.NoError(t, config.loadChain()) + + _, err := config.buildNode() + assert.NoError(t, err) + + ethConfig, err := config.buildEth(nil, nil) + assert.NoError(t, err) + + assert.NotNil(t, ethConfig.Genesis.Config.Bor.TargetGasPercentage) + assert.Equal(t, targetVal, *ethConfig.Genesis.Config.Bor.TargetGasPercentage) + } + }) + + t.Run("Invalid value >100 returns error", func(t *testing.T) { + testCases := []uint64{101, 200, 1000} + + for _, invalidVal := range testCases { + config := DefaultConfig() + config.Sealer.TargetGasPercentage = invalidVal + + assert.NoError(t, config.loadChain()) + + _, err := config.buildNode() + assert.NoError(t, err) + + _, err = config.buildEth(nil, nil) + assert.Error(t, err) + assert.Contains(t, err.Error(), "miner.targetGasPercentage must be between 1-100") + } + }) +} + +func TestSealerBaseFeeChangeDenominatorConfig(t *testing.T) { + t.Run("Valid custom value sets BorConfig", func(t *testing.T) { + testCases := []uint64{1, 8, 16, 32, 64, 128} + + for _, denomVal := range testCases { + config := DefaultConfig() + config.Sealer.BaseFeeChangeDenominator = denomVal + + assert.NoError(t, config.loadChain()) + + _, err := config.buildNode() + assert.NoError(t, err) + + ethConfig, err := config.buildEth(nil, nil) + assert.NoError(t, err) + + assert.NotNil(t, ethConfig.Genesis.Config.Bor.BaseFeeChangeDenominator) + assert.Equal(t, denomVal, *ethConfig.Genesis.Config.Bor.BaseFeeChangeDenominator) + } + }) +} + +func TestSealerBothGasParametersConfig(t *testing.T) { + t.Run("Both parameters set together", func(t *testing.T) { + config := DefaultConfig() + config.Sealer.TargetGasPercentage = 75 + config.Sealer.BaseFeeChangeDenominator = 128 + + assert.NoError(t, config.loadChain()) + + _, err := config.buildNode() + assert.NoError(t, err) + + ethConfig, err := config.buildEth(nil, nil) + assert.NoError(t, err) + + assert.NotNil(t, ethConfig.Genesis.Config.Bor.TargetGasPercentage) + assert.Equal(t, uint64(75), *ethConfig.Genesis.Config.Bor.TargetGasPercentage) + + assert.NotNil(t, ethConfig.Genesis.Config.Bor.BaseFeeChangeDenominator) + assert.Equal(t, uint64(128), *ethConfig.Genesis.Config.Bor.BaseFeeChangeDenominator) + }) + + t.Run("Only TargetGasPercentage set", func(t *testing.T) { + config := DefaultConfig() + config.Sealer.TargetGasPercentage = 80 + config.Sealer.BaseFeeChangeDenominator = 0 + + assert.NoError(t, config.loadChain()) + + _, err := config.buildNode() + assert.NoError(t, err) + + ethConfig, err := config.buildEth(nil, nil) + assert.NoError(t, err) + + assert.NotNil(t, ethConfig.Genesis.Config.Bor.TargetGasPercentage) + assert.Equal(t, uint64(80), *ethConfig.Genesis.Config.Bor.TargetGasPercentage) + }) + + t.Run("Only BaseFeeChangeDenominator set", func(t *testing.T) { + config := DefaultConfig() + config.Sealer.TargetGasPercentage = 0 + config.Sealer.BaseFeeChangeDenominator = 256 + + assert.NoError(t, config.loadChain()) + + _, err := config.buildNode() + assert.NoError(t, err) + + ethConfig, err := config.buildEth(nil, nil) + assert.NoError(t, err) + + assert.NotNil(t, ethConfig.Genesis.Config.Bor.BaseFeeChangeDenominator) + assert.Equal(t, uint64(256), *ethConfig.Genesis.Config.Bor.BaseFeeChangeDenominator) + }) + + t.Run("Invalid TargetGasPercentage with valid BaseFeeChangeDenominator", func(t *testing.T) { + config := DefaultConfig() + config.Sealer.TargetGasPercentage = 150 + config.Sealer.BaseFeeChangeDenominator = 64 + + assert.NoError(t, config.loadChain()) + + _, err := config.buildNode() + assert.NoError(t, err) + + _, err = config.buildEth(nil, nil) + assert.Error(t, err) + assert.Contains(t, err.Error(), "miner.targetGasPercentage must be between 1-100") + }) +} diff --git a/params/config_test.go b/params/config_test.go index fe27c4ca53..08cd0de200 100644 --- a/params/config_test.go +++ b/params/config_test.go @@ -630,3 +630,280 @@ func TestCalculateCoinbase(t *testing.T) { } }) } + +func TestGetTargetGasPercentage(t *testing.T) { + t.Parallel() + + t.Run("Pre-Dandeli block returns 0", func(t *testing.T) { + config := &BorConfig{ + DandeliBlock: big.NewInt(1000), + } + + // Test before Dandeli fork + result := config.GetTargetGasPercentage(big.NewInt(500)) + if result != 0 { + t.Errorf("Pre-Dandeli: expected 0, got %d", result) + } + + result = config.GetTargetGasPercentage(big.NewInt(999)) + if result != 0 { + t.Errorf("Pre-Dandeli (block 999): expected 0, got %d", result) + } + }) + + t.Run("Post-Dandeli with nil TargetGasPercentage returns default", func(t *testing.T) { + config := &BorConfig{ + DandeliBlock: big.NewInt(1000), + TargetGasPercentage: nil, + } + + result := config.GetTargetGasPercentage(big.NewInt(1000)) + if result != TargetGasPercentagePostDandeli { + t.Errorf("Post-Dandeli with nil: expected %d, got %d", TargetGasPercentagePostDandeli, result) + } + + result = config.GetTargetGasPercentage(big.NewInt(2000)) + if result != TargetGasPercentagePostDandeli { + t.Errorf("Post-Dandeli with nil (block 2000): expected %d, got %d", TargetGasPercentagePostDandeli, result) + } + }) + + t.Run("Post-Dandeli with valid custom values", func(t *testing.T) { + testCases := []struct { + customValue uint64 + description string + }{ + {1, "minimum valid value"}, + {50, "mid-range value"}, + {100, "maximum valid value"}, + {65, "default value"}, + } + + for _, tc := range testCases { + t.Run(tc.description, func(t *testing.T) { + val := tc.customValue + config := &BorConfig{ + DandeliBlock: big.NewInt(1000), + TargetGasPercentage: &val, + } + + result := config.GetTargetGasPercentage(big.NewInt(1000)) + if result != tc.customValue { + t.Errorf("Post-Dandeli with custom value %d: expected %d, got %d", + tc.customValue, tc.customValue, result) + } + }) + } + }) + + t.Run("Post-Dandeli with invalid value 0 falls back to default", func(t *testing.T) { + invalidVal := uint64(0) + config := &BorConfig{ + DandeliBlock: big.NewInt(1000), + TargetGasPercentage: &invalidVal, + } + + result := config.GetTargetGasPercentage(big.NewInt(1000)) + if result != TargetGasPercentagePostDandeli { + t.Errorf("Post-Dandeli with invalid value 0: expected %d, got %d", + TargetGasPercentagePostDandeli, result) + } + }) + + t.Run("Post-Dandeli with invalid value >100 falls back to default", func(t *testing.T) { + testCases := []uint64{101, 200, 1000} + + for _, invalidVal := range testCases { + t.Run(fmt.Sprintf("value_%d", invalidVal), func(t *testing.T) { + val := invalidVal + config := &BorConfig{ + DandeliBlock: big.NewInt(1000), + TargetGasPercentage: &val, + } + + result := config.GetTargetGasPercentage(big.NewInt(1000)) + if result != TargetGasPercentagePostDandeli { + t.Errorf("Post-Dandeli with invalid value %d: expected %d, got %d", + invalidVal, TargetGasPercentagePostDandeli, result) + } + }) + } + }) +} + +func TestGetBaseFeeChangeDenominator(t *testing.T) { + t.Parallel() + + t.Run("Pre-Delhi returns DefaultBaseFeeChangeDenominator", func(t *testing.T) { + config := &BorConfig{ + DelhiBlock: big.NewInt(1000), + BhilaiBlock: nil, + } + + result := config.GetBaseFeeChangeDenominator(big.NewInt(500)) + if result != DefaultBaseFeeChangeDenominator { + t.Errorf("Pre-Delhi: expected %d, got %d", DefaultBaseFeeChangeDenominator, result) + } + + result = config.GetBaseFeeChangeDenominator(big.NewInt(999)) + if result != DefaultBaseFeeChangeDenominator { + t.Errorf("Pre-Delhi (block 999): expected %d, got %d", DefaultBaseFeeChangeDenominator, result) + } + }) + + t.Run("Post-Delhi Pre-Bhilai returns BaseFeeChangeDenominatorPostDelhi", func(t *testing.T) { + config := &BorConfig{ + DelhiBlock: big.NewInt(1000), + BhilaiBlock: big.NewInt(2000), + DandeliBlock: nil, + } + + result := config.GetBaseFeeChangeDenominator(big.NewInt(1000)) + if result != BaseFeeChangeDenominatorPostDelhi { + t.Errorf("Post-Delhi, Pre-Bhilai (block 1000): expected %d, got %d", + BaseFeeChangeDenominatorPostDelhi, result) + } + + result = config.GetBaseFeeChangeDenominator(big.NewInt(1500)) + if result != BaseFeeChangeDenominatorPostDelhi { + t.Errorf("Post-Delhi, Pre-Bhilai (block 1500): expected %d, got %d", + BaseFeeChangeDenominatorPostDelhi, result) + } + + result = config.GetBaseFeeChangeDenominator(big.NewInt(1999)) + if result != BaseFeeChangeDenominatorPostDelhi { + t.Errorf("Post-Delhi, Pre-Bhilai (block 1999): expected %d, got %d", + BaseFeeChangeDenominatorPostDelhi, result) + } + }) + + t.Run("Post-Bhilai Pre-Dandeli returns BaseFeeChangeDenominatorPostBhilai", func(t *testing.T) { + config := &BorConfig{ + DelhiBlock: big.NewInt(1000), + BhilaiBlock: big.NewInt(2000), + DandeliBlock: big.NewInt(3000), + } + + result := config.GetBaseFeeChangeDenominator(big.NewInt(2000)) + if result != BaseFeeChangeDenominatorPostBhilai { + t.Errorf("Post-Bhilai, Pre-Dandeli (block 2000): expected %d, got %d", + BaseFeeChangeDenominatorPostBhilai, result) + } + + result = config.GetBaseFeeChangeDenominator(big.NewInt(2500)) + if result != BaseFeeChangeDenominatorPostBhilai { + t.Errorf("Post-Bhilai, Pre-Dandeli (block 2500): expected %d, got %d", + BaseFeeChangeDenominatorPostBhilai, result) + } + + result = config.GetBaseFeeChangeDenominator(big.NewInt(2999)) + if result != BaseFeeChangeDenominatorPostBhilai { + t.Errorf("Post-Bhilai, Pre-Dandeli (block 2999): expected %d, got %d", + BaseFeeChangeDenominatorPostBhilai, result) + } + }) + + t.Run("Post-Dandeli with nil custom value falls back to Bhilai default", func(t *testing.T) { + config := &BorConfig{ + DelhiBlock: big.NewInt(1000), + BhilaiBlock: big.NewInt(2000), + DandeliBlock: big.NewInt(3000), + BaseFeeChangeDenominator: nil, + } + + result := config.GetBaseFeeChangeDenominator(big.NewInt(3000)) + if result != BaseFeeChangeDenominatorPostBhilai { + t.Errorf("Post-Dandeli with nil custom value: expected %d, got %d", + BaseFeeChangeDenominatorPostBhilai, result) + } + + result = config.GetBaseFeeChangeDenominator(big.NewInt(4000)) + if result != BaseFeeChangeDenominatorPostBhilai { + t.Errorf("Post-Dandeli with nil custom value (block 4000): expected %d, got %d", + BaseFeeChangeDenominatorPostBhilai, result) + } + }) + + t.Run("Post-Dandeli with valid custom value", func(t *testing.T) { + testCases := []uint64{1, 8, 16, 32, 64, 128} + + for _, customVal := range testCases { + t.Run(fmt.Sprintf("value_%d", customVal), func(t *testing.T) { + val := customVal + config := &BorConfig{ + DelhiBlock: big.NewInt(1000), + BhilaiBlock: big.NewInt(2000), + DandeliBlock: big.NewInt(3000), + BaseFeeChangeDenominator: &val, + } + + result := config.GetBaseFeeChangeDenominator(big.NewInt(3000)) + if result != customVal { + t.Errorf("Post-Dandeli with custom value %d: expected %d, got %d", + customVal, customVal, result) + } + }) + } + }) + + t.Run("Post-Dandeli with invalid value 0 falls back to Bhilai default", func(t *testing.T) { + invalidVal := uint64(0) + config := &BorConfig{ + DelhiBlock: big.NewInt(1000), + BhilaiBlock: big.NewInt(2000), + DandeliBlock: big.NewInt(3000), + BaseFeeChangeDenominator: &invalidVal, + } + + result := config.GetBaseFeeChangeDenominator(big.NewInt(3000)) + if result != BaseFeeChangeDenominatorPostBhilai { + t.Errorf("Post-Dandeli with invalid value 0: expected %d, got %d", + BaseFeeChangeDenominatorPostBhilai, result) + } + }) + + t.Run("Pre-Dandeli with custom value ignores it", func(t *testing.T) { + customVal := uint64(999) + config := &BorConfig{ + DelhiBlock: big.NewInt(1000), + BhilaiBlock: big.NewInt(2000), + DandeliBlock: big.NewInt(3000), + BaseFeeChangeDenominator: &customVal, + } + + // Before Dandeli, custom value should be ignored + result := config.GetBaseFeeChangeDenominator(big.NewInt(2500)) + if result != BaseFeeChangeDenominatorPostBhilai { + t.Errorf("Pre-Dandeli with custom value (block 2500): expected %d, got %d", + BaseFeeChangeDenominatorPostBhilai, result) + } + }) + + t.Run("Post-Dandeli but Pre-Bhilai falls back to Delhi default", func(t *testing.T) { + config := &BorConfig{ + DelhiBlock: big.NewInt(1000), + BhilaiBlock: nil, + DandeliBlock: big.NewInt(2000), + } + + result := config.GetBaseFeeChangeDenominator(big.NewInt(2000)) + if result != BaseFeeChangeDenominatorPostDelhi { + t.Errorf("Post-Dandeli, Pre-Bhilai: expected %d, got %d", + BaseFeeChangeDenominatorPostDelhi, result) + } + }) + + t.Run("Post-Dandeli but Pre-Delhi falls back to default", func(t *testing.T) { + config := &BorConfig{ + DelhiBlock: nil, + BhilaiBlock: nil, + DandeliBlock: big.NewInt(1000), + } + + result := config.GetBaseFeeChangeDenominator(big.NewInt(1000)) + if result != DefaultBaseFeeChangeDenominator { + t.Errorf("Post-Dandeli, Pre-Delhi: expected %d, got %d", + DefaultBaseFeeChangeDenominator, result) + } + }) +} From 54753109abf00799b312d722121c8bfbea10b691 Mon Sep 17 00:00:00 2001 From: Lucca Martins Date: Mon, 26 Jan 2026 09:49:03 -0300 Subject: [PATCH 06/14] minor missing cover scenario --- internal/cli/server/config.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/internal/cli/server/config.go b/internal/cli/server/config.go index 52e8f9db81..d8018d5cc0 100644 --- a/internal/cli/server/config.go +++ b/internal/cli/server/config.go @@ -1293,6 +1293,9 @@ func (c *Config) buildEth(stack *node.Node, accountManager *accounts.Manager) (* if n.Genesis != nil && n.Genesis.Config != nil && n.Genesis.Config.Bor != nil { // Only set if non-zero (0 means not set via CLI, use defaults from consensus) if c.Sealer.TargetGasPercentage > 0 { + if c.Sealer.TargetGasPercentage > 100 { + return nil, fmt.Errorf("miner.targetGasPercentage must be between 1-100, got %d", c.Sealer.TargetGasPercentage) + } n.Genesis.Config.Bor.TargetGasPercentage = &c.Sealer.TargetGasPercentage } if c.Sealer.BaseFeeChangeDenominator > 0 { From 7f29e4c8cb8c0dad21277ff049f8b66200533aee Mon Sep 17 00:00:00 2001 From: Lucca Martins Date: Mon, 26 Jan 2026 09:49:43 -0300 Subject: [PATCH 07/14] lint fix --- params/config_test.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/params/config_test.go b/params/config_test.go index 08cd0de200..bdf8072c5f 100644 --- a/params/config_test.go +++ b/params/config_test.go @@ -653,8 +653,8 @@ func TestGetTargetGasPercentage(t *testing.T) { t.Run("Post-Dandeli with nil TargetGasPercentage returns default", func(t *testing.T) { config := &BorConfig{ - DandeliBlock: big.NewInt(1000), - TargetGasPercentage: nil, + DandeliBlock: big.NewInt(1000), + TargetGasPercentage: nil, } result := config.GetTargetGasPercentage(big.NewInt(1000)) @@ -753,8 +753,8 @@ func TestGetBaseFeeChangeDenominator(t *testing.T) { t.Run("Post-Delhi Pre-Bhilai returns BaseFeeChangeDenominatorPostDelhi", func(t *testing.T) { config := &BorConfig{ - DelhiBlock: big.NewInt(1000), - BhilaiBlock: big.NewInt(2000), + DelhiBlock: big.NewInt(1000), + BhilaiBlock: big.NewInt(2000), DandeliBlock: nil, } From 4d2a7df519a7a988f87a355bc197cf2117a99caf Mon Sep 17 00:00:00 2001 From: Lucca Martins Date: Mon, 26 Jan 2026 10:03:01 -0300 Subject: [PATCH 08/14] remove claude ignore --- .gitignore | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/.gitignore b/.gitignore index 892b80b06a..83f06b0c22 100644 --- a/.gitignore +++ b/.gitignore @@ -60,7 +60,4 @@ cmd/ethkey/ethkey cmd/evm/evm cmd/geth/geth cmd/rlpdump/rlpdump -cmd/workload/workload - -# Claude AI assistant context -.claude.md \ No newline at end of file +cmd/workload/workload \ No newline at end of file From 810dc609e669e64b72b0608613ad729644ee992d Mon Sep 17 00:00:00 2001 From: Lucca Martins Date: Mon, 26 Jan 2026 11:03:43 -0300 Subject: [PATCH 09/14] remove duplicate and fix tests --- consensus/misc/eip1559/eip1559.go | 21 +------------ params/config.go | 49 ------------------------------- params/config_test.go | 30 +++++++++---------- params/protocol_params.go | 22 ++++++++++++-- 4 files changed, 36 insertions(+), 86 deletions(-) diff --git a/consensus/misc/eip1559/eip1559.go b/consensus/misc/eip1559/eip1559.go index a6f4e7e330..0b11ae51f9 100644 --- a/consensus/misc/eip1559/eip1559.go +++ b/consensus/misc/eip1559/eip1559.go @@ -80,7 +80,7 @@ func CalcBaseFee(config *params.ChainConfig, parent *types.Header) *big.Int { var ( num = new(big.Int) denom = new(big.Int) - baseFeeChangeDenominatorUint64 = baseFeeChangeDenominator(config, parent.Number) + baseFeeChangeDenominatorUint64 = params.BaseFeeChangeDenominator(config.Bor, parent.Number) ) if parent.GasUsed > parentGasTarget { @@ -110,25 +110,6 @@ func CalcBaseFee(config *params.ChainConfig, parent *types.Header) *big.Int { } } -// baseFeeChangeDenominator returns the denominator used to bound the amount the base fee can change between blocks. -// The value varies based on the hard fork: -// - Pre-Delhi: 8 (default) -// - Post-Delhi: 16 -// - Post-Bhilai: 64 -// If borConfig is nil, returns the default value of 8. -func baseFeeChangeDenominator(config *params.ChainConfig, number *big.Int) uint64 { - if config.Bor == nil { - return params.DefaultBaseFeeChangeDenominator - } - if config.Bor.IsBhilai(number) { - return params.BaseFeeChangeDenominatorPostBhilai - } else if config.Bor.IsDelhi(number) { - return params.BaseFeeChangeDenominatorPostDelhi - } else { - return params.DefaultBaseFeeChangeDenominator - } -} - // calcParentGasTarget calculates the target gas based on parent block gas limit. Earlier // it was derived by `ElasticityMultiplier` as it had an integer multiplier value. Post // Dandeli HF, a percentage value is used to calculate the gas target (validated with fallback to default). diff --git a/params/config.go b/params/config.go index 8b691473cd..1c65d835bc 100644 --- a/params/config.go +++ b/params/config.go @@ -1001,31 +1001,6 @@ func (c *BorConfig) GetTargetGasPercentage(number *big.Int) uint64 { return TargetGasPercentagePostDandeli } -// GetBaseFeeChangeDenominator returns the base fee change denominator. -// After Dandeli hard fork, this value can be configured via CLI flags (stored in BorConfig at runtime). -// It validates the configured value and falls back to hard fork based defaults if invalid or nil. -func (c *BorConfig) GetBaseFeeChangeDenominator(number *big.Int) uint64 { - // If Dandeli is active and custom value is set, validate and use it - if c.IsDandeli(number) && c.BaseFeeChangeDenominator != nil { - val := *c.BaseFeeChangeDenominator - // Validate: must be non-zero to prevent division by zero - if val > 0 { - return val - } - // Invalid value - log error and fall back to default - log.Error("Invalid BaseFeeChangeDenominator in BorConfig (must be > 0), falling back to default", - "configured", val) - } - - // Fall back to hard fork based values - if c.IsBhilai(number) { - return BaseFeeChangeDenominatorPostBhilai - } else if c.IsDelhi(number) { - return BaseFeeChangeDenominatorPostDelhi - } - return DefaultBaseFeeChangeDenominator -} - func (c *BorConfig) IsSprintStart(number uint64) bool { return number%c.CalculateSprint(number) == 0 } @@ -1695,30 +1670,6 @@ func newBlockCompatError(what string, storedblock, newblock *big.Int) *ConfigCom return err } -// nolint -func newTimestampCompatError(what string, storedtime, newtime *uint64) *ConfigCompatError { - var rew *uint64 - switch { - case storedtime == nil: - rew = newtime - case newtime == nil || *storedtime < *newtime: - rew = storedtime - default: - rew = newtime - } - err := &ConfigCompatError{ - What: what, - StoredTime: storedtime, - NewTime: newtime, - RewindToTime: 0, - } - if rew != nil && *rew != 0 { - err.RewindToTime = *rew - 1 - } - - return err -} - func (err *ConfigCompatError) Error() string { if err.StoredBlock != nil { return fmt.Sprintf("mismatching %s in database (have block %d, want block %d, rewindto block %d)", err.What, err.StoredBlock, err.NewBlock, err.RewindToBlock) diff --git a/params/config_test.go b/params/config_test.go index bdf8072c5f..5e4cf279a3 100644 --- a/params/config_test.go +++ b/params/config_test.go @@ -740,12 +740,12 @@ func TestGetBaseFeeChangeDenominator(t *testing.T) { BhilaiBlock: nil, } - result := config.GetBaseFeeChangeDenominator(big.NewInt(500)) + result := BaseFeeChangeDenominator(config, big.NewInt(500)) if result != DefaultBaseFeeChangeDenominator { t.Errorf("Pre-Delhi: expected %d, got %d", DefaultBaseFeeChangeDenominator, result) } - result = config.GetBaseFeeChangeDenominator(big.NewInt(999)) + result = BaseFeeChangeDenominator(config, big.NewInt(999)) if result != DefaultBaseFeeChangeDenominator { t.Errorf("Pre-Delhi (block 999): expected %d, got %d", DefaultBaseFeeChangeDenominator, result) } @@ -758,19 +758,19 @@ func TestGetBaseFeeChangeDenominator(t *testing.T) { DandeliBlock: nil, } - result := config.GetBaseFeeChangeDenominator(big.NewInt(1000)) + result := BaseFeeChangeDenominator(config, big.NewInt(1000)) if result != BaseFeeChangeDenominatorPostDelhi { t.Errorf("Post-Delhi, Pre-Bhilai (block 1000): expected %d, got %d", BaseFeeChangeDenominatorPostDelhi, result) } - result = config.GetBaseFeeChangeDenominator(big.NewInt(1500)) + result = BaseFeeChangeDenominator(config, big.NewInt(1500)) if result != BaseFeeChangeDenominatorPostDelhi { t.Errorf("Post-Delhi, Pre-Bhilai (block 1500): expected %d, got %d", BaseFeeChangeDenominatorPostDelhi, result) } - result = config.GetBaseFeeChangeDenominator(big.NewInt(1999)) + result = BaseFeeChangeDenominator(config, big.NewInt(1999)) if result != BaseFeeChangeDenominatorPostDelhi { t.Errorf("Post-Delhi, Pre-Bhilai (block 1999): expected %d, got %d", BaseFeeChangeDenominatorPostDelhi, result) @@ -784,19 +784,19 @@ func TestGetBaseFeeChangeDenominator(t *testing.T) { DandeliBlock: big.NewInt(3000), } - result := config.GetBaseFeeChangeDenominator(big.NewInt(2000)) + result := BaseFeeChangeDenominator(config, big.NewInt(2000)) if result != BaseFeeChangeDenominatorPostBhilai { t.Errorf("Post-Bhilai, Pre-Dandeli (block 2000): expected %d, got %d", BaseFeeChangeDenominatorPostBhilai, result) } - result = config.GetBaseFeeChangeDenominator(big.NewInt(2500)) + result = BaseFeeChangeDenominator(config, big.NewInt(2500)) if result != BaseFeeChangeDenominatorPostBhilai { t.Errorf("Post-Bhilai, Pre-Dandeli (block 2500): expected %d, got %d", BaseFeeChangeDenominatorPostBhilai, result) } - result = config.GetBaseFeeChangeDenominator(big.NewInt(2999)) + result = BaseFeeChangeDenominator(config, big.NewInt(2999)) if result != BaseFeeChangeDenominatorPostBhilai { t.Errorf("Post-Bhilai, Pre-Dandeli (block 2999): expected %d, got %d", BaseFeeChangeDenominatorPostBhilai, result) @@ -811,13 +811,13 @@ func TestGetBaseFeeChangeDenominator(t *testing.T) { BaseFeeChangeDenominator: nil, } - result := config.GetBaseFeeChangeDenominator(big.NewInt(3000)) + result := BaseFeeChangeDenominator(config, big.NewInt(3000)) if result != BaseFeeChangeDenominatorPostBhilai { t.Errorf("Post-Dandeli with nil custom value: expected %d, got %d", BaseFeeChangeDenominatorPostBhilai, result) } - result = config.GetBaseFeeChangeDenominator(big.NewInt(4000)) + result = BaseFeeChangeDenominator(config, big.NewInt(4000)) if result != BaseFeeChangeDenominatorPostBhilai { t.Errorf("Post-Dandeli with nil custom value (block 4000): expected %d, got %d", BaseFeeChangeDenominatorPostBhilai, result) @@ -837,7 +837,7 @@ func TestGetBaseFeeChangeDenominator(t *testing.T) { BaseFeeChangeDenominator: &val, } - result := config.GetBaseFeeChangeDenominator(big.NewInt(3000)) + result := BaseFeeChangeDenominator(config, big.NewInt(3000)) if result != customVal { t.Errorf("Post-Dandeli with custom value %d: expected %d, got %d", customVal, customVal, result) @@ -855,7 +855,7 @@ func TestGetBaseFeeChangeDenominator(t *testing.T) { BaseFeeChangeDenominator: &invalidVal, } - result := config.GetBaseFeeChangeDenominator(big.NewInt(3000)) + result := BaseFeeChangeDenominator(config, big.NewInt(3000)) if result != BaseFeeChangeDenominatorPostBhilai { t.Errorf("Post-Dandeli with invalid value 0: expected %d, got %d", BaseFeeChangeDenominatorPostBhilai, result) @@ -872,7 +872,7 @@ func TestGetBaseFeeChangeDenominator(t *testing.T) { } // Before Dandeli, custom value should be ignored - result := config.GetBaseFeeChangeDenominator(big.NewInt(2500)) + result := BaseFeeChangeDenominator(config, big.NewInt(2500)) if result != BaseFeeChangeDenominatorPostBhilai { t.Errorf("Pre-Dandeli with custom value (block 2500): expected %d, got %d", BaseFeeChangeDenominatorPostBhilai, result) @@ -886,7 +886,7 @@ func TestGetBaseFeeChangeDenominator(t *testing.T) { DandeliBlock: big.NewInt(2000), } - result := config.GetBaseFeeChangeDenominator(big.NewInt(2000)) + result := BaseFeeChangeDenominator(config, big.NewInt(2000)) if result != BaseFeeChangeDenominatorPostDelhi { t.Errorf("Post-Dandeli, Pre-Bhilai: expected %d, got %d", BaseFeeChangeDenominatorPostDelhi, result) @@ -900,7 +900,7 @@ func TestGetBaseFeeChangeDenominator(t *testing.T) { DandeliBlock: big.NewInt(1000), } - result := config.GetBaseFeeChangeDenominator(big.NewInt(1000)) + result := BaseFeeChangeDenominator(config, big.NewInt(1000)) if result != DefaultBaseFeeChangeDenominator { t.Errorf("Post-Dandeli, Pre-Delhi: expected %d, got %d", DefaultBaseFeeChangeDenominator, result) diff --git a/params/protocol_params.go b/params/protocol_params.go index 4eb1070208..a592274f71 100644 --- a/params/protocol_params.go +++ b/params/protocol_params.go @@ -20,6 +20,7 @@ import ( "math/big" "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/log" ) const ( @@ -247,6 +248,23 @@ func BaseFeeChangeDenominator(borConfig *BorConfig, number *big.Int) uint64 { if borConfig == nil { return DefaultBaseFeeChangeDenominator } - // Use the helper function which includes validation - return borConfig.GetBaseFeeChangeDenominator(number) + // If Dandeli is active and custom value is set, validate and use it + if borConfig.IsDandeli(number) && borConfig.BaseFeeChangeDenominator != nil { + val := *borConfig.BaseFeeChangeDenominator + // Validate: must be non-zero to prevent division by zero + if val > 0 { + return val + } + // Invalid value - log error and fall back to default + log.Error("Invalid BaseFeeChangeDenominator in BorConfig (must be > 0), falling back to default", + "configured", val) + } + + // Fall back to hard fork based values + if borConfig.IsBhilai(number) { + return BaseFeeChangeDenominatorPostBhilai + } else if borConfig.IsDelhi(number) { + return BaseFeeChangeDenominatorPostDelhi + } + return DefaultBaseFeeChangeDenominator } From 7247a70d0f9bc818e386480780ed5d93dae3b39f Mon Sep 17 00:00:00 2001 From: Lucca Martins Date: Mon, 26 Jan 2026 17:41:10 -0300 Subject: [PATCH 10/14] increase coverage --- internal/cli/server/config_test.go | 80 ++++++++++++++++++++++++++++++ 1 file changed, 80 insertions(+) diff --git a/internal/cli/server/config_test.go b/internal/cli/server/config_test.go index 604f8d22b0..3b5725657f 100644 --- a/internal/cli/server/config_test.go +++ b/internal/cli/server/config_test.go @@ -291,3 +291,83 @@ func TestSealerBothGasParametersConfig(t *testing.T) { assert.Contains(t, err.Error(), "miner.targetGasPercentage must be between 1-100") }) } + +// TestDeveloperModeGasParameters tests the developer mode specific code path +// for setting TargetGasPercentage and BaseFeeChangeDenominator (lines 1293-1304 in config.go). +// The default config uses mainnet which has Bor config, so these tests actually execute lines 1293-1304. +func TestDeveloperModeGasParameters(t *testing.T) { + t.Run("Scenario 1: Both TargetGasPercentage > 0 AND BaseFeeChangeDenominator > 0", func(t *testing.T) { + // Tests lines 1295-1300 AND 1301-1303 both execute + config := DefaultConfig() + config.Developer.Enabled = true + config.Developer.Period = 0 + config.Sealer.TargetGasPercentage = 75 // > 0, so line 1295 true + config.Sealer.BaseFeeChangeDenominator = 128 // > 0, so line 1301 true + + server, err := CreateMockServer(config) + assert.NoError(t, err) + defer CloseMockServer(server) + + // Mainnet config has Bor, so lines 1295-1303 execute and set both values + chainConfig := server.backend.BlockChain().GetChainConfig() + assert.NotNil(t, chainConfig.Bor) + assert.NotNil(t, chainConfig.Bor.TargetGasPercentage) + assert.Equal(t, uint64(75), *chainConfig.Bor.TargetGasPercentage) + assert.NotNil(t, chainConfig.Bor.BaseFeeChangeDenominator) + assert.Equal(t, uint64(128), *chainConfig.Bor.BaseFeeChangeDenominator) + }) + + t.Run("Scenario 2: Only TargetGasPercentage > 0, BaseFeeChangeDenominator == 0", func(t *testing.T) { + // Tests line 1295 true (executes), line 1301 false (skips) + config := DefaultConfig() + config.Developer.Enabled = true + config.Developer.Period = 0 + config.Sealer.TargetGasPercentage = 80 // > 0, so line 1295 true + config.Sealer.BaseFeeChangeDenominator = 0 // == 0, so line 1301 false + + server, err := CreateMockServer(config) + assert.NoError(t, err) + defer CloseMockServer(server) + + chainConfig := server.backend.BlockChain().GetChainConfig() + assert.NotNil(t, chainConfig.Bor) + // Line 1299 sets TargetGasPercentage + assert.NotNil(t, chainConfig.Bor.TargetGasPercentage) + assert.Equal(t, uint64(80), *chainConfig.Bor.TargetGasPercentage) + // Line 1301 condition is false, so BaseFeeChangeDenominator uses default from chain + // (not nil, but not set by our config) + }) + + t.Run("Scenario 3: TargetGasPercentage == 0, only BaseFeeChangeDenominator > 0", func(t *testing.T) { + // Tests line 1295 false (skips), line 1301 true (executes) + config := DefaultConfig() + config.Developer.Enabled = true + config.Developer.Period = 0 + config.Sealer.TargetGasPercentage = 0 // == 0, so line 1295 false + config.Sealer.BaseFeeChangeDenominator = 64 // > 0, so line 1301 true + + server, err := CreateMockServer(config) + assert.NoError(t, err) + defer CloseMockServer(server) + + chainConfig := server.backend.BlockChain().GetChainConfig() + assert.NotNil(t, chainConfig.Bor) + // Line 1295 condition is false, so TargetGasPercentage uses default from chain + // Line 1302 sets BaseFeeChangeDenominator + assert.NotNil(t, chainConfig.Bor.BaseFeeChangeDenominator) + assert.Equal(t, uint64(64), *chainConfig.Bor.BaseFeeChangeDenominator) + }) + + t.Run("Validation: TargetGasPercentage > 100 fails", func(t *testing.T) { + // Tests line 1296-1298 validation executes and returns error + config := DefaultConfig() + config.Developer.Enabled = true + config.Developer.Period = 0 + config.Sealer.TargetGasPercentage = 150 // Invalid value + + // With mainnet Bor config, validation at line 1296-1298 executes and returns error + _, err := CreateMockServer(config) + assert.Error(t, err) + assert.Contains(t, err.Error(), "miner.targetGasPercentage must be between 1-100") + }) +} From 711b75d86bdd97df6ef2c3b36130378bee0c32df Mon Sep 17 00:00:00 2001 From: Lucca Martins Date: Mon, 26 Jan 2026 17:48:48 -0300 Subject: [PATCH 11/14] lint --- internal/cli/server/config_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/cli/server/config_test.go b/internal/cli/server/config_test.go index 3b5725657f..803cf000e1 100644 --- a/internal/cli/server/config_test.go +++ b/internal/cli/server/config_test.go @@ -301,7 +301,7 @@ func TestDeveloperModeGasParameters(t *testing.T) { config := DefaultConfig() config.Developer.Enabled = true config.Developer.Period = 0 - config.Sealer.TargetGasPercentage = 75 // > 0, so line 1295 true + config.Sealer.TargetGasPercentage = 75 // > 0, so line 1295 true config.Sealer.BaseFeeChangeDenominator = 128 // > 0, so line 1301 true server, err := CreateMockServer(config) @@ -322,7 +322,7 @@ func TestDeveloperModeGasParameters(t *testing.T) { config := DefaultConfig() config.Developer.Enabled = true config.Developer.Period = 0 - config.Sealer.TargetGasPercentage = 80 // > 0, so line 1295 true + config.Sealer.TargetGasPercentage = 80 // > 0, so line 1295 true config.Sealer.BaseFeeChangeDenominator = 0 // == 0, so line 1301 false server, err := CreateMockServer(config) From f2d9690152c6f0e030b4207f1f0afb6b9092e40f Mon Sep 17 00:00:00 2001 From: Lucca Martins Date: Tue, 27 Jan 2026 10:25:06 -0300 Subject: [PATCH 12/14] Including integration tests for config changes and params divergence --- tests/bor/bor_config_change_test.go | 598 ++++++++++++++++++++++++++++ 1 file changed, 598 insertions(+) create mode 100644 tests/bor/bor_config_change_test.go diff --git a/tests/bor/bor_config_change_test.go b/tests/bor/bor_config_change_test.go new file mode 100644 index 0000000000..c55d897887 --- /dev/null +++ b/tests/bor/bor_config_change_test.go @@ -0,0 +1,598 @@ +//go:build integration +// +build integration + +package bor + +import ( + "crypto/ecdsa" + "math/big" + "os" + "testing" + "time" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/fdlimit" + "github.com/ethereum/go-ethereum/consensus/bor" + "github.com/ethereum/go-ethereum/consensus/misc/eip1559" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/log" + "github.com/ethereum/go-ethereum/params" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// TestBorConfigParameterChange tests that when BaseFeeChangeDenominator and TargetGasPercentage +// are changed at a certain block, blocks produced with the new parameters are accepted by validators. +// This test verifies the validate header flow and shows the dynamic change of BorConfig parameters. +func TestBorConfigParameterChange(t *testing.T) { + t.Parallel() + log.SetDefault(log.NewLogger(log.NewTerminalHandlerWithLevel(os.Stderr, log.LevelInfo, true))) + fdlimit.Raise(2048) + + // Generate a batch of accounts to seal and fund with + faucets := make([]*ecdsa.PrivateKey, 128) + for i := 0; i < len(faucets); i++ { + faucets[i], _ = crypto.GenerateKey() + } + + // Initialize genesis with 2 validators + genesis := InitGenesis(t, faucets, "./testdata/genesis_2val.json", 16) + + // Enable London fork from genesis to test EIP-1559 + genesis.Config.LondonBlock = common.Big0 + genesis.Config.Bor.JaipurBlock = common.Big0 + + // Enable Dandeli fork at block 20 - this is where we'll change the parameters + dandeliBlock := big.NewInt(20) + genesis.Config.Bor.DandeliBlock = dandeliBlock + + // Set custom BaseFeeChangeDenominator and TargetGasPercentage that will take effect at Dandeli + customBaseFeeChangeDenominator := uint64(32) // Different from default (64) + customTargetGasPercentage := uint64(70) // Different from default (65) + genesis.Config.Bor.BaseFeeChangeDenominator = &customBaseFeeChangeDenominator + genesis.Config.Bor.TargetGasPercentage = &customTargetGasPercentage + + // Setup 2 nodes: one producer and one validator + stacks, nodes, _ := setupMiner(t, 2, genesis) + + defer func() { + for _, stack := range stacks { + stack.Close() + } + }() + + // Start mining on both nodes + for _, node := range nodes { + if err := node.StartMining(); err != nil { + t.Fatal("Error occurred while starting miner", "node", node, "error", err) + } + } + + // Wait for blocks to be mined beyond the Dandeli fork block + targetBlockNum := dandeliBlock.Uint64() + 10 // Mine 10 blocks after the fork + timeout := time.After(120 * time.Second) + + for { + select { + case <-timeout: + t.Fatal("Timeout waiting for blocks to be mined") + default: + currentBlock := nodes[0].BlockChain().CurrentHeader() + if currentBlock.Number.Uint64() >= targetBlockNum { + goto checkResults + } + time.Sleep(500 * time.Millisecond) + } + } + +checkResults: + // Verify that both nodes have the same chain (validator accepted producer's blocks) + chain0 := nodes[0].BlockChain() + chain1 := nodes[1].BlockChain() + + t.Logf("Node 0 current block: %d", chain0.CurrentHeader().Number.Uint64()) + t.Logf("Node 1 current block: %d", chain1.CurrentHeader().Number.Uint64()) + + // Check that both nodes are synced + require.Equal(t, chain0.CurrentHeader().Number.Uint64(), chain1.CurrentHeader().Number.Uint64(), + "Both nodes should be at the same block height") + + // Verify blocks around the Dandeli fork + preDandeliBlock := dandeliBlock.Uint64() - 1 + atDandeliBlock := dandeliBlock.Uint64() + postDandeliBlock := dandeliBlock.Uint64() + 5 + + // Check block hashes match between nodes (validator accepted producer's blocks) + for _, blockNum := range []uint64{preDandeliBlock, atDandeliBlock, postDandeliBlock} { + header0 := chain0.GetHeaderByNumber(blockNum) + header1 := chain1.GetHeaderByNumber(blockNum) + + require.NotNil(t, header0, "Node 0 should have block %d", blockNum) + require.NotNil(t, header1, "Node 1 should have block %d", blockNum) + + assert.Equal(t, header0.Hash(), header1.Hash(), + "Both nodes should have the same block hash at height %d", blockNum) + + t.Logf("Block %d: Hash=%s, BaseFee=%s, GasLimit=%d, GasUsed=%d", + blockNum, header0.Hash().Hex()[:10], header0.BaseFee.String(), header0.GasLimit, header0.GasUsed) + } + + // Verify that the BaseFeeChangeDenominator and TargetGasPercentage are being used correctly + // by checking the base fee calculation before and after Dandeli fork + + // Pre-Dandeli block: should use default parameters + preHeader := chain0.GetHeaderByNumber(preDandeliBlock) + preParentHeader := chain0.GetHeaderByNumber(preDandeliBlock - 1) + + if preParentHeader != nil && preHeader != nil { + // Calculate expected base fee using pre-Dandeli parameters + expectedPreBaseFee := eip1559.CalcBaseFee(genesis.Config, preParentHeader) + t.Logf("Pre-Dandeli block %d: Expected BaseFee=%s, Actual BaseFee=%s", + preDandeliBlock, expectedPreBaseFee.String(), preHeader.BaseFee.String()) + // Note: We don't assert equality here because the block producer might have chosen different gas usage + } + + // Post-Dandeli block: should use custom parameters + postHeader := chain0.GetHeaderByNumber(postDandeliBlock) + postParentHeader := chain0.GetHeaderByNumber(postDandeliBlock - 1) + + if postParentHeader != nil && postHeader != nil { + // Calculate expected base fee using post-Dandeli parameters + expectedPostBaseFee := eip1559.CalcBaseFee(genesis.Config, postParentHeader) + t.Logf("Post-Dandeli block %d: Expected BaseFee=%s, Actual BaseFee=%s", + postDandeliBlock, expectedPostBaseFee.String(), postHeader.BaseFee.String()) + + // After Dandeli, base fee validation is skipped to allow dynamic configuration + // But we can verify that the calculation uses the new parameters by checking the target gas + targetPercentage := genesis.Config.Bor.GetTargetGasPercentage(postParentHeader.Number) + expectedTargetGas := postParentHeader.GasLimit * targetPercentage / 100 + t.Logf("Post-Dandeli target gas calculation: TargetPercentage=%d, GasLimit=%d, Expected TargetGas=%d", + targetPercentage, postParentHeader.GasLimit, expectedTargetGas) + + // Verify the custom target percentage is being used + assert.Equal(t, customTargetGasPercentage, targetPercentage, + "Custom TargetGasPercentage should be used after Dandeli fork") + + // Verify the custom base fee change denominator is being used + baseFeeChangeDenom := params.BaseFeeChangeDenominator(genesis.Config.Bor, postParentHeader.Number) + assert.Equal(t, customBaseFeeChangeDenominator, baseFeeChangeDenom, + "Custom BaseFeeChangeDenominator should be used after Dandeli fork") + } + + // Verify both nodes successfully validated the blocks with new parameters + // by checking that the validator node has the same blocks as the producer + for blockNum := atDandeliBlock; blockNum <= postDandeliBlock; blockNum++ { + header0 := chain0.GetHeaderByNumber(blockNum) + header1 := chain1.GetHeaderByNumber(blockNum) + + // Get the block authors to identify producer vs validator + author0, err := nodes[0].Engine().Author(header0) + require.NoError(t, err) + author1, err := nodes[1].Engine().Author(header1) + require.NoError(t, err) + + t.Logf("Block %d: Node0 Author=%s, Node1 Author=%s, Same=%v", + blockNum, author0.Hex()[:10], author1.Hex()[:10], author0 == author1) + + // Both nodes should have accepted the same block + assert.Equal(t, header0.Hash(), header1.Hash(), + "Validator should accept blocks produced with new BorConfig parameters at block %d", blockNum) + } + + t.Log("Test completed successfully: Validator accepted blocks with changed BorConfig parameters") +} + +// TestBorConfigParameterChangeVerification tests that the header verification +// correctly handles the parameter changes by verifying headers explicitly +func TestBorConfigParameterChangeVerification(t *testing.T) { + t.Parallel() + log.SetDefault(log.NewLogger(log.NewTerminalHandlerWithLevel(os.Stderr, log.LevelInfo, true))) + fdlimit.Raise(2048) + + // Generate a batch of accounts + faucets := make([]*ecdsa.PrivateKey, 128) + for i := 0; i < len(faucets); i++ { + faucets[i], _ = crypto.GenerateKey() + } + + // Initialize genesis + genesis := InitGenesis(t, faucets, "./testdata/genesis_2val.json", 16) + genesis.Config.LondonBlock = common.Big0 + genesis.Config.Bor.JaipurBlock = common.Big0 + + // Enable Dandeli fork at block 10 + dandeliBlock := big.NewInt(10) + genesis.Config.Bor.DandeliBlock = dandeliBlock + + // Set custom parameters + customBaseFeeChangeDenominator := uint64(128) // Very different from default + customTargetGasPercentage := uint64(80) // Very different from default + genesis.Config.Bor.BaseFeeChangeDenominator = &customBaseFeeChangeDenominator + genesis.Config.Bor.TargetGasPercentage = &customTargetGasPercentage + + // Setup a single node to produce blocks + stack, ethBackend, err := InitMiner(genesis, keys[0], true) + require.NoError(t, err) + defer stack.Close() + + // Start mining + err = ethBackend.StartMining() + require.NoError(t, err) + + // Wait for blocks to be mined beyond the Dandeli fork + targetBlockNum := dandeliBlock.Uint64() + 5 + timeout := time.After(60 * time.Second) + + for { + select { + case <-timeout: + t.Fatal("Timeout waiting for blocks to be mined") + default: + currentBlock := ethBackend.BlockChain().CurrentHeader() + if currentBlock.Number.Uint64() >= targetBlockNum { + goto verifyHeaders + } + time.Sleep(500 * time.Millisecond) + } + } + +verifyHeaders: + chain := ethBackend.BlockChain() + engine := ethBackend.Engine().(*bor.Bor) + + // Verify headers before and after the fork + for blockNum := uint64(1); blockNum <= targetBlockNum; blockNum++ { + header := chain.GetHeaderByNumber(blockNum) + require.NotNil(t, header, "Header %d should exist", blockNum) + + // Explicitly verify the header using the consensus engine + err := engine.VerifyHeader(chain, header) + require.NoError(t, err, "Header %d should be valid", blockNum) + + // Check which parameters are in effect + if blockNum >= dandeliBlock.Uint64() { + // Post-Dandeli: custom parameters should be used + targetPercentage := genesis.Config.Bor.GetTargetGasPercentage(header.Number) + baseFeeChangeDenom := params.BaseFeeChangeDenominator(genesis.Config.Bor, header.Number) + + assert.Equal(t, customTargetGasPercentage, targetPercentage, + "Block %d should use custom TargetGasPercentage", blockNum) + assert.Equal(t, customBaseFeeChangeDenominator, baseFeeChangeDenom, + "Block %d should use custom BaseFeeChangeDenominator", blockNum) + + t.Logf("Block %d (Post-Dandeli): TargetGasPercentage=%d, BaseFeeChangeDenominator=%d, BaseFee=%s", + blockNum, targetPercentage, baseFeeChangeDenom, header.BaseFee.String()) + } else { + // Pre-Dandeli: default parameters should be used + targetPercentage := genesis.Config.Bor.GetTargetGasPercentage(header.Number) + baseFeeChangeDenom := params.BaseFeeChangeDenominator(genesis.Config.Bor, header.Number) + + // Pre-Dandeli should use pre-Dandeli defaults (not our custom values) + assert.NotEqual(t, customTargetGasPercentage, targetPercentage, + "Block %d should NOT use custom TargetGasPercentage", blockNum) + + t.Logf("Block %d (Pre-Dandeli): TargetGasPercentage=%d, BaseFeeChangeDenominator=%d, BaseFee=%s", + blockNum, targetPercentage, baseFeeChangeDenom, header.BaseFee.String()) + } + } + + t.Log("Test completed successfully: Headers verified correctly with parameter changes") +} + +// TestBorConfigParameterDivergence tests that when producer and validator use different +// BaseFeeChangeDenominator and TargetGasPercentage values post-Dandeli, blocks are still +// accepted by validators. This demonstrates the flexibility of skipping base fee validation +// after Dandeli fork, allowing for dynamic configuration across the network. +func TestBorConfigParameterDivergence(t *testing.T) { + t.Parallel() + log.SetDefault(log.NewLogger(log.NewTerminalHandlerWithLevel(os.Stderr, log.LevelInfo, true))) + fdlimit.Raise(2048) + + // Generate a batch of accounts to seal and fund with + faucets := make([]*ecdsa.PrivateKey, 128) + for i := 0; i < len(faucets); i++ { + faucets[i], _ = crypto.GenerateKey() + } + + // Create genesis for producer (Node 0) with first set of parameters + genesisProducer := InitGenesis(t, faucets, "./testdata/genesis_2val.json", 16) + genesisProducer.Config.LondonBlock = common.Big0 + genesisProducer.Config.Bor.JaipurBlock = common.Big0 + genesisProducer.Config.Bor.DandeliBlock = big.NewInt(10) // Enable Dandeli early + + // Producer uses first set of parameters + producerBaseFeeChangeDenominator := uint64(32) + producerTargetGasPercentage := uint64(70) + genesisProducer.Config.Bor.BaseFeeChangeDenominator = &producerBaseFeeChangeDenominator + genesisProducer.Config.Bor.TargetGasPercentage = &producerTargetGasPercentage + + // Create genesis for validator (Node 1) with DIFFERENT parameters + genesisValidator := InitGenesis(t, faucets, "./testdata/genesis_2val.json", 16) + genesisValidator.Config.LondonBlock = common.Big0 + genesisValidator.Config.Bor.JaipurBlock = common.Big0 + genesisValidator.Config.Bor.DandeliBlock = big.NewInt(10) // Same Dandeli activation + + // Validator uses DIFFERENT parameters (simulating a "second change") + validatorBaseFeeChangeDenominator := uint64(128) // Much larger denominator + validatorTargetGasPercentage := uint64(80) // Higher target percentage + genesisValidator.Config.Bor.BaseFeeChangeDenominator = &validatorBaseFeeChangeDenominator + genesisValidator.Config.Bor.TargetGasPercentage = &validatorTargetGasPercentage + + // Setup producer node + stackProducer, nodeProducer, err := InitMiner(genesisProducer, keys[0], true) + require.NoError(t, err) + defer stackProducer.Close() + + // Wait for producer node to be ready + for stackProducer.Server().NodeInfo().Ports.Listener == 0 { + time.Sleep(250 * time.Millisecond) + } + + // Setup validator node with different config + stackValidator, nodeValidator, err := InitMiner(genesisValidator, keys[1], true) + require.NoError(t, err) + defer stackValidator.Close() + + // Wait for validator node to be ready + for stackValidator.Server().NodeInfo().Ports.Listener == 0 { + time.Sleep(250 * time.Millisecond) + } + + // Connect validator to producer + producerEnode := stackProducer.Server().Self() + stackValidator.Server().AddPeer(producerEnode) + + // Start mining on producer only (validator will sync) + err = nodeProducer.StartMining() + require.NoError(t, err) + + // Let producer mine blocks with its parameter set + time.Sleep(5 * time.Second) + + // Now start validator's mining to see it accept producer's blocks + err = nodeValidator.StartMining() + require.NoError(t, err) + + // Wait for both to mine blocks beyond Dandeli fork + targetBlockNum := uint64(25) + timeout := time.After(90 * time.Second) + + for { + select { + case <-timeout: + t.Fatal("Timeout waiting for blocks to be mined") + default: + producerBlock := nodeProducer.BlockChain().CurrentHeader() + validatorBlock := nodeValidator.BlockChain().CurrentHeader() + if producerBlock.Number.Uint64() >= targetBlockNum && + validatorBlock.Number.Uint64() >= targetBlockNum-5 { // Validator might lag slightly + goto checkDivergence + } + time.Sleep(500 * time.Millisecond) + } + } + +checkDivergence: + chainProducer := nodeProducer.BlockChain() + chainValidator := nodeValidator.BlockChain() + + t.Logf("Producer current block: %d", chainProducer.CurrentHeader().Number.Uint64()) + t.Logf("Validator current block: %d", chainValidator.CurrentHeader().Number.Uint64()) + + // Verify blocks at key positions + dandeliBlock := uint64(10) + checkBlocks := []uint64{dandeliBlock, dandeliBlock + 5, dandeliBlock + 10} + + for _, blockNum := range checkBlocks { + producerHeader := chainProducer.GetHeaderByNumber(blockNum) + validatorHeader := chainValidator.GetHeaderByNumber(blockNum) + + if producerHeader == nil || validatorHeader == nil { + continue // Skip if block not yet available + } + + // Check if validator accepted producer's block + assert.Equal(t, producerHeader.Hash(), validatorHeader.Hash(), + "Validator should accept producer's block %d despite different config parameters", blockNum) + + // Get the parameters each node would use for calculation + producerTargetPct := genesisProducer.Config.Bor.GetTargetGasPercentage(producerHeader.Number) + producerDenom := params.BaseFeeChangeDenominator(genesisProducer.Config.Bor, producerHeader.Number) + + validatorTargetPct := genesisValidator.Config.Bor.GetTargetGasPercentage(validatorHeader.Number) + validatorDenom := params.BaseFeeChangeDenominator(genesisValidator.Config.Bor, validatorHeader.Number) + + t.Logf("Block %d: Hash=%s", blockNum, producerHeader.Hash().Hex()[:10]) + t.Logf(" Producer config: TargetGasPercentage=%d, BaseFeeChangeDenominator=%d", + producerTargetPct, producerDenom) + t.Logf(" Validator config: TargetGasPercentage=%d, BaseFeeChangeDenominator=%d", + validatorTargetPct, validatorDenom) + t.Logf(" Block BaseFee=%s, GasLimit=%d, GasUsed=%d", + producerHeader.BaseFee.String(), producerHeader.GasLimit, producerHeader.GasUsed) + + // Post-Dandeli: verify nodes are using their respective different configs + if blockNum >= dandeliBlock { + assert.Equal(t, producerTargetGasPercentage, producerTargetPct, + "Producer should use its own TargetGasPercentage") + assert.Equal(t, producerBaseFeeChangeDenominator, producerDenom, + "Producer should use its own BaseFeeChangeDenominator") + + assert.Equal(t, validatorTargetGasPercentage, validatorTargetPct, + "Validator should use its own TargetGasPercentage") + assert.Equal(t, validatorBaseFeeChangeDenominator, validatorDenom, + "Validator should use its own BaseFeeChangeDenominator") + + // Despite different configs, blocks should match (validator accepts producer's blocks) + assert.Equal(t, producerHeader.Hash(), validatorHeader.Hash(), + "Blocks should match despite different parameter configs") + } + } + + // Verify validator explicitly accepts producer's headers despite config divergence + engineValidator := nodeValidator.Engine().(*bor.Bor) + for blockNum := dandeliBlock; blockNum <= dandeliBlock+10; blockNum++ { + producerHeader := chainProducer.GetHeaderByNumber(blockNum) + if producerHeader == nil { + continue + } + + // Validator's consensus engine should accept producer's header + // even though validator would calculate base fee differently + err := engineValidator.VerifyHeader(chainValidator, producerHeader) + require.NoError(t, err, + "Validator should accept producer's block %d despite different base fee calculation parameters", + blockNum) + } + + t.Log("Test completed successfully: Validator accepted blocks from producer despite divergent BorConfig parameters") + t.Log("This demonstrates that post-Dandeli base fee validation skip allows for flexible parameter updates") +} + +// TestBorConfigMultipleParameterChanges tests a scenario where parameters are conceptually +// "changed" multiple times by having blocks at different heights that would use different +// parameter sets if the system supported block-height-keyed parameters. +// This test demonstrates header validation across multiple "transition points". +func TestBorConfigMultipleParameterChanges(t *testing.T) { + t.Parallel() + log.SetDefault(log.NewLogger(log.NewTerminalHandlerWithLevel(os.Stderr, log.LevelInfo, true))) + fdlimit.Raise(2048) + + // Generate a batch of accounts + faucets := make([]*ecdsa.PrivateKey, 128) + for i := 0; i < len(faucets); i++ { + faucets[i], _ = crypto.GenerateKey() + } + + // Initialize genesis with Dandeli fork at block 10 + genesis := InitGenesis(t, faucets, "./testdata/genesis_2val.json", 16) + genesis.Config.LondonBlock = common.Big0 + genesis.Config.Bor.JaipurBlock = common.Big0 + genesis.Config.Bor.DandeliBlock = big.NewInt(10) + + // Start with first set of parameters (will be used from block 10 onward) + firstBaseFeeChangeDenominator := uint64(32) + firstTargetGasPercentage := uint64(70) + genesis.Config.Bor.BaseFeeChangeDenominator = &firstBaseFeeChangeDenominator + genesis.Config.Bor.TargetGasPercentage = &firstTargetGasPercentage + + // Setup a single node + stack, ethBackend, err := InitMiner(genesis, keys[0], true) + require.NoError(t, err) + defer stack.Close() + + // Start mining + err = ethBackend.StartMining() + require.NoError(t, err) + + // Mine blocks with first parameter set + firstPhaseTarget := uint64(20) + timeout := time.After(60 * time.Second) + + for { + select { + case <-timeout: + t.Fatal("Timeout waiting for first phase blocks") + default: + currentBlock := ethBackend.BlockChain().CurrentHeader() + if currentBlock.Number.Uint64() >= firstPhaseTarget { + goto firstPhaseComplete + } + time.Sleep(500 * time.Millisecond) + } + } + +firstPhaseComplete: + chain := ethBackend.BlockChain() + engine := ethBackend.Engine().(*bor.Bor) + + t.Log("=== Phase 1 Complete: Blocks 1-20 with first parameter set ===") + + // Verify first phase blocks + for blockNum := uint64(10); blockNum <= firstPhaseTarget; blockNum++ { + header := chain.GetHeaderByNumber(blockNum) + require.NotNil(t, header) + + err := engine.VerifyHeader(chain, header) + require.NoError(t, err, "Header %d should be valid", blockNum) + + targetPct := genesis.Config.Bor.GetTargetGasPercentage(header.Number) + denom := params.BaseFeeChangeDenominator(genesis.Config.Bor, header.Number) + + assert.Equal(t, firstTargetGasPercentage, targetPct) + assert.Equal(t, firstBaseFeeChangeDenominator, denom) + + if blockNum%5 == 0 { + t.Logf("Block %d: TargetGasPercentage=%d, BaseFeeChangeDenominator=%d, BaseFee=%s", + blockNum, targetPct, denom, header.BaseFee.String()) + } + } + + // Now simulate a "second parameter change" by updating the config + // Note: In production, this would require a new hard fork. Here we're testing + // that the validation logic would handle such changes gracefully. + secondBaseFeeChangeDenominator := uint64(128) + secondTargetGasPercentage := uint64(80) + + // Update the config (this simulates what a second hard fork would do) + genesis.Config.Bor.BaseFeeChangeDenominator = &secondBaseFeeChangeDenominator + genesis.Config.Bor.TargetGasPercentage = &secondTargetGasPercentage + + t.Log("=== Phase 2: Simulating parameter change at block 21 ===") + t.Logf("New parameters: TargetGasPercentage=%d, BaseFeeChangeDenominator=%d", + secondTargetGasPercentage, secondBaseFeeChangeDenominator) + + // Continue mining with new parameters + secondPhaseTarget := uint64(35) + timeout = time.After(60 * time.Second) + + for { + select { + case <-timeout: + t.Fatal("Timeout waiting for second phase blocks") + default: + currentBlock := ethBackend.BlockChain().CurrentHeader() + if currentBlock.Number.Uint64() >= secondPhaseTarget { + goto secondPhaseComplete + } + time.Sleep(500 * time.Millisecond) + } + } + +secondPhaseComplete: + t.Log("=== Phase 2 Complete: Blocks 21-35 with second parameter set ===") + + // Verify second phase blocks + for blockNum := firstPhaseTarget + 1; blockNum <= secondPhaseTarget; blockNum++ { + header := chain.GetHeaderByNumber(blockNum) + require.NotNil(t, header) + + // Headers should still be valid even with parameter changes + err := engine.VerifyHeader(chain, header) + require.NoError(t, err, "Header %d should be valid with new parameters", blockNum) + + targetPct := genesis.Config.Bor.GetTargetGasPercentage(header.Number) + denom := params.BaseFeeChangeDenominator(genesis.Config.Bor, header.Number) + + // After updating config, new parameters should be in effect + assert.Equal(t, secondTargetGasPercentage, targetPct) + assert.Equal(t, secondBaseFeeChangeDenominator, denom) + + if blockNum%5 == 0 { + t.Logf("Block %d: TargetGasPercentage=%d, BaseFeeChangeDenominator=%d, BaseFee=%s", + blockNum, targetPct, denom, header.BaseFee.String()) + } + } + + // Verify all blocks in the chain are still valid + t.Log("=== Verifying entire chain remains valid ===") + for blockNum := uint64(1); blockNum <= secondPhaseTarget; blockNum++ { + header := chain.GetHeaderByNumber(blockNum) + require.NotNil(t, header) + + err := engine.VerifyHeader(chain, header) + require.NoError(t, err, "Header %d should remain valid", blockNum) + } + + t.Log("Test completed successfully: Multiple parameter changes handled correctly") + t.Log("Chain remains valid across parameter transitions") +} From e6129e7d3af6ce3a824070879dafcb3317c1eb12 Mon Sep 17 00:00:00 2001 From: Lucca Martins Date: Tue, 27 Jan 2026 16:52:06 -0300 Subject: [PATCH 13/14] boundary limits --- consensus/misc/eip1559/eip1559.go | 66 ++- .../eip1559/eip1559_basefee_boundary_test.go | 438 ++++++++++++++++++ consensus/misc/eip1559/eip1559_test.go | 62 ++- 3 files changed, 540 insertions(+), 26 deletions(-) create mode 100644 consensus/misc/eip1559/eip1559_basefee_boundary_test.go diff --git a/consensus/misc/eip1559/eip1559.go b/consensus/misc/eip1559/eip1559.go index 0b11ae51f9..7071916b6c 100644 --- a/consensus/misc/eip1559/eip1559.go +++ b/consensus/misc/eip1559/eip1559.go @@ -27,9 +27,19 @@ import ( "github.com/ethereum/go-ethereum/params" ) +const ( + // MaxBaseFeeChangePercent limits the maximum base fee change per block to 5% of parent base fee. + // This prevents excessive fee volatility by capping both increases and decreases. + // The 5% limit provides protection against aggressive parameter configurations while + // accommodating the natural behavior of default post-Dandeli parameters (maximum ~1.7% change). + MaxBaseFeeChangePercent = 5 +) + // VerifyEIP1559Header verifies some header attributes which were changed in EIP-1559, // - gas limit check -// - basefee check (pre-Dandeli only; after Dandeli HF base fee validation is removed to allow dynamic setting) +// - basefee check with different rules pre/post Dandeli: +// - Pre-Dandeli: Strict validation (baseFee must exactly match calculated value) +// - Post-Dandeli: Boundary validation (baseFee change must be within MaxBaseFeeChangePercent) func VerifyEIP1559Header(config *params.ChainConfig, parent, header *types.Header) error { // Verify that the gas limit remains within allowed bounds parentGasLimit := parent.GasLimit @@ -44,13 +54,9 @@ func VerifyEIP1559Header(config *params.ChainConfig, parent, header *types.Heade return errors.New("header is missing baseFee") } - // After Dandeli hard fork, base fee validation is removed to allow dynamic configuration. - // This enables validators to set base fees according to new consensus rules without - // strict protocol enforcement, supporting more flexible fee markets. - // Pre-Dandeli blocks still require strict base fee validation for consensus safety. + // Post-Dandeli: Validate that base fee changes are within allowed boundaries if config.Bor != nil && config.Bor.IsDandeli(header.Number) { - // Post-Dandeli: Skip base fee validation - return nil + return verifyBaseFeeWithinBoundaries(parent, header) } // Pre-Dandeli: Verify the baseFee is correct based on the parent header @@ -63,6 +69,33 @@ func VerifyEIP1559Header(config *params.ChainConfig, parent, header *types.Heade return nil } +// verifyBaseFeeWithinBoundaries checks that the base fee change is within the allowed boundary. +// This prevents excessive fee volatility while allowing dynamic fee adjustment post-Dandeli. +// The boundary limit is defined by MaxBaseFeeChangePercent constant. +func verifyBaseFeeWithinBoundaries(parent, header *types.Header) error { + // Calculate the maximum allowed change (MaxBaseFeeChangePercent of parent base fee) + maxAllowedChange := new(big.Int).Mul(parent.BaseFee, big.NewInt(MaxBaseFeeChangePercent)) + maxAllowedChange.Div(maxAllowedChange, big.NewInt(100)) + + // Calculate the actual change in base fee + actualChange := new(big.Int) + if header.BaseFee.Cmp(parent.BaseFee) >= 0 { + // Base fee increased or stayed the same + actualChange.Sub(header.BaseFee, parent.BaseFee) + } else { + // Base fee decreased + actualChange.Sub(parent.BaseFee, header.BaseFee) + } + + // Verify the change is within the allowed boundary + if actualChange.Cmp(maxAllowedChange) > 0 { + return fmt.Errorf("baseFee change exceeds %d%% limit: change=%s, maxAllowed=%s, parentBaseFee=%s, headerBaseFee=%s", + MaxBaseFeeChangePercent, actualChange, maxAllowedChange, parent.BaseFee, header.BaseFee) + } + + return nil +} + // CalcBaseFee calculates the basefee of the header. func CalcBaseFee(config *params.ChainConfig, parent *types.Header) *big.Int { // If the current block is the first EIP-1559 block, return the InitialBaseFee. @@ -83,6 +116,14 @@ func CalcBaseFee(config *params.ChainConfig, parent *types.Header) *big.Int { baseFeeChangeDenominatorUint64 = params.BaseFeeChangeDenominator(config.Bor, parent.Number) ) + // Calculate maximum allowed change (only applies post-Dandeli) + var maxAllowedChange *big.Int + applyBoundaryCap := config.Bor != nil && config.Bor.IsDandeli(parent.Number) + if applyBoundaryCap { + maxAllowedChange = new(big.Int).Mul(parent.BaseFee, big.NewInt(MaxBaseFeeChangePercent)) + maxAllowedChange.Div(maxAllowedChange, big.NewInt(100)) + } + if parent.GasUsed > parentGasTarget { // If the parent block used more gas than its target, the baseFee should increase. // max(1, parentBaseFee * gasUsedDelta / parentGasTarget / baseFeeChangeDenominator) @@ -90,6 +131,12 @@ func CalcBaseFee(config *params.ChainConfig, parent *types.Header) *big.Int { num.Mul(num, parent.BaseFee) num.Div(num, denom.SetUint64(parentGasTarget)) num.Div(num, denom.SetUint64(baseFeeChangeDenominatorUint64)) + + // Cap the increase to MaxBaseFeeChangePercent post-Dandeli + if applyBoundaryCap && num.Cmp(maxAllowedChange) > 0 { + num.Set(maxAllowedChange) + } + if num.Cmp(common.Big1) < 0 { return num.Add(parent.BaseFee, common.Big1) } @@ -102,6 +149,11 @@ func CalcBaseFee(config *params.ChainConfig, parent *types.Header) *big.Int { num.Div(num, denom.SetUint64(parentGasTarget)) num.Div(num, denom.SetUint64(baseFeeChangeDenominatorUint64)) + // Cap the decrease to MaxBaseFeeChangePercent post-Dandeli + if applyBoundaryCap && num.Cmp(maxAllowedChange) > 0 { + num.Set(maxAllowedChange) + } + baseFee := num.Sub(parent.BaseFee, num) if baseFee.Cmp(common.Big0) < 0 { baseFee = common.Big0 diff --git a/consensus/misc/eip1559/eip1559_basefee_boundary_test.go b/consensus/misc/eip1559/eip1559_basefee_boundary_test.go new file mode 100644 index 0000000000..6c513db875 --- /dev/null +++ b/consensus/misc/eip1559/eip1559_basefee_boundary_test.go @@ -0,0 +1,438 @@ +// Copyright 2021 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package eip1559 + +import ( + "math/big" + "strings" + "testing" + + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/params" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// TestBaseFeeBoundary tests the MaxBaseFeeChangePercent base fee change limit post-Dandeli. +// It verifies both the CalcBaseFee capping and VerifyEIP1559Header validation. +func TestBaseFeeBoundary(t *testing.T) { + t.Parallel() + + testConfig := copyConfig(config()) + testConfig.Bor.DandeliBlock = big.NewInt(10) + testConfig.Bor.BhilaiBlock = big.NewInt(5) + + tests := []struct { + name string + blockNumber uint64 + parentBaseFee *big.Int + headerBaseFee *big.Int + expectError bool + errorContains string + }{ + { + name: "Post-Dandeli: Increase at 3% (within boundary)", + blockNumber: 15, + parentBaseFee: big.NewInt(1000000000), + headerBaseFee: big.NewInt(1030000000), // 3% increase + expectError: false, + }, + { + name: "Post-Dandeli: Increase at 5% (exactly at boundary)", + blockNumber: 15, + parentBaseFee: big.NewInt(1000000000), + headerBaseFee: big.NewInt(1050000000), // 5% increase + expectError: false, + }, + { + name: "Post-Dandeli: Increase exceeding boundary (7% increase)", + blockNumber: 15, + parentBaseFee: big.NewInt(1000000000), + headerBaseFee: big.NewInt(1070000000), // 7% increase + expectError: true, + errorContains: "baseFee change exceeds", + }, + { + name: "Post-Dandeli: Decrease at 3% (within boundary)", + blockNumber: 15, + parentBaseFee: big.NewInt(1000000000), + headerBaseFee: big.NewInt(970000000), // 3% decrease + expectError: false, + }, + { + name: "Post-Dandeli: Decrease at 5% (exactly at boundary)", + blockNumber: 15, + parentBaseFee: big.NewInt(1000000000), + headerBaseFee: big.NewInt(950000000), // 5% decrease + expectError: false, + }, + { + name: "Post-Dandeli: Decrease exceeding boundary (7% decrease)", + blockNumber: 15, + parentBaseFee: big.NewInt(1000000000), + headerBaseFee: big.NewInt(930000000), // 7% decrease + expectError: true, + errorContains: "baseFee change exceeds", + }, + { + name: "Post-Dandeli: No change", + blockNumber: 15, + parentBaseFee: big.NewInt(1000000000), + headerBaseFee: big.NewInt(1000000000), // 0% change + expectError: false, + }, + { + name: "Post-Dandeli: Large base fee with 2% increase", + blockNumber: 15, + parentBaseFee: big.NewInt(100000000000), // 100 gwei + headerBaseFee: big.NewInt(102000000000), // 2% increase + expectError: false, + }, + { + name: "Post-Dandeli: Small base fee with 4% increase", + blockNumber: 15, + parentBaseFee: big.NewInt(100000000), // 0.1 gwei + headerBaseFee: big.NewInt(104000000), // 4% increase + expectError: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + parent := &types.Header{ + Number: big.NewInt(int64(tt.blockNumber - 1)), + GasLimit: 30000000, + GasUsed: 15000000, + BaseFee: tt.parentBaseFee, + } + + header := &types.Header{ + Number: big.NewInt(int64(tt.blockNumber)), + GasLimit: 30000000, + GasUsed: 20000000, + BaseFee: tt.headerBaseFee, + } + + err := VerifyEIP1559Header(testConfig, parent, header) + + if tt.expectError { + require.Error(t, err, "Expected error for %s", tt.name) + if tt.errorContains != "" { + assert.Contains(t, err.Error(), tt.errorContains) + } + } else { + require.NoError(t, err, "Expected no error for %s", tt.name) + } + }) + } +} + +// TestBaseFeeBoundaryPreDandeli tests that pre-Dandeli blocks still use strict validation +func TestBaseFeeBoundaryPreDandeli(t *testing.T) { + t.Parallel() + + testConfig := copyConfig(config()) + testConfig.Bor.DandeliBlock = big.NewInt(20) + + parent := &types.Header{ + Number: big.NewInt(10), // Pre-Dandeli + GasLimit: 30000000, + GasUsed: 15000000, + BaseFee: big.NewInt(1000000000), + } + + t.Run("pre-Dandeli: exact match passes", func(t *testing.T) { + calculatedBaseFee := CalcBaseFee(testConfig, parent) + + header := &types.Header{ + Number: big.NewInt(11), + GasLimit: 30000000, + GasUsed: 20000000, + BaseFee: calculatedBaseFee, // Exact match + } + + err := VerifyEIP1559Header(testConfig, parent, header) + require.NoError(t, err) + }) + + t.Run("pre-Dandeli: off by 1 wei fails", func(t *testing.T) { + calculatedBaseFee := CalcBaseFee(testConfig, parent) + offByOne := new(big.Int).Add(calculatedBaseFee, big.NewInt(1)) + + header := &types.Header{ + Number: big.NewInt(11), + GasLimit: 30000000, + GasUsed: 20000000, + BaseFee: offByOne, // Off by 1 wei + } + + err := VerifyEIP1559Header(testConfig, parent, header) + require.Error(t, err) + assert.Contains(t, err.Error(), "invalid baseFee") + }) + + t.Run("pre-Dandeli: 3% difference fails", func(t *testing.T) { + calculatedBaseFee := CalcBaseFee(testConfig, parent) + threePercent := new(big.Int).Mul(calculatedBaseFee, big.NewInt(103)) + threePercent.Div(threePercent, big.NewInt(100)) + + header := &types.Header{ + Number: big.NewInt(11), + GasLimit: 30000000, + GasUsed: 20000000, + BaseFee: threePercent, // 3% different + } + + err := VerifyEIP1559Header(testConfig, parent, header) + require.Error(t, err) + assert.Contains(t, err.Error(), "invalid baseFee") + }) +} + +// TestAggressiveParametersExceedBoundary tests that aggressive parameter configurations +// (low denominators) are properly capped at MaxBaseFeeChangePercent by CalcBaseFee post-Dandeli +func TestAggressiveParametersExceedBoundary(t *testing.T) { + t.Parallel() + + testConfig := copyConfig(config()) + testConfig.Bor.DandeliBlock = big.NewInt(10) + testConfig.Bor.BhilaiBlock = big.NewInt(5) + + tests := []struct { + name string + baseFeeChangeDenominator uint64 + targetGasPercentage uint64 + parentGasUsed uint64 + parentGasLimit uint64 + parentBaseFee int64 + description string + }{ + { + name: "Very aggressive denominator (8) at full usage", + baseFeeChangeDenominator: 8, // Very aggressive + targetGasPercentage: 65, // Default target + parentGasUsed: 30000000, // 100% usage + parentGasLimit: 30000000, + parentBaseFee: 1000000000, // 1 gwei + description: "With denominator=8, full block usage would naturally exceed boundary without cap", + }, + { + name: "Aggressive denominator (16) at full usage", + baseFeeChangeDenominator: 16, // Aggressive + targetGasPercentage: 65, + parentGasUsed: 30000000, // 100% usage + parentGasLimit: 30000000, + parentBaseFee: 1000000000, + description: "With denominator=16, full block usage should be capped at boundary", + }, + { + name: "Very aggressive denominator (8) at zero usage", + baseFeeChangeDenominator: 8, + targetGasPercentage: 65, + parentGasUsed: 0, // 0% usage + parentGasLimit: 30000000, + parentBaseFee: 1000000000, + description: "With denominator=8, zero usage would naturally exceed boundary without cap", + }, + { + name: "Aggressive denominator (16) at zero usage", + baseFeeChangeDenominator: 16, + targetGasPercentage: 65, + parentGasUsed: 0, // 0% usage + parentGasLimit: 30000000, + parentBaseFee: 1000000000, + description: "With denominator=16, zero usage should be capped at boundary", + }, + { + name: "Extreme denominator (4) at full usage", + baseFeeChangeDenominator: 4, // Extremely aggressive + targetGasPercentage: 65, + parentGasUsed: 30000000, + parentGasLimit: 30000000, + parentBaseFee: 1000000000, + description: "With denominator=4, should be heavily capped at boundary", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + config := copyConfig(testConfig) + config.Bor.BaseFeeChangeDenominator = &tt.baseFeeChangeDenominator + config.Bor.TargetGasPercentage = &tt.targetGasPercentage + + parent := &types.Header{ + Number: big.NewInt(20), // Post-Dandeli, Post-Bhilai + GasLimit: tt.parentGasLimit, + GasUsed: tt.parentGasUsed, + BaseFee: big.NewInt(tt.parentBaseFee), + } + + calculatedBaseFee := CalcBaseFee(config, parent) + + // Calculate the actual change percentage + actualChange := new(big.Int) + if calculatedBaseFee.Cmp(parent.BaseFee) >= 0 { + actualChange.Sub(calculatedBaseFee, parent.BaseFee) + } else { + actualChange.Sub(parent.BaseFee, calculatedBaseFee) + } + + changePercentBig := new(big.Int).Mul(actualChange, big.NewInt(10000)) + changePercentBig.Div(changePercentBig, parent.BaseFee) + changePercent := float64(changePercentBig.Int64()) / 100.0 + + t.Logf("%s", tt.description) + t.Logf(" Parent BaseFee: %s wei", parent.BaseFee.String()) + t.Logf(" Calculated BaseFee: %s wei", calculatedBaseFee.String()) + t.Logf(" Change: %.2f%%", changePercent) + t.Logf(" Parameters: Denominator=%d, TargetGas=%d%%", tt.baseFeeChangeDenominator, tt.targetGasPercentage) + + // Assert that the change is capped at 5% + assert.LessOrEqual(t, changePercent, float64(MaxBaseFeeChangePercent), + "Base fee change should be capped at %d%%, got %.2f%%", + MaxBaseFeeChangePercent, changePercent) + + // Verify that a block with this calculated base fee would be accepted + header := &types.Header{ + Number: big.NewInt(21), + GasLimit: parent.GasLimit, + GasUsed: parent.GasUsed, + BaseFee: calculatedBaseFee, + } + + err := VerifyEIP1559Header(config, parent, header) + require.NoError(t, err, "Block with calculated base fee should be accepted") + }) + } +} + +// TestDefaultParametersWithinBoundary documents and verifies that default post-Dandeli +// parameters stay well within the MaxBaseFeeChangePercent boundary at all gas usage levels +func TestDefaultParametersWithinBoundary(t *testing.T) { + t.Parallel() + + config := copyConfig(params.TestChainConfig) + config.LondonBlock = big.NewInt(0) + config.Bor = ¶ms.BorConfig{ + DandeliBlock: big.NewInt(10), + BhilaiBlock: big.NewInt(5), // Critical: Bhilai changes denominator to 64 + // No custom parameters - will use defaults: + // BaseFeeChangeDenominator = 64 (post-Bhilai) + // TargetGasPercentage = 65 + } + + baseBaseFee := int64(1000000000) // 1 gwei + gasLimit := uint64(30000000) // 30M gas limit + + // Test at various gas usage levels + tests := []struct { + name string + gasUsed uint64 + usagePercent string + }{ + {"0% gas usage (max decrease pressure)", 0, "0%"}, + {"10% gas usage", gasLimit * 10 / 100, "10%"}, + {"25% gas usage", gasLimit * 25 / 100, "25%"}, + {"50% gas usage", gasLimit * 50 / 100, "50%"}, + {"65% gas usage (at target)", gasLimit * 65 / 100, "65%"}, + {"75% gas usage", gasLimit * 75 / 100, "75%"}, + {"90% gas usage", gasLimit * 90 / 100, "90%"}, + {"100% gas usage (max increase pressure)", gasLimit, "100%"}, + } + + t.Logf("\n%s", strings.Repeat("=", 80)) + t.Logf("Default Post-Dandeli Parameters within %d%% Boundary", MaxBaseFeeChangePercent) + t.Logf("%s", strings.Repeat("=", 80)) + t.Logf("Configuration:") + t.Logf(" BaseFeeChangeDenominator: 64 (default post-Bhilai)") + t.Logf(" TargetGasPercentage: 65%% (default)") + t.Logf(" DandeliBlock: 10") + t.Logf(" BhilaiBlock: 5") + t.Logf("") + + maxChangePercent := 0.0 + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + parent := &types.Header{ + Number: big.NewInt(20), // Post-Dandeli, Post-Bhilai + GasLimit: gasLimit, + GasUsed: tt.gasUsed, + BaseFee: big.NewInt(baseBaseFee), + } + + calculatedBaseFee := CalcBaseFee(config, parent) + + // Calculate change percentage + actualChange := new(big.Int) + direction := "no change" + if calculatedBaseFee.Cmp(parent.BaseFee) > 0 { + actualChange.Sub(calculatedBaseFee, parent.BaseFee) + direction = "increase" + } else if calculatedBaseFee.Cmp(parent.BaseFee) < 0 { + actualChange.Sub(parent.BaseFee, calculatedBaseFee) + direction = "decrease" + } + + changePercent := 0.0 + if actualChange.Cmp(big.NewInt(0)) > 0 { + changePercentBig := new(big.Int).Mul(actualChange, big.NewInt(10000)) + changePercentBig.Div(changePercentBig, parent.BaseFee) + changePercent = float64(changePercentBig.Int64()) / 100.0 + } + + if changePercent > maxChangePercent { + maxChangePercent = changePercent + } + + // Calculate margin below 5% boundary + marginBelowBoundary := float64(MaxBaseFeeChangePercent) - changePercent + marginPercent := (marginBelowBoundary / float64(MaxBaseFeeChangePercent)) * 100.0 + + t.Logf("Gas Usage: %s → BaseFee %s by %.2f%% (%.1f%% margin below %d%% boundary)", + tt.usagePercent, direction, changePercent, marginPercent, MaxBaseFeeChangePercent) + + // Assert within boundary + assert.LessOrEqual(t, changePercent, float64(MaxBaseFeeChangePercent), + "Change should be within %d%% boundary", MaxBaseFeeChangePercent) + + // Verify header would be accepted + header := &types.Header{ + Number: big.NewInt(21), + GasLimit: gasLimit, + GasUsed: tt.gasUsed, + BaseFee: calculatedBaseFee, + } + + err := VerifyEIP1559Header(config, parent, header) + require.NoError(t, err, "Header should be valid with default parameters") + }) + } + + t.Logf("") + t.Logf("Summary:") + t.Logf(" Maximum change observed: %.2f%%", maxChangePercent) + t.Logf(" Boundary limit: %d%%", MaxBaseFeeChangePercent) + t.Logf(" Margin: %.2f%% (%.1f%% of boundary)", + float64(MaxBaseFeeChangePercent)-maxChangePercent, + ((float64(MaxBaseFeeChangePercent)-maxChangePercent)/float64(MaxBaseFeeChangePercent))*100.0) + t.Logf("%s", strings.Repeat("=", 80)) + + // Assert that default parameters stay well within the 5% boundary + assert.Less(t, maxChangePercent, float64(MaxBaseFeeChangePercent)*0.5, + "Default parameters should use less than 50%% of the %d%% boundary", MaxBaseFeeChangePercent) +} diff --git a/consensus/misc/eip1559/eip1559_test.go b/consensus/misc/eip1559/eip1559_test.go index 596aebb635..b06597f127 100644 --- a/consensus/misc/eip1559/eip1559_test.go +++ b/consensus/misc/eip1559/eip1559_test.go @@ -615,13 +615,14 @@ func TestDynamicBaseFeeChangeDenominator(t *testing.T) { }) } -// TestVerifyEIP1559HeaderNoBaseFeeValidation tests that after removing the base fee validation, -// headers with any base fee are accepted (as long as it's not nil) +// TestVerifyEIP1559HeaderNoBaseFeeValidation tests post-Dandeli boundary validation +// instead of strict validation. Base fees must be within MaxBaseFeeChangePercent boundary. func TestVerifyEIP1559HeaderNoBaseFeeValidation(t *testing.T) { t.Parallel() testConfig := copyConfig(config()) testConfig.Bor.DandeliBlock = big.NewInt(20) + testConfig.Bor.BhilaiBlock = big.NewInt(5) parent := &types.Header{ Number: big.NewInt(20), @@ -630,25 +631,25 @@ func TestVerifyEIP1559HeaderNoBaseFeeValidation(t *testing.T) { BaseFee: big.NewInt(1_000_000_000), } - t.Run("accepts arbitrary base fee after validation removal", func(t *testing.T) { - // Header with a base fee that doesn't match the calculated value - // This should be accepted since we removed the validation + t.Run("accepts base fee within boundary", func(t *testing.T) { + // Header with a base fee that doesn't match calculated value but is within MaxBaseFeeChangePercent boundary header := &types.Header{ Number: big.NewInt(21), GasLimit: 30_000_000, GasUsed: 20_000_000, - BaseFee: big.NewInt(5_000_000_000), // Arbitrary value + BaseFee: big.NewInt(1_040_000_000), // 4% increase - within boundary } err := VerifyEIP1559Header(testConfig, parent, header) - require.NoError(t, err, "should accept header with arbitrary base fee") + require.NoError(t, err, "should accept header with base fee within boundary") }) - t.Run("accepts base fee different from calculated", func(t *testing.T) { + t.Run("accepts base fee different from calculated but within boundary", func(t *testing.T) { calculatedBaseFee := CalcBaseFee(testConfig, parent) - // Use a completely different base fee - differentBaseFee := new(big.Int).Mul(calculatedBaseFee, big.NewInt(10)) + // Use a different base fee that's still within MaxBaseFeeChangePercent of parent + differentBaseFee := new(big.Int).Mul(parent.BaseFee, big.NewInt(103)) + differentBaseFee.Div(differentBaseFee, big.NewInt(100)) // 3% increase header := &types.Header{ Number: big.NewInt(21), @@ -658,7 +659,21 @@ func TestVerifyEIP1559HeaderNoBaseFeeValidation(t *testing.T) { } err := VerifyEIP1559Header(testConfig, parent, header) - require.NoError(t, err, "should accept header with base fee different from calculated") + require.NoError(t, err, "should accept header with base fee within boundary even if different from calculated: calculated=%s, header=%s", calculatedBaseFee, differentBaseFee) + }) + + t.Run("rejects base fee exceeding boundary", func(t *testing.T) { + // Base fee that exceeds MaxBaseFeeChangePercent boundary + header := &types.Header{ + Number: big.NewInt(21), + GasLimit: 30_000_000, + GasUsed: 20_000_000, + BaseFee: big.NewInt(1_100_000_000), // 10% increase - exceeds boundary + } + + err := VerifyEIP1559Header(testConfig, parent, header) + require.Error(t, err, "should reject header with base fee exceeding boundary") + require.Contains(t, err.Error(), "baseFee change exceeds", "error should mention boundary exceeded") }) t.Run("rejects nil base fee", func(t *testing.T) { @@ -674,16 +689,23 @@ func TestVerifyEIP1559HeaderNoBaseFeeValidation(t *testing.T) { require.Contains(t, err.Error(), "baseFee", "error should mention baseFee") }) - t.Run("accepts zero base fee", func(t *testing.T) { + t.Run("accepts zero base fee when parent is also zero", func(t *testing.T) { + parentZero := &types.Header{ + Number: big.NewInt(20), + GasLimit: 30_000_000, + GasUsed: 15_000_000, + BaseFee: big.NewInt(0), + } + header := &types.Header{ Number: big.NewInt(21), GasLimit: 30_000_000, GasUsed: 20_000_000, - BaseFee: big.NewInt(0), // Zero is valid + BaseFee: big.NewInt(0), // Zero is valid when parent is also zero } - err := VerifyEIP1559Header(testConfig, parent, header) - require.NoError(t, err, "should accept header with zero base fee") + err := VerifyEIP1559Header(testConfig, parentZero, header) + require.NoError(t, err, "should accept header with zero base fee when parent is zero") }) } @@ -864,7 +886,7 @@ func TestBaseFeeValidationPreDandeli(t *testing.T) { require.NoError(t, err, "should accept correct base fee pre-Dandeli") }) - t.Run("post-Dandeli: accepts any base fee", func(t *testing.T) { + t.Run("post-Dandeli: accepts base fee within boundary", func(t *testing.T) { parent := &types.Header{ Number: big.NewInt(20), // Post-Dandeli GasLimit: 30_000_000, @@ -873,16 +895,18 @@ func TestBaseFeeValidationPreDandeli(t *testing.T) { } calculatedBaseFee := CalcBaseFee(testConfig, parent) - arbitraryBaseFee := new(big.Int).Mul(calculatedBaseFee, big.NewInt(5)) + // Use a base fee within MaxBaseFeeChangePercent boundary (3% increase) + baseFeeWithinBoundary := new(big.Int).Mul(parent.BaseFee, big.NewInt(103)) + baseFeeWithinBoundary.Div(baseFeeWithinBoundary, big.NewInt(100)) header := &types.Header{ Number: big.NewInt(21), GasLimit: 30_000_000, GasUsed: 20_000_000, - BaseFee: arbitraryBaseFee, // Arbitrary base fee + BaseFee: baseFeeWithinBoundary, // Within boundary } err := VerifyEIP1559Header(testConfig, parent, header) - require.NoError(t, err, "should accept arbitrary base fee post-Dandeli") + require.NoError(t, err, "should accept base fee within boundary post-Dandeli: calculated=%s, header=%s", calculatedBaseFee, baseFeeWithinBoundary) }) } From 524d5f1f5a69c03f72c61456b24903c85399240d Mon Sep 17 00:00:00 2001 From: Lucca Martins Date: Tue, 27 Jan 2026 17:49:10 -0300 Subject: [PATCH 14/14] race condition fix --- eth/peer.go | 4 +- eth/peer_test.go | 116 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 118 insertions(+), 2 deletions(-) diff --git a/eth/peer.go b/eth/peer.go index d8126b5d74..cc75590fb4 100644 --- a/eth/peer.go +++ b/eth/peer.go @@ -512,13 +512,13 @@ func (p *ethPeer) doWitnessRequest( } }() witReqsWg.Add(1) + mapsMu.Lock() *witReqs = append(*witReqs, witReq) if page >= witTotalRequest[hash] { - mapsMu.Lock() witTotalRequest[hash]++ - mapsMu.Unlock() } + mapsMu.Unlock() return nil } diff --git a/eth/peer_test.go b/eth/peer_test.go index 5596cc4487..80e5aabd85 100644 --- a/eth/peer_test.go +++ b/eth/peer_test.go @@ -357,3 +357,119 @@ func TestRequestWitnesses_PartialFailureNoPanic(t *testing.T) { t.Fatal("timed out waiting for response") } } + +// TestDoWitnessRequest_RaceCondition_WitTotalRequest exposes the race condition in doWitnessRequest +// where witTotalRequest[hash] is read without lock protection but written with lock protection. +// Run this test with: go test -race -run TestDoWitnessRequest_RaceCondition_WitTotalRequest +func TestDoWitnessRequest_RaceCondition_WitTotalRequest(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + hash := common.Hash{0xab, 0xcd} + + mockWitPeer := NewMockWitnessPeer(ctrl) + mockWitPeer.EXPECT().Log().Return(log.New()).AnyTimes() + mockWitPeer.EXPECT().ID().Return("test-peer").AnyTimes() + + p := ðPeer{ + Peer: eth.NewPeer(1, p2p.NewPeer(enode.ID{0x01}, "test-peer", []p2p.Cap{}), nil, nil), + witPeer: &witPeer{Peer: mockWitPeer}, + } + + // Mock RequestWitness to simulate responses with small delays + mockWitPeer.EXPECT(). + RequestWitness(gomock.Any(), gomock.Any()). + DoAndReturn(func(wpr []wit.WitnessPageRequest, ch chan *wit.Response) (*wit.Request, error) { + go func() { + // Add small delay to increase chance of concurrent access + time.Sleep(1 * time.Millisecond) + ch <- &wit.Response{ + Res: &wit.WitnessPacketRLPPacket{ + WitnessPacketResponse: []wit.WitnessPageResponse{{ + Page: wpr[0].Page, + TotalPages: 100, // Large number to trigger many requests + Hash: hash, + Data: []byte{0x01, 0x02}, + }}, + }, + Done: make(chan error, 10), + } + }() + return &wit.Request{}, nil + }). + AnyTimes() + + // Setup the data structures that doWitnessRequest uses + witTotalPages := make(map[common.Hash]uint64) + witTotalRequest := make(map[common.Hash]uint64) + witReqResCh := make(chan *witReqRes, DefaultConcurrentResponsesHandled) + witReqSem := make(chan int, DefaultConcurrentRequestsPerPeer) + var mapsMu sync.RWMutex + var witReqs []*wit.Request + var witReqsWg sync.WaitGroup + + // Initialize with TotalPages so we know how many requests to make + witTotalPages[hash] = 50 + witTotalRequest[hash] = 0 + + // Run multiple iterations to increase the chance of catching the race + const iterations = 10 + for iter := 0; iter < iterations; iter++ { + // Reset the counter for each iteration + mapsMu.Lock() + witTotalRequest[hash] = 0 + mapsMu.Unlock() + + // Launch many concurrent doWitnessRequest calls + // This simulates concurrent goroutines trying to request different pages + const numConcurrentRequests = 20 + var wg sync.WaitGroup + + for i := 0; i < numConcurrentRequests; i++ { + wg.Add(1) + page := uint64(i) + + go func(pg uint64) { + defer wg.Done() + + // Call doWitnessRequest which contains the race condition + // Multiple goroutines will simultaneously: + // 1. Read witTotalRequest[hash] (unprotected) at line 731 + // 2. Compare page >= witTotalRequest[hash] + // 3. Write witTotalRequest[hash]++ (protected) at line 733 + err := p.doWitnessRequest( + hash, + pg, + &witReqs, + &witReqsWg, + witReqResCh, + witReqSem, + &mapsMu, + witTotalRequest, + ) + + // Consume from semaphore to prevent blocking + select { + case <-witReqSem: + case <-time.After(100 * time.Millisecond): + } + + if err != nil { + // Error is expected in some cases, not critical for this test + t.Logf("doWitnessRequest returned error: %v", err) + } + }(page) + } + + // Wait for all goroutines to complete + wg.Wait() + + // Small delay between iterations + time.Sleep(10 * time.Millisecond) + } + + // The test itself doesn't need to assert anything specific + // The race detector will catch the unprotected read if run with -race flag + t.Logf("Test completed %d iterations with %d concurrent requests each", iterations, 20) + t.Log("If running with -race flag, any data race will be reported by the race detector") +}