-
Notifications
You must be signed in to change notification settings - Fork 772
Add Verification + Tracking Logic To Simplex Blocks #4052
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
base: master
Are you sure you want to change the base?
Conversation
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.
Pull Request Overview
This PR refactors change tracking in the Merkle DB view and history layers, replacing the old keyChanges
+sortedKeys
approach with a unified values
map and a new recordValueChange
method. It also updates the view iterator to reflect the map-based storage and overhauls history retrieval functions to collapse and filter changes via the new map structure.
- Replaces
keyChanges
andsortedKeys
withvalues
map andrecordValueChange
inview.go
- Updates
NewIteratorWithStartAndPrefix
to iterate overvalues
and then sort the results inview_iterator.go
- Refactors history functions (
getValueChanges
,getChangesToGetToRoot
) to use the new map-based change summary inhistory.go
and updates tests
Reviewed Changes
Copilot reviewed 89 out of 91 changed files in this pull request and generated 1 comment.
File | Description |
---|---|
x/merkledb/view.go | Introduced recordValueChange method and values map |
x/merkledb/view_iterator.go | Changed iterator to range over values and sort KeyChanges |
x/merkledb/history.go | Refactored history change accumulation to use changeSummary |
x/merkledb/history_test.go | Updated tests to assert on values instead of slices |
Comments suppressed due to low confidence (2)
x/merkledb/view_iterator.go:27
- [nitpick] The local variable
changes
shadowsv.changes
and may be confusing. Consider renaming it to something likeresult
orfilteredChanges
.
changes = make([]KeyChange, 0, len(v.changes.values))
simplex/block.go
Outdated
|
||
"github.com/ava-labs/simplex" | ||
|
||
"github.com/ava-labs/avalanchego/snow/consensus/snowman" | ||
"github.com/ava-labs/avalanchego/snow/engine/snowman/block" | ||
"github.com/ava-labs/avalanchego/utils/hashing" | ||
"github.com/ava-labs/avalanchego/vms/proposervm/tree" |
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 move vms/proposervm/tree to somewhere like snow/tree
? It's a bit insanitary that Simplex uses something that is in proposervm
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.
moved to utils
simplex/block.go
Outdated
} | ||
|
||
// if we do not have it in the map, it's possible for it to be the last accepted block | ||
lastID, err := b.blockTracker.vm.LastAccepted(ctx) |
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.
Instead of doing that can't we load the last accepted to simplexDigestsToBlock
upon creation? This way we also don't depend on the implementation of the VM's LastAccepted
in this method, for instance if the VM doesn't implement this efficiently.
But the bigger benefit is that we should always find the previous block this way (unless it's the genesis block) and we have less special handling.
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.
simplex/block.go
Outdated
return fmt.Errorf("failed to get last accepted block: %w", err) | ||
} | ||
|
||
if lastID != b.vmBlock.Parent() { |
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 would very much like us to explicitly make sure that the chain induced by Simplex is aligned with the chain induced by the VM, and not only the inner VMs are aligned:
prevBlock.BlockHeader().Digest == b.metadata.Prev && prevBlock.vmBlock.ID() == b.vmBlock.Parent()
|
||
bd, exists := bt.simplexDigestsToBlock[digest] | ||
if !exists { | ||
return fmt.Errorf("%w: %s", errDigestNotFound, digest) |
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 case isn't covered by tests. Can we add a test for this?
simplex/block.go
Outdated
simplexDigestsToBlock map[simplex.Digest]*Block | ||
|
||
// lastAcceptedID is the ID of the last accepted block at the time of the block tracker creation. | ||
lastAcceptedID ids.ID |
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.
shouldn't we update these two values once we we call indexBlock
?
Can we make a test that is like TestVerifyInnerBlockBreaksHashChain
but goes through several 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.
i dont think we need to update them, since we will have always have the last accepted id/digest in the block trackers memory. I do think the name is misleading though. Added to this test
if b.blockTracker.isBlockAlreadyVerified(b.vmBlock) { | ||
return b, 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.
I think we need to grab the context lock here, because we're executing this function asynchronously.
I think that there is something I overlooked, please see comment.
return fmt.Errorf("%w: %s", errDigestNotFound, b.metadata.Prev) | ||
} | ||
|
||
if b.vmBlock.Parent() != prevBlock.vmBlock.ID() || b.metadata.Prev != prevBlock.digest { |
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.
When can b.metadata.Prev != prevBlock.digest
ever be true? Removing this check doesn't break any tests.
simplexDigestsToBlock := make(map[simplex.Digest]*Block) | ||
simplexDigestsToBlock[latestBlock.digest] = latestBlock | ||
|
||
return &blockTracker{ | ||
tree: tree.New(), | ||
simplexDigestsToBlock: simplexDigestsToBlock, | ||
} |
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.
simplexDigestsToBlock := make(map[simplex.Digest]*Block) | |
simplexDigestsToBlock[latestBlock.digest] = latestBlock | |
return &blockTracker{ | |
tree: tree.New(), | |
simplexDigestsToBlock: simplexDigestsToBlock, | |
} | |
return &blockTracker{ | |
tree: tree.New(), | |
simplexDigestsToBlock: map[simplex.Digest]*Block{ | |
latestBlock.digest: latestBlock, | |
}, | |
} |
// newBlockWithDigest is a helper function that creates a new block and sets its digest. | ||
// This is helpful since otherwise we would need the blockDeserializer to create the block. | ||
func newBlockWithDigest(t *testing.T, vmBlock snowman.Block, tracker *blockTracker, round, seq uint64, prev simplex.Digest) *Block { |
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 have found in other tests that having various newBlockWithXXX(
, newBlock(
ect scales really annoyingly.
If we don't foresee adding more things here, then feel free to ignore... But it might make sense to make a single newBlock
function which takes in a config with reasonable zero values.
That has proven to be much more flexible in my experience.
if b.blockTracker.isBlockAlreadyVerified(b.vmBlock) { | ||
return b, 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.
Don't we still need to add b
to the simplexDigestsToBlock
map here?
err := b.verifyParentMatchesPrevBlock() | ||
if 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.
For simple checks like these (where err
shouldn't be referenced later) I think we typically try to use inline if
statements. The only exception to this is with very long (multi-line) statements, which are hard to read imo.
err := b.verifyParentMatchesPrevBlock() | |
if err != nil { | |
if err := b.verifyParentMatchesPrevBlock(); err != nil { |
err = b.vmBlock.Verify(ctx) | ||
if 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.
See above comment
err = b.vmBlock.Verify(ctx) | |
if err != nil { | |
if err := b.vmBlock.Verify(ctx); err != nil { |
prevBlock := b.blockTracker.getBlockByDigest(b.metadata.Prev) | ||
if prevBlock == 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.
I know that maps return the zero value when looking up something fails, and that we never expect to actually place a nil
value in the map. But in avalanchego we typically do explicit success checks (so prevBlock, ok :=
...)
Why this should be merged
In addition to calling
Verify
on the blocks inner vmBlock, we need to do some additional checks to ensure a simplex block can be verified. Furthermore, we need to ensure that any verified blocks are eventually accepted or rejected.How this works
In simplex we do some pre-verification logic. However this is not enough, as we do not have access to the inner
vmBlock
pointed to by theprev
field in theProtocolMetadata
. This means our verification logic in avalanchego must verify thatprev
for the block we are currently verifying points to that blocks parent. To understand more about why this important, see this test.This PR also introduces a
blockTracker
struct to keep track of blocks that have been verified. Whenindex
is called on a digest, theblockTracker
callsAccept
on that block andReject
on any competing blocks.How this was tested
Need to be documented in RELEASES.md?