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
base: master
Are you sure you want to change the base?
Conversation
@aarshkshah1992 - can you please assign a reviewer? |
Well, this is bringing back past nightmares with respect to #10216. |
@Stebalien Are you saying that this wont work ? |
node/impl/full/eth.go
Outdated
// Try calling until we find a height with no migration. | ||
for { | ||
res, err = a.StateManager.CallWithGas(ctx, msg, []types.ChainMsg{}, ts, applyTsMessages) | ||
st, _, err := a.StateManager.TipSetState(ctx, ts) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Working through the logic here - TipSetState
will end up executing the messages on top of parent state if the requested tipset doesn't already have a height+1 not on a different fork that we can use to get the state from.
When you get down to the bottom of tipSetState()
, having failed to find a valid existing height+1 state, you run ExecuteTipSet
, which itself will call HandleStateForks
which the callInternal()
code above in this PR does so a bunch of work to avoid calling if it can tell that there's an "expensive" migration.
So we might need to port that logic down to here to handle the expensive-migration case.
But other than that, I think this results in roughly the same outcome of having all current tipset messages applied to produce a new state and then applying only the requested msg
on top of that.
I'm not sure if/how null rounds come in to play with all of this though, maybe it doesn't matter as per the discussion in #11205.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Given that TipSetState
already handles state forks for us, can we just not re-do it in callInternal
for our use case ? We can control that behaviour in callInternal
with a flag.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
already handles state forks for us,
but that's the problem, imagine if we did this across an expensive migration boundary and triggered a migration just to get the state at this tipset.
All of this logic exists to check that condition:
Lines 104 to 132 in 187e837
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 { | |
pts, err = sm.cs.GetTipSetFromKey(ctx, ts.Parents()) | |
if err != nil { | |
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) { | |
break | |
} | |
ts = pts | |
} | |
} else if ts.Height() > 0 { | |
pts, err = sm.cs.GetTipSetFromKey(ctx, ts.Parents()) | |
if err != nil { | |
return nil, xerrors.Errorf("failed to find a non-forking epoch: %w", err) | |
} | |
if sm.hasExpensiveForkBetween(pts.Height(), ts.Height()+1) { | |
return nil, ErrExpensiveFork | |
} | |
} |
But that's only applied after you've potentially executed that tipset because TipSetState
can result in an ExecuteTipSet
if it doesn't already have it handy, that executes ApplyBlocks
which in turn runs HandleStateForks
.
So I think, if you run this, it'll still do what it says in the comment at the top of the for
("Try calling until we find a height with no migration."), but it'll actually perform a migration before it checks whether there's an expensive migration.
I believe what we want is to run an hasExpensiveForkBetween(pts.Height(), ts.Height()+1)
ourselves and walk back up parents until it's false
. But currently hasExpensiveForkBetween
is a private method of StateManager
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ahaa good catch @rvagg I have fixed this.
Hey @raulk This PR needs a review from someone who understands the ETH RPC stack really well. But @Stebalien is fully focused on F3 currently. Would you or Mikers or Fridrick be able to help review this by any chance ? |
@rvagg Have addressed both your review comments. Please can you take one final look at the PR ? |
if ts.Height() > 0 { | ||
for { | ||
pts, err := a.Chain.GetTipSetFromKey(ctx, ts.Parents()) | ||
if err != nil { | ||
return nil, xerrors.Errorf("failed to find a non-forking epoch: %w", err) | ||
} | ||
if !a.StateManager.HasExpensiveForkBetween(pts.Height(), ts.Height()+1) { | ||
break | ||
} | ||
ts, err = a.Chain.GetTipSetFromKey(ctx, ts.Parents()) | ||
if err != nil { | ||
return nil, xerrors.Errorf("getting parent tipset: %w", err) | ||
} | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually I think the correct thing to do is not loop because the request is for an explicit tipset, so we can't just adjust the tipset they want. I believe the right behaviour is to just check whether there's an expensive fork and error if not. Perhaps like this:
if ts.Height() > 0 { | |
for { | |
pts, err := a.Chain.GetTipSetFromKey(ctx, ts.Parents()) | |
if err != nil { | |
return nil, xerrors.Errorf("failed to find a non-forking epoch: %w", err) | |
} | |
if !a.StateManager.HasExpensiveForkBetween(pts.Height(), ts.Height()+1) { | |
break | |
} | |
ts, err = a.Chain.GetTipSetFromKey(ctx, ts.Parents()) | |
if err != nil { | |
return nil, xerrors.Errorf("getting parent tipset: %w", err) | |
} | |
} | |
} | |
if ts.Height() > 0 { | |
pts, err := a.Chain.GetTipSetFromKey(ctx, ts.Parents()) | |
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 | |
} | |
} |
Then the call just won't work for those major upgrade epochs. I don't know if that's going to cause complaints but it's only a few every year.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@rvagg But the pre-existing behaviour does loop back till it can find an appropriate tipset. I am not sure we should change that behaviour.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
only if ts
is not supplied; but I think here we always have a ts
so we're in the else
part of this block:
Lines 104 to 132 in f75bdd1
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 { | |
pts, err = sm.cs.GetTipSetFromKey(ctx, ts.Parents()) | |
if err != nil { | |
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) { | |
break | |
} | |
ts = pts | |
} | |
} else if ts.Height() > 0 { | |
pts, err = sm.cs.GetTipSetFromKey(ctx, ts.Parents()) | |
if err != nil { | |
return nil, xerrors.Errorf("failed to find a non-forking epoch: %w", err) | |
} | |
if sm.HasExpensiveForkBetween(pts.Height(), ts.Height()+1) { | |
return nil, ErrExpensiveFork | |
} | |
} |
Related Issues
Closes #11205
Proposed Changes
ETH Call should use the parent state root of the subsequent tipset. This can be obtained from the
TipsetState
on theStatemanager
Additional Info
Checklist
Before you mark the PR ready for review, please make sure that:
<PR type>: <area>: <change being made>
fix: mempool: Introduce a cache for valid signatures
PR type
: fix, feat, build, chore, ci, docs, perf, refactor, revert, style, testarea
, e.g. api, chain, state, market, mempool, multisig, networking, paych, proving, sealing, wallet, deps