Skip to content

Commit

Permalink
Merge pull request #4231 from filecoin-project/steb/call-at-upgrade
Browse files Browse the repository at this point in the history
reject explicit "calls" at the upgrade height
  • Loading branch information
Stebalien authored Oct 7, 2020
2 parents de3f411 + d97eb10 commit 413c357
Show file tree
Hide file tree
Showing 7 changed files with 189 additions and 27 deletions.
44 changes: 42 additions & 2 deletions chain/stmgr/call.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package stmgr

import (
"context"
"errors"
"fmt"

"github.com/filecoin-project/go-address"
Expand All @@ -17,17 +18,38 @@ import (
"github.com/filecoin-project/lotus/chain/vm"
)

var ErrExpensiveFork = errors.New("refusing explicit call due to state fork at epoch")

func (sm *StateManager) Call(ctx context.Context, msg *types.Message, ts *types.TipSet) (*api.InvocResult, error) {
ctx, span := trace.StartSpan(ctx, "statemanager.Call")
defer span.End()

// If no tipset is provided, try to find one without a fork.
if ts == nil {
ts = sm.cs.GetHeaviestTipSet()

// Search back till we find a height with no fork, or we reach the beginning.
for ts.Height() > 0 && sm.hasExpensiveFork(ctx, ts.Height()-1) {
var err error
ts, err = sm.cs.GetTipSetFromKey(ts.Parents())
if err != nil {
return nil, xerrors.Errorf("failed to find a non-forking epoch: %w", err)
}
}
}

bstate := ts.ParentState()
bheight := ts.Height()

// If we have to run an expensive migration, and we're not at genesis,
// return an error because the migration will take too long.
//
// We allow this at height 0 for at-genesis migrations (for testing).
if bheight-1 > 0 && sm.hasExpensiveFork(ctx, bheight-1) {
return nil, ErrExpensiveFork
}

// Run the (not expensive) migration.
bstate, err := sm.handleStateForks(ctx, bstate, bheight-1, nil, ts)
if err != nil {
return nil, fmt.Errorf("failed to handle fork: %w", err)
Expand All @@ -44,7 +66,7 @@ func (sm *StateManager) Call(ctx context.Context, msg *types.Message, ts *types.
BaseFee: types.NewInt(0),
}

vmi, err := vm.NewVM(ctx, vmopt)
vmi, err := sm.newVM(ctx, vmopt)
if err != nil {
return nil, xerrors.Errorf("failed to set up vm: %w", err)
}
Expand Down Expand Up @@ -106,6 +128,24 @@ func (sm *StateManager) CallWithGas(ctx context.Context, msg *types.Message, pri

if ts == nil {
ts = sm.cs.GetHeaviestTipSet()

// Search back till we find a height with no fork, or we reach the beginning.
// We need the _previous_ height to have no fork, because we'll
// run the fork logic in `sm.TipSetState`. We need the _current_
// height to have no fork, because we'll run it inside this
// function before executing the given message.
for ts.Height() > 0 && (sm.hasExpensiveFork(ctx, ts.Height()) || sm.hasExpensiveFork(ctx, ts.Height()-1)) {
var err error
ts, err = sm.cs.GetTipSetFromKey(ts.Parents())
if err != nil {
return nil, xerrors.Errorf("failed to find a non-forking epoch: %w", err)
}
}
}

// When we're not at the genesis block, make sure we don't have an expensive migration.
if ts.Height() > 0 && (sm.hasExpensiveFork(ctx, ts.Height()) || sm.hasExpensiveFork(ctx, ts.Height()-1)) {
return nil, ErrExpensiveFork
}

state, _, err := sm.TipSetState(ctx, ts)
Expand Down Expand Up @@ -138,7 +178,7 @@ func (sm *StateManager) CallWithGas(ctx context.Context, msg *types.Message, pri
NtwkVersion: sm.GetNtwkVersion,
BaseFee: ts.Blocks()[0].ParentBaseFee,
}
vmi, err := vm.NewVM(ctx, vmopt)
vmi, err := sm.newVM(ctx, vmopt)
if err != nil {
return nil, xerrors.Errorf("failed to set up vm: %w", err)
}
Expand Down
30 changes: 20 additions & 10 deletions chain/stmgr/forks.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,19 @@ import (
"github.com/filecoin-project/lotus/chain/vm"
)

type UpgradeFunc func(context.Context, *StateManager, ExecCallback, cid.Cid, *types.TipSet) (cid.Cid, error)
// UpgradeFunc is a migration function run at every upgrade.
//
// - The oldState is the state produced by the upgrade epoch.
// - The returned newState is the new state that will be used by the next epoch.
// - The height is the upgrade epoch height (already executed).
// - The tipset is the tipset for the last non-null block before the upgrade. Do
// not assume that ts.Height() is the upgrade height.
type UpgradeFunc func(ctx context.Context, sm *StateManager, cb ExecCallback, oldState cid.Cid, height abi.ChainEpoch, ts *types.TipSet) (newState cid.Cid, err error)

type Upgrade struct {
Height abi.ChainEpoch
Network network.Version
Expensive bool
Migration UpgradeFunc
}

Expand All @@ -61,6 +69,7 @@ func DefaultUpgradeSchedule() UpgradeSchedule {
}, {
Height: build.UpgradeActorsV2Height,
Network: network.Version4,
Expensive: true,
Migration: UpgradeActorsV2,
}, {
Height: build.UpgradeLiftoffHeight,
Expand Down Expand Up @@ -108,7 +117,7 @@ func (sm *StateManager) handleStateForks(ctx context.Context, root cid.Cid, heig
var err error
f, ok := sm.stateMigrations[height]
if ok {
retCid, err = f(ctx, sm, cb, root, ts)
retCid, err = f(ctx, sm, cb, root, height, ts)
if err != nil {
return cid.Undef, err
}
Expand All @@ -117,6 +126,11 @@ func (sm *StateManager) handleStateForks(ctx context.Context, root cid.Cid, heig
return retCid, nil
}

func (sm *StateManager) hasExpensiveFork(ctx context.Context, height abi.ChainEpoch) bool {
_, ok := sm.expensiveUpgrades[height]
return ok
}

func doTransfer(cb ExecCallback, tree types.StateTree, from, to address.Address, amt abi.TokenAmount) error {
fromAct, err := tree.GetActor(from)
if err != nil {
Expand Down Expand Up @@ -179,7 +193,7 @@ func doTransfer(cb ExecCallback, tree types.StateTree, from, to address.Address,
return nil
}

func UpgradeFaucetBurnRecovery(ctx context.Context, sm *StateManager, cb ExecCallback, root cid.Cid, ts *types.TipSet) (cid.Cid, error) {
func UpgradeFaucetBurnRecovery(ctx context.Context, sm *StateManager, cb ExecCallback, root cid.Cid, epoch abi.ChainEpoch, ts *types.TipSet) (cid.Cid, error) {
// Some initial parameters
FundsForMiners := types.FromFil(1_000_000)
LookbackEpoch := abi.ChainEpoch(32000)
Expand Down Expand Up @@ -431,11 +445,9 @@ func UpgradeFaucetBurnRecovery(ctx context.Context, sm *StateManager, cb ExecCal
return tree.Flush(ctx)
}

func UpgradeIgnition(ctx context.Context, sm *StateManager, cb ExecCallback, root cid.Cid, ts *types.TipSet) (cid.Cid, error) {
func UpgradeIgnition(ctx context.Context, sm *StateManager, cb ExecCallback, root cid.Cid, epoch abi.ChainEpoch, ts *types.TipSet) (cid.Cid, error) {
store := sm.cs.Store(ctx)

epoch := ts.Height() - 1

if build.UpgradeLiftoffHeight <= epoch {
return cid.Undef, xerrors.Errorf("liftoff height must be beyond ignition height")
}
Expand Down Expand Up @@ -488,11 +500,9 @@ func UpgradeIgnition(ctx context.Context, sm *StateManager, cb ExecCallback, roo
return tree.Flush(ctx)
}

func UpgradeActorsV2(ctx context.Context, sm *StateManager, cb ExecCallback, root cid.Cid, ts *types.TipSet) (cid.Cid, error) {
func UpgradeActorsV2(ctx context.Context, sm *StateManager, cb ExecCallback, root cid.Cid, epoch abi.ChainEpoch, ts *types.TipSet) (cid.Cid, error) {
store := sm.cs.Store(ctx)

epoch := ts.Height() - 1

info, err := store.Put(ctx, new(types.StateInfo0))
if err != nil {
return cid.Undef, xerrors.Errorf("failed to create new state info for actors v2: %w", err)
Expand Down Expand Up @@ -539,7 +549,7 @@ func UpgradeActorsV2(ctx context.Context, sm *StateManager, cb ExecCallback, roo
return newRoot, nil
}

func UpgradeLiftoff(ctx context.Context, sm *StateManager, cb ExecCallback, root cid.Cid, ts *types.TipSet) (cid.Cid, error) {
func UpgradeLiftoff(ctx context.Context, sm *StateManager, cb ExecCallback, root cid.Cid, epoch abi.ChainEpoch, ts *types.TipSet) (cid.Cid, error) {
tree, err := sm.StateTree(root)
if err != nil {
return cid.Undef, xerrors.Errorf("getting state tree: %w", err)
Expand Down
90 changes: 86 additions & 4 deletions chain/stmgr/forks_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,9 @@ import (
"github.com/filecoin-project/go-state-types/abi"
"github.com/filecoin-project/go-state-types/cbor"
"github.com/filecoin-project/specs-actors/actors/builtin"
init_ "github.com/filecoin-project/specs-actors/actors/builtin/init"
init0 "github.com/filecoin-project/specs-actors/actors/builtin/init"
"github.com/filecoin-project/specs-actors/actors/runtime"
"github.com/stretchr/testify/require"
"golang.org/x/xerrors"

"github.com/filecoin-project/lotus/chain/actors"
Expand Down Expand Up @@ -76,7 +77,7 @@ func (ta testActor) Exports() []interface{} {
func (ta *testActor) Constructor(rt runtime.Runtime, params *abi.EmptyValue) *abi.EmptyValue {
rt.ValidateImmediateCallerAcceptAny()
rt.StateCreate(&testActorState{11})
fmt.Println("NEW ACTOR ADDRESS IS: ", rt.Receiver())
//fmt.Println("NEW ACTOR ADDRESS IS: ", rt.Receiver())

return abi.Empty
}
Expand Down Expand Up @@ -120,7 +121,7 @@ func TestForkHeightTriggers(t *testing.T) {
Network: 1,
Height: testForkHeight,
Migration: func(ctx context.Context, sm *StateManager, cb ExecCallback,
root cid.Cid, ts *types.TipSet) (cid.Cid, error) {
root cid.Cid, height abi.ChainEpoch, ts *types.TipSet) (cid.Cid, error) {
cst := ipldcbor.NewCborStore(sm.ChainStore().Blockstore())

st, err := sm.StateTree(root)
Expand Down Expand Up @@ -173,7 +174,7 @@ func TestForkHeightTriggers(t *testing.T) {

var msgs []*types.SignedMessage

enc, err := actors.SerializeParams(&init_.ExecParams{CodeCID: (testActor{}).Code()})
enc, err := actors.SerializeParams(&init0.ExecParams{CodeCID: (testActor{}).Code()})
if err != nil {
t.Fatal(err)
}
Expand Down Expand Up @@ -233,3 +234,84 @@ func TestForkHeightTriggers(t *testing.T) {
}
}
}

func TestForkRefuseCall(t *testing.T) {
logging.SetAllLoggers(logging.LevelInfo)

ctx := context.TODO()

cg, err := gen.NewGenerator()
if err != nil {
t.Fatal(err)
}

sm, err := NewStateManagerWithUpgradeSchedule(
cg.ChainStore(), UpgradeSchedule{{
Network: 1,
Expensive: true,
Height: testForkHeight,
Migration: func(ctx context.Context, sm *StateManager, cb ExecCallback,
root cid.Cid, height abi.ChainEpoch, ts *types.TipSet) (cid.Cid, error) {
return root, nil
}}})
if err != nil {
t.Fatal(err)
}

inv := vm.NewActorRegistry()
inv.Register(nil, testActor{})

sm.SetVMConstructor(func(ctx context.Context, vmopt *vm.VMOpts) (*vm.VM, error) {
nvm, err := vm.NewVM(ctx, vmopt)
if err != nil {
return nil, err
}
nvm.SetInvoker(inv)
return nvm, nil
})

cg.SetStateManager(sm)

enc, err := actors.SerializeParams(&init0.ExecParams{CodeCID: (testActor{}).Code()})
if err != nil {
t.Fatal(err)
}

m := &types.Message{
From: cg.Banker(),
To: lotusinit.Address,
Method: builtin.MethodsInit.Exec,
Params: enc,
GasLimit: types.TestGasLimit,
Value: types.NewInt(0),
GasPremium: types.NewInt(0),
GasFeeCap: types.NewInt(0),
}

for i := 0; i < 50; i++ {
ts, err := cg.NextTipSet()
if err != nil {
t.Fatal(err)
}

ret, err := sm.CallWithGas(ctx, m, nil, ts.TipSet.TipSet())
switch ts.TipSet.TipSet().Height() {
case testForkHeight, testForkHeight + 1:
// If I had a fork, or I _will_ have a fork, it should fail.
require.Equal(t, ErrExpensiveFork, err)
default:
require.NoError(t, err)
require.True(t, ret.MsgRct.ExitCode.IsSuccess())
}
// Call just runs on the parent state for a tipset, so we only
// expect an error at the fork height.
ret, err = sm.Call(ctx, m, ts.TipSet.TipSet())
switch ts.TipSet.TipSet().Height() {
case testForkHeight + 1:
require.Equal(t, ErrExpensiveFork, err)
default:
require.NoError(t, err)
require.True(t, ret.MsgRct.ExitCode.IsSuccess())
}
}
}
23 changes: 16 additions & 7 deletions chain/stmgr/stmgr.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,10 @@ type StateManager struct {

// Maps chain epochs to upgrade functions.
stateMigrations map[abi.ChainEpoch]UpgradeFunc
// A set of potentially expensive/time consuming upgrades. Explicit
// calls for, e.g., gas estimation fail against this epoch with
// ErrExpensiveFork.
expensiveUpgrades map[abi.ChainEpoch]struct{}

stCache map[string][]cid.Cid
compWait map[string]chan struct{}
Expand All @@ -78,6 +82,7 @@ func NewStateManagerWithUpgradeSchedule(cs *store.ChainStore, us UpgradeSchedule
}

stateMigrations := make(map[abi.ChainEpoch]UpgradeFunc, len(us))
expensiveUpgrades := make(map[abi.ChainEpoch]struct{}, len(us))
var networkVersions []versionSpec
lastVersion := network.Version0
if len(us) > 0 {
Expand All @@ -87,6 +92,9 @@ func NewStateManagerWithUpgradeSchedule(cs *store.ChainStore, us UpgradeSchedule
if upgrade.Migration != nil {
stateMigrations[upgrade.Height] = upgrade.Migration
}
if upgrade.Expensive {
expensiveUpgrades[upgrade.Height] = struct{}{}
}
networkVersions = append(networkVersions, versionSpec{
networkVersion: lastVersion,
atOrBelow: upgrade.Height,
Expand All @@ -99,13 +107,14 @@ func NewStateManagerWithUpgradeSchedule(cs *store.ChainStore, us UpgradeSchedule
}

return &StateManager{
networkVersions: networkVersions,
latestVersion: lastVersion,
stateMigrations: stateMigrations,
newVM: vm.NewVM,
cs: cs,
stCache: make(map[string][]cid.Cid),
compWait: make(map[string]chan struct{}),
networkVersions: networkVersions,
latestVersion: lastVersion,
stateMigrations: stateMigrations,
expensiveUpgrades: expensiveUpgrades,
newVM: vm.NewVM,
cs: cs,
stCache: make(map[string][]cid.Cid),
compWait: make(map[string]chan struct{}),
}, nil
}

Expand Down
2 changes: 1 addition & 1 deletion chain/stmgr/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -387,7 +387,7 @@ func ComputeState(ctx context.Context, sm *StateManager, height abi.ChainEpoch,
NtwkVersion: sm.GetNtwkVersion,
BaseFee: ts.Blocks()[0].ParentBaseFee,
}
vmi, err := vm.NewVM(ctx, vmopt)
vmi, err := sm.newVM(ctx, vmopt)
if err != nil {
return cid.Undef, nil, err
}
Expand Down
13 changes: 12 additions & 1 deletion node/impl/full/gas.go
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,18 @@ func (a *GasAPI) GasEstimateGasLimit(ctx context.Context, msgIn *types.Message,
priorMsgs = append(priorMsgs, m)
}

res, err := a.Stmgr.CallWithGas(ctx, &msg, priorMsgs, ts)
// Try calling until we find a height with no migration.
var res *api.InvocResult
for {
res, err = a.Stmgr.CallWithGas(ctx, &msg, priorMsgs, ts)
if err != stmgr.ErrExpensiveFork {
break
}
ts, err = a.Chain.GetTipSetFromKey(ts.Parents())
if err != nil {
return -1, xerrors.Errorf("getting parent tipset: %w", err)
}
}
if err != nil {
return -1, xerrors.Errorf("CallWithGas failed: %w", err)
}
Expand Down
Loading

0 comments on commit 413c357

Please sign in to comment.