diff --git a/.circleci/config.yml b/.circleci/config.yml index be70019a3..75d747c69 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -966,6 +966,12 @@ workflows: - build suite: itest-splitstore target: "./itests/splitstore_test.go" + - test: + name: test-itest-supply + requires: + - build + suite: itest-supply + target: "./itests/supply_test.go" - test: name: test-itest-verifreg requires: diff --git a/chain/stmgr/supply.go b/chain/stmgr/supply.go index 1aea5cc65..8ee369750 100644 --- a/chain/stmgr/supply.go +++ b/chain/stmgr/supply.go @@ -10,6 +10,7 @@ import ( "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/network" msig0 "github.com/filecoin-project/specs-actors/actors/builtin/multisig" "github.com/filecoin-project/lotus/api" @@ -303,7 +304,10 @@ func getFilPowerLocked(ctx context.Context, st *state.StateTree) (abi.TokenAmoun return pst.TotalLocked() } -func GetFilLocked(ctx context.Context, st *state.StateTree) (abi.TokenAmount, error) { +func GetFilLocked(ctx context.Context, st *state.StateTree, nv network.Version) (abi.TokenAmount, error) { + if nv >= network.Version23 { + return getFilPowerLocked(ctx, st) + } filMarketLocked, err := getFilMarketLocked(ctx, st) if err != nil { @@ -337,6 +341,7 @@ func (sm *StateManager) GetVMCirculatingSupply(ctx context.Context, height abi.C } func (sm *StateManager) GetVMCirculatingSupplyDetailed(ctx context.Context, height abi.ChainEpoch, st *state.StateTree) (api.CirculatingSupply, error) { + nv := sm.GetNetworkVersion(ctx, height) filVested, err := sm.GetFilVested(ctx, height) if err != nil { return api.CirculatingSupply{}, xerrors.Errorf("failed to calculate filVested: %w", err) @@ -360,7 +365,7 @@ func (sm *StateManager) GetVMCirculatingSupplyDetailed(ctx context.Context, heig return api.CirculatingSupply{}, xerrors.Errorf("failed to calculate filBurnt: %w", err) } - filLocked, err := GetFilLocked(ctx, st) + filLocked, err := GetFilLocked(ctx, st, nv) if err != nil { return api.CirculatingSupply{}, xerrors.Errorf("failed to calculate filLocked: %w", err) } @@ -387,6 +392,8 @@ func (sm *StateManager) GetVMCirculatingSupplyDetailed(ctx context.Context, heig func (sm *StateManager) GetCirculatingSupply(ctx context.Context, height abi.ChainEpoch, st *state.StateTree) (abi.TokenAmount, error) { circ := big.Zero() unCirc := big.Zero() + nv := sm.GetNetworkVersion(ctx, height) + err := st.ForEach(func(a address.Address, actor *types.Actor) error { // this can be a lengthy operation, we need to cancel early when // the context is cancelled to avoid resource exhaustion @@ -415,18 +422,22 @@ func (sm *StateManager) GetCirculatingSupply(ctx context.Context, height abi.Cha unCirc = big.Add(unCirc, actor.Balance) case a == market.Address: - mst, err := market.Load(sm.cs.ActorStore(ctx), actor) - if err != nil { - return err - } + if nv >= network.Version23 { + circ = big.Add(circ, actor.Balance) + } else { + mst, err := market.Load(sm.cs.ActorStore(ctx), actor) + if err != nil { + return err + } - lb, err := mst.TotalLocked() - if err != nil { - return err - } + lb, err := mst.TotalLocked() + if err != nil { + return err + } - circ = big.Add(circ, big.Sub(actor.Balance, lb)) - unCirc = big.Add(unCirc, lb) + circ = big.Add(circ, big.Sub(actor.Balance, lb)) + unCirc = big.Add(unCirc, lb) + } case builtin.IsAccountActor(actor.Code) || builtin.IsPaymentChannelActor(actor.Code) || diff --git a/itests/supply_test.go b/itests/supply_test.go new file mode 100644 index 000000000..5c603338d --- /dev/null +++ b/itests/supply_test.go @@ -0,0 +1,205 @@ +package itests + +import ( + "bytes" + "context" + "testing" + "time" + + "github.com/ipfs/go-cid" + "github.com/stretchr/testify/assert" + "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/builtin" + "github.com/filecoin-project/go-state-types/builtin/v9/market" + "github.com/filecoin-project/go-state-types/crypto" + "github.com/filecoin-project/go-state-types/network" + + "github.com/filecoin-project/lotus/chain/actors" + "github.com/filecoin-project/lotus/chain/types" + "github.com/filecoin-project/lotus/itests/kit" +) + +func TestCirciulationSupplyUpgrade(t *testing.T) { + kit.QuietMiningLogs() + ctx := context.Background() + + // Choosing something divisible by epochs per day to remove error with simple deal duration + lockedClientBalance := big.Mul(abi.NewTokenAmount(11_520_000), abi.NewTokenAmount(1e18)) + lockedProviderBalance := big.Mul(abi.NewTokenAmount(1_000_000), abi.NewTokenAmount(1e18)) + var height0 abi.ChainEpoch + var height1 abi.ChainEpoch + // Lock collateral in market on nv22 network + fullNode0, blockMiner0, ens0 := kit.EnsembleMinimal(t, + kit.GenesisNetworkVersion(network.Version22), + kit.MockProofs(), + ) + { + + worker0 := blockMiner0.OwnerKey.Address + ens0.InterconnectAll().BeginMining(50 * time.Millisecond) + + // Lock collateral in market actor + wallet0, err := fullNode0.WalletDefaultAddress(ctx) + require.NoError(t, err) + + // Add 1 FIL to cover provider collateral + c00, err := fullNode0.MarketAddBalance(ctx, wallet0, wallet0, lockedClientBalance) + require.NoError(t, err) + fullNode0.WaitMsg(ctx, c00) + c01, err := blockMiner0.FullNode.MarketAddBalance(ctx, worker0, blockMiner0.ActorAddr, lockedProviderBalance) + require.NoError(t, err) + fullNode0.WaitMsg(ctx, c01) + + psd0, err := fullNode0.MpoolPushMessage(ctx, + makePSDMessage( + ctx, + t, + blockMiner0.ActorAddr, + worker0, + wallet0, + lockedProviderBalance, + lockedClientBalance, + fullNode0.WalletSign, + ), + nil, + ) + require.NoError(t, err) + fullNode0.WaitMsg(ctx, psd0.Cid()) + head, err := fullNode0.ChainHead(ctx) + require.NoError(t, err) + height0 = head.Height() + } + + // Lock collateral in market on nv23 network + fullNode1, blockMiner1, ens1 := kit.EnsembleMinimal(t, + kit.GenesisNetworkVersion(network.Version23), + kit.MockProofs(), + ) + { + worker1 := blockMiner1.OwnerKey.Address + ens1.InterconnectAll().BeginMining(50 * time.Millisecond) + + // Lock collateral in market actor + wallet1, err := fullNode1.WalletDefaultAddress(ctx) + require.NoError(t, err) + c10, err := fullNode1.MarketAddBalance(ctx, wallet1, wallet1, lockedClientBalance) + require.NoError(t, err) + fullNode1.WaitMsg(ctx, c10) + c11, err := blockMiner1.FullNode.MarketAddBalance(ctx, worker1, blockMiner1.ActorAddr, lockedProviderBalance) + require.NoError(t, err) + fullNode1.WaitMsg(ctx, c11) + + psd1, err := fullNode1.MpoolPushMessage(ctx, + makePSDMessage( + ctx, + t, + blockMiner1.ActorAddr, + worker1, + wallet1, + lockedProviderBalance, + lockedClientBalance, + fullNode1.WalletSign, + ), + nil, + ) + require.NoError(t, err) + fullNode1.WaitMsg(ctx, psd1.Cid()) + head, err := fullNode1.ChainHead(ctx) + require.NoError(t, err) + height1 = head.Height() + } + + // Measure each circulating supply at the latest height where market balance was locked + // This allows us to normalize against fluctuations in circulating supply based on the underlying + // dynamics irrelevant to this change + + max := height0 + if height0 < height1 { + max = height1 + } + max = max + 1 // Measure supply at height after the deal locking funds was published + + // Let both chains catch up + fullNode0.WaitTillChain(ctx, func(ts *types.TipSet) bool { + return ts.Height() >= max + }) + fullNode1.WaitTillChain(ctx, func(ts *types.TipSet) bool { + return ts.Height() >= max + }) + + ts0, err := fullNode0.ChainGetTipSetByHeight(ctx, max, types.EmptyTSK) + require.NoError(t, err) + ts1, err := fullNode1.ChainGetTipSetByHeight(ctx, max, types.EmptyTSK) + require.NoError(t, err) + + nv22Supply, err := fullNode0.StateVMCirculatingSupplyInternal(ctx, ts0.Key()) + require.NoError(t, err, "Failed to fetch nv22 circulating supply") + nv23Supply, err := fullNode1.StateVMCirculatingSupplyInternal(ctx, ts1.Key()) + require.NoError(t, err, "Failed to fetch nv23 circulating supply") + + // Unfortunately there's still some non-determinism in supply dynamics so check for equality within a tolerance + tolerance := big.Mul(abi.NewTokenAmount(1000), abi.NewTokenAmount(1e18)) + totalLocked := big.Sum(lockedClientBalance, lockedProviderBalance) + diff := big.Sub( + big.Sum(totalLocked, nv23Supply.FilLocked), + nv22Supply.FilLocked, + ) + assert.Equal(t, -1, big.Cmp( + diff.Abs(), + tolerance), "Difference from expected locked supply between versions exceeds tolerance") +} + +// Message will be valid and lock funds but the data is fake so the deal will never be activated +func makePSDMessage( + ctx context.Context, + t *testing.T, + provider, + worker, + client address.Address, + providerLocked, + clientLocked abi.TokenAmount, + signFunc func(context.Context, address.Address, []byte) (*crypto.Signature, error)) *types.Message { + + dummyCid, err := cid.Parse("baga6ea4seaqflae5c3k2odz4sqfufmrmoegplhk5jbq3co4fgmmy56yc2qfh4aq") + require.NoError(t, err) + + duration := 2880 * 200 // 200 days + ppe := big.Div(clientLocked, big.NewInt(2880*200)) + proposal := market.DealProposal{ + PieceCID: dummyCid, + PieceSize: abi.PaddedPieceSize(128), + VerifiedDeal: false, + Client: client, + Provider: provider, + ClientCollateral: big.Zero(), + ProviderCollateral: providerLocked, + StartEpoch: 10000, + EndEpoch: 10000 + abi.ChainEpoch(duration), + StoragePricePerEpoch: ppe, + } + buf := bytes.NewBuffer(nil) + require.NoError(t, proposal.MarshalCBOR(buf)) + sig, err := signFunc(ctx, client, buf.Bytes()) + require.NoError(t, err) + // Publish storage deal + params, err := actors.SerializeParams(&market.PublishStorageDealsParams{ + Deals: []market.ClientDealProposal{ + { + Proposal: proposal, + ClientSignature: *sig, + }, + }, + }) + require.NoError(t, err) + return &types.Message{ + To: builtin.StorageMarketActorAddr, + From: worker, + Value: types.NewInt(0), + Method: builtin.MethodsMarket.PublishStorageDeals, + Params: params, + } +}