-
Notifications
You must be signed in to change notification settings - Fork 1.3k
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: correctly handle null blocks when detecting an expensive fork #7210
Changes from 1 commit
165735d
e690230
0820791
91da70f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -99,7 +99,6 @@ func (sm *StateManager) ApplyBlocks(ctx context.Context, parentEpoch abi.ChainEp | |
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: not at all your bug but reading the above code it looks like line 82's error message is wrong and confusing and makes sense to fix now. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'd rather not touch that for now. I don't think that error is consensus critical, but I'd rather not touch it. |
||
|
||
// handle state forks | ||
// XXX: The state tree | ||
newState, err := sm.handleStateForks(ctx, pstate, i, em, ts) | ||
if err != nil { | ||
return cid.Undef, cid.Undef, xerrors.Errorf("error handling state forks: %w", err) | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -41,8 +41,11 @@ type MigrationCache interface { | |
// - 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. | ||
// - The tipset is the first non-null tipset after the upgrade height (the tipset in | ||
// which the upgrade is executed). Do not assume that ts.Height() is the upgrade height. | ||
// | ||
// NOTE: In StateCompute and CallWithGas, the passed tipset is actually the tipset _before_ the | ||
ZenGround0 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
// upgrade. The tipset should really only be used for referencing the "current chain". | ||
type MigrationFunc func( | ||
ctx context.Context, | ||
sm *StateManager, cache MigrationCache, | ||
|
@@ -208,7 +211,21 @@ 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 { | ||
// Returns true if executing the current tipset would trigger an expensive fork. | ||
// | ||
// - If the tipset is the genesis, this function always returns false. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. As written now this invariant depends on the sm.expensiveUpgrades table which in reasonable scenarios could potentially contain "expensive" upgrades if say we did all migrations at the genesis epoch. It might be better to override the table to always return false on 0 to maintain this invariant. Any network doing this would probably be doing very little work in the expensive migration since nothing has happened in state yet. Also all existing callers seem to implicitly assume that this is the case anyway by only checking heights > 0 so why not make it official. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Because the function is called At the moment, genesis matters to all callers because there's really no other option. But I'd like to keep that as an explicit decision. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. But I've fixed the issue in Call where I wasn't handling this consistently. |
||
// - If inclusive is true, this function will also return true if applying a message on-top-of the | ||
Stebalien marked this conversation as resolved.
Show resolved
Hide resolved
|
||
// tipset would trigger a fork. | ||
func (sm *StateManager) hasExpensiveForkBetween(parent, height abi.ChainEpoch) bool { | ||
for h := parent; h < height; h++ { | ||
if _, ok := sm.expensiveUpgrades[h]; ok { | ||
ZenGround0 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
return true | ||
} | ||
} | ||
return false | ||
} | ||
|
||
func (sm *StateManager) hasExpensiveFork(height abi.ChainEpoch) bool { | ||
_, ok := sm.expensiveUpgrades[height] | ||
return ok | ||
} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -242,20 +242,35 @@ func TestForkHeightTriggers(t *testing.T) { | |
func TestForkRefuseCall(t *testing.T) { | ||
logging.SetAllLoggers(logging.LevelInfo) | ||
|
||
for after := 0; after < 3; after++ { | ||
for before := 0; before < 3; before++ { | ||
// Makes the lints happy... | ||
after := after | ||
before := before | ||
t.Run(fmt.Sprintf("after:%d,before:%d", after, before), func(t *testing.T) { | ||
testForkRefuseCall(t, before, after) | ||
}) | ||
} | ||
} | ||
|
||
} | ||
func testForkRefuseCall(t *testing.T, nullsBefore, nullsAfter int) { | ||
ctx := context.TODO() | ||
|
||
cg, err := gen.NewGenerator() | ||
if err != nil { | ||
t.Fatal(err) | ||
} | ||
|
||
var migrationCount int | ||
sm, err := NewStateManagerWithUpgradeSchedule( | ||
cg.ChainStore(), cg.StateManager().VMSys(), UpgradeSchedule{{ | ||
Network: network.Version1, | ||
Expensive: true, | ||
Height: testForkHeight, | ||
Migration: func(ctx context.Context, sm *StateManager, cache MigrationCache, cb ExecMonitor, | ||
root cid.Cid, height abi.ChainEpoch, ts *types.TipSet) (cid.Cid, error) { | ||
migrationCount++ | ||
return root, nil | ||
}}}) | ||
if err != nil { | ||
|
@@ -292,14 +307,20 @@ func TestForkRefuseCall(t *testing.T) { | |
GasFeeCap: types.NewInt(0), | ||
} | ||
|
||
nullStart := abi.ChainEpoch(testForkHeight - nullsBefore) | ||
nullLength := abi.ChainEpoch(nullsBefore + nullsAfter) | ||
|
||
for i := 0; i < 50; i++ { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not your bug: I don't love that testForkHeight is a global and that this test relies on knowing that its value is 40 in order to actually test anything. Maybe make There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Well, being a global is intentional. But yeah, 50 being hard-coded is a problem. |
||
ts, err := cg.NextTipSet() | ||
pts := cg.CurTipset.TipSet() | ||
skip := abi.ChainEpoch(0) | ||
if pts.Height() == nullStart { | ||
skip = nullLength | ||
} | ||
ts, err := cg.NextTipSetFromMiners(pts, cg.Miners, skip) | ||
if err != nil { | ||
t.Fatal(err) | ||
} | ||
|
||
pts, err := cg.ChainStore().LoadTipSet(ts.TipSet.TipSet().Parents()) | ||
require.NoError(t, err) | ||
parentHeight := pts.Height() | ||
currentHeight := ts.TipSet.TipSet().Height() | ||
|
||
|
@@ -321,7 +342,20 @@ func TestForkRefuseCall(t *testing.T) { | |
require.NoError(t, err) | ||
require.True(t, ret.MsgRct.ExitCode.IsSuccess()) | ||
} | ||
|
||
// Calls without a tipset should walk back to the last non-fork tipset. | ||
// We _verify_ that the migration wasn't run multiple times at the end of the | ||
// test. | ||
ret, err = sm.CallWithGas(ctx, m, nil, nil) | ||
require.NoError(t, err) | ||
require.True(t, ret.MsgRct.ExitCode.IsSuccess()) | ||
|
||
ret, err = sm.Call(ctx, m, nil) | ||
require.NoError(t, err) | ||
require.True(t, ret.MsgRct.ExitCode.IsSuccess()) | ||
} | ||
// Make sure we didn't execute the migration multiple times. | ||
require.Equal(t, migrationCount, 1) | ||
} | ||
|
||
func TestForkPreMigration(t *testing.T) { | ||
|
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.
Is there a good reason why
Call
happens against tipset parent state andCallWithGas
happens against tipset state? As far as I understand is what requires the introduction ofhasExpensiveForkBetween
. Beyond removing extra work it would also just be nice to standardize these two similar callsThere 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.
CallWithGas applies the message on-top-of the tipset to provide better gas estimation, I think. By executing it on-top-of the last tipset, instead of on-top-of the parent tipset state, we get the best gas estimates.
I'd prefer to always use the parent state, but I'm not sure what might break. I'm especially concerned that, e.g., it might (rarely) cause issues with window post: