Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: ETH RPC API: ETH Call should use the parent state root of the subsequent tipset #11905

Merged
merged 7 commits into from
Jun 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
92 changes: 58 additions & 34 deletions chain/stmgr/call.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,14 @@ import (
"github.com/filecoin-project/lotus/chain/vm"
)

type execMessageStrategy int

const (
execNoMessages execMessageStrategy = iota // apply no prior or current tipset messages
execAllMessages // apply all prior and current tipset messages
execSameSenderMessages // apply all prior messages and any current tipset messages from the same sender
)

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

// Call applies the given message to the given tipset's parent state, at the epoch following the
Expand All @@ -48,12 +56,24 @@ func (sm *StateManager) Call(ctx context.Context, msg *types.Message, ts *types.
msg.Value = types.NewInt(0)
}

return sm.callInternal(ctx, msg, nil, ts, cid.Undef, sm.GetNetworkVersion, false, false)
return sm.callInternal(ctx, msg, nil, ts, cid.Undef, sm.GetNetworkVersion, false, execSameSenderMessages)
}

// ApplyOnStateWithGas applies the given message on top of the given state root with gas tracing enabled
func (sm *StateManager) ApplyOnStateWithGas(ctx context.Context, stateCid cid.Cid, msg *types.Message, ts *types.TipSet) (*api.InvocResult, error) {
return sm.callInternal(ctx, msg, nil, ts, stateCid, sm.GetNetworkVersion, true, execNoMessages)
}

// CallWithGas calculates the state for a given tipset, and then applies the given message on top of that state.
func (sm *StateManager) CallWithGas(ctx context.Context, msg *types.Message, priorMsgs []types.ChainMsg, ts *types.TipSet, applyTsMessages bool) (*api.InvocResult, error) {
return sm.callInternal(ctx, msg, priorMsgs, ts, cid.Undef, sm.GetNetworkVersion, true, applyTsMessages)
var strategy execMessageStrategy
if applyTsMessages {
strategy = execAllMessages
} else {
strategy = execSameSenderMessages
}

return sm.callInternal(ctx, msg, priorMsgs, ts, cid.Undef, sm.GetNetworkVersion, true, strategy)
}

// CallAtStateAndVersion allows you to specify a message to execute on the given stateCid and network version.
Expand All @@ -64,14 +84,14 @@ func (sm *StateManager) CallAtStateAndVersion(ctx context.Context, msg *types.Me
nvGetter := func(context.Context, abi.ChainEpoch) network.Version {
return v
}

return sm.callInternal(ctx, msg, nil, nil, stateCid, nvGetter, true, false)
return sm.callInternal(ctx, msg, nil, nil, stateCid, nvGetter, true, execSameSenderMessages)
}

// - If no tipset is specified, the first tipset without an expensive migration or one in its parent is used.
// - If executing a message at a given tipset or its parent would trigger an expensive migration, the call will
// fail with ErrExpensiveFork.
func (sm *StateManager) callInternal(ctx context.Context, msg *types.Message, priorMsgs []types.ChainMsg, ts *types.TipSet, stateCid cid.Cid, nvGetter rand.NetworkVersionGetter, checkGas, applyTsMessages bool) (*api.InvocResult, error) {
func (sm *StateManager) callInternal(ctx context.Context, msg *types.Message, priorMsgs []types.ChainMsg, ts *types.TipSet, stateCid cid.Cid,
nvGetter rand.NetworkVersionGetter, checkGas bool, strategy execMessageStrategy) (*api.InvocResult, error) {
ctx, span := trace.StartSpan(ctx, "statemanager.callInternal")
defer span.End()

Expand All @@ -95,7 +115,7 @@ func (sm *StateManager) callInternal(ctx context.Context, msg *types.Message, pr
return nil, xerrors.Errorf("failed to find a non-forking epoch: %w", err)
}
// Checks for expensive forks from the parents to the tipset, including nil tipsets
if !sm.hasExpensiveForkBetween(pts.Height(), ts.Height()+1) {
if !sm.HasExpensiveForkBetween(pts.Height(), ts.Height()+1) {
break
}

Expand All @@ -106,7 +126,7 @@ func (sm *StateManager) callInternal(ctx context.Context, msg *types.Message, pr
if err != nil {
return nil, xerrors.Errorf("failed to find a non-forking epoch: %w", err)
}
if sm.hasExpensiveForkBetween(pts.Height(), ts.Height()+1) {
if sm.HasExpensiveForkBetween(pts.Height(), ts.Height()+1) {
return nil, ErrExpensiveFork
}
}
Expand All @@ -117,24 +137,6 @@ func (sm *StateManager) callInternal(ctx context.Context, msg *types.Message, pr
if stateCid == cid.Undef {
stateCid = ts.ParentState()
}
tsMsgs, err := sm.cs.MessagesForTipset(ctx, ts)
if err != nil {
return nil, xerrors.Errorf("failed to lookup messages for parent tipset: %w", err)
}

if applyTsMessages {
priorMsgs = append(tsMsgs, priorMsgs...)
} else {
var filteredTsMsgs []types.ChainMsg
for _, tsMsg := range tsMsgs {
//TODO we should technically be normalizing the filecoin address of from when we compare here
if tsMsg.VMMessage().From == msg.VMMessage().From {
filteredTsMsgs = append(filteredTsMsgs, tsMsg)
}
}
priorMsgs = append(filteredTsMsgs, priorMsgs...)
}

// Technically, the tipset we're passing in here should be ts+1, but that may not exist.
stateCid, err = sm.HandleStateForks(ctx, stateCid, ts.Height(), nil, ts)
if err != nil {
Expand Down Expand Up @@ -169,18 +171,40 @@ func (sm *StateManager) callInternal(ctx context.Context, msg *types.Message, pr
if err != nil {
return nil, xerrors.Errorf("failed to set up vm: %w", err)
}
for i, m := range priorMsgs {
_, err = vmi.ApplyMessage(ctx, m)

switch strategy {
case execNoMessages:
// Do nothing
case execAllMessages, execSameSenderMessages:
tsMsgs, err := sm.cs.MessagesForTipset(ctx, ts)
if err != nil {
return nil, xerrors.Errorf("applying prior message (%d, %s): %w", i, m.Cid(), err)
return nil, xerrors.Errorf("failed to lookup messages for parent tipset: %w", err)
}
if strategy == execAllMessages {
priorMsgs = append(tsMsgs, priorMsgs...)
} else if strategy == execSameSenderMessages {
var filteredTsMsgs []types.ChainMsg
for _, tsMsg := range tsMsgs {
//TODO we should technically be normalizing the filecoin address of from when we compare here
if tsMsg.VMMessage().From == msg.VMMessage().From {
filteredTsMsgs = append(filteredTsMsgs, tsMsg)
}
}
priorMsgs = append(filteredTsMsgs, priorMsgs...)
}
for i, m := range priorMsgs {
_, err = vmi.ApplyMessage(ctx, m)
if err != nil {
return nil, xerrors.Errorf("applying prior message (%d, %s): %w", i, m.Cid(), err)
}
}
}

// We flush to get the VM's view of the state tree after applying the above messages
// This is needed to get the correct nonce from the actor state to match the VM
stateCid, err = vmi.Flush(ctx)
if err != nil {
return nil, xerrors.Errorf("flushing vm: %w", err)
// We flush to get the VM's view of the state tree after applying the above messages
// This is needed to get the correct nonce from the actor state to match the VM
stateCid, err = vmi.Flush(ctx)
if err != nil {
return nil, xerrors.Errorf("flushing vm: %w", err)
}
}

stTree, err := state.LoadStateTree(cbor.NewCborStore(buffStore), stateCid)
Expand Down
2 changes: 1 addition & 1 deletion chain/stmgr/forks.go
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,7 @@ func (sm *StateManager) HandleStateForks(ctx context.Context, root cid.Cid, heig
// Returns true executing tipsets between the specified heights would trigger an expensive
// migration. NOTE: migrations occurring _at_ the target height are not included, as they're
// executed _after_ the target height.
func (sm *StateManager) hasExpensiveForkBetween(parent, height abi.ChainEpoch) bool {
func (sm *StateManager) HasExpensiveForkBetween(parent, height abi.ChainEpoch) bool {
for h := parent; h < height; h++ {
if _, ok := sm.expensiveUpgrades[h]; ok {
return true
Expand Down
29 changes: 15 additions & 14 deletions node/impl/full/eth.go
Original file line number Diff line number Diff line change
Expand Up @@ -980,25 +980,26 @@ func (a *EthModule) applyMessage(ctx context.Context, msg *types.Message, tsk ty
return nil, xerrors.Errorf("cannot get tipset: %w", err)
}

applyTsMessages := true
if os.Getenv("LOTUS_SKIP_APPLY_TS_MESSAGE_CALL_WITH_GAS") == "1" {
applyTsMessages = false
}

// Try calling until we find a height with no migration.
for {
res, err = a.StateManager.CallWithGas(ctx, msg, []types.ChainMsg{}, ts, applyTsMessages)
if err != stmgr.ErrExpensiveFork {
break
}
ts, err = a.Chain.GetTipSetFromKey(ctx, ts.Parents())
if ts.Height() > 0 {
pts, err := a.Chain.GetTipSetFromKey(ctx, ts.Parents())
if err != nil {
return nil, xerrors.Errorf("getting parent tipset: %w", err)
return nil, xerrors.Errorf("failed to find a non-forking epoch: %w", err)
}
// Check for expensive forks from the parents to the tipset, including nil tipsets
if a.StateManager.HasExpensiveForkBetween(pts.Height(), ts.Height()+1) {
return nil, stmgr.ErrExpensiveFork
}
}

st, _, err := a.StateManager.TipSetState(ctx, ts)
if err != nil {
return nil, xerrors.Errorf("cannot get tipset state: %w", err)
}
res, err = a.StateManager.ApplyOnStateWithGas(ctx, st, msg, ts)
if err != nil {
return nil, xerrors.Errorf("CallWithGas failed: %w", err)
return nil, xerrors.Errorf("ApplyWithGasOnState failed: %w", err)
}

if res.MsgRct.ExitCode.IsError() {
reason := parseEthRevert(res.MsgRct.Return)
return nil, xerrors.Errorf("message execution failed: exit %s, revert reason: %s, vm error: %s", res.MsgRct.ExitCode, reason, res.Error)
Expand Down
Loading