diff --git a/app/submodule/eth/eth_test.go b/app/submodule/eth/eth_test.go index 66a32f3ab7..1e06cda5de 100644 --- a/app/submodule/eth/eth_test.go +++ b/app/submodule/eth/eth_test.go @@ -194,12 +194,12 @@ func TestReward(t *testing.T) { {maxFeePerGas: big.NewInt(600), maxPriorityFeePerGas: big.NewInt(500), answer: big.NewInt(500)}, {maxFeePerGas: big.NewInt(600), maxPriorityFeePerGas: big.NewInt(600), answer: big.NewInt(500)}, {maxFeePerGas: big.NewInt(600), maxPriorityFeePerGas: big.NewInt(1000), answer: big.NewInt(500)}, - {maxFeePerGas: big.NewInt(50), maxPriorityFeePerGas: big.NewInt(200), answer: big.NewInt(-50)}, + {maxFeePerGas: big.NewInt(50), maxPriorityFeePerGas: big.NewInt(200), answer: big.Zero()}, } for _, tc := range testcases { msg := &types.Message{GasFeeCap: tc.maxFeePerGas, GasPremium: tc.maxPriorityFeePerGas} reward := msg.EffectiveGasPremium(baseFee) - require.Equal(t, 0, reward.Int.Cmp(tc.answer.Int), reward, tc.answer) + require.True(t, big.Cmp(reward, tc.answer) == 0, "reward: %v, answer: %v", reward, tc.answer) } } diff --git a/pkg/chain/export_test.go b/pkg/chain/export_test.go new file mode 100644 index 0000000000..9c80a8dfb8 --- /dev/null +++ b/pkg/chain/export_test.go @@ -0,0 +1,5 @@ +package chain + +// Export internal functions for testing +var NextBaseFeeFromPremium = nextBaseFeeFromPremium +var WeightedQuickSelectInternal = weightedQuickSelect diff --git a/pkg/chain/message_store.go b/pkg/chain/message_store.go index 48f8df127b..6c434a76b6 100644 --- a/pkg/chain/message_store.go +++ b/pkg/chain/message_store.go @@ -4,12 +4,15 @@ import ( "bytes" "context" "fmt" + "math/rand" + "time" "github.com/filecoin-project/go-address" "github.com/filecoin-project/go-state-types/abi" "github.com/filecoin-project/go-state-types/big" cbor2 "github.com/filecoin-project/go-state-types/cbor" "github.com/filecoin-project/specs-actors/actors/util/adt" + xerrors "golang.org/x/xerrors" blockstore "github.com/ipfs/boxo/blockstore" blocks "github.com/ipfs/go-block-format" @@ -565,15 +568,22 @@ func ComputeNextBaseFee(baseFee abi.TokenAmount, gasLimitUsed int64, noOfBlocks // todo move to a more suitable position func (ms *MessageStore) ComputeBaseFee(ctx context.Context, ts *types.TipSet, upgrade *config.ForkUpgradeConfig) (abi.TokenAmount, error) { - zero := abi.NewTokenAmount(0) baseHeight := ts.Height() - if upgrade.UpgradeBreezeHeight >= 0 && baseHeight > upgrade.UpgradeBreezeHeight && baseHeight < upgrade.UpgradeBreezeHeight+upgrade.BreezeGasTampingDuration { return abi.NewTokenAmount(100), nil } + if baseHeight < upgrade.UpgradeFireHorseHeight { + return ms.ComputeNextBaseFeeFromUtilization(ctx, ts, upgrade) + } + return ms.ComputeNextBaseFeeFromPremiums(ctx, ts) +} +func (ms *MessageStore) ComputeNextBaseFeeFromUtilization(ctx context.Context, ts *types.TipSet, upgrade *config.ForkUpgradeConfig) (abi.TokenAmount, error) { // totalLimit is sum of GasLimits of unique messages in a tipset totalLimit := int64(0) + zero := abi.NewTokenAmount(0) + + baseHeight := ts.Height() seen := make(map[cid.Cid]struct{}) @@ -604,6 +614,123 @@ func (ms *MessageStore) ComputeBaseFee(ctx context.Context, ts *types.TipSet, up return ComputeNextBaseFee(parentBaseFee, totalLimit, len(ts.Blocks()), baseHeight, upgrade), nil } +type SenderNonce struct { + Sender address.Address + Nonce uint64 +} + +func (ms *MessageStore) ComputeNextBaseFeeFromPremiums(ctx context.Context, ts *types.TipSet) (abi.TokenAmount, error) { + zero := abi.NewTokenAmount(0) + parentBaseFee := ts.Blocks()[0].ParentBaseFee + var premiums []abi.TokenAmount + var limits []int64 + seen := make(map[SenderNonce]struct{}) + + for _, b := range ts.Blocks() { + secpMsgs, blsMsgs, err := ms.LoadMetaMessages(ctx, b.Messages) + if err != nil { + return zero, xerrors.Errorf("error getting messages for: %s: %w", b.Cid(), err) + } + for _, msg := range blsMsgs { + senderNonce := SenderNonce{msg.From, msg.Nonce} + if _, ok := seen[senderNonce]; !ok { + limits = append(limits, msg.GasLimit) + premiums = append(premiums, msg.EffectiveGasPremium(parentBaseFee)) + seen[senderNonce] = struct{}{} + } + } + for _, signed := range secpMsgs { + senderNonce := SenderNonce{signed.Message.From, signed.Message.Nonce} + if _, ok := seen[senderNonce]; !ok { + limits = append(limits, signed.Message.GasLimit) + premiums = append(premiums, signed.Message.EffectiveGasPremium(parentBaseFee)) + seen[senderNonce] = struct{}{} + } + } + } + + percentilePremium := WeightedQuickSelect(premiums, limits, constants.BlockGasTargetIndex) + + return nextBaseFeeFromPremium(parentBaseFee, percentilePremium), nil +} + +func nextBaseFeeFromPremium(baseFee, premiumP abi.TokenAmount) abi.TokenAmount { + denom := big.NewInt(constants.BaseFeeMaxChangeDenom) + maxAdj := big.Div(big.Add(baseFee, big.Sub(denom, big.NewInt(1))), denom) + return big.Max( + big.NewInt(constants.MinimumBaseFee), + big.Add( + baseFee, + big.Min(maxAdj, big.Sub(premiumP, maxAdj)), + ), + ) +} + +type RandInt interface { + Intn(n int) int +} + +func WeightedQuickSelect(premiums []abi.TokenAmount, limits []int64, index int64) abi.TokenAmount { + rng := rand.New(rand.NewSource(time.Now().UnixNano())) + return weightedQuickSelect(premiums, limits, index, rng) +} + +func weightedQuickSelect(premiums []abi.TokenAmount, limits []int64, index int64, randImpl RandInt) abi.TokenAmount { + for { + if len(premiums) == 0 { + return big.Zero() + } + if len(premiums) == 1 { + if limits[0] <= index { + return big.Zero() + } + return premiums[0] + } + + pivot := premiums[randImpl.Intn(len(premiums))] + + var less []abi.TokenAmount + var lessWeights []int64 + var lessW int64 + var eqW int64 + var more []abi.TokenAmount + var moreWeights []int64 + var moreW int64 + + for i, premium := range premiums { + cmp := big.Cmp(premium, pivot) + if cmp < 0 { + less = append(less, premium) + lessWeights = append(lessWeights, limits[i]) + lessW += limits[i] + } else if cmp == 0 { + eqW += limits[i] + } else { + more = append(more, premium) + moreWeights = append(moreWeights, limits[i]) + moreW += limits[i] + } + } + + if index < moreW { + premiums = more + limits = moreWeights + continue + } + index -= moreW + if index < eqW { + return pivot + } + index -= eqW + if index < lessW { + premiums = less + limits = lessWeights + continue + } + return big.Zero() + } +} + func GetReceiptRoot(receipts []types.MessageReceipt) (cid.Cid, error) { bs := blockstore.NewBlockstore(datastore.NewMapDatastore()) as := cbor.NewCborStore(bs) diff --git a/pkg/chain/message_store_test.go b/pkg/chain/message_store_test.go index eb39ed9e8c..5207af0126 100644 --- a/pkg/chain/message_store_test.go +++ b/pkg/chain/message_store_test.go @@ -6,6 +6,8 @@ import ( "io" "testing" + "github.com/filecoin-project/go-state-types/abi" + "github.com/filecoin-project/go-state-types/big" "github.com/filecoin-project/venus/venus-shared/testutil" "github.com/ipfs/go-cid" cbg "github.com/whyrusleeping/cbor-gen" @@ -13,6 +15,7 @@ import ( "github.com/filecoin-project/specs-actors/actors/util/adt" "github.com/filecoin-project/venus/pkg/chain" "github.com/filecoin-project/venus/pkg/config" + "github.com/filecoin-project/venus/pkg/constants" "github.com/filecoin-project/venus/pkg/testhelpers" "github.com/filecoin-project/venus/pkg/testhelpers/testflags" blockstoreutil "github.com/filecoin-project/venus/venus-shared/blockstore" @@ -92,7 +95,7 @@ func TestMessageStoreMessagesHappy(t *testing.T) { as := cbor.NewCborStore(bs) - var goodCid = signedMsgs[0].Cid() + goodCid := signedMsgs[0].Cid() secpMsgArr := adt.MakeEmptyArray(adt.WrapStore(ctx, as)) assert.NoError(t, secpMsgArr.Set(0, (*cbg.CborCid)(&goodCid))) @@ -229,3 +232,122 @@ func TestMessageStoreLoadMessage(t *testing.T) { _, err = ms.LoadUnsignedMessagesFromCids(ctx, []cid.Cid{badMsgCID}) assert.Error(t, err) } + +// Test randomized algorithm by trying all permutations +type AllPermutations struct { + t *testing.T + // current level + level int + // known domain (n) at current level + domain []int + // current iteration positions (i < n) in the domain + dfs []int +} + +func (p *AllPermutations) Reset() { + // same outputs should result in same inputs + assert.Equal(p.t, p.level, len(p.domain), "nondeterminism detected (fewer queries)") + + for len(p.dfs) > 0 && p.dfs[len(p.dfs)-1]+1 == p.domain[len(p.domain)-1] { + // pop finished domains + p.domain = p.domain[:len(p.domain)-1] + p.dfs = p.dfs[:len(p.dfs)-1] + } + if len(p.dfs) > 0 { + p.dfs[len(p.dfs)-1]++ + } + // next iteration in the permutation + p.level = 0 +} + +func (p *AllPermutations) Done() bool { + return len(p.domain) == 0 +} + +func (p *AllPermutations) Intn(n int) int { + assert.True(p.t, n > 0, "Intn(0)") + if p.level < len(p.domain) { + // same outputs should result in same inputs + assert.Equal(p.t, p.domain[p.level], n, "nondeterminism detected (different queries)") + } else { + // expand search domain + p.domain = append(p.domain, n) + p.dfs = append(p.dfs, 0) + } + p.level++ + return p.dfs[p.level-1] +} + +// TestWeightedQuickSelect tests the tipset gas percentile cases. +// BlockGasLimit = 10_000_000_000, P = 20. +func TestWeightedQuickSelect(t *testing.T) { + tests := []struct { + premiums []int64 + limits []int64 + expected int64 + }{ + {[]int64{}, []int64{}, 0}, + {[]int64{123, 100}, []int64{5_999_999_999, 2_000_000_000}, 0}, + {[]int64{123, 0}, []int64{5_999_999_999, 2_000_000_001}, 0}, + {[]int64{123, 100}, []int64{5_999_999_999, 2_000_000_001}, 100}, + {[]int64{123, 100}, []int64{7_999_999_999, 2_000_000_001}, 100}, + {[]int64{123, 100}, []int64{8_000_000_000, 2_000_000_000}, 123}, + {[]int64{123, 100}, []int64{8_000_000_000, 9_000_000_000}, 123}, + {[]int64{100, 200, 300, 400, 500, 600, 700}, []int64{4_000_000_000, 1_000_000_000, 2_000_000_000, 1_000_000_000, 2_000_000_000, 2_000_000_000, 3_000_000_000}, 400}, + } + for _, tc := range tests { + premiums := make([]abi.TokenAmount, len(tc.premiums)) + for i, p := range tc.premiums { + premiums[i] = big.NewInt(p) + } + rand := &AllPermutations{} + rand.t = t + for { + got := chain.WeightedQuickSelectInternal(premiums, tc.limits, constants.BlockGasTargetIndex, rand) + assert.Equal(t, big.NewInt(tc.expected).String(), got.String(), + "premiums=%v limits=%v", tc.premiums, tc.limits) + rand.Reset() + if rand.Done() { + break + } + } + } +} + +// TestNextBaseFeeFromPremium tests the BaseFee_next formula: +// MaxAdj = ceil(BaseFee / 8) +// BaseFee_next = Max(MinBaseFee, BaseFee + Min(MaxAdj, Premium_P - MaxAdj)) +func TestNextBaseFeeFromPremium(t *testing.T) { + tests := []struct { + baseFee int64 + premiumP int64 + expected int64 + }{ + {100, 0, 100}, + {100, 13, 100}, + {100, 14, 101}, + {100, 26, 113}, + {801, 0, 700}, + {801, 20, 720}, + {801, 40, 740}, + {801, 60, 760}, + {801, 80, 780}, + {801, 100, 800}, + {801, 120, 820}, + {801, 140, 840}, + {801, 160, 860}, + {801, 180, 880}, + {801, 200, 900}, + {801, 201, 901}, + {808, 0, 707}, + {808, 1, 708}, + {808, 201, 908}, + {808, 202, 909}, + {808, 203, 909}, + } + for _, tc := range tests { + got := chain.NextBaseFeeFromPremium(big.NewInt(tc.baseFee), big.NewInt(tc.premiumP)) + assert.Equal(t, big.NewInt(tc.expected).String(), got.String(), + "baseFee=%d premiumP=%d", tc.baseFee, tc.premiumP) + } +} diff --git a/pkg/constants/chain_parameters.go b/pkg/constants/chain_parameters.go index 4268fc9e8a..939dfa4493 100644 --- a/pkg/constants/chain_parameters.go +++ b/pkg/constants/chain_parameters.go @@ -23,8 +23,12 @@ var ExpectedLeadersPerEpoch = builtin0.ExpectedLeadersPerEpoch // BlockGasLimit is the maximum amount of gas that can be used to execute messages in a single block. const ( - BlockGasLimit = 10_000_000_000 - BlockGasTarget = BlockGasLimit / 2 + BlockGasLimit = 10_000_000_000 + BlockGasTargetIndex = BlockGasLimit*80/100 - 1 + + // todo: consider remove this after fip-0115 is activated + BlockGasTarget = BlockGasLimit / 2 + BaseFeeMaxChangeDenom = 8 // 12.5% InitialBaseFee = 100e6 MinimumBaseFee = 100 diff --git a/venus-shared/actors/types/message.go b/venus-shared/actors/types/message.go index 4b30b2af68..4a1051c942 100644 --- a/venus-shared/actors/types/message.go +++ b/venus-shared/actors/types/message.go @@ -225,6 +225,11 @@ func (m *Message) ValidForBlockInclusion(minGas int64, version network.Version) // specified premium. func (m *Message) EffectiveGasPremium(baseFee abi.TokenAmount) abi.TokenAmount { available := big.Sub(m.GasFeeCap, baseFee) + // It's possible that storage providers may include messages with gasFeeCap less than the baseFee + // In such cases, their reward should be viewed as zero + if big.Cmp(available, big.Zero()) < 0 { + available = big.Zero() + } if big.Cmp(m.GasPremium, available) <= 0 { return m.GasPremium } diff --git a/venus-shared/actors/types/message_test.go b/venus-shared/actors/types/message_test.go new file mode 100644 index 0000000000..64ad793c59 --- /dev/null +++ b/venus-shared/actors/types/message_test.go @@ -0,0 +1,33 @@ +package types + +import ( + "testing" + + "github.com/filecoin-project/go-state-types/big" + "github.com/stretchr/testify/require" +) + +func TestEffectiveGasPremium(t *testing.T) { + tests := []struct { + baseFee int64 + maxFeePerGas int64 + maxPriorityFeePerGas int64 + expected int64 + }{ + {8, 8, 8, 0}, + {8, 16, 7, 7}, + {8, 19, 10, 10}, + {123456, 123455, 123455, 0}, + {123456, 1234567, 1111112, 1111111}, + } + for _, tc := range tests { + msg := &Message{ + GasFeeCap: big.NewInt(tc.maxFeePerGas), + GasPremium: big.NewInt(tc.maxPriorityFeePerGas), + } + got := msg.EffectiveGasPremium(big.NewInt(tc.baseFee)) + expected := big.NewInt(tc.expected) + require.True(t, big.Cmp(expected, got) == 0, + "baseFee=%d maxFeePerGas=%d maxPriorityFeePerGas=%d", tc.baseFee, tc.maxFeePerGas, tc.maxPriorityFeePerGas) + } +}