diff --git a/.github/workflows/kurtosis-e2e.yml b/.github/workflows/kurtosis-e2e.yml index d01b993cae..f99ecf2ec9 100644 --- a/.github/workflows/kurtosis-e2e.yml +++ b/.github/workflows/kurtosis-e2e.yml @@ -86,6 +86,7 @@ jobs: - name: Pre kurtosis run uses: ./.github/actions/setup with: + main_branch: develop docker_username: ${{ secrets.DOCKERHUB }} docker_token: ${{ secrets.DOCKERHUB_KEY }} @@ -129,6 +130,9 @@ jobs: - name: Inspect enclave run: kurtosis enclave inspect ${{ env.ENCLAVE_NAME }} + - name: Sleep for 1 minute + run: sleep 60 + - name: Test state syncs uses: ./.github/actions/test-state-sync with: @@ -165,4 +169,5 @@ jobs: if: always() uses: ./.github/actions/cleanup with: + main_branch: develop enclave_name: ${{ env.ENCLAVE_NAME }} diff --git a/.github/workflows/kurtosis-stateless-e2e.yml b/.github/workflows/kurtosis-stateless-e2e.yml index 45edf08168..d688ed37dd 100644 --- a/.github/workflows/kurtosis-stateless-e2e.yml +++ b/.github/workflows/kurtosis-stateless-e2e.yml @@ -86,6 +86,7 @@ jobs: - name: Pre kurtosis run uses: ./.github/actions/setup with: + main_branch: develop docker_username: ${{ secrets.DOCKERHUB }} docker_token: ${{ secrets.DOCKERHUB_KEY }} @@ -154,4 +155,5 @@ jobs: if: always() uses: ./.github/actions/cleanup with: + main_branch: develop enclave_name: ${{ env.ENCLAVE_NAME }} diff --git a/builder/files/genesis-amoy.json b/builder/files/genesis-amoy.json index ceada67a8e..99f4743f7e 100644 --- a/builder/files/genesis-amoy.json +++ b/builder/files/genesis-amoy.json @@ -26,6 +26,7 @@ "madhugiriBlock": 28899616, "madhugiriProBlock": 29287400, "dandeliBlock": 31890000, + "lisovoBlock": 33634700, "skipValidatorByteCheck": [26160367, 26161087, 26171567, 26173743, 26175647], "stateSyncConfirmationDelay": { "0": 128 diff --git a/consensus/misc/eip1559/eip1559.go b/consensus/misc/eip1559/eip1559.go index 7071916b6c..c5b88d3329 100644 --- a/consensus/misc/eip1559/eip1559.go +++ b/consensus/misc/eip1559/eip1559.go @@ -37,9 +37,9 @@ const ( // VerifyEIP1559Header verifies some header attributes which were changed in EIP-1559, // - gas limit check -// - 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) +// - basefee check with different rules pre/post Lisovo: +// - Pre-Lisovo: Strict validation (baseFee must exactly match calculated value) +// - Post-Lisovo: 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 @@ -54,12 +54,12 @@ func VerifyEIP1559Header(config *params.ChainConfig, parent, header *types.Heade return errors.New("header is missing baseFee") } - // Post-Dandeli: Validate that base fee changes are within allowed boundaries - if config.Bor != nil && config.Bor.IsDandeli(header.Number) { + // Post-Lisovo: Validate that base fee changes are within allowed boundaries + if config.Bor != nil && config.Bor.IsLisovo(header.Number) { return verifyBaseFeeWithinBoundaries(parent, header) } - // Pre-Dandeli: Verify the baseFee is correct based on the parent header + // Pre-Lisovo: 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", @@ -70,13 +70,19 @@ func VerifyEIP1559Header(config *params.ChainConfig, parent, header *types.Heade } // verifyBaseFeeWithinBoundaries checks that the base fee change is within the allowed boundary. -// This prevents excessive fee volatility while allowing dynamic fee adjustment post-Dandeli. +// This prevents excessive fee volatility while allowing dynamic fee adjustment post-Lisovo. // 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)) + // Ensure minimum 1 wei cap to prevent unlimited growth at very low base fees. + // This matches the logic in CalcBaseFee. + if maxAllowedChange.Cmp(common.Big1) < 0 { + maxAllowedChange = new(big.Int).Set(common.Big1) + } + // Calculate the actual change in base fee actualChange := new(big.Int) if header.BaseFee.Cmp(parent.BaseFee) >= 0 { @@ -116,12 +122,19 @@ 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) + // Calculate maximum allowed change (only applies post-Lisovo) var maxAllowedChange *big.Int - applyBoundaryCap := config.Bor != nil && config.Bor.IsDandeli(parent.Number) + applyBoundaryCap := config.Bor != nil && config.Bor.IsLisovo(parent.Number) if applyBoundaryCap { maxAllowedChange = new(big.Int).Mul(parent.BaseFee, big.NewInt(MaxBaseFeeChangePercent)) maxAllowedChange.Div(maxAllowedChange, big.NewInt(100)) + + // Ensure minimum 1 wei cap to prevent unlimited growth at very low base fees. + // When percentage calculation rounds to 0 (baseFee < 20 wei), this ensures + // there's still an absolute cap of 1 wei per block. + if maxAllowedChange.Cmp(common.Big1) < 0 { + maxAllowedChange = new(big.Int).Set(common.Big1) + } } if parent.GasUsed > parentGasTarget { @@ -132,7 +145,7 @@ func CalcBaseFee(config *params.ChainConfig, parent *types.Header) *big.Int { num.Div(num, denom.SetUint64(parentGasTarget)) num.Div(num, denom.SetUint64(baseFeeChangeDenominatorUint64)) - // Cap the increase to MaxBaseFeeChangePercent post-Dandeli + // Cap the increase to MaxBaseFeeChangePercent post-Lisovo if applyBoundaryCap && num.Cmp(maxAllowedChange) > 0 { num.Set(maxAllowedChange) } @@ -149,7 +162,7 @@ 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 + // Cap the decrease to MaxBaseFeeChangePercent post-Lisovo if applyBoundaryCap && num.Cmp(maxAllowedChange) > 0 { num.Set(maxAllowedChange) } diff --git a/consensus/misc/eip1559/eip1559_basefee_boundary_test.go b/consensus/misc/eip1559/eip1559_basefee_boundary_test.go index 6c513db875..d80e205a1f 100644 --- a/consensus/misc/eip1559/eip1559_basefee_boundary_test.go +++ b/consensus/misc/eip1559/eip1559_basefee_boundary_test.go @@ -27,12 +27,13 @@ import ( "github.com/stretchr/testify/require" ) -// TestBaseFeeBoundary tests the MaxBaseFeeChangePercent base fee change limit post-Dandeli. +// TestBaseFeeBoundary tests the MaxBaseFeeChangePercent base fee change limit post-Lisovo. // It verifies both the CalcBaseFee capping and VerifyEIP1559Header validation. func TestBaseFeeBoundary(t *testing.T) { t.Parallel() testConfig := copyConfig(config()) + testConfig.Bor.LisovoBlock = big.NewInt(15) testConfig.Bor.DandeliBlock = big.NewInt(10) testConfig.Bor.BhilaiBlock = big.NewInt(5) @@ -141,8 +142,8 @@ func TestBaseFeeBoundary(t *testing.T) { } } -// TestBaseFeeBoundaryPreDandeli tests that pre-Dandeli blocks still use strict validation -func TestBaseFeeBoundaryPreDandeli(t *testing.T) { +// TestBaseFeeBoundaryPreLisovo tests that pre-Lisovo blocks still use strict validation +func TestBaseFeeBoundaryPreLisovo(t *testing.T) { t.Parallel() testConfig := copyConfig(config()) @@ -204,11 +205,12 @@ func TestBaseFeeBoundaryPreDandeli(t *testing.T) { } // TestAggressiveParametersExceedBoundary tests that aggressive parameter configurations -// (low denominators) are properly capped at MaxBaseFeeChangePercent by CalcBaseFee post-Dandeli +// (low denominators) are properly capped at MaxBaseFeeChangePercent by CalcBaseFee post-Lisovo func TestAggressiveParametersExceedBoundary(t *testing.T) { t.Parallel() testConfig := copyConfig(config()) + testConfig.Bor.LisovoBlock = big.NewInt(15) testConfig.Bor.DandeliBlock = big.NewInt(10) testConfig.Bor.BhilaiBlock = big.NewInt(5) @@ -320,8 +322,8 @@ func TestAggressiveParametersExceedBoundary(t *testing.T) { } } -// TestDefaultParametersWithinBoundary documents and verifies that default post-Dandeli -// parameters stay well within the MaxBaseFeeChangePercent boundary at all gas usage levels +// TestDefaultParametersWithinBoundary documents and verifies that default post-Dandeli parameters +// (65% gas target) stay well within the MaxBaseFeeChangePercent boundary post-Lisovo at all gas usage levels func TestDefaultParametersWithinBoundary(t *testing.T) { t.Parallel() diff --git a/consensus/misc/eip1559/eip1559_test.go b/consensus/misc/eip1559/eip1559_test.go index b06597f127..1305c8df14 100644 --- a/consensus/misc/eip1559/eip1559_test.go +++ b/consensus/misc/eip1559/eip1559_test.go @@ -72,6 +72,7 @@ func copyConfig(original *params.ChainConfig) *params.ChainConfig { MadhugiriBlock: original.Bor.MadhugiriBlock, MadhugiriProBlock: original.Bor.MadhugiriProBlock, DandeliBlock: original.Bor.DandeliBlock, + LisovoBlock: original.Bor.LisovoBlock, } } return config @@ -238,6 +239,7 @@ func TestCalcParentGasTarget(t *testing.T) { t.Parallel() testConfig := copyConfig(config()) + testConfig.Bor.LisovoBlock = big.NewInt(20) testConfig.Bor.DandeliBlock = big.NewInt(20) defaultGasLimit := uint64(60_000_000) @@ -332,6 +334,7 @@ func TestCalcBaseFeeDandeli(t *testing.T) { testConfig := copyConfig(config()) testConfig.Bor.BhilaiBlock = big.NewInt(8) + testConfig.Bor.LisovoBlock = big.NewInt(20) testConfig.Bor.DandeliBlock = big.NewInt(20) // Case 1: Create pre-dandeli cases where HF is defined in future. Validate @@ -445,12 +448,13 @@ func TestCalcBaseFeeDandeli(t *testing.T) { } // TestDynamicTargetGasPercentage verifies that the TargetGasPercentage parameter -// can be dynamically set after Dandeli HF and affects base fee calculations correctly +// can be dynamically set after Lisovo 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.LisovoBlock = big.NewInt(20) testConfig.Bor.DandeliBlock = big.NewInt(20) // Test with 70% target gas percentage @@ -530,12 +534,13 @@ func TestDynamicTargetGasPercentage(t *testing.T) { } // TestDynamicBaseFeeChangeDenominator verifies that the BaseFeeChangeDenominator parameter -// can be dynamically set after Dandeli HF and affects the rate of base fee change correctly +// can be dynamically set after Lisovo 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.LisovoBlock = big.NewInt(20) testConfig.Bor.DandeliBlock = big.NewInt(20) gasLimit := uint64(60_000_000) @@ -615,12 +620,13 @@ func TestDynamicBaseFeeChangeDenominator(t *testing.T) { }) } -// TestVerifyEIP1559HeaderNoBaseFeeValidation tests post-Dandeli boundary validation +// TestVerifyEIP1559HeaderNoBaseFeeValidation tests post-Lisovo 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.LisovoBlock = big.NewInt(20) testConfig.Bor.DandeliBlock = big.NewInt(20) testConfig.Bor.BhilaiBlock = big.NewInt(5) @@ -716,6 +722,7 @@ func TestInvalidTargetGasPercentage(t *testing.T) { testConfig := copyConfig(config()) testConfig.Bor.BhilaiBlock = big.NewInt(8) + testConfig.Bor.LisovoBlock = big.NewInt(20) testConfig.Bor.DandeliBlock = big.NewInt(20) gasLimit := uint64(60_000_000) @@ -793,6 +800,7 @@ func TestInvalidBaseFeeChangeDenominator(t *testing.T) { testConfig := copyConfig(config()) testConfig.Bor.BhilaiBlock = big.NewInt(8) + testConfig.Bor.LisovoBlock = big.NewInt(20) testConfig.Bor.DandeliBlock = big.NewInt(20) gasLimit := uint64(60_000_000) @@ -847,6 +855,7 @@ func TestBaseFeeValidationPreDandeli(t *testing.T) { t.Parallel() testConfig := copyConfig(config()) + testConfig.Bor.LisovoBlock = big.NewInt(20) testConfig.Bor.DandeliBlock = big.NewInt(20) parent := &types.Header{ @@ -888,7 +897,7 @@ func TestBaseFeeValidationPreDandeli(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 + Number: big.NewInt(25), // Post-Lisovo (boundary validation active) GasLimit: 30_000_000, GasUsed: 15_000_000, BaseFee: big.NewInt(1_000_000_000), @@ -900,7 +909,7 @@ func TestBaseFeeValidationPreDandeli(t *testing.T) { baseFeeWithinBoundary.Div(baseFeeWithinBoundary, big.NewInt(100)) header := &types.Header{ - Number: big.NewInt(21), + Number: big.NewInt(26), GasLimit: 30_000_000, GasUsed: 20_000_000, BaseFee: baseFeeWithinBoundary, // Within boundary @@ -910,3 +919,461 @@ func TestBaseFeeValidationPreDandeli(t *testing.T) { require.NoError(t, err, "should accept base fee within boundary post-Dandeli: calculated=%s, header=%s", calculatedBaseFee, baseFeeWithinBoundary) }) } + +// TestCalcBaseFeeVeryLowFeesCapping tests that base fee changes are capped +// even at very low base fees (1-20 wei) post-Lisovo +func TestCalcBaseFeeVeryLowFeesCapping(t *testing.T) { + t.Parallel() + + testConfig := copyConfig(config()) + testConfig.Bor.LisovoBlock = big.NewInt(15) + testConfig.Bor.DandeliBlock = big.NewInt(10) + testConfig.Bor.BhilaiBlock = big.NewInt(5) + + // Use aggressive parameters (low denominator) to force large increases + // that would exceed the 5% boundary at very low base fees + aggressiveDenom := uint64(4) + testConfig.Bor.BaseFeeChangeDenominator = &aggressiveDenom + + tests := []struct { + name string + parentBaseFee int64 + parentGasUsed uint64 + maxAllowedChange int64 // Expected max change in wei + }{ + { + name: "1 wei base fee - aggressive params", + parentBaseFee: 1, + parentGasUsed: 30_000_000, // 100% usage + maxAllowedChange: 1, // 5% rounds to 0, should cap at 1 wei + }, + { + name: "10 wei base fee - aggressive params", + parentBaseFee: 10, + parentGasUsed: 30_000_000, + maxAllowedChange: 1, // 5% = 0.5, rounds to 0, should cap at 1 wei + }, + { + name: "20 wei base fee - aggressive params", + parentBaseFee: 20, + parentGasUsed: 30_000_000, + maxAllowedChange: 1, // 5% = 1 wei exactly + }, + { + name: "100 wei base fee - aggressive params", + parentBaseFee: 100, + parentGasUsed: 30_000_000, + maxAllowedChange: 5, // 5% = 5 wei + }, + { + name: "1 gwei base fee - aggressive params", + parentBaseFee: 1_000_000_000, + parentGasUsed: 30_000_000, + maxAllowedChange: 50_000_000, // 5% of 1 gwei + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + parent := &types.Header{ + Number: big.NewInt(20), // Post-Dandeli + GasLimit: 30_000_000, + GasUsed: tt.parentGasUsed, + BaseFee: big.NewInt(tt.parentBaseFee), + } + + calculatedBaseFee := CalcBaseFee(testConfig, parent) + actualChange := new(big.Int).Sub(calculatedBaseFee, parent.BaseFee) + + // Verify change is capped + require.LessOrEqual(t, actualChange.Int64(), tt.maxAllowedChange, + "Base fee increase should be capped at %d wei, got %d wei (%.2f%%)", + tt.maxAllowedChange, actualChange.Int64(), + float64(actualChange.Int64())*100.0/float64(parent.BaseFee.Int64())) + }) + } +} + +// TestVerifyBaseFeeWithinBoundariesLowFees tests header validation at very low base fees +func TestVerifyBaseFeeWithinBoundariesLowFees(t *testing.T) { + t.Parallel() + + testConfig := copyConfig(config()) + testConfig.Bor.LisovoBlock = big.NewInt(15) + testConfig.Bor.DandeliBlock = big.NewInt(10) + testConfig.Bor.BhilaiBlock = big.NewInt(5) + + tests := []struct { + name string + parentBaseFee int64 + headerBaseFee int64 + expectError bool + description string + }{ + { + name: "1 wei → 2 wei (within 1 wei cap)", + parentBaseFee: 1, + headerBaseFee: 2, + expectError: false, + description: "1 wei increase should be accepted", + }, + { + name: "1 wei → 3 wei (exceeds 1 wei cap)", + parentBaseFee: 1, + headerBaseFee: 3, + expectError: true, + description: "2 wei increase should be rejected", + }, + { + name: "10 wei → 11 wei (within 1 wei cap)", + parentBaseFee: 10, + headerBaseFee: 11, + expectError: false, + description: "1 wei increase should be accepted", + }, + { + name: "10 wei → 12 wei (exceeds 1 wei cap)", + parentBaseFee: 10, + headerBaseFee: 12, + expectError: true, + description: "2 wei increase should be rejected", + }, + { + name: "20 wei → 21 wei (5% = 1 wei)", + parentBaseFee: 20, + headerBaseFee: 21, + expectError: false, + description: "1 wei increase (5%) should be accepted", + }, + { + name: "20 wei → 22 wei (exceeds 5%)", + parentBaseFee: 20, + headerBaseFee: 22, + expectError: true, + description: "2 wei increase (10%) should be rejected", + }, + { + name: "100 wei → 105 wei (5%)", + parentBaseFee: 100, + headerBaseFee: 105, + expectError: false, + description: "5% increase should be accepted", + }, + { + name: "100 wei → 107 wei (exceeds 5%)", + parentBaseFee: 100, + headerBaseFee: 107, + expectError: true, + description: "7% increase should be rejected", + }, + { + name: "1 gwei → 1.05 gwei (5%)", + parentBaseFee: 1_000_000_000, + headerBaseFee: 1_050_000_000, + expectError: false, + description: "5% increase should be accepted", + }, + { + name: "1 gwei → 1.1 gwei (10%)", + parentBaseFee: 1_000_000_000, + headerBaseFee: 1_100_000_000, + expectError: true, + description: "10% increase should be rejected", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + parent := &types.Header{ + Number: big.NewInt(20), // Post-Dandeli + GasLimit: 30_000_000, + GasUsed: 15_000_000, + BaseFee: big.NewInt(tt.parentBaseFee), + } + + header := &types.Header{ + Number: big.NewInt(21), + GasLimit: 30_000_000, + GasUsed: 20_000_000, + BaseFee: big.NewInt(tt.headerBaseFee), + } + + err := VerifyEIP1559Header(testConfig, parent, header) + + if tt.expectError { + require.Error(t, err, tt.description) + require.Contains(t, err.Error(), "baseFee change exceeds", "should mention boundary exceeded") + } else { + require.NoError(t, err, tt.description) + } + }) + } +} + +// TestLowBaseFeeEdgeCases tests edge cases at very low base fees +func TestLowBaseFeeEdgeCases(t *testing.T) { + t.Parallel() + + testConfig := copyConfig(config()) + testConfig.Bor.LisovoBlock = big.NewInt(15) + testConfig.Bor.DandeliBlock = big.NewInt(10) + testConfig.Bor.BhilaiBlock = big.NewInt(5) + + t.Run("exact 5% boundary at 20 wei", func(t *testing.T) { + parent := &types.Header{ + Number: big.NewInt(20), + GasLimit: 30_000_000, + GasUsed: 15_000_000, + BaseFee: big.NewInt(20), // 5% = 1 wei + } + + header := &types.Header{ + Number: big.NewInt(21), + GasLimit: 30_000_000, + GasUsed: 20_000_000, + BaseFee: big.NewInt(21), // +1 wei = exactly 5% + } + + err := VerifyEIP1559Header(testConfig, parent, header) + require.NoError(t, err, "exactly 5% increase should be accepted") + }) + + t.Run("just over 5% at 20 wei", func(t *testing.T) { + parent := &types.Header{ + Number: big.NewInt(20), + GasLimit: 30_000_000, + GasUsed: 15_000_000, + BaseFee: big.NewInt(20), + } + + header := &types.Header{ + Number: big.NewInt(21), + GasLimit: 30_000_000, + GasUsed: 20_000_000, + BaseFee: big.NewInt(22), // +2 wei = 10% + } + + err := VerifyEIP1559Header(testConfig, parent, header) + require.Error(t, err, "10% increase should be rejected") + }) + + t.Run("decrease capping at very low fees", func(t *testing.T) { + parent := &types.Header{ + Number: big.NewInt(20), + GasLimit: 30_000_000, + GasUsed: 25_000_000, // High usage to get increase first + BaseFee: big.NewInt(2), + } + + calculatedBaseFee := CalcBaseFee(testConfig, parent) + require.GreaterOrEqual(t, calculatedBaseFee.Int64(), parent.BaseFee.Int64(), + "high usage should not decrease base fee") + + // Now test decrease + parent2 := &types.Header{ + Number: big.NewInt(21), + GasLimit: 30_000_000, + GasUsed: 10_000_000, // Low usage for decrease + BaseFee: big.NewInt(10), + } + + header := &types.Header{ + Number: big.NewInt(22), + GasLimit: 30_000_000, + GasUsed: 10_000_000, + BaseFee: big.NewInt(9), // -1 wei + } + + err := VerifyEIP1559Header(testConfig, parent2, header) + require.NoError(t, err, "1 wei decrease should be accepted") + + // Test exceeding decrease + header.BaseFee = big.NewInt(8) // -2 wei + err = VerifyEIP1559Header(testConfig, parent2, header) + require.Error(t, err, "2 wei decrease should be rejected") + }) + + t.Run("multiple blocks of growth from 1 wei", func(t *testing.T) { + baseFee := big.NewInt(1) + + // Simulate 5 blocks of full usage + for i := 0; i < 5; i++ { + parent := &types.Header{ + Number: big.NewInt(int64(20 + i)), + GasLimit: 30_000_000, + GasUsed: 30_000_000, // Full usage + BaseFee: baseFee, + } + + newBaseFee := CalcBaseFee(testConfig, parent) + change := new(big.Int).Sub(newBaseFee, baseFee) + + // Each block should be capped at 1 wei increase + require.LessOrEqual(t, change.Int64(), int64(1), + "block %d: increase should be capped at 1 wei", i) + + baseFee = newBaseFee + } + + // After 5 blocks of 1 wei increases, should be at most 6 wei + require.LessOrEqual(t, baseFee.Int64(), int64(6), + "after 5 blocks starting from 1 wei, should be at most 6 wei") + }) + + t.Run("zero base fee edge case", func(t *testing.T) { + // Test starting from 0 wei (theoretical edge case) + parentZero := &types.Header{ + Number: big.NewInt(20), + GasLimit: 30_000_000, + GasUsed: 30_000_000, // Full usage + BaseFee: big.NewInt(0), + } + + // Calculate from 0 wei - should be able to increase + calculatedBaseFee := CalcBaseFee(testConfig, parentZero) + require.Greater(t, calculatedBaseFee.Int64(), int64(0), + "base fee should be able to increase from 0") + require.LessOrEqual(t, calculatedBaseFee.Int64(), int64(1), + "increase from 0 wei should be capped at 1 wei") + + // Test validation: 0 → 0 should pass (no change) + headerZero := &types.Header{ + Number: big.NewInt(21), + GasLimit: 30_000_000, + GasUsed: 30_000_000, + BaseFee: big.NewInt(0), + } + err := VerifyEIP1559Header(testConfig, parentZero, headerZero) + require.NoError(t, err, "0 → 0 wei should be accepted") + + // Test validation: 0 → 1 should pass (within 1 wei cap) + headerOne := &types.Header{ + Number: big.NewInt(21), + GasLimit: 30_000_000, + GasUsed: 30_000_000, + BaseFee: big.NewInt(1), + } + err = VerifyEIP1559Header(testConfig, parentZero, headerOne) + require.NoError(t, err, "0 → 1 wei should be accepted (within 1 wei cap)") + + // Test validation: 0 → 2 should fail (exceeds 1 wei cap) + headerTwo := &types.Header{ + Number: big.NewInt(21), + GasLimit: 30_000_000, + GasUsed: 30_000_000, + BaseFee: big.NewInt(2), + } + err = VerifyEIP1559Header(testConfig, parentZero, headerTwo) + require.Error(t, err, "0 → 2 wei should be rejected (exceeds 1 wei cap)") + require.Contains(t, err.Error(), "baseFee change exceeds", + "error should mention boundary exceeded") + }) +} + +// TestLowBaseFeesPreLisovo tests that pre-Lisovo strict validation still works at low fees +func TestLowBaseFeesPreLisovo(t *testing.T) { + t.Parallel() + + testConfig := copyConfig(config()) + testConfig.Bor.DandeliBlock = big.NewInt(100) // Far in future + testConfig.Bor.BhilaiBlock = big.NewInt(5) + + t.Run("pre-Dandeli: low fees use strict validation", func(t *testing.T) { + parent := &types.Header{ + Number: big.NewInt(20), // Pre-Dandeli + GasLimit: 30_000_000, + GasUsed: 15_000_000, + BaseFee: big.NewInt(10), // Very low, but pre-Dandeli + } + + calculatedBaseFee := CalcBaseFee(testConfig, parent) + + // Even 1 wei off should fail pre-Dandeli + wrongBaseFee := new(big.Int).Add(calculatedBaseFee, big.NewInt(1)) + + header := &types.Header{ + Number: big.NewInt(21), + GasLimit: 30_000_000, + GasUsed: 20_000_000, + BaseFee: wrongBaseFee, + } + + err := VerifyEIP1559Header(testConfig, parent, header) + require.Error(t, err, "should reject even 1 wei difference pre-Dandeli") + require.Contains(t, err.Error(), "invalid baseFee", "should use strict validation") + }) + + t.Run("pre-Dandeli: exact match accepted", func(t *testing.T) { + parent := &types.Header{ + Number: big.NewInt(20), + GasLimit: 30_000_000, + GasUsed: 15_000_000, + BaseFee: big.NewInt(10), + } + + calculatedBaseFee := CalcBaseFee(testConfig, parent) + + header := &types.Header{ + Number: big.NewInt(21), + GasLimit: 30_000_000, + GasUsed: 20_000_000, + BaseFee: calculatedBaseFee, + } + + err := VerifyEIP1559Header(testConfig, parent, header) + require.NoError(t, err, "exact match should be accepted pre-Dandeli") + }) +} + +// TestLowBaseFeeIntegrationWithExistingBoundary verifies new behavior doesn't break existing tests +func TestLowBaseFeeIntegrationWithExistingBoundary(t *testing.T) { + t.Parallel() + + testConfig := copyConfig(config()) + testConfig.Bor.LisovoBlock = big.NewInt(15) + testConfig.Bor.DandeliBlock = big.NewInt(10) + testConfig.Bor.BhilaiBlock = big.NewInt(5) + + // Normal production base fees - should behave identically to before + parent := &types.Header{ + Number: big.NewInt(20), + GasLimit: 30_000_000, + GasUsed: 15_000_000, + BaseFee: big.NewInt(25_000_000_000), // 25 gwei (typical production) + } + + t.Run("normal fees: 3% increase accepted", func(t *testing.T) { + header := &types.Header{ + Number: big.NewInt(21), + GasLimit: 30_000_000, + GasUsed: 20_000_000, + BaseFee: big.NewInt(25_750_000_000), // 3% increase + } + err := VerifyEIP1559Header(testConfig, parent, header) + require.NoError(t, err, "3% increase should be accepted") + }) + + t.Run("normal fees: 7% increase rejected", func(t *testing.T) { + header := &types.Header{ + Number: big.NewInt(21), + GasLimit: 30_000_000, + GasUsed: 20_000_000, + BaseFee: big.NewInt(26_750_000_000), // 7% increase + } + err := VerifyEIP1559Header(testConfig, parent, header) + require.Error(t, err, "7% increase should be rejected") + require.Contains(t, err.Error(), "baseFee change exceeds", "should mention boundary exceeded") + }) + + t.Run("normal fees: CalcBaseFee produces value within boundary", func(t *testing.T) { + calculatedBaseFee := CalcBaseFee(testConfig, parent) + + // Calculate percentage change + change := new(big.Int).Sub(calculatedBaseFee, parent.BaseFee) + changePercent := new(big.Int).Mul(change, big.NewInt(100)) + changePercent.Div(changePercent, parent.BaseFee) + + require.LessOrEqual(t, changePercent.Int64(), int64(5), + "CalcBaseFee should produce value within 5%% boundary") + }) +} diff --git a/core/vm/contracts.go b/core/vm/contracts.go index 672b702878..18336ad55d 100644 --- a/core/vm/contracts.go +++ b/core/vm/contracts.go @@ -224,7 +224,31 @@ var PrecompiledContractsMadhugiriPro = PrecompiledContracts{ common.BytesToAddress([]byte{0x01, 0x00}): &p256Verify{}, } +// PrecompiledContractsLisovo contains the set of pre-compiled Ethereum +// contracts used in the Lisovo release (bor HF). +var PrecompiledContractsLisovo = PrecompiledContracts{ + common.BytesToAddress([]byte{0x01}): &ecrecover{}, + common.BytesToAddress([]byte{0x02}): &sha256hash{}, + common.BytesToAddress([]byte{0x03}): &ripemd160hash{}, + common.BytesToAddress([]byte{0x04}): &dataCopy{}, + common.BytesToAddress([]byte{0x05}): &bigModExp{eip2565: true, eip7823: true, eip7883: true}, + common.BytesToAddress([]byte{0x06}): &bn256AddIstanbul{}, + common.BytesToAddress([]byte{0x07}): &bn256ScalarMulIstanbul{}, + common.BytesToAddress([]byte{0x08}): &bn256PairingIstanbul{}, + common.BytesToAddress([]byte{0x09}): &blake2F{}, + common.BytesToAddress([]byte{0x0a}): &kzgPointEvaluation{}, + common.BytesToAddress([]byte{0x0b}): &bls12381G1Add{}, + common.BytesToAddress([]byte{0x0c}): &bls12381G1MultiExp{}, + common.BytesToAddress([]byte{0x0d}): &bls12381G2Add{}, + common.BytesToAddress([]byte{0x0e}): &bls12381G2MultiExp{}, + common.BytesToAddress([]byte{0x0f}): &bls12381Pairing{}, + common.BytesToAddress([]byte{0x10}): &bls12381MapG1{}, + common.BytesToAddress([]byte{0x11}): &bls12381MapG2{}, + common.BytesToAddress([]byte{0x01, 0x00}): &p256Verify{eip7951: true}, +} + var ( + PrecompiledAddressesLisovo []common.Address PrecompiledAddressesMadhugiriPro []common.Address PrecompiledAddressesMadhugiri []common.Address PrecompiledAddressesOsaka []common.Address @@ -267,10 +291,15 @@ func init() { for k := range PrecompiledContractsMadhugiriPro { PrecompiledAddressesMadhugiriPro = append(PrecompiledAddressesMadhugiriPro, k) } + for k := range PrecompiledContractsLisovo { + PrecompiledAddressesLisovo = append(PrecompiledAddressesLisovo, k) + } } func activePrecompiledContracts(rules params.Rules) PrecompiledContracts { switch { + case rules.IsLisovo: + return PrecompiledContractsLisovo case rules.IsMadhugiriPro: return PrecompiledContractsMadhugiriPro case rules.IsMadhugiri: @@ -302,6 +331,8 @@ func ActivePrecompiledContracts(rules params.Rules) PrecompiledContracts { // ActivePrecompiles returns the precompile addresses enabled with the current configuration. func ActivePrecompiles(rules params.Rules) []common.Address { switch { + case rules.IsLisovo: + return PrecompiledAddressesLisovo case rules.IsMadhugiriPro: return PrecompiledAddressesMadhugiriPro case rules.IsMadhugiri: diff --git a/core/vm/contracts_test.go b/core/vm/contracts_test.go index 97aa89917f..acf9cd217b 100644 --- a/core/vm/contracts_test.go +++ b/core/vm/contracts_test.go @@ -534,6 +534,7 @@ func TestReinforceMultiClientPreCompilesTest(t *testing.T) { "IsVerkle", "IsMadhugiri", "IsMadhugiriPro", + "IsLisovo", } if len(actual) != len(expected) { @@ -547,3 +548,41 @@ func TestReinforceMultiClientPreCompilesTest(t *testing.T) { } } } + +// TestLisovoP256VerifyGasCost verifies P256 precompile gas cost changes at Lisovo. +func TestLisovoP256VerifyGasCost(t *testing.T) { + preLisovo := &p256Verify{eip7951: false} + postLisovo := &p256Verify{eip7951: true} + + preGas := preLisovo.RequiredGas(nil) + postGas := postLisovo.RequiredGas(nil) + + if preGas != params.P256VerifyGas { + t.Errorf("pre-Lisovo gas: got %d, want %d", preGas, params.P256VerifyGas) + } + if postGas != params.P256VerifyGasEIP7951 { + t.Errorf("post-Lisovo gas: got %d, want %d", postGas, params.P256VerifyGasEIP7951) + } + if preGas >= postGas { + t.Errorf("post-Lisovo gas (%d) should be higher than pre-Lisovo (%d)", postGas, preGas) + } +} + +// TestLisovoCLZOpcode verifies CLZ opcode availability at Lisovo. +func TestLisovoCLZOpcode(t *testing.T) { + preLisovo := newPragueInstructionSet() + postLisovo := newLisovoInstructionSet() + + // Pre-Lisovo: CLZ should be undefined. + if preLisovo[CLZ].execute != nil && preLisovo[CLZ].constantGas != 0 { + t.Error("CLZ opcode should not be defined pre-Lisovo") + } + + // Post-Lisovo: CLZ should be defined. + if postLisovo[CLZ].execute == nil { + t.Error("CLZ opcode should be defined post-Lisovo") + } + if postLisovo[CLZ].constantGas != GasFastStep { + t.Errorf("CLZ gas: got %d, want %d", postLisovo[CLZ].constantGas, GasFastStep) + } +} diff --git a/core/vm/evm.go b/core/vm/evm.go index 59c1272995..961a4d1d35 100644 --- a/core/vm/evm.go +++ b/core/vm/evm.go @@ -150,10 +150,11 @@ func NewEVM(blockCtx BlockContext, statedb StateDB, chainConfig *params.ChainCon evm.precompiles = activePrecompiledContracts(evm.chainRules) switch { + case evm.chainRules.IsLisovo: + evm.table = &lisovoInstructionSet case evm.chainRules.IsOsaka: evm.table = &osakaInstructionSet case evm.chainRules.IsVerkle: - // TODO replace with proper instruction set when fork is specified evm.table = &verkleInstructionSet case evm.chainRules.IsPrague: evm.table = &pragueInstructionSet diff --git a/core/vm/jump_table.go b/core/vm/jump_table.go index aa235143c7..de89b1224a 100644 --- a/core/vm/jump_table.go +++ b/core/vm/jump_table.go @@ -63,6 +63,7 @@ var ( verkleInstructionSet = newVerkleInstructionSet() pragueInstructionSet = newPragueInstructionSet() osakaInstructionSet = newOsakaInstructionSet() + lisovoInstructionSet = newLisovoInstructionSet() ) // JumpTable contains the EVM opcodes supported at a given fork. @@ -87,9 +88,9 @@ func validate(jt JumpTable) JumpTable { return jt } -func newVerkleInstructionSet() JumpTable { - instructionSet := newShanghaiInstructionSet() - enable4762(&instructionSet) +func newLisovoInstructionSet() JumpTable { + instructionSet := newPragueInstructionSet() + enable7939(&instructionSet) // EIP-7939 (CLZ opcode) return validate(instructionSet) } @@ -99,6 +100,12 @@ func newOsakaInstructionSet() JumpTable { return validate(instructionSet) } +func newVerkleInstructionSet() JumpTable { + instructionSet := newShanghaiInstructionSet() + enable4762(&instructionSet) + return validate(instructionSet) +} + func newPragueInstructionSet() JumpTable { instructionSet := newCancunInstructionSet() enable7702(&instructionSet) // EIP-7702 Setcode transaction type diff --git a/core/vm/jump_table_export.go b/core/vm/jump_table_export.go index 68d0c21885..f8c918e0ab 100644 --- a/core/vm/jump_table_export.go +++ b/core/vm/jump_table_export.go @@ -26,14 +26,18 @@ import ( // the rules. func LookupInstructionSet(rules params.Rules) (JumpTable, error) { switch { - case rules.IsVerkle: - return newCancunInstructionSet(), errors.New("verkle-fork not defined yet") + // Note: geth only returns an error for the verkle-fork. + // Return nil for other forks. + case rules.IsLisovo: + return newLisovoInstructionSet(), nil case rules.IsMadhugiriPro: - return newPragueInstructionSet(), errors.New("madhugiriPro-fork not defined yet") + return newPragueInstructionSet(), nil case rules.IsMadhugiri: - return newPragueInstructionSet(), errors.New("madhugiri-fork not defined yet") + return newPragueInstructionSet(), nil case rules.IsOsaka: return newOsakaInstructionSet(), nil + case rules.IsVerkle: + return newCancunInstructionSet(), errors.New("verkle-fork not defined yet") case rules.IsPrague: return newPragueInstructionSet(), nil case rules.IsCancun: diff --git a/docs/cli/server.md b/docs/cli/server.md index 5da0aeb50d..b10194153c 100644 --- a/docs/cli/server.md +++ b/docs/cli/server.md @@ -324,9 +324,9 @@ The ```bor server``` command runs the Bor client. - ```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.targetGasPercentage```: Target gas as percentage of gas limit (1-100, default 65) for post-Lisovo blocks (default: 0) -- ```miner.baseFeeChangeDenominator```: Base fee change rate denominator (must be >0, default 64) for post-Dandeli blocks (default: 0) +- ```miner.baseFeeChangeDenominator```: Base fee change rate denominator (must be >0, default 64) for post-Lisovo blocks (default: 0) ### Telemetry Options diff --git a/internal/cli/server/chains/amoy.go b/internal/cli/server/chains/amoy.go index 714b0e282a..3524abcefa 100644 --- a/internal/cli/server/chains/amoy.go +++ b/internal/cli/server/chains/amoy.go @@ -39,6 +39,7 @@ var amoyTestnet = &Chain{ MadhugiriBlock: big.NewInt(28899616), MadhugiriProBlock: big.NewInt(29287400), DandeliBlock: big.NewInt(31890000), + LisovoBlock: big.NewInt(33634700), StateSyncConfirmationDelay: map[string]uint64{ "0": 128, }, diff --git a/internal/cli/server/config.go b/internal/cli/server/config.go index 082edcb2c8..cee9219214 100644 --- a/internal/cli/server/config.go +++ b/internal/cli/server/config.go @@ -421,10 +421,10 @@ type SealerConfig struct { 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 is the target gas as percentage of gas limit (1-100, default 65) for post-Lisovo 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 is the base fee change rate (must be >0, default 64) for post-Lisovo blocks BaseFeeChangeDenominator uint64 `hcl:"base-fee-change-denominator,optional" toml:"base-fee-change-denominator,optional"` } diff --git a/internal/cli/server/flags.go b/internal/cli/server/flags.go index 5984ff9398..a7ae19265c 100644 --- a/internal/cli/server/flags.go +++ b/internal/cli/server/flags.go @@ -443,14 +443,14 @@ func (c *Command) Flags(config *Config) *flagset.Flagset { }) f.Uint64Flag(&flagset.Uint64Flag{ Name: "miner.targetGasPercentage", - Usage: "Target gas as percentage of gas limit (1-100, default 65) for post-Dandeli blocks", + Usage: "Target gas as percentage of gas limit (1-100, default 65) for post-Lisovo blocks", Value: &c.cliConfig.Sealer.TargetGasPercentage, Default: c.cliConfig.Sealer.TargetGasPercentage, Group: "Sealer", }) f.Uint64Flag(&flagset.Uint64Flag{ Name: "miner.baseFeeChangeDenominator", - Usage: "Base fee change rate denominator (must be >0, default 64) for post-Dandeli blocks", + Usage: "Base fee change rate denominator (must be >0, default 64) for post-Lisovo blocks", Value: &c.cliConfig.Sealer.BaseFeeChangeDenominator, Default: c.cliConfig.Sealer.BaseFeeChangeDenominator, Group: "Sealer", diff --git a/params/config.go b/params/config.go index 03010b2514..e121983e24 100644 --- a/params/config.go +++ b/params/config.go @@ -343,6 +343,7 @@ var ( MadhugiriBlock: big.NewInt(28899616), MadhugiriProBlock: big.NewInt(29287400), DandeliBlock: big.NewInt(31890000), + LisovoBlock: big.NewInt(33634700), StateSyncConfirmationDelay: map[string]uint64{ "0": 128, }, @@ -729,6 +730,7 @@ var ( MadhugiriBlock: big.NewInt(0), MadhugiriProBlock: big.NewInt(0), DandeliBlock: big.NewInt(0), + LisovoBlock: big.NewInt(0), }, } @@ -915,8 +917,8 @@ type BorConfig struct { // 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 + TargetGasPercentage *uint64 `json:"-"` // Post-Lisovo: target gas as % of gas limit (configurable post-Lisovo, default 65% post-Dandeli). Set via --miner.target-gas-percentage + BaseFeeChangeDenominator *uint64 `json:"-"` // Post-Lisovo: base fee change rate (configurable post-Lisovo, 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) @@ -928,6 +930,7 @@ type BorConfig struct { 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) + LisovoBlock *big.Int `json:"lisovoBlock"` // Lisovo switch block (nil = no fork, 0 = already on lisovo) } // String implements the stringer interface, returning the consensus engine details. @@ -991,8 +994,12 @@ func (c *BorConfig) IsDandeli(number *big.Int) bool { return isBlockForked(c.DandeliBlock, number) } +func (c *BorConfig) IsLisovo(number *big.Int) bool { + return isBlockForked(c.LisovoBlock, number) +} + // GetTargetGasPercentage returns the target gas percentage for gas limit calculation. -// After Dandeli hard fork, this value can be configured via CLI flags (stored in BorConfig at runtime). +// After Lisovo 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 { @@ -1002,7 +1009,7 @@ func (c *BorConfig) GetTargetGasPercentage(number *big.Int) uint64 { } // If custom value is set, validate it - if c.TargetGasPercentage != nil { + if c.TargetGasPercentage != nil && c.IsLisovo(number) { val := *c.TargetGasPercentage // Validate: must be between 1 and 100 if val > 0 && val <= 100 { @@ -1124,6 +1131,9 @@ func (c *ChainConfig) Description() string { if c.Bor.DandeliBlock != nil { banner += fmt.Sprintf(" - Dandeli: #%-8v\n", c.Bor.DandeliBlock) } + if c.Bor.LisovoBlock != nil { + banner += fmt.Sprintf(" - Lisovo: #%-8v\n", c.Bor.LisovoBlock) + } return banner } @@ -1760,6 +1770,7 @@ type Rules struct { IsVerkle bool IsMadhugiri bool IsMadhugiriPro bool + IsLisovo bool } // Rules ensures c's ChainID is not nil. @@ -1793,5 +1804,6 @@ func (c *ChainConfig) Rules(num *big.Int, isMerge bool, _ uint64) Rules { IsEIP4762: c.IsVerkle(num), IsMadhugiri: c.Bor != nil && c.Bor.IsMadhugiri(num), IsMadhugiriPro: c.Bor != nil && c.Bor.IsMadhugiriPro(num), + IsLisovo: c.Bor != nil && c.Bor.IsLisovo(num), } } diff --git a/params/config_test.go b/params/config_test.go index da57ced1c4..8653ec50cb 100644 --- a/params/config_test.go +++ b/params/config_test.go @@ -813,7 +813,7 @@ func TestGetTargetGasPercentage(t *testing.T) { } }) - t.Run("Post-Dandeli with valid custom values", func(t *testing.T) { + t.Run("Post-Lisovo with valid custom values", func(t *testing.T) { testCases := []struct { customValue uint64 description string @@ -829,12 +829,13 @@ func TestGetTargetGasPercentage(t *testing.T) { val := tc.customValue config := &BorConfig{ DandeliBlock: big.NewInt(1000), + LisovoBlock: big.NewInt(1000), // Configurable params require Lisovo TargetGasPercentage: &val, } result := config.GetTargetGasPercentage(big.NewInt(1000)) if result != tc.customValue { - t.Errorf("Post-Dandeli with custom value %d: expected %d, got %d", + t.Errorf("Post-Lisovo with custom value %d: expected %d, got %d", tc.customValue, tc.customValue, result) } }) @@ -948,28 +949,29 @@ func TestGetBaseFeeChangeDenominator(t *testing.T) { } }) - t.Run("Post-Dandeli with nil custom value falls back to Bhilai default", func(t *testing.T) { + t.Run("Post-Lisovo 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), + LisovoBlock: big.NewInt(3000), // Configurable params require Lisovo BaseFeeChangeDenominator: nil, } result := BaseFeeChangeDenominator(config, big.NewInt(3000)) if result != BaseFeeChangeDenominatorPostBhilai { - t.Errorf("Post-Dandeli with nil custom value: expected %d, got %d", + t.Errorf("Post-Lisovo with nil custom value: expected %d, got %d", BaseFeeChangeDenominatorPostBhilai, result) } result = BaseFeeChangeDenominator(config, big.NewInt(4000)) if result != BaseFeeChangeDenominatorPostBhilai { - t.Errorf("Post-Dandeli with nil custom value (block 4000): expected %d, got %d", + t.Errorf("Post-Lisovo with nil custom value (block 4000): expected %d, got %d", BaseFeeChangeDenominatorPostBhilai, result) } }) - t.Run("Post-Dandeli with valid custom value", func(t *testing.T) { + t.Run("Post-Lisovo with valid custom value", func(t *testing.T) { testCases := []uint64{1, 8, 16, 32, 64, 128} for _, customVal := range testCases { @@ -979,47 +981,50 @@ func TestGetBaseFeeChangeDenominator(t *testing.T) { DelhiBlock: big.NewInt(1000), BhilaiBlock: big.NewInt(2000), DandeliBlock: big.NewInt(3000), + LisovoBlock: big.NewInt(3000), // Configurable params require Lisovo BaseFeeChangeDenominator: &val, } result := BaseFeeChangeDenominator(config, big.NewInt(3000)) if result != customVal { - t.Errorf("Post-Dandeli with custom value %d: expected %d, got %d", + t.Errorf("Post-Lisovo 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) { + t.Run("Post-Lisovo 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), + LisovoBlock: big.NewInt(3000), // Configurable params require Lisovo BaseFeeChangeDenominator: &invalidVal, } result := BaseFeeChangeDenominator(config, big.NewInt(3000)) if result != BaseFeeChangeDenominatorPostBhilai { - t.Errorf("Post-Dandeli with invalid value 0: expected %d, got %d", + t.Errorf("Post-Lisovo with invalid value 0: expected %d, got %d", BaseFeeChangeDenominatorPostBhilai, result) } }) - t.Run("Pre-Dandeli with custom value ignores it", func(t *testing.T) { + t.Run("Pre-Lisovo 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), + LisovoBlock: big.NewInt(4000), // Lisovo after Dandeli BaseFeeChangeDenominator: &customVal, } - // Before Dandeli, custom value should be ignored - result := BaseFeeChangeDenominator(config, big.NewInt(2500)) + // Before Lisovo, custom value should be ignored + result := BaseFeeChangeDenominator(config, big.NewInt(3500)) if result != BaseFeeChangeDenominatorPostBhilai { - t.Errorf("Pre-Dandeli with custom value (block 2500): expected %d, got %d", + t.Errorf("Pre-Lisovo with custom value (block 3500): expected %d, got %d", BaseFeeChangeDenominatorPostBhilai, result) } }) diff --git a/params/protocol_params.go b/params/protocol_params.go index 8109e26a90..17d9fafae8 100644 --- a/params/protocol_params.go +++ b/params/protocol_params.go @@ -246,15 +246,15 @@ 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) +// - Post-Lisovo: 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 Dandeli is active and custom value is set, validate and use it - if borConfig.IsDandeli(number) && borConfig.BaseFeeChangeDenominator != nil { + // If Lisovo is active and custom value is set, validate and use it + if borConfig.IsLisovo(number) && borConfig.BaseFeeChangeDenominator != nil { val := *borConfig.BaseFeeChangeDenominator // Validate: must be non-zero to prevent division by zero if val > 0 { diff --git a/tests/bor/bor_config_change_test.go b/tests/bor/bor_config_change_test.go index c55d897887..3628e9da10 100644 --- a/tests/bor/bor_config_change_test.go +++ b/tests/bor/bor_config_change_test.go @@ -42,11 +42,15 @@ func TestBorConfigParameterChange(t *testing.T) { 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) + // Enable Dandeli fork at block 15 for percentage-based calculation + dandeliBlock := big.NewInt(15) genesis.Config.Bor.DandeliBlock = dandeliBlock - // Set custom BaseFeeChangeDenominator and TargetGasPercentage that will take effect at Dandeli + // Enable Lisovo fork at block 20 - this is where configurable parameters take effect + lisovoBlock := big.NewInt(20) + genesis.Config.Bor.LisovoBlock = lisovoBlock + + // Set custom BaseFeeChangeDenominator and TargetGasPercentage that will take effect at Lisovo customBaseFeeChangeDenominator := uint64(32) // Different from default (64) customTargetGasPercentage := uint64(70) // Different from default (65) genesis.Config.Bor.BaseFeeChangeDenominator = &customBaseFeeChangeDenominator @@ -68,8 +72,8 @@ func TestBorConfigParameterChange(t *testing.T) { } } - // Wait for blocks to be mined beyond the Dandeli fork block - targetBlockNum := dandeliBlock.Uint64() + 10 // Mine 10 blocks after the fork + // Wait for blocks to be mined beyond the Lisovo fork block + targetBlockNum := lisovoBlock.Uint64() + 10 // Mine 10 blocks after the fork timeout := time.After(120 * time.Second) for { @@ -97,13 +101,13 @@ checkResults: 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 + // Verify blocks around the Lisovo fork (where configurable parameters activate) + preLisovoBlock := lisovoBlock.Uint64() - 1 + atLisovoBlock := lisovoBlock.Uint64() + postLisovoBlock := lisovoBlock.Uint64() + 5 // Check block hashes match between nodes (validator accepted producer's blocks) - for _, blockNum := range []uint64{preDandeliBlock, atDandeliBlock, postDandeliBlock} { + for _, blockNum := range []uint64{preLisovoBlock, atLisovoBlock, postLisovoBlock} { header0 := chain0.GetHeaderByNumber(blockNum) header1 := chain1.GetHeaderByNumber(blockNum) @@ -118,50 +122,50 @@ checkResults: } // Verify that the BaseFeeChangeDenominator and TargetGasPercentage are being used correctly - // by checking the base fee calculation before and after Dandeli fork + // by checking the base fee calculation before and after Lisovo fork (configurable parameters) - // Pre-Dandeli block: should use default parameters - preHeader := chain0.GetHeaderByNumber(preDandeliBlock) - preParentHeader := chain0.GetHeaderByNumber(preDandeliBlock - 1) + // Pre-Lisovo block: should use default parameters + preHeader := chain0.GetHeaderByNumber(preLisovoBlock) + preParentHeader := chain0.GetHeaderByNumber(preLisovoBlock - 1) if preParentHeader != nil && preHeader != nil { - // Calculate expected base fee using pre-Dandeli parameters + // Calculate expected base fee using pre-Lisovo parameters (default 65%) expectedPreBaseFee := eip1559.CalcBaseFee(genesis.Config, preParentHeader) - t.Logf("Pre-Dandeli block %d: Expected BaseFee=%s, Actual BaseFee=%s", - preDandeliBlock, expectedPreBaseFee.String(), preHeader.BaseFee.String()) + t.Logf("Pre-Lisovo block %d: Expected BaseFee=%s, Actual BaseFee=%s", + preLisovoBlock, 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) + // Post-Lisovo block: should use custom parameters + postHeader := chain0.GetHeaderByNumber(postLisovoBlock) + postParentHeader := chain0.GetHeaderByNumber(postLisovoBlock - 1) if postParentHeader != nil && postHeader != nil { - // Calculate expected base fee using post-Dandeli parameters + // Calculate expected base fee using post-Lisovo 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()) + t.Logf("Post-Lisovo block %d: Expected BaseFee=%s, Actual BaseFee=%s", + postLisovoBlock, 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 + // After Lisovo, configurable parameters take effect + // Verify that the calculation uses the custom 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", + t.Logf("Post-Lisovo 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") + "Custom TargetGasPercentage should be used after Lisovo 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") + "Custom BaseFeeChangeDenominator should be used after Lisovo 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++ { + for blockNum := atLisovoBlock; blockNum <= postLisovoBlock; blockNum++ { header0 := chain0.GetHeaderByNumber(blockNum) header1 := chain1.GetHeaderByNumber(blockNum) @@ -200,10 +204,14 @@ func TestBorConfigParameterChangeVerification(t *testing.T) { genesis.Config.LondonBlock = common.Big0 genesis.Config.Bor.JaipurBlock = common.Big0 - // Enable Dandeli fork at block 10 - dandeliBlock := big.NewInt(10) + // Enable Dandeli fork at block 5 (percentage-based calculation) + dandeliBlock := big.NewInt(5) genesis.Config.Bor.DandeliBlock = dandeliBlock + // Enable Lisovo fork at block 10 (configurable parameters) + lisovoBlock := big.NewInt(10) + genesis.Config.Bor.LisovoBlock = lisovoBlock + // Set custom parameters customBaseFeeChangeDenominator := uint64(128) // Very different from default customTargetGasPercentage := uint64(80) // Very different from default @@ -219,8 +227,8 @@ func TestBorConfigParameterChangeVerification(t *testing.T) { err = ethBackend.StartMining() require.NoError(t, err) - // Wait for blocks to be mined beyond the Dandeli fork - targetBlockNum := dandeliBlock.Uint64() + 5 + // Wait for blocks to be mined beyond the Lisovo fork + targetBlockNum := lisovoBlock.Uint64() + 5 timeout := time.After(60 * time.Second) for { @@ -250,8 +258,8 @@ verifyHeaders: 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 + if blockNum >= lisovoBlock.Uint64() { + // Post-Lisovo: custom parameters should be used targetPercentage := genesis.Config.Bor.GetTargetGasPercentage(header.Number) baseFeeChangeDenom := params.BaseFeeChangeDenominator(genesis.Config.Bor, header.Number) @@ -260,18 +268,18 @@ verifyHeaders: assert.Equal(t, customBaseFeeChangeDenominator, baseFeeChangeDenom, "Block %d should use custom BaseFeeChangeDenominator", blockNum) - t.Logf("Block %d (Post-Dandeli): TargetGasPercentage=%d, BaseFeeChangeDenominator=%d, BaseFee=%s", + t.Logf("Block %d (Post-Lisovo): TargetGasPercentage=%d, BaseFeeChangeDenominator=%d, BaseFee=%s", blockNum, targetPercentage, baseFeeChangeDenom, header.BaseFee.String()) } else { - // Pre-Dandeli: default parameters should be used + // Pre-Lisovo: 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) + // Pre-Lisovo should use default parameters (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", + t.Logf("Block %d (Pre-Lisovo): TargetGasPercentage=%d, BaseFeeChangeDenominator=%d, BaseFee=%s", blockNum, targetPercentage, baseFeeChangeDenom, header.BaseFee.String()) } } @@ -298,7 +306,8 @@ func TestBorConfigParameterDivergence(t *testing.T) { 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 + genesisProducer.Config.Bor.DandeliBlock = big.NewInt(5) // Enable Dandeli early (percentage-based calc) + genesisProducer.Config.Bor.LisovoBlock = big.NewInt(10) // Enable Lisovo (configurable params) // Producer uses first set of parameters producerBaseFeeChangeDenominator := uint64(32) @@ -310,7 +319,8 @@ func TestBorConfigParameterDivergence(t *testing.T) { 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 + genesisValidator.Config.Bor.DandeliBlock = big.NewInt(5) // Same Dandeli activation + genesisValidator.Config.Bor.LisovoBlock = big.NewInt(10) // Same Lisovo activation // Validator uses DIFFERENT parameters (simulating a "second change") validatorBaseFeeChangeDenominator := uint64(128) // Much larger denominator @@ -379,9 +389,9 @@ checkDivergence: 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} + // Verify blocks at key positions (focus on post-Lisovo where configurable params apply) + lisovoBlock := uint64(10) + checkBlocks := []uint64{lisovoBlock, lisovoBlock + 5, lisovoBlock + 10} for _, blockNum := range checkBlocks { producerHeader := chainProducer.GetHeaderByNumber(blockNum) @@ -410,8 +420,8 @@ checkDivergence: 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 { + // Post-Lisovo: verify nodes are using their respective different configs + if blockNum >= lisovoBlock { assert.Equal(t, producerTargetGasPercentage, producerTargetPct, "Producer should use its own TargetGasPercentage") assert.Equal(t, producerBaseFeeChangeDenominator, producerDenom, @@ -430,7 +440,7 @@ checkDivergence: // Verify validator explicitly accepts producer's headers despite config divergence engineValidator := nodeValidator.Engine().(*bor.Bor) - for blockNum := dandeliBlock; blockNum <= dandeliBlock+10; blockNum++ { + for blockNum := lisovoBlock; blockNum <= lisovoBlock+10; blockNum++ { producerHeader := chainProducer.GetHeaderByNumber(blockNum) if producerHeader == nil { continue @@ -445,7 +455,7 @@ checkDivergence: } 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") + t.Log("This demonstrates that post-Lisovo boundary validation allows for flexible parameter updates") } // TestBorConfigMultipleParameterChanges tests a scenario where parameters are conceptually @@ -463,11 +473,12 @@ func TestBorConfigMultipleParameterChanges(t *testing.T) { faucets[i], _ = crypto.GenerateKey() } - // Initialize genesis with Dandeli fork at block 10 + // Initialize genesis with Dandeli fork at block 5 and Lisovo 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) + genesis.Config.Bor.DandeliBlock = big.NewInt(5) // Percentage-based calculation + genesis.Config.Bor.LisovoBlock = big.NewInt(10) // Configurable parameters // Start with first set of parameters (will be used from block 10 onward) firstBaseFeeChangeDenominator := uint64(32)