package itests import ( "context" "math" "testing" "time" "github.com/stretchr/testify/require" "github.com/filecoin-project/go-address" "github.com/filecoin-project/go-state-types/abi" "github.com/filecoin-project/go-state-types/big" "github.com/filecoin-project/go-state-types/exitcode" "github.com/filecoin-project/lotus/api" "github.com/filecoin-project/lotus/build" "github.com/filecoin-project/lotus/chain/actors/builtin" "github.com/filecoin-project/lotus/chain/actors/builtin/account" "github.com/filecoin-project/lotus/chain/types" "github.com/filecoin-project/lotus/chain/vm" "github.com/filecoin-project/lotus/itests/kit" ) func TestEstimateGasNoFunds(t *testing.T) { ctx := context.Background() kit.QuietMiningLogs() client, _, ens := kit.EnsembleMinimal(t, kit.MockProofs()) ens.InterconnectAll().BeginMining(10 * time.Millisecond) // create a new address addr, err := client.WalletNew(ctx, types.KTBLS) require.NoError(t, err) // Create that address. msg := &types.Message{ From: client.DefaultKey.Address, To: addr, Value: big.Zero(), } sm, err := client.MpoolPushMessage(ctx, msg, nil) require.NoError(t, err) ret, err := client.StateWaitMsg(ctx, sm.Cid(), 3, api.LookbackNoLimit, true) require.NoError(t, err) require.True(t, ret.Receipt.ExitCode.IsSuccess()) // Make sure we can estimate gas even if we have no funds. msg2 := &types.Message{ From: addr, To: client.DefaultKey.Address, Method: account.Methods.PubkeyAddress, Value: big.Zero(), } limit, err := client.GasEstimateGasLimit(ctx, msg2, types.EmptyTSK) require.NoError(t, err) require.NotZero(t, limit) } // Make sure that we correctly calculate the inclusion cost. Especially, make sure the FVM and Lotus // agree and that: // 1. The FVM will never charge _less_ than the inclusion cost. // 2. The FVM will never fine a storage provider for including a message that costs exactly the // inclusion cost. func TestEstimateInclusion(t *testing.T) { ctx := context.Background() kit.QuietMiningLogs() // We need this to be "correct" in this test so that lotus can get the correct gas value // (which, unfortunately, looks at the height and not the current network version). oldPrices := vm.Prices vm.Prices = map[abi.ChainEpoch]vm.Pricelist{ 0: oldPrices[build.UpgradeHyggeHeight], } t.Cleanup(func() { vm.Prices = oldPrices }) client, _, ens := kit.EnsembleMinimal(t, kit.MockProofs()) ens.InterconnectAll().BeginMining(10 * time.Millisecond) // First, try sending a message that should have no fees beyond the inclusion cost. I.e., it // does absolutely nothing: msg := &types.Message{ From: client.DefaultKey.Address, To: client.DefaultKey.Address, Value: big.Zero(), GasLimit: 0, GasFeeCap: abi.NewTokenAmount(10000), GasPremium: big.Zero(), } burntBefore, err := client.WalletBalance(ctx, builtin.BurntFundsActorAddr) require.NoError(t, err) balanceBefore, err := client.WalletBalance(ctx, client.DefaultKey.Address) require.NoError(t, err) // Sign the message and compute the correct inclusion cost. var smsg *types.SignedMessage for i := 0; ; i++ { var err error smsg, err = client.WalletSignMessage(ctx, client.DefaultKey.Address, msg) require.NoError(t, err) estimatedGas := vm.PricelistByEpoch(math.MaxInt).OnChainMessage(smsg.ChainLength()).Total() if estimatedGas == msg.GasLimit { break } // Try 10 times to get the right gas value. require.Less(t, i, 10, "unable to estimate gas: %s != %s", estimatedGas, msg.GasLimit) msg.GasLimit = estimatedGas } cid, err := client.MpoolPush(ctx, smsg) require.NoError(t, err) ret, err := client.StateWaitMsg(ctx, cid, 3, api.LookbackNoLimit, true) require.NoError(t, err) require.True(t, ret.Receipt.ExitCode.IsSuccess()) require.Equal(t, msg.GasLimit, ret.Receipt.GasUsed) // Then try sending a message of the same size that tries to create an actor. This should // get successfully included, but fail with out of gas: // Mutate the last byte to get a new address of the same length. toBytes := msg.To.Bytes() toBytes[len(toBytes)-1] += 1 //nolint:golint newAddr, err := address.NewFromBytes(toBytes) require.NoError(t, err) msg.Nonce = 1 msg.To = newAddr smsg, err = client.WalletSignMessage(ctx, client.DefaultKey.Address, msg) require.NoError(t, err) cid, err = client.MpoolPush(ctx, smsg) require.NoError(t, err) ret, err = client.StateWaitMsg(ctx, cid, 3, api.LookbackNoLimit, true) require.NoError(t, err) require.Equal(t, ret.Receipt.ExitCode, exitcode.SysErrOutOfGas) require.Equal(t, msg.GasLimit, ret.Receipt.GasUsed) // Now make sure that the client is the only contributor to the burnt funds actor (the // miners should not have been fined for either message). burntAfter, err := client.WalletBalance(ctx, builtin.BurntFundsActorAddr) require.NoError(t, err) balanceAfter, err := client.WalletBalance(ctx, client.DefaultKey.Address) require.NoError(t, err) burnt := big.Sub(burntAfter, burntBefore) spent := big.Sub(balanceBefore, balanceAfter) require.Equal(t, burnt, spent) // Finally, try to submit a message with too little gas. This should fail. msg.Nonce = 2 msg.To = msg.From msg.GasLimit -= 1 //nolint:golint smsg, err = client.WalletSignMessage(ctx, client.DefaultKey.Address, msg) require.NoError(t, err) _, err = client.MpoolPush(ctx, smsg) require.ErrorContains(t, err, "will not be included in a block") require.ErrorContains(t, err, "cannot be less than the cost of storing a message") }