-
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
feat: chainstore: FRC-0051: Remove all equivocated blocks from tipsets #11104
Conversation
chain/store/store.go
Outdated
@@ -1027,7 +1027,7 @@ func (cs *ChainStore) persistBlockHeaders(ctx context.Context, b ...*types.Block | |||
return err | |||
} | |||
|
|||
func (cs *ChainStore) expandTipset(ctx context.Context, b *types.BlockHeader) (*types.TipSet, error) { | |||
func (cs *ChainStore) ExpandTipset(ctx context.Context, b *types.BlockHeader) (*types.TipSet, error) { |
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.
Exported for testing.
chain/store/store.go
Outdated
@@ -1052,7 +1052,15 @@ func (cs *ChainStore) expandTipset(ctx context.Context, b *types.BlockHeader) (* | |||
} | |||
|
|||
if cid, found := inclMiners[h.Miner]; found { | |||
log.Warnf("Have multiple blocks from miner %s at height %d in our tipset cache %s-%s", h.Miner, h.Height, h.Cid(), cid) | |||
log.Warnf("Have multiple blocks from miner %s at height %d in our tipset cache %s-%s, purging them all", h.Miner, h.Height, h.Cid(), cid) | |||
newAll := make([]*types.BlockHeader, 0, len(all)) |
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.
We could ALSO drop the blocks from cs.tipsets
entirely if we wanted to. I'm not sure this is the best thing to do, though, since that tracker is currently split between the "writer" method AddToTipsetTracker
and the "reader" method ExpandTipset
.
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.
I'd leave them so we can detect new ones.
chain/store/store.go
Outdated
@@ -1052,7 +1052,15 @@ func (cs *ChainStore) expandTipset(ctx context.Context, b *types.BlockHeader) (* | |||
} | |||
|
|||
if cid, found := inclMiners[h.Miner]; found { | |||
log.Warnf("Have multiple blocks from miner %s at height %d in our tipset cache %s-%s", h.Miner, h.Height, h.Cid(), cid) | |||
log.Warnf("Have multiple blocks from miner %s at height %d in our tipset cache %s-%s, purging them all", h.Miner, h.Height, h.Cid(), cid) | |||
newAll := make([]*types.BlockHeader, 0, len(all)) |
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.
I'd leave them so we can detect new ones.
chain/store/store.go
Outdated
newAll := make([]*types.BlockHeader, 0, len(all)) | ||
for _, blk := range all { | ||
if blk.Miner != h.Miner { | ||
newAll = append(newAll, blk) |
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.
nit: I'd just collect all these blocks into a miner -> block map, then put them into an array at the very end.
5ac13fb
to
0904d80
Compare
chain/gen/gen.go
Outdated
return nil, xerrors.Errorf("failed to persist tipset: %w", err) | ||
} | ||
|
||
for _, blk := range fts.TipSet().Blocks() { |
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.
Not strictly necessary since we do this in PutTipSet
too, but doesn't seem harmful to do for tests.
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.
I left comments (mostly nitpicks) and questions. Be aware that I'm neither a proficient coder nor am I familiar with this code base (hence, possibly trivial questions).
My two main comments are:
- I think that conceptually it's better to consider a winning ticket rather than the proposing miner. Is there an implementation reason for considering the miner?
- If we don't keep the equivocating blocks (or just their cids), we may run into future trouble when we'd like to prove the miner's bad behavior (i.e., create an irrefutable proof of misbehavior).
chain/store/store.go
Outdated
if blkCid, found := inclMiners[h.Miner]; found { | ||
log.Warnf("Have multiple blocks from miner %s at height %d in our tipset cache %s-%s, purging them all", h.Miner, h.Height, h.Cid(), blkCid) | ||
badMiners[h.Miner] = struct{}{} | ||
delete(inclMiners, h.Miner) |
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.
If you delete h.Miner from the inclMiners, then wouldn't the next block from the equivocating miner (e.g., the 3rd block with the same winning ticket) gets included?
If this is not the case (because badMiners is used for that), then what's the benefit of badMiners instead of just looking in inclMiners again? (Seems inefficient.)
chain/store/store.go
Outdated
if types.CidArrsEqual(h.Parents, b.Parents) { | ||
all = append(all, h) | ||
inclMiners[h.Miner] = bhc | ||
if blkCid, found := inclMiners[h.Miner]; found { |
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.
I think it's better to reject based on the same ticket rather than the same miner.
chain/store/store.go
Outdated
all := []*types.BlockHeader{b} | ||
|
||
tsets, ok := cs.tipsets[b.Height] | ||
blocks, ok := cs.tipsets[height] |
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.
Are these blocks from a SINGLE tipset at that height or can these contain blocks from different tipsets with the same height (possible forks)?
chain/store/store.go
Outdated
@@ -1027,44 +1043,51 @@ func (cs *ChainStore) persistBlockHeaders(ctx context.Context, b ...*types.Block | |||
return err | |||
} | |||
|
|||
func (cs *ChainStore) expandTipset(ctx context.Context, b *types.BlockHeader) (*types.TipSet, error) { | |||
func (cs *ChainStore) ExpandTipsetByHeightAndParent(ctx context.Context, height abi.ChainEpoch, parentTipsets []cid.Cid) (types.TipSetKey, error) { |
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 parntTipsets a single tipset or could there be multiple parent tipsets?
chain/store/store.go
Outdated
} | ||
|
||
var ret []cid.Cid | ||
for _, blk := range inclMiners { |
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.
Extending on a previous comment.
Personally, I find it confusing the use of included miners instead of included blocks (winning tickets). Currently there's a 1-1 relation between the two, but who's to say that this relation will remain in the future? Maybe there would be an implementation where a "very lucky" miner gets to include more blocks in a single tipset?
There's probably nothing you can do with it here, but maybe you have a reason that I failed to understand.
chain/store/store.go
Outdated
if err := cs.PersistTipsets(ctx, []*types.TipSet{ts}); err != nil { | ||
return xerrors.Errorf("failed to persist tipset: %w", err) | ||
} | ||
|
||
expanded, err := cs.expandTipset(ctx, ts.Blocks()[0]) | ||
expanded, err := cs.ExpandTipsetByHeightAndParent(ctx, ts.Height(), ts.Parents().Cids()) |
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.
I'm tempted to just use the new FormBestTipSetForHeight
method here as well, and drop ExpandTipsetByHeightAndParent
entirely.
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.
Any reason not to?
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.
It's a bit slower, but I think I'd prefer doing it anyway
53c1520
to
b6504dc
Compare
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.
Some initial comments but I want to walk through this together tomorrow.
return xerrors.Errorf("failed to add block to ts tracker: %w", err) | ||
} | ||
} | ||
|
||
if err := cs.PersistTipsets(ctx, []*types.TipSet{ts}); err != nil { |
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.
Can we persist inside AddToTipSetTracker
? Or do it first?
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.
I think the right sequence of steps is:
- Persist, when you receive a block of interest
- Track, when you actually validate it
- Assume in "PutTipSet" that Persisting and Tracking has already happened
I'm gonna implement that and state the assumption for now.
chain/store/store.go
Outdated
if err := cs.PersistTipsets(ctx, []*types.TipSet{ts}); err != nil { | ||
return xerrors.Errorf("failed to persist tipset: %w", err) | ||
} | ||
|
||
expanded, err := cs.expandTipset(ctx, ts.Blocks()[0]) | ||
expanded, err := cs.ExpandTipsetByHeightAndParent(ctx, ts.Height(), ts.Parents().Cids()) |
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.
Any reason not to?
chain/store/store.go
Outdated
return types.NewTipSetKey(ret...), nil | ||
} | ||
|
||
func (cs *ChainStore) FormBestTipSetForHeight(ctx context.Context, height abi.ChainEpoch) (*types.TipSet, types.BigInt, error) { |
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.
This should not be exported.
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 exported for tests, which need to be in a separate package (since they depend on the chain generator, which depends on the chainstore...)
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.
Ugh.... ok.
chain/store/store.go
Outdated
if cs.heaviest != nil { | ||
heaviestHeight = cs.heaviest.Height() | ||
} | ||
newHeaviest, newHeaviestWeight, err := cs.FormBestTipSetForHeight(ctx, heaviestHeight) |
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.
nit: FormHeaviestTipSetForHeight?
chain/store/store.go
Outdated
@@ -409,7 +420,7 @@ func (cs *ChainStore) PutTipSet(ctx context.Context, ts *types.TipSet) error { | |||
// MaybeTakeHeavierTipSet evaluates the incoming tipset and locks it in our | |||
// internal state as our new head, if and only if it is heavier than the current | |||
// head and does not exceed the maximum fork length. | |||
func (cs *ChainStore) MaybeTakeHeavierTipSet(ctx context.Context, ts *types.TipSet) error { | |||
func (cs *ChainStore) MaybeTakeHeavierTipSet(ctx context.Context, newTsk types.TipSetKey) error { |
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.
can we unexport this method?
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.
and may be call it informNewTipSet
?
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.
Basically, make it clear that we're telling the system "hey, we have this new tipset", maybe take it, maybe take something entirely different!
chain/store/store.go
Outdated
|
||
if newHeaviest == nil || newHeaviestWeight.LessThan(heaviestWeight) { | ||
log.Warnf("chainstore heaviest tipset's weight SHRANK from %d (%s) to %d (%s) due to equivocation", heaviestWeight, cs.heaviest, newHeaviestWeight, newHeaviest) | ||
// refresh heaviestWeight 10 times moving up and down |
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.
Needs lots of comments.
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.
Still needs some comments explaining why.
c7c4bdc
to
261a398
Compare
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.
nits, but this looks correct
chain/sync.go
Outdated
@@ -247,6 +247,7 @@ func (syncer *Syncer) InformNewHead(from peer.ID, fts *store.FullTipSet) bool { | |||
return false | |||
} | |||
|
|||
// TODO: this method name is a lie |
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.
- Can we document why it's a lie?
- Can we add a 🍰 emoji?
expanded, err := cs.expandTipset(ctx, ts.Blocks()[0]) | ||
if err != nil { | ||
return xerrors.Errorf("errored while expanding tipset: %w", err) | ||
if err := cs.AddToTipSetTracker(ctx, b); err != nil { |
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.
nit: it would be nice if this just handled writing blocks internally, but I guess that's probably not critical right now?
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.
Yeah, not critical I think. Also not super easy to do, because we want to be batching those writes, and this method tracks things one block at a time.
if err = cs.chainLocalBlockstore.Put(ctx, tsBlk); err != nil { | ||
return xerrors.Errorf("failed to put tipset key block: %w", err) | ||
} | ||
if err := cs.RefreshHeaviestTipSet(ctx, ts.Height()); err != nil { |
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.
Hm... for genesis, are we sure any of this is right? Shouldn't we force replace everything? Like:
- Discard all tipsets.
- Replace the first tipset.
- Move on?
Otherwise, e.g., the metadataDs.Put
line below could do the wrong thing.
(not like this is likely to cause issues, but it's still a bit funky)
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.
I think you're right, but I was going for matching existing behaviour here, which this should do?
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.
Yeah, you're right.
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.
Let's just keep a list of TODOs, merge this, then we can make followup PRs (if/when we get time).
chain/store/store.go
Outdated
|
||
if newHeaviest == nil || newHeaviestWeight.LessThan(heaviestWeight) { | ||
log.Warnf("chainstore heaviest tipset's weight SHRANK from %d (%s) to %d (%s) due to equivocation", heaviestWeight, cs.heaviest, newHeaviestWeight, newHeaviest) | ||
// refresh heaviestWeight 10 times moving up and down |
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.
Still needs some comments explaining why.
} | ||
|
||
// if the new height we were notified about isn't what we just refreshed at, see if we have a heavier tipset there | ||
if newTsHeight != newHeaviest.Height() { |
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.
I'm not sure how this is possible? If we refresh at a different height, shouldn't we have picked the heaviest tipset at that height?
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.
No, not necessarily. We'll only refresh at different heights if there was equivocation. Basically, assume current height heaviestHeight
is 10, and we were informed about newTsHeight
of 11.
- We refresh by finding the heaviest tipset at 10
- If the "refreshed" tipset at 10 is not lighter than our current heaviest tipset at 10, we'll just move on
- At that point, we hit this line, and decide we should form the heaviest tipset at height 11, and compare the two.
(If the "refreshed" tipset WAS lighter, though, then yes we would likely form a heavier tipset at 11, and immediately take that as our head, in which case we'd short-circuit here)
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.
I think I understand?
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.
Let's talk through tomorrow, but I'm pretty sure it's correct.
// fix pretty easily | ||
// FormHeaviestTipSetForHeight looks up all valid blocks at a given height, and returns the heaviest tipset that can be made at that height | ||
// It does not consider ANY blocks from miners that have "equivocated" (produced 2 blocks at the same height) | ||
func (cs *ChainStore) FormHeaviestTipSetForHeight(ctx context.Context, height abi.ChainEpoch) (*types.TipSet, types.BigInt, error) { |
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.
This looks correct and probably "fast enough". But I'd like to poke at making it faster in the future by pre-grouping by height, etc.
} | ||
|
||
// if the new height we were notified about isn't what we just refreshed at, see if we have a heavier tipset there | ||
if newTsHeight != newHeaviest.Height() { |
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.
I think I understand?
Ok, actually. I'm sorry, but I'm going to need to do this: is there any way to write a simple equivocation test? |
That's at least kiiinda what this test is intended to be. Definitely others that could be written, though... |
(but it would be nice to get one that tests switching away from a tipset) |
@Stebalien Added more in latest commit, please suggest more if you think of them -- we need to get this right. |
Well, the tests I'd like are:
|
@Stebalien Thanks, all good ideas. I've added all 3 cases in latest commit, please take a look. |
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.
LGTM!
Related Issues
#10793
Proposed Changes
As discussed in #10793, we now reject ALL blocks with the same miner and same height (irrespective of other details like common parents, common ticket, etc.). Further, when asked to consider taking a new tipset as head, we first refresh our
heaviest
tipset in order to check whether it's weight has DECREASED as a result of equivocation.This PR also refactors / simplifies much of the "taking heavier tipset" logic. We now simply:
PersistTipsets
as we receive them, which also writes the tipset keys to the blockstoreAddToTipsetTracker
as we validate tipsets (blocks)RefreshHeaviestTipSet
with the height of the new tipset.RefreshHeaviestTipSet
works by: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