feat: mempool: Reduce minimum replace fee from 1.25x to 1.1x (#10416)

However, we're leaving the default at 1.25x for backwards compatibility, for now.

Also:

1. Actually use the configured replace fee ratio.
2. Store said ratios as percentages instead of floats. 1.25, or 1+1/(2^2),
can be represented as a float. 1.1, or 1 + 1/(2 * 5), cannot.

fixes #10415
This commit is contained in:
Steven Allen 2023-03-09 13:17:17 -08:00 committed by GitHub
parent b75ebcac81
commit 58900a7033
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 112 additions and 19 deletions

View File

@ -401,6 +401,10 @@ func init() {
FromBlock: pstring("2301220"), FromBlock: pstring("2301220"),
Address: []ethtypes.EthAddress{ethaddr}, Address: []ethtypes.EthAddress{ethaddr},
}) })
percent := types.Percent(123)
addExample(percent)
addExample(&percent)
} }
func GetAPIType(name, pkg string) (i interface{}, t reflect.Type, permStruct []reflect.Type) { func GetAPIType(name, pkg string) (i interface{}, t reflect.Type, permStruct []reflect.Type) {

Binary file not shown.

View File

@ -13,7 +13,11 @@ import (
) )
var ( var (
ReplaceByFeeRatioDefault = 1.25 ReplaceByFeePercentageMinimum types.Percent = 110
ReplaceByFeePercentageDefault types.Percent = 125
)
var (
MemPoolSizeLimitHiDefault = 30000 MemPoolSizeLimitHiDefault = 30000
MemPoolSizeLimitLoDefault = 20000 MemPoolSizeLimitLoDefault = 20000
PruneCooldownDefault = time.Minute PruneCooldownDefault = time.Minute
@ -60,9 +64,9 @@ func (mp *MessagePool) getConfig() *types.MpoolConfig {
} }
func validateConfg(cfg *types.MpoolConfig) error { func validateConfg(cfg *types.MpoolConfig) error {
if cfg.ReplaceByFeeRatio < ReplaceByFeeRatioDefault { if cfg.ReplaceByFeeRatio < ReplaceByFeePercentageMinimum {
return fmt.Errorf("'ReplaceByFeeRatio' is less than required %f < %f", return fmt.Errorf("'ReplaceByFeeRatio' is less than required %s < %s",
cfg.ReplaceByFeeRatio, ReplaceByFeeRatioDefault) cfg.ReplaceByFeeRatio, ReplaceByFeePercentageMinimum)
} }
if cfg.GasLimitOverestimation < 1 { if cfg.GasLimitOverestimation < 1 {
return fmt.Errorf("'GasLimitOverestimation' cannot be less than 1") return fmt.Errorf("'GasLimitOverestimation' cannot be less than 1")
@ -91,7 +95,7 @@ func DefaultConfig() *types.MpoolConfig {
return &types.MpoolConfig{ return &types.MpoolConfig{
SizeLimitHigh: MemPoolSizeLimitHiDefault, SizeLimitHigh: MemPoolSizeLimitHiDefault,
SizeLimitLow: MemPoolSizeLimitLoDefault, SizeLimitLow: MemPoolSizeLimitLoDefault,
ReplaceByFeeRatio: ReplaceByFeeRatioDefault, ReplaceByFeeRatio: ReplaceByFeePercentageDefault,
PruneCooldown: PruneCooldownDefault, PruneCooldown: PruneCooldownDefault,
GasLimitOverestimation: GasLimitOverestimation, GasLimitOverestimation: GasLimitOverestimation,
} }

View File

@ -47,10 +47,8 @@ var log = logging.Logger("messagepool")
var futureDebug = false var futureDebug = false
var rbfNumBig = types.NewInt(uint64((ReplaceByFeeRatioDefault - 1) * RbfDenom)) var rbfNumBig = types.NewInt(uint64(ReplaceByFeePercentageMinimum))
var rbfDenomBig = types.NewInt(RbfDenom) var rbfDenomBig = types.NewInt(100)
const RbfDenom = 256
var RepublishInterval = time.Duration(10*build.BlockDelaySecs+build.PropagationDelaySecs) * time.Second var RepublishInterval = time.Duration(10*build.BlockDelaySecs+build.PropagationDelaySecs) * time.Second
@ -197,7 +195,13 @@ func newMsgSet(nonce uint64) *msgSet {
} }
func ComputeMinRBF(curPrem abi.TokenAmount) abi.TokenAmount { func ComputeMinRBF(curPrem abi.TokenAmount) abi.TokenAmount {
minPrice := types.BigAdd(curPrem, types.BigDiv(types.BigMul(curPrem, rbfNumBig), rbfDenomBig)) minPrice := types.BigDiv(types.BigMul(curPrem, rbfNumBig), rbfDenomBig)
return types.BigAdd(minPrice, types.NewInt(1))
}
func ComputeRBF(curPrem abi.TokenAmount, replaceByFeeRatio types.Percent) abi.TokenAmount {
rbfNumBig := types.NewInt(uint64(replaceByFeeRatio))
minPrice := types.BigDiv(types.BigMul(curPrem, rbfNumBig), rbfDenomBig)
return types.BigAdd(minPrice, types.NewInt(1)) return types.BigAdd(minPrice, types.NewInt(1))
} }

View File

@ -10,7 +10,7 @@ type MpoolConfig struct {
PriorityAddrs []address.Address PriorityAddrs []address.Address
SizeLimitHigh int SizeLimitHigh int
SizeLimitLow int SizeLimitLow int
ReplaceByFeeRatio float64 ReplaceByFeeRatio Percent
PruneCooldown time.Duration PruneCooldown time.Duration
GasLimitOverestimation float64 GasLimitOverestimation float64
} }

39
chain/types/percent.go Normal file
View File

@ -0,0 +1,39 @@
package types
import (
"fmt"
"math"
"strconv"
"golang.org/x/xerrors"
)
// Percent stores a signed percentage as an int64. When converted to a string (or json), it's stored
// as a decimal with two places (e.g., 100% -> 1.00).
type Percent int64
func (p Percent) String() string {
abs := p
sign := ""
if abs < 0 {
abs = -abs
sign = "-"
}
return fmt.Sprintf(`%s%d.%d`, sign, abs/100, abs%100)
}
func (p Percent) MarshalJSON() ([]byte, error) {
return []byte(p.String()), nil
}
func (p *Percent) UnmarshalJSON(b []byte) error {
flt, err := strconv.ParseFloat(string(b)+"e2", 64)
if err != nil {
return xerrors.Errorf("unable to parse ratio %s: %w", string(b), err)
}
if math.Trunc(flt) != flt {
return xerrors.Errorf("ratio may only have two decimals: %s", string(b))
}
*p = Percent(flt)
return nil
}

View File

@ -0,0 +1,34 @@
package types
import (
"fmt"
"testing"
"github.com/stretchr/testify/require"
)
func TestPercent(t *testing.T) {
for _, tc := range []struct {
p Percent
s string
}{
{100, "1.0"},
{111, "1.11"},
{12, "0.12"},
{-12, "-0.12"},
{1012, "10.12"},
{-1012, "-10.12"},
{0, "0.0"},
} {
tc := tc
t.Run(fmt.Sprintf("%d <> %s", tc.p, tc.s), func(t *testing.T) {
m, err := tc.p.MarshalJSON()
require.NoError(t, err)
require.Equal(t, tc.s, string(m))
var p Percent
require.NoError(t, p.UnmarshalJSON([]byte(tc.s)))
require.Equal(t, tc.p, p)
})
}
}

View File

@ -461,7 +461,12 @@ var MpoolReplaceCmd = &cli.Command{
msg := found.Message msg := found.Message
if cctx.Bool("auto") { if cctx.Bool("auto") {
minRBF := messagepool.ComputeMinRBF(msg.GasPremium) cfg, err := api.MpoolGetConfig(ctx)
if err != nil {
return xerrors.Errorf("failed to lookup the message pool config: %w", err)
}
defaultRBF := messagepool.ComputeRBF(msg.GasPremium, cfg.ReplaceByFeeRatio)
var mss *lapi.MessageSendSpec var mss *lapi.MessageSendSpec
if cctx.IsSet("fee-limit") { if cctx.IsSet("fee-limit") {
@ -482,7 +487,7 @@ var MpoolReplaceCmd = &cli.Command{
return xerrors.Errorf("failed to estimate gas values: %w", err) return xerrors.Errorf("failed to estimate gas values: %w", err)
} }
msg.GasPremium = big.Max(retm.GasPremium, minRBF) msg.GasPremium = big.Max(retm.GasPremium, defaultRBF)
msg.GasFeeCap = big.Max(retm.GasFeeCap, msg.GasPremium) msg.GasFeeCap = big.Max(retm.GasFeeCap, msg.GasPremium)
mff := func() (abi.TokenAmount, error) { mff := func() (abi.TokenAmount, error) {

View File

@ -15,6 +15,7 @@ import (
"github.com/filecoin-project/go-state-types/big" "github.com/filecoin-project/go-state-types/big"
"github.com/filecoin-project/lotus/api" "github.com/filecoin-project/lotus/api"
"github.com/filecoin-project/lotus/chain/messagepool"
"github.com/filecoin-project/lotus/chain/types" "github.com/filecoin-project/lotus/chain/types"
"github.com/filecoin-project/lotus/chain/types/mock" "github.com/filecoin-project/lotus/chain/types/mock"
"github.com/filecoin-project/lotus/chain/wallet" "github.com/filecoin-project/lotus/chain/wallet"
@ -298,6 +299,7 @@ func TestReplace(t *testing.T) {
mockApi.EXPECT().ChainGetMessage(ctx, sm.Cid()).Return(&sm.Message, nil), mockApi.EXPECT().ChainGetMessage(ctx, sm.Cid()).Return(&sm.Message, nil),
mockApi.EXPECT().ChainHead(ctx).Return(nil, nil), mockApi.EXPECT().ChainHead(ctx).Return(nil, nil),
mockApi.EXPECT().MpoolPending(ctx, types.EmptyTSK).Return([]*types.SignedMessage{sm}, nil), mockApi.EXPECT().MpoolPending(ctx, types.EmptyTSK).Return([]*types.SignedMessage{sm}, nil),
mockApi.EXPECT().MpoolGetConfig(ctx).Return(messagepool.DefaultConfig(), nil),
// use gomock.any to match the message in expected api calls // use gomock.any to match the message in expected api calls
// since the replace function modifies the message between calls, it would be pointless to try to match the exact argument // since the replace function modifies the message between calls, it would be pointless to try to match the exact argument
mockApi.EXPECT().GasEstimateMessageGas(ctx, gomock.Any(), &mss, types.EmptyTSK).Return(&sm.Message, nil), mockApi.EXPECT().GasEstimateMessageGas(ctx, gomock.Any(), &mss, types.EmptyTSK).Return(&sm.Message, nil),
@ -342,6 +344,7 @@ func TestReplace(t *testing.T) {
gomock.InOrder( gomock.InOrder(
mockApi.EXPECT().ChainHead(ctx).Return(nil, nil), mockApi.EXPECT().ChainHead(ctx).Return(nil, nil),
mockApi.EXPECT().MpoolPending(ctx, types.EmptyTSK).Return([]*types.SignedMessage{sm}, nil), mockApi.EXPECT().MpoolPending(ctx, types.EmptyTSK).Return([]*types.SignedMessage{sm}, nil),
mockApi.EXPECT().MpoolGetConfig(ctx).Return(messagepool.DefaultConfig(), nil),
// use gomock.any to match the message in expected api calls // use gomock.any to match the message in expected api calls
// since the replace function modifies the message between calls, it would be pointless to try to match the exact argument // since the replace function modifies the message between calls, it would be pointless to try to match the exact argument
mockApi.EXPECT().GasEstimateMessageGas(ctx, gomock.Any(), &mss, types.EmptyTSK).Return(&sm.Message, nil), mockApi.EXPECT().GasEstimateMessageGas(ctx, gomock.Any(), &mss, types.EmptyTSK).Return(&sm.Message, nil),
@ -538,7 +541,7 @@ func TestConfig(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
mpoolCfg := &types.MpoolConfig{PriorityAddrs: []address.Address{senderAddr}, SizeLimitHigh: 1234567, SizeLimitLow: 6, ReplaceByFeeRatio: 0.25} mpoolCfg := &types.MpoolConfig{PriorityAddrs: []address.Address{senderAddr}, SizeLimitHigh: 1234567, SizeLimitLow: 6, ReplaceByFeeRatio: types.Percent(25)}
gomock.InOrder( gomock.InOrder(
mockApi.EXPECT().MpoolGetConfig(ctx).Return(mpoolCfg, nil), mockApi.EXPECT().MpoolGetConfig(ctx).Return(mpoolCfg, nil),
) )
@ -566,7 +569,7 @@ func TestConfig(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
mpoolCfg := &types.MpoolConfig{PriorityAddrs: []address.Address{senderAddr}, SizeLimitHigh: 234567, SizeLimitLow: 3, ReplaceByFeeRatio: 0.33} mpoolCfg := &types.MpoolConfig{PriorityAddrs: []address.Address{senderAddr}, SizeLimitHigh: 234567, SizeLimitLow: 3, ReplaceByFeeRatio: types.Percent(33)}
gomock.InOrder( gomock.InOrder(
mockApi.EXPECT().MpoolSetConfig(ctx, mpoolCfg).Return(nil), mockApi.EXPECT().MpoolSetConfig(ctx, mpoolCfg).Return(nil),
) )

View File

@ -2879,7 +2879,7 @@ Response:
], ],
"SizeLimitHigh": 123, "SizeLimitHigh": 123,
"SizeLimitLow": 123, "SizeLimitLow": 123,
"ReplaceByFeeRatio": 12.3, "ReplaceByFeeRatio": 1.23,
"PruneCooldown": 60000000000, "PruneCooldown": 60000000000,
"GasLimitOverestimation": 12.3 "GasLimitOverestimation": 12.3
} }
@ -3167,7 +3167,7 @@ Inputs:
], ],
"SizeLimitHigh": 123, "SizeLimitHigh": 123,
"SizeLimitLow": 123, "SizeLimitLow": 123,
"ReplaceByFeeRatio": 12.3, "ReplaceByFeeRatio": 1.23,
"PruneCooldown": 60000000000, "PruneCooldown": 60000000000,
"GasLimitOverestimation": 12.3 "GasLimitOverestimation": 12.3
} }

View File

@ -3882,7 +3882,7 @@ Response:
], ],
"SizeLimitHigh": 123, "SizeLimitHigh": 123,
"SizeLimitLow": 123, "SizeLimitLow": 123,
"ReplaceByFeeRatio": 12.3, "ReplaceByFeeRatio": 1.23,
"PruneCooldown": 60000000000, "PruneCooldown": 60000000000,
"GasLimitOverestimation": 12.3 "GasLimitOverestimation": 12.3
} }
@ -4170,7 +4170,7 @@ Inputs:
], ],
"SizeLimitHigh": 123, "SizeLimitHigh": 123,
"SizeLimitLow": 123, "SizeLimitLow": 123,
"ReplaceByFeeRatio": 12.3, "ReplaceByFeeRatio": 1.23,
"PruneCooldown": 60000000000, "PruneCooldown": 60000000000,
"GasLimitOverestimation": 12.3 "GasLimitOverestimation": 12.3
} }