diff --git a/x/feemarket/README.md b/x/feemarket/README.md index 98d9437..ad67a1b 100644 --- a/x/feemarket/README.md +++ b/x/feemarket/README.md @@ -44,7 +44,7 @@ The calculation for the updated base fee for the next block is as follows: ```golang // sumBlockSizesInWindow returns the sum of the block sizes in the window. -blockConsumption := sumBlockSizesInWindow(window) / (window * targetBlockSize) +blockConsumption := sumBlockSizesInWindow(window) / (window * maxBlockSize) if blockConsumption < gamma || blockConsumption > 1 - gamma { // MAX_LEARNING_RATE is a constant that configured by the chain developer diff --git a/x/feemarket/types/state.go b/x/feemarket/types/state.go index f35a92a..0dcb1cd 100644 --- a/x/feemarket/types/state.go +++ b/x/feemarket/types/state.go @@ -18,6 +18,101 @@ func NewState(window uint64, baseFee math.Int, learningRate math.LegacyDec) Stat } } +// UpdateBaseFee updates the learning rate and base fee based on the AIMD +// learning rate adjustment algorithm. The learning rate is updated +// based on the average utilization of the block window. The base fee is +// update using the new learning rate and the delta adjustment. Please +// see the EIP-1559 specification for more details. +func (s *State) UpdateBaseFee(params Params) math.Int { + // Update the learning rate. + s.UpdateLearningRate(params) + + // Calculate the new base fee with the learning rate adjustment. + currentBlockSize := math.LegacyNewDecFromInt(math.NewIntFromUint64(s.Window[s.Index])) + targetBlockSize := math.LegacyNewDecFromInt(math.NewIntFromUint64(params.TargetBlockUtilization)) + utilization := (currentBlockSize.Sub(targetBlockSize)).Quo(targetBlockSize) + + // Truncate the learning rate adjustment to an integer. + // + // This is equivalent to + // 1 + (learningRate * (currentBlockSize - targetBlockSize) / targetBlockSize) + learningRateAdjustment := math.LegacyOneDec().Add(s.LearningRate.Mul(utilization)) + + // Calculate the delta adjustment. + net := s.GetNetUtilization(params.TargetBlockUtilization) + delta := params.Delta.Mul(math.LegacyNewDecFromInt(net)) + + // Update the base fee. + s.BaseFee = (math.LegacyNewDecFromInt(s.BaseFee).Mul(learningRateAdjustment)).Add(delta).TruncateInt() + return s.BaseFee +} + +// UpdateLearningRate updates the learning rate based on the AIMD +// learning rate adjustment algorithm. The learning rate is updated +// based on the average utilization of the block window. There are +// two cases that can occur: +// +// 1. The average utilization is above the target threshold. In this +// case, the learning rate is increased by the alpha parameter. This occurs +// when blocks are nearly full or empty. +// 2. The average utilization is below the target threshold. In this +// case, the learning rate is decreased by the beta parameter. This occurs +// when blocks are relatively close to the target block utilization. +// +// For more details, please see the EIP-1559 specification. +func (s *State) UpdateLearningRate(params Params) math.LegacyDec { + // Calculate the average utilization of the block window. + avg := s.GetAverageUtilization(params.MaxBlockUtilization) + + // Determine if the average utilization is above or below the target + // threshold and adjust the learning rate accordingly. + var updatedLearningRate math.LegacyDec + if avg.LTE(params.Theta) || avg.GTE(math.LegacyOneDec().Sub(params.Theta)) { + updatedLearningRate = params.Alpha.Add(s.LearningRate) + if updatedLearningRate.GT(params.MaxLearningRate) { + updatedLearningRate = params.MaxLearningRate + } + } else { + updatedLearningRate = s.LearningRate.Mul(params.Beta) + if updatedLearningRate.LT(params.MinLearningRate) { + updatedLearningRate = params.MinLearningRate + } + } + + // Update the current learning rate. + s.LearningRate = updatedLearningRate + return s.LearningRate +} + +// GetNetUtilization returns the net utilization of the block window. +func (s *State) GetNetUtilization(target uint64) math.Int { + net := math.NewInt(0) + + targetUtilization := math.NewIntFromUint64(target) + for _, utilization := range s.Window { + diff := math.NewIntFromUint64(utilization).Sub(targetUtilization) + net = net.Add(diff) + } + + return net +} + +// GetAverageUtilization returns the average utilization of the block +// window. +func (s *State) GetAverageUtilization(max uint64) math.LegacyDec { + var total uint64 + for _, utilization := range s.Window { + total += utilization + } + + sum := math.LegacyNewDecFromInt(math.NewIntFromUint64(total)) + + multiple := math.LegacyNewDecFromInt(math.NewIntFromUint64(uint64(len(s.Window)))) + divisor := math.LegacyNewDecFromInt(math.NewIntFromUint64(max)).Mul(multiple) + + return sum.Quo(divisor) +} + // ValidateBasic performs basic validation on the state. func (s *State) ValidateBasic() error { if s.Window == nil || len(s.Window) == 0 { diff --git a/x/feemarket/types/state_test.go b/x/feemarket/types/state_test.go index 79b388b..da905d1 100644 --- a/x/feemarket/types/state_test.go +++ b/x/feemarket/types/state_test.go @@ -1,13 +1,523 @@ package types_test import ( + "math/rand" "testing" + "cosmossdk.io/math" "github.com/stretchr/testify/require" "github.com/skip-mev/feemarket/x/feemarket/types" ) +var ( + OneHundred = math.LegacyNewDecFromInt(math.NewInt(100)) +) + +func TestState_UpdateBaseFee(t *testing.T) { + t.Run("empty block with default eip-1559", func(t *testing.T) { + state := types.DefaultState() + state.BaseFee = math.NewInt(1000) + + params := types.DefaultParams() + + newBaseFee := state.UpdateBaseFee(params) + expectedBaseFee := math.NewInt(875) + require.True(t, expectedBaseFee.Equal(newBaseFee)) + }) + + t.Run("target block with default eip-1559", func(t *testing.T) { + state := types.DefaultState() + state.BaseFee = math.NewInt(1000) + + params := types.DefaultParams() + + state.Window[0] = params.TargetBlockUtilization + + newBaseFee := state.UpdateBaseFee(params) + expectedBaseFee := math.NewInt(1000) + require.True(t, expectedBaseFee.Equal(newBaseFee)) + }) + + t.Run("full block with default eip-1559", func(t *testing.T) { + state := types.DefaultState() + state.BaseFee = math.NewInt(1000) + + params := types.DefaultParams() + + state.Window[0] = params.MaxBlockUtilization + + newBaseFee := state.UpdateBaseFee(params) + expectedBaseFee := math.NewInt(1125) + require.True(t, expectedBaseFee.Equal(newBaseFee)) + }) + + t.Run("empty block with default aimd eip-1559", func(t *testing.T) { + state := types.DefaultAIMDState() + state.BaseFee = math.NewInt(1000) + state.LearningRate = math.LegacyMustNewDecFromStr("0.125") + + params := types.DefaultAIMDParams() + + newBaseFee := state.UpdateBaseFee(params) + expectedBaseFee := math.NewInt(850) + require.True(t, expectedBaseFee.Equal(newBaseFee)) + }) + + t.Run("target block with default aimd eip-1559", func(t *testing.T) { + state := types.DefaultAIMDState() + state.BaseFee = math.NewInt(1000) + state.LearningRate = math.LegacyMustNewDecFromStr("0.125") + + params := types.DefaultAIMDParams() + + for i := 0; i < len(state.Window); i++ { + state.Window[i] = params.TargetBlockUtilization + } + + newBaseFee := state.UpdateBaseFee(params) + expectedBaseFee := math.NewInt(1000) + require.True(t, expectedBaseFee.Equal(newBaseFee)) + }) + + t.Run("full blocks with default aimd eip-1559", func(t *testing.T) { + state := types.DefaultAIMDState() + state.BaseFee = math.NewInt(1000) + state.LearningRate = math.LegacyMustNewDecFromStr("0.125") + + params := types.DefaultAIMDParams() + + for i := 0; i < len(state.Window); i++ { + state.Window[i] = params.MaxBlockUtilization + } + + newBaseFee := state.UpdateBaseFee(params) + expectedBaseFee := math.NewInt(1150) + require.True(t, expectedBaseFee.Equal(newBaseFee)) + }) +} + +func TestState_UpdateLearningRate(t *testing.T) { + t.Run("empty block with default eip-1559", func(t *testing.T) { + state := types.DefaultState() + params := types.DefaultParams() + + state.UpdateLearningRate(params) + expectedLearningRate := math.LegacyMustNewDecFromStr("0.125") + require.True(t, expectedLearningRate.Equal(state.LearningRate)) + }) + + t.Run("target block with default eip-1559", func(t *testing.T) { + state := types.DefaultState() + params := types.DefaultParams() + + state.Window[0] = params.TargetBlockUtilization + + state.UpdateLearningRate(params) + expectedLearningRate := math.LegacyMustNewDecFromStr("0.125") + require.True(t, expectedLearningRate.Equal(state.LearningRate)) + }) + + t.Run("full block with default eip-1559", func(t *testing.T) { + state := types.DefaultState() + params := types.DefaultParams() + + state.Window[0] = params.MaxBlockUtilization + + state.UpdateLearningRate(params) + expectedLearningRate := math.LegacyMustNewDecFromStr("0.125") + require.True(t, expectedLearningRate.Equal(state.LearningRate)) + }) + + t.Run("between 0 and target with default eip-1559", func(t *testing.T) { + state := types.DefaultState() + params := types.DefaultParams() + + state.Window[0] = 50000 + + state.UpdateLearningRate(params) + expectedLearningRate := math.LegacyMustNewDecFromStr("0.125") + require.True(t, expectedLearningRate.Equal(state.LearningRate)) + }) + + t.Run("between target and max with default eip-1559", func(t *testing.T) { + state := types.DefaultState() + params := types.DefaultParams() + + state.Window[0] = 100000 + + state.UpdateLearningRate(params) + expectedLearningRate := math.LegacyMustNewDecFromStr("0.125") + require.True(t, expectedLearningRate.Equal(state.LearningRate)) + }) + + t.Run("random value with default eip-1559", func(t *testing.T) { + state := types.DefaultState() + params := types.DefaultParams() + + randomValue := rand.Int63n(1000000000) + state.Window[0] = uint64(randomValue) + + state.UpdateLearningRate(params) + expectedLearningRate := math.LegacyMustNewDecFromStr("0.125") + require.True(t, expectedLearningRate.Equal(state.LearningRate)) + }) + + t.Run("empty block with default aimd eip-1559", func(t *testing.T) { + state := types.DefaultAIMDState() + params := types.DefaultAIMDParams() + + state.UpdateLearningRate(params) + expectedLearningRate := params.MinLearningRate.Add(params.Alpha) + require.True(t, expectedLearningRate.Equal(state.LearningRate)) + }) + + t.Run("target block with default aimd eip-1559", func(t *testing.T) { + state := types.DefaultAIMDState() + defaultLR := math.LegacyMustNewDecFromStr("0.125") + state.LearningRate = defaultLR + + params := types.DefaultAIMDParams() + + for i := 0; i < len(state.Window); i++ { + state.Window[i] = params.TargetBlockUtilization + } + + state.UpdateLearningRate(params) + expectedLearningRate := defaultLR.Mul(params.Beta) + require.True(t, expectedLearningRate.Equal(state.LearningRate)) + }) + + t.Run("full blocks with default aimd eip-1559", func(t *testing.T) { + state := types.DefaultAIMDState() + defaultLR := math.LegacyMustNewDecFromStr("0.125") + state.LearningRate = defaultLR + + params := types.DefaultAIMDParams() + + for i := 0; i < len(state.Window); i++ { + state.Window[i] = params.MaxBlockUtilization + } + + state.UpdateLearningRate(params) + expectedLearningRate := defaultLR.Add(params.Alpha) + require.True(t, expectedLearningRate.Equal(state.LearningRate)) + }) + + t.Run("varying blocks with default aimd eip-1559", func(t *testing.T) { + state := types.DefaultAIMDState() + defaultLR := math.LegacyMustNewDecFromStr("0.125") + state.LearningRate = defaultLR + + params := types.DefaultAIMDParams() + + for i := 0; i < len(state.Window); i++ { + if i%2 == 0 { + state.Window[i] = params.MaxBlockUtilization + } else { + state.Window[i] = 0 + } + } + + state.UpdateLearningRate(params) + expectedLearningRate := defaultLR.Mul(params.Beta) + require.True(t, expectedLearningRate.Equal(state.LearningRate)) + }) + + t.Run("exceeds threshold with default aimd eip-1559", func(t *testing.T) { + state := types.DefaultAIMDState() + defaultLR := math.LegacyMustNewDecFromStr("0.125") + state.LearningRate = defaultLR + + params := types.DefaultAIMDParams() + + for i := 0; i < len(state.Window); i++ { + if i%2 == 0 { + state.Window[i] = params.MaxBlockUtilization + } else { + state.Window[i] = params.TargetBlockUtilization + 1 + } + } + + state.UpdateLearningRate(params) + expectedLearningRate := defaultLR.Add(params.Alpha) + require.True(t, expectedLearningRate.Equal(state.LearningRate)) + }) +} + +func TestState_GetNetUtilization(t *testing.T) { + t.Run("empty block with default eip-1559", func(t *testing.T) { + state := types.DefaultState() + params := types.DefaultParams() + + netUtilization := state.GetNetUtilization(params.TargetBlockUtilization) + expectedUtilization := math.NewInt(0).Sub(math.NewIntFromUint64(params.TargetBlockUtilization)) + require.True(t, expectedUtilization.Equal(netUtilization)) + }) + + t.Run("target block with default eip-1559", func(t *testing.T) { + state := types.DefaultState() + params := types.DefaultParams() + + state.Window[0] = params.TargetBlockUtilization + + netUtilization := state.GetNetUtilization(params.TargetBlockUtilization) + expectedUtilization := math.NewInt(0) + require.True(t, expectedUtilization.Equal(netUtilization)) + }) + + t.Run("full block with default eip-1559", func(t *testing.T) { + state := types.DefaultState() + params := types.DefaultParams() + + state.Window[0] = params.MaxBlockUtilization + + netUtilization := state.GetNetUtilization(params.TargetBlockUtilization) + expectedUtilization := math.NewIntFromUint64(params.MaxBlockUtilization - params.TargetBlockUtilization) + require.True(t, expectedUtilization.Equal(netUtilization)) + }) + + t.Run("empty block with default aimd eip-1559", func(t *testing.T) { + state := types.DefaultAIMDState() + params := types.DefaultAIMDParams() + + netUtilization := state.GetNetUtilization(params.TargetBlockUtilization) + + multiple := math.NewIntFromUint64(params.Window) + expectedUtilization := math.NewInt(0).Sub(math.NewIntFromUint64(params.TargetBlockUtilization)).Mul(multiple) + require.True(t, expectedUtilization.Equal(netUtilization)) + }) + + t.Run("full blocks with default aimd eip-1559", func(t *testing.T) { + state := types.DefaultAIMDState() + params := types.DefaultAIMDParams() + + for i := 0; i < len(state.Window); i++ { + state.Window[i] = params.MaxBlockUtilization + } + + netUtilization := state.GetNetUtilization(params.TargetBlockUtilization) + + multiple := math.NewIntFromUint64(params.Window) + expectedUtilization := math.NewIntFromUint64(params.MaxBlockUtilization).Sub(math.NewIntFromUint64(params.TargetBlockUtilization)).Mul(multiple) + require.True(t, expectedUtilization.Equal(netUtilization)) + }) + + t.Run("varying blocks with default aimd eip-1559", func(t *testing.T) { + state := types.DefaultAIMDState() + params := types.DefaultAIMDParams() + + for i := 0; i < len(state.Window); i++ { + if i%2 == 0 { + state.Window[i] = params.MaxBlockUtilization + } else { + state.Window[i] = 0 + } + } + + netUtilization := state.GetNetUtilization(params.TargetBlockUtilization) + expectedUtilization := math.ZeroInt() + require.True(t, expectedUtilization.Equal(netUtilization)) + }) + + t.Run("exceeds target rate with default aimd eip-1559", func(t *testing.T) { + state := types.DefaultAIMDState() + params := types.DefaultAIMDParams() + + for i := 0; i < len(state.Window); i++ { + if i%2 == 0 { + state.Window[i] = params.MaxBlockUtilization + } else { + state.Window[i] = params.TargetBlockUtilization + } + } + + netUtilization := state.GetNetUtilization(params.TargetBlockUtilization) + first := math.NewIntFromUint64(params.MaxBlockUtilization).Mul(math.NewIntFromUint64(params.Window / 2)) + second := math.NewIntFromUint64(params.TargetBlockUtilization).Mul(math.NewIntFromUint64(params.Window / 2)) + expectedUtilization := first.Add(second).Sub(math.NewIntFromUint64(params.TargetBlockUtilization).Mul(math.NewIntFromUint64(params.Window))) + require.True(t, expectedUtilization.Equal(netUtilization)) + }) + + t.Run("state with 4 entries in window with different updates", func(t *testing.T) { + state := types.DefaultAIMDState() + state.Window = make([]uint64, 4) + + params := types.DefaultAIMDParams() + params.Window = 4 + params.TargetBlockUtilization = 100 + params.MaxBlockUtilization = 200 + + state.Window[0] = 100 + state.Window[1] = 200 + state.Window[2] = 0 + state.Window[3] = 50 + + netUtilization := state.GetNetUtilization(params.TargetBlockUtilization) + expectedUtilization := math.NewIntFromUint64(50).Mul(math.NewInt(-1)) + require.True(t, expectedUtilization.Equal(netUtilization)) + }) + + t.Run("state with 4 entries in window with monotonically increasing updates", func(t *testing.T) { + state := types.DefaultAIMDState() + state.Window = make([]uint64, 4) + + params := types.DefaultAIMDParams() + params.Window = 4 + params.TargetBlockUtilization = 100 + params.MaxBlockUtilization = 200 + + state.Window[0] = 0 + state.Window[1] = 25 + state.Window[2] = 50 + state.Window[3] = 75 + + netUtilization := state.GetNetUtilization(params.TargetBlockUtilization) + expectedUtilization := math.NewIntFromUint64(250).Mul(math.NewInt(-1)) + require.True(t, expectedUtilization.Equal(netUtilization)) + }) +} + +func TestState_GetAverageUtilization(t *testing.T) { + t.Run("empty block with default eip-1559", func(t *testing.T) { + state := types.DefaultState() + params := types.DefaultParams() + + avgUtilization := state.GetAverageUtilization(params.MaxBlockUtilization) + expectedUtilization := math.LegacyZeroDec() + require.True(t, expectedUtilization.Equal(avgUtilization)) + }) + + t.Run("target block with default eip-1559", func(t *testing.T) { + state := types.DefaultState() + params := types.DefaultParams() + + state.Window[0] = params.TargetBlockUtilization + + avgUtilization := state.GetAverageUtilization(params.MaxBlockUtilization) + expectedUtilization := math.LegacyMustNewDecFromStr("0.5") + require.True(t, expectedUtilization.Equal(avgUtilization)) + }) + + t.Run("full block with default eip-1559", func(t *testing.T) { + state := types.DefaultState() + params := types.DefaultParams() + + state.Window[0] = params.MaxBlockUtilization + + avgUtilization := state.GetAverageUtilization(params.MaxBlockUtilization) + expectedUtilization := math.LegacyMustNewDecFromStr("1.0") + require.True(t, expectedUtilization.Equal(avgUtilization)) + }) + + t.Run("empty block with default aimd eip-1559", func(t *testing.T) { + state := types.DefaultAIMDState() + params := types.DefaultAIMDParams() + + avgUtilization := state.GetAverageUtilization(params.MaxBlockUtilization) + expectedUtilization := math.LegacyZeroDec() + require.True(t, expectedUtilization.Equal(avgUtilization)) + }) + + t.Run("target block with default aimd eip-1559", func(t *testing.T) { + state := types.DefaultAIMDState() + params := types.DefaultAIMDParams() + + for i := 0; i < len(state.Window); i++ { + state.Window[i] = params.TargetBlockUtilization + } + + avgUtilization := state.GetAverageUtilization(params.MaxBlockUtilization) + expectedUtilization := math.LegacyMustNewDecFromStr("0.5") + require.True(t, expectedUtilization.Equal(avgUtilization)) + }) + + t.Run("full blocks with default aimd eip-1559", func(t *testing.T) { + state := types.DefaultAIMDState() + params := types.DefaultAIMDParams() + + for i := 0; i < len(state.Window); i++ { + state.Window[i] = params.MaxBlockUtilization + } + + avgUtilization := state.GetAverageUtilization(params.MaxBlockUtilization) + expectedUtilization := math.LegacyMustNewDecFromStr("1.0") + require.True(t, expectedUtilization.Equal(avgUtilization)) + }) + + t.Run("varying blocks with default aimd eip-1559", func(t *testing.T) { + state := types.DefaultAIMDState() + params := types.DefaultAIMDParams() + + for i := 0; i < len(state.Window); i++ { + if i%2 == 0 { + state.Window[i] = params.MaxBlockUtilization + } else { + state.Window[i] = 0 + } + } + + avgUtilization := state.GetAverageUtilization(params.MaxBlockUtilization) + expectedUtilization := math.LegacyMustNewDecFromStr("0.5") + require.True(t, expectedUtilization.Equal(avgUtilization)) + }) + + t.Run("exceeds target rate with default aimd eip-1559", func(t *testing.T) { + state := types.DefaultAIMDState() + params := types.DefaultAIMDParams() + + for i := 0; i < len(state.Window); i++ { + if i%2 == 0 { + state.Window[i] = params.MaxBlockUtilization + } else { + state.Window[i] = params.TargetBlockUtilization + } + } + + avgUtilization := state.GetAverageUtilization(params.MaxBlockUtilization) + expectedUtilization := math.LegacyMustNewDecFromStr("0.75") + require.True(t, expectedUtilization.Equal(avgUtilization)) + }) + + t.Run("state with 4 entries in window with different updates", func(t *testing.T) { + state := types.DefaultAIMDState() + state.Window = make([]uint64, 4) + + params := types.DefaultAIMDParams() + params.Window = 4 + params.TargetBlockUtilization = 100 + params.MaxBlockUtilization = 200 + + state.Window[0] = 100 + state.Window[1] = 200 + state.Window[2] = 0 + state.Window[3] = 50 + + avgUtilization := state.GetAverageUtilization(params.MaxBlockUtilization) + expectedUtilization := math.LegacyMustNewDecFromStr("0.4375") + require.True(t, expectedUtilization.Equal(avgUtilization)) + }) + + t.Run("state with 4 entries in window with monotonically increasing updates", func(t *testing.T) { + state := types.DefaultAIMDState() + state.Window = make([]uint64, 4) + + params := types.DefaultAIMDParams() + params.Window = 4 + params.TargetBlockUtilization = 100 + params.MaxBlockUtilization = 200 + + state.Window[0] = 0 + state.Window[1] = 25 + state.Window[2] = 50 + state.Window[3] = 75 + + avgUtilization := state.GetAverageUtilization(params.MaxBlockUtilization) + expectedUtilization := math.LegacyMustNewDecFromStr("0.1875") + require.True(t, expectedUtilization.Equal(avgUtilization)) + }) +} + func TestState_ValidateBasic(t *testing.T) { testCases := []struct { name string @@ -24,6 +534,55 @@ func TestState_ValidateBasic(t *testing.T) { state: types.DefaultAIMDState(), expectErr: false, }, + { + name: "invalid window", + state: types.State{}, + expectErr: true, + }, + { + name: "invalid negative base fee", + state: types.State{ + Window: make([]uint64, 1), + BaseFee: math.NewInt(-1), + }, + expectErr: true, + }, + { + name: "invalid learning rate", + state: types.State{ + Window: make([]uint64, 1), + BaseFee: math.NewInt(1), + LearningRate: math.LegacyMustNewDecFromStr("-1.0"), + }, + expectErr: true, + }, + { + name: "valid other state", + state: types.State{ + Window: make([]uint64, 1), + BaseFee: math.NewInt(1), + LearningRate: math.LegacyMustNewDecFromStr("0.5"), + }, + expectErr: false, + }, + { + name: "invalid zero base fee", + state: types.State{ + Window: make([]uint64, 1), + BaseFee: math.ZeroInt(), + LearningRate: math.LegacyMustNewDecFromStr("0.5"), + }, + expectErr: true, + }, + { + name: "invalid zero learning rate", + state: types.State{ + Window: make([]uint64, 1), + BaseFee: math.NewInt(1), + LearningRate: math.LegacyZeroDec(), + }, + expectErr: true, + }, } for _, tc := range testCases {