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

* fix eth call

* tests

* changes as per review

* changes as per review

* Update node/impl/full/eth.go

Co-authored-by: Rod Vagg <rod@vagg.org>

* fix as per review

---------

Co-authored-by: Rod Vagg <rod@vagg.org>
This commit is contained in:
Aarsh Shah 2024-06-11 19:07:17 +04:00 committed by Phi
parent ab1cd85afd
commit 9772b1c1a5
No known key found for this signature in database
GPG Key ID: AFAE959F938CC79E
3 changed files with 77 additions and 52 deletions

View File

@ -25,6 +25,14 @@ import (
"github.com/filecoin-project/lotus/chain/vm" "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") 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 // Call applies the given message to the given tipset's parent state, at the epoch following the
@ -48,12 +56,24 @@ func (sm *StateManager) Call(ctx context.Context, msg *types.Message, ts *types.
msg.Value = types.NewInt(0) 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. // 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) { 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. // CallAtStateAndVersion allows you to specify a message to execute on the given stateCid and network version.
@ -64,14 +84,14 @@ func (sm *StateManager) CallAtStateAndVersion(ctx context.Context, msg *types.Me
nvGetter := func(context.Context, abi.ChainEpoch) network.Version { nvGetter := func(context.Context, abi.ChainEpoch) network.Version {
return v return v
} }
return sm.callInternal(ctx, msg, nil, nil, stateCid, nvGetter, true, execSameSenderMessages)
return sm.callInternal(ctx, msg, nil, nil, stateCid, nvGetter, true, false)
} }
// - If no tipset is specified, the first tipset without an expensive migration or one in its parent is used. // - 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 // - If executing a message at a given tipset or its parent would trigger an expensive migration, the call will
// fail with ErrExpensiveFork. // 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") ctx, span := trace.StartSpan(ctx, "statemanager.callInternal")
defer span.End() defer span.End()
@ -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) 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 // 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 break
} }
@ -106,7 +126,7 @@ func (sm *StateManager) callInternal(ctx context.Context, msg *types.Message, pr
if err != nil { if err != nil {
return nil, xerrors.Errorf("failed to find a non-forking epoch: %w", err) 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 return nil, ErrExpensiveFork
} }
} }
@ -117,24 +137,6 @@ func (sm *StateManager) callInternal(ctx context.Context, msg *types.Message, pr
if stateCid == cid.Undef { if stateCid == cid.Undef {
stateCid = ts.ParentState() 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. // 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) stateCid, err = sm.HandleStateForks(ctx, stateCid, ts.Height(), nil, ts)
if err != nil { if err != nil {
@ -169,18 +171,40 @@ func (sm *StateManager) callInternal(ctx context.Context, msg *types.Message, pr
if err != nil { if err != nil {
return nil, xerrors.Errorf("failed to set up vm: %w", err) return nil, xerrors.Errorf("failed to set up vm: %w", err)
} }
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 switch strategy {
// This is needed to get the correct nonce from the actor state to match the VM case execNoMessages:
stateCid, err = vmi.Flush(ctx) // Do nothing
if err != nil { case execAllMessages, execSameSenderMessages:
return nil, xerrors.Errorf("flushing vm: %w", err) tsMsgs, err := sm.cs.MessagesForTipset(ctx, ts)
if err != nil {
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)
}
} }
stTree, err := state.LoadStateTree(cbor.NewCborStore(buffStore), stateCid) stTree, err := state.LoadStateTree(cbor.NewCborStore(buffStore), stateCid)

View File

@ -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 // 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 // migration. NOTE: migrations occurring _at_ the target height are not included, as they're
// executed _after_ the target height. // 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++ { for h := parent; h < height; h++ {
if _, ok := sm.expensiveUpgrades[h]; ok { if _, ok := sm.expensiveUpgrades[h]; ok {
return true return true

View File

@ -1028,25 +1028,26 @@ func (a *EthModule) applyMessage(ctx context.Context, msg *types.Message, tsk ty
return nil, xerrors.Errorf("cannot get tipset: %w", err) return nil, xerrors.Errorf("cannot get tipset: %w", err)
} }
applyTsMessages := true if ts.Height() > 0 {
if os.Getenv("LOTUS_SKIP_APPLY_TS_MESSAGE_CALL_WITH_GAS") == "1" { pts, err := a.Chain.GetTipSetFromKey(ctx, ts.Parents())
applyTsMessages = false if err != nil {
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
}
} }
// Try calling until we find a height with no migration. st, _, err := a.StateManager.TipSetState(ctx, ts)
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 err != nil {
return nil, xerrors.Errorf("getting parent tipset: %w", err)
}
}
if err != nil { if err != nil {
return nil, xerrors.Errorf("CallWithGas failed: %w", err) 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("ApplyWithGasOnState failed: %w", err)
}
if res.MsgRct.ExitCode.IsError() { if res.MsgRct.ExitCode.IsError() {
reason := parseEthRevert(res.MsgRct.Return) 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) return nil, xerrors.Errorf("message execution failed: exit %s, revert reason: %s, vm error: %s", res.MsgRct.ExitCode, reason, res.Error)