Skip to content
This repository was archived by the owner on Nov 25, 2025. It is now read-only.

Track Proposals at hash time #1238

@alarso16

Description

@alarso16

Currently in the Firewood implementation in coreth, we create a Trie, which tracks all writes attempted to the database. Once Hash() is called, a proposal is created, generating the state root, and the proposal is dropped. This is because there is very little information at this point (namely, only the parent state root). If the state root generated is incorrect, then Commit() is never called. If Commit() is called, this takes all keys and values from the list, adds it to a map, and passes it to triedb through the statedb to create the "actual" proposal. This proposal is then the one used for the remainder of its lifetime. This has the following issues:

  1. Creating multiple proposals is generally unnecessary, since we will almost always create the proposal again.
  2. Batch ordering is arbitrary - this is important because firewood uses prefix deletion, which could potentially be manipulated to cause an invalid state

However, the solution is not intuitive:

  • When hashing a batch for the database, we create a Firewood proposal, which needs explicitly dropped.
    Because it needs explicitly dropped, unless you drop it immediately after hashing, youcan't guarantee it will happen when executing blocks (e.g. state roots don't match, return early)
  • Several APIs are also granted access to the statedb and will hash, but we don't want that polluting our proposal tracking
  • There are several possible edge cases to be aware of (e.g. empty proposals, duplicate state roots).

The obvious solution is to have a cleanup method on the statedb and the account trie, and for the geth tries, this could do nothing, but on ours, drops any stateful objects. Other options include upgrading to Go 1.24 and using cleanup methods (so trusting the GC), or using some LRU with all recent proposals sufficiently large to not flush the "important" proposals, and potentially drop API requests.

The other especially strange portion of this is the lack of information in the Trie object, explicitly we can't uniquely identify the parent proposal (duplicate state roots):

       P4
       |
       P3
      /  \
    P1    P2
      \  /
       R

Since P3 must exist twice (for both blocks), when trying to make P4 and only know the parent root P3, how do you decide which proposal to create? You must create it for both P3, and then cleanup the unused one in Update, once you know the block hashes to identify the parent.

Metadata

Metadata

Assignees

Type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions