Skip to content

feat(ffi): add proposal root retrieval #910

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

Merged
merged 17 commits into from
Jun 2, 2025
Merged

Conversation

alarso16
Copy link
Contributor

This is needed to enable support of Trie wrapping in geth.

@alarso16 alarso16 marked this pull request as ready for review May 27, 2025 17:35
Copy link
Collaborator

@rkuris rkuris left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would have thought that you would just change the code so that propose always returns the hash along with the proposal ID, rather than having to look things up again and dealing with errors.

Copy link
Collaborator

@rkuris rkuris left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would have thought that you would just change the code so that propose always returns the hash along with the proposal ID, rather than having to look things up again and dealing with errors.

@alarso16 alarso16 marked this pull request as draft May 28, 2025 14:47
// Get the root hash of the new proposal.
let mut root_hash: Value = match new_proposal.root_hash_sync().map_err(|e| e.to_string())? {
Some(root) => Value::from(root.as_slice()),
None => String::new().into(),
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@rkuris This shouldn't occur in the ethhash case, but it does. Honestly I'd prefer that db.root_hash_sync() also returns nil in the no hash, but definitely out of scope. However, this must return a root for no data (the failing ethhash test)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should be addressed in #918

@@ -204,13 +204,19 @@ func kvForTest(i int) KeyValue {
// 1. By calling [Database.Propose] and then [Proposal.Commit].
// 2. By calling [Database.Update] directly - no proposal storage is needed.
func TestInsert100(t *testing.T) {
type dbView interface {
Get(key []byte) ([]byte, error)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@aaronbuchwald I think this conflicts with something you're doing, so I can get rid of this or edit if you like

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No need to worry about a conflict here, will be easy to fix when we get to it

@alarso16 alarso16 marked this pull request as ready for review May 29, 2025 19:33
Copy link
Collaborator

@rkuris rkuris left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rust code is fine, recommended some rewording of comments.

Would be good to get Aaron to look at the go code better than me.

ffi/firewood.h Outdated
Comment on lines 293 to 294
* A `Value` containing {id, root} if creating the proposal succeeded.
* The root will always be 32 bytes, and the id will be non-zero.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had to read this a few times; the second sentence is only true if the proposal succeeds.

Suggested change
* A `Value` containing {id, root} if creating the proposal succeeded.
* The root will always be 32 bytes, and the id will be non-zero.
* On success, a `Value` containing {len=id, data=hash}. In this case, the
* hash will always be 32 bytes, and the id will be non-zero.

ffi/firewood.h Outdated
@@ -318,7 +319,8 @@ struct Value fwd_propose_on_db(const struct DatabaseHandle *db,
*
* # Returns
*
* A `Value` containing {id, nil} if creating the proposal succeeded.
* A `Value` containing {id, root} if creating the proposal succeeded.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same as above

Comment on lines +45 to +48
// If the hash is empty, return the empty root hash.
if p.root == nil {
return make([]byte, RootLength), nil
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the rationale to do this lazily rather than ensure it's populated with the empty root hash if needed?

This may save a memory allocation, but seems that it will likely be simpler if we just guarantee proposals are always fully populated.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is an issue #918 for this in the ethhash front, but it might be better to enforce this on "normal" firewood as well.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm I think #918 is a separate issue.

We have two choices - what to do when there's an empty root in firewood/ethhash mode and separately how to populate the golang proposal struct. My point only applies to the latter ie. if a nil root indicates that it must actually be represented by this zeroed out byte slice, then should we just populate it when creating the proposal instance?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ahhh ok that makes sense. I'll address that.

Comment on lines +446 to +450
// TODO: Check proposal root
// Requires #918
// hash, err := proposal.Root()
// require.NoError(t, err, "%T.Root() after commit", proposal)
// require.Equalf(t, expectedHash, hash, "%T.Root() of empty trie", db)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need to include this TODO here? If this requires 918, would it make more sense to include the added check in 918?

Copy link
Contributor Author

@alarso16 alarso16 May 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

#918 is an issue, but the code is trivial so we could remove that and fill it in when addressing the issue.

@alarso16 alarso16 force-pushed the alarso16/ffi-proposal-root branch from 2a69cc4 to 531b32a Compare May 30, 2025 16:47
ffi/src/lib.rs Outdated
Comment on lines 593 to 594
/// This is used in several different ways, and should be interpreted based on the
/// function that returned it.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this the best way to handle this comment? Maybe it just depends on the context and it is, but not sure as I'm reading it

// Pin the returned value to prevent it from being garbage collected.
defer runtime.KeepAlive(v)

if v == nil {
return errNilBuffer
return nil, 0, errNilBuffer
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this case 1 as well?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should never happen, since the FFI layer is always returned an instance of the C Value, not a pointer to it. However, I thought it would be best to keep this case to avoid panics, but I can add it to the comment

// case | data | len | meaning
//
// 1. | nil | 0 | invalid
// 2. | nil | non-0 | proposal deleted everything
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

to make sure I understand - the proposal ID is non-zero because it's a real proposal, but the root is empty because the trie ends up being empty as well?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. The proposal ID is created from a counter initialized to 1, so we are guaranteed that it's nonzero.

Comment on lines +45 to +48
// If the hash is empty, return the empty root hash.
if p.root == nil {
return make([]byte, RootLength), nil
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm I think #918 is a separate issue.

We have two choices - what to do when there's an empty root in firewood/ethhash mode and separately how to populate the golang proposal struct. My point only applies to the latter ie. if a nil root indicates that it must actually be represented by this zeroed out byte slice, then should we just populate it when creating the proposal instance?

@alarso16 alarso16 enabled auto-merge (squash) June 2, 2025 15:27
@alarso16 alarso16 merged commit 5af8b75 into main Jun 2, 2025
26 checks passed
@alarso16 alarso16 deleted the alarso16/ffi-proposal-root branch June 2, 2025 15:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants