Skip to content
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 52 additions & 0 deletions libevm/triedb/firewood/firewood.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
// Copyright 2025 the libevm authors.
//
// The libevm additions to go-ethereum are free software: you can redistribute
// them and/or modify them under the terms of the GNU Lesser General Public License
// as published by the Free Software Foundation, either version 3 of the License,
// or (at your option) any later version.
//
// The libevm additions are distributed in the hope that they will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser
// General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
Copy link

Choose a reason for hiding this comment

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

Is this the correct license for our firewood code?

// along with the go-ethereum library. If not, see
// <http://www.gnu.org/licenses/>.

// The firewood package provides a [triedb.DBOverride] backed by [Firewood].
//
// [Firewood]: https://github.com/ava-labs/firewood
package firewood

import (
"errors"
"runtime"

"github.com/ava-labs/libevm/common"
"github.com/ava-labs/libevm/libevm/stateconf"
"github.com/ava-labs/libevm/trie/trienode"
"github.com/ava-labs/libevm/trie/triestate"
"github.com/ava-labs/libevm/triedb"
)

var _ triedb.DBOverride = (*database)(nil)

type database struct {
triedb.DBOverride // TODO(alarso16) remove once this type implements the interface
}

func (db *database) Update(root, parent common.Hash, block uint64, nodes *trienode.MergedNodeSet, states *triestate.Set, opts ...stateconf.TrieDBUpdateOption) error {
// TODO(alarso16)
var _ *proposal = extras.MergedNodeSet.Get(nodes)

db.afterUpdate(nodes) // MUST be the last statement before the final return
return errors.New("unimplemented")
}

// afterUpdate MUST be called at the end of [database.Update] to ensure that the
// Rust handle isn't freed any earlier. This is an overly cautious, defensive
// approach that will make Rustaceans scream "I told you so".
func (db *database) afterUpdate(nodes *trienode.MergedNodeSet) {
runtime.KeepAlive(extras.MergedNodeSet.Get(nodes))
}
89 changes: 89 additions & 0 deletions libevm/triedb/firewood/proposals.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
// Copyright 2025 the libevm authors.
//
// The libevm additions to go-ethereum are free software: you can redistribute
// them and/or modify them under the terms of the GNU Lesser General Public License
// as published by the Free Software Foundation, either version 3 of the License,
// or (at your option) any later version.
//
// The libevm additions are distributed in the hope that they will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser
// General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with the go-ethereum library. If not, see
// <http://www.gnu.org/licenses/>.

package firewood

import (
"fmt"
"runtime"

"github.com/ava-labs/libevm/common"
"github.com/ava-labs/libevm/trie/trienode"
)

// RegisterExtras registers Firewood proposals with [trienode.RegisterExtras].
// This MUST be called in and only in tests / package main to avoid polluting
// other packages. A call to RegisterExtras is required for the rest of this
// package to function correctly.
func RegisterExtras() {
extras = trienode.RegisterExtras[proposal, proposal, struct{}]()
}

var extras trienode.ExtraPayloads[*proposal, *proposal, *struct{}]

// A proposal is embedded as a payload in the [trienode.NodeSet] object returned
// by trie `Commit()`. A preceding call to [RegisterExtras] ensures that the
// proposal will be propagated to [Database.Update].
//
// After construction, [proposal.setFinalizer] SHOULD be called to ensure
// release of resources via [proposal.free] once the proposal is garbage
// collected.
type proposal struct {
// root MUST match the argument returned by the trie's `Commit()` method.
root common.Hash

// TODO(alarso16) add handles etc. here and clean them up in [proposal.free]

finalized chan struct{} // https://go.dev/doc/gc-guide#Testing_object_death
}

func (p *proposal) injectInto(ns *trienode.NodeSet) {
extras.NodeSet.Set(ns, p)
}

// setFinalizer calls [runtime.SetFinalizer] with `p`.
func (p *proposal) setFinalizer() {
p.finalized = make(chan struct{})
runtime.SetFinalizer(p, (*proposal).finalizer)
}

// finalizer is expected to be passed to [runtime.SetFinalizer], abstracted as a
// method to guarantee that it doesn't accidentally capture the value being
// collected, thus resurrecting it.
func (p *proposal) finalizer() {
p.free()
close(p.finalized)
}

// free is called when the [proposal] is no longer reachable.
func (p *proposal) free() {
// TODO(alarso16) free the Rust object(s).
}

// AfterMergeNodeSet implements [trienode.MergedNodeSetHooks], copying at most
// one proposal handle into the merged set.
func (h *proposal) AfterMergeNodeSet(into *trienode.MergedNodeSet, ns *trienode.NodeSet) error {
if p := extras.MergedNodeSet.Get(into); p.root != (common.Hash{}) {
return fmt.Errorf(">1 %T carrying non-zero %T", ns, p)
}
// The GC finalizer is attached to the [payload], not to the [handle], so
// we have to copy the entire object to ensure that it remains reachable.
extras.MergedNodeSet.Set(into, extras.NodeSet.Get(ns))
return nil
}

// AfterAddNode implements [trienode.NodeSetHooks] as a noop.
func (h *proposal) AfterAddNode(*trienode.NodeSet, []byte, *trienode.Node) {}
140 changes: 140 additions & 0 deletions libevm/triedb/firewood/proposals_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
// Copyright 2025 the libevm authors.
//
// The libevm additions to go-ethereum are free software: you can redistribute
// them and/or modify them under the terms of the GNU Lesser General Public License
// as published by the Free Software Foundation, either version 3 of the License,
// or (at your option) any later version.
//
// The libevm additions are distributed in the hope that they will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser
// General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with the go-ethereum library. If not, see
// <http://www.gnu.org/licenses/>.

package firewood

import (
"os"
"runtime"
"testing"
"time"

"github.com/stretchr/testify/require"

"github.com/ava-labs/libevm/common"
"github.com/ava-labs/libevm/core/rawdb"
"github.com/ava-labs/libevm/core/state"
"github.com/ava-labs/libevm/core/types"
"github.com/ava-labs/libevm/ethdb"
"github.com/ava-labs/libevm/libevm/stateconf"
"github.com/ava-labs/libevm/trie"
"github.com/ava-labs/libevm/trie/trienode"
"github.com/ava-labs/libevm/trie/triestate"
"github.com/ava-labs/libevm/triedb"
databasepkg "github.com/ava-labs/libevm/triedb/database"
"github.com/ava-labs/libevm/triedb/hashdb"
)

func TestMain(m *testing.M) {
RegisterExtras()
os.Exit(m.Run())
}

// A cacheWithDummyProposals overrides `OpenTrie()` to returns a
// [trieWithDummyProposals].
type cacheWithDummyProposals struct {
state.Database
}

func (db *cacheWithDummyProposals) OpenTrie(root common.Hash) (state.Trie, error) {
t, err := db.Database.OpenTrie(root)
if err != nil {
return nil, err
}
return &trieWithDummyProposals{Trie: t}, nil
}

// A trieWithDummyProposals overrides `Commit()` to inject a [proposal] into the
// returned [trienode.NodeSet].
type trieWithDummyProposals struct {
state.Trie
}

func (t *trieWithDummyProposals) Commit(collectLeaf bool) (common.Hash, *trienode.NodeSet, error) {
root, set, err := t.Trie.Commit(collectLeaf)
if err != nil {
return common.Hash{}, nil, err
}
// This, combined with [proposalPayload.MergeNodeSet], is where the magic
// happens. We use the existing geth plumbing to carry the proposal back to
// [hashDBWithDummyProposals.Update], knowing that the Go GC will trigger
// the FFI call to free the Rust memory.
p := &proposal{root: root}
p.setFinalizer()
p.injectInto(set)

return root, set, nil
}

// A hashDBWithDummyProposals overrides `Update()` to capture the [proposal]
// propagated from [trieWithDummyProposals.Commit].
type hashDBWithDummyProposals struct {
*hashdb.Database
got *proposal
}

func (db *hashDBWithDummyProposals) Reader(root common.Hash) (databasepkg.Reader, error) {
return db.Database.Reader(root)
}

func (db *hashDBWithDummyProposals) Update(root, parent common.Hash, block uint64, nodes *trienode.MergedNodeSet, states *triestate.Set, opts ...stateconf.TrieDBUpdateOption) error {
db.got = extras.MergedNodeSet.Get(nodes)
return db.Database.Update(root, parent, block, nodes, states, opts...)
}

func TestProposalPropagation(t *testing.T) {
db := rawdb.NewMemoryDatabase()
backend := &hashDBWithDummyProposals{
Database: hashdb.New(db, nil, trie.MerkleResolver{}),
}
tdb := triedb.NewDatabase(db, &triedb.Config{
DBOverride: func(db ethdb.Database) triedb.DBOverride {
return backend
},
})

cache := &cacheWithDummyProposals{
Database: state.NewDatabaseWithNodeDB(db, tdb),
}
sdb, err := state.New(types.EmptyRootHash, cache, nil)
require.NoError(t, err, "state.New([empty root], ...)")

sdb.SetState(common.Address{}, common.Hash{}, common.Hash{42})
root, err := sdb.Commit(1, false)
require.NoErrorf(t, err, "%T.Commit()", sdb)

if got, want := backend.got.root, root; got != want {
t.Errorf("got %v; want %v", got, want)
}

t.Run("GC_finalizer_invoked", func(t *testing.T) {
finalized := backend.got.finalized

sdb = nil
cache = nil
tdb = nil
backend = nil

// Note that [runtime.GC] doesn't block on finalizers; see
// https://go.dev/doc/gc-guide#Testing_object_death
runtime.GC()
select {
case <-finalized:
case <-time.After(5 * time.Second):
t.Errorf("%T finalizer did not run", &proposal{})
}
})
}
10 changes: 9 additions & 1 deletion trie/trienode/node.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import (
"strings"

"github.com/ava-labs/libevm/common"
"github.com/ava-labs/libevm/libevm/pseudo"
)

// Node is a wrapper which contains the encoded blob of the trie node and its
Expand All @@ -30,6 +31,8 @@ import (
type Node struct {
Hash common.Hash // Node hash, empty for deleted node
Blob []byte // Encoded node blob, nil for the deleted node

extra *pseudo.Type // libevm
}

// Size returns the total memory size used by this node.
Expand Down Expand Up @@ -64,6 +67,8 @@ type NodeSet struct {
Nodes map[string]*Node
updates int // the count of updated and inserted nodes
deletes int // the count of deleted nodes

extra *pseudo.Type // libevm
}

// NewNodeSet initializes a node set. The owner is zero for the account trie and
Expand Down Expand Up @@ -97,6 +102,7 @@ func (set *NodeSet) AddNode(path []byte, n *Node) {
set.updates += 1
}
set.Nodes[string(path)] = n
set.mergePayload(path, n)
}

// Merge adds a set of nodes into the set.
Expand Down Expand Up @@ -164,6 +170,8 @@ func (set *NodeSet) Summary() string {
// MergedNodeSet represents a merged node set for a group of tries.
type MergedNodeSet struct {
Sets map[common.Hash]*NodeSet

extra *pseudo.Type // libevm
}

// NewMergedNodeSet initializes an empty merged set.
Expand All @@ -180,7 +188,7 @@ func NewWithNodeSet(set *NodeSet) *MergedNodeSet {

// Merge merges the provided dirty nodes of a trie into the set. The assumption
// is held that no duplicated set belonging to the same trie will be merged twice.
func (set *MergedNodeSet) Merge(other *NodeSet) error {
func (set *MergedNodeSet) merge(other *NodeSet) error {
subset, present := set.Sets[other.Owner]
if present {
return subset.Merge(other.Owner, other.Nodes)
Expand Down
Loading
Loading