Skip to content

Commit 058bc81

Browse files
committed
feat: allow multiple handles per proposal payload
1 parent 079a98e commit 058bc81

File tree

3 files changed

+63
-34
lines changed

3 files changed

+63
-34
lines changed

libevm/triedb/firewood/firewood.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ type database struct {
3838

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

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

libevm/triedb/firewood/proposals.go

Lines changed: 30 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -29,53 +29,59 @@ import (
2929
// other packages. A call to RegisterExtras is required for the rest of this
3030
// package to function correctly.
3131
func RegisterExtras() {
32-
extras = trienode.RegisterExtras[proposal, proposal, struct{}]()
32+
extras = trienode.RegisterExtras[proposals, proposals, struct{}]()
3333
}
3434

35-
var extras trienode.ExtraPayloads[*proposal, *proposal, *struct{}]
35+
var extras trienode.ExtraPayloads[*proposals, *proposals, *struct{}]
3636

37-
// A proposal is embedded as a payload in the [trienode.NodeSet] object returned
38-
// by trie `Commit()`. A preceding call to [RegisterExtras] ensures that the
39-
// proposal will be propagated to [Database.Update].
40-
//
41-
// After construction, [proposal.setFinalizer] SHOULD be called to ensure
42-
// release of resources via [proposal.free] once the proposal is garbage
43-
// collected.
44-
type proposal struct {
37+
// A proposals carrier is embedded as a payload in the [trienode.NodeSet] object
38+
// returned by trie `Commit()`. A preceding call to [RegisterExtras] ensures
39+
// that the proposals will be propagated to [Database.Update].
40+
type proposals struct {
4541
// root MUST match the argument returned by the trie's `Commit()` method.
4642
root common.Hash
47-
48-
// TODO(alarso16) add handles etc. here and clean them up in [proposal.free]
49-
50-
finalized chan struct{} // https://go.dev/doc/gc-guide#Testing_object_death
43+
// handles MAY carry >=1 handle, based off different parents, but all MUST
44+
// result in the same root (i.e. the one specified in the other field).
45+
handles []*handle
5146
}
5247

53-
func (p *proposal) injectInto(ns *trienode.NodeSet) {
48+
func (p *proposals) injectInto(ns *trienode.NodeSet) {
5449
extras.NodeSet.Set(ns, p)
5550
}
5651

52+
// A handle carries a Firewood FFI proposal handle (i.e. Rust-owned memory).
53+
// After construction, [handle.setFinalizer] SHOULD be called to ensure release
54+
// of resources via [handle.free] once the handle is garbage collected.
55+
type handle struct {
56+
// TODO(alarso16) place the FFI handle here
57+
58+
// finalized is set by [handle.setFinalizer] to signal when said finalizer
59+
// has run; see https://go.dev/doc/gc-guide#Testing_object_death
60+
finalized chan struct{}
61+
}
62+
5763
// setFinalizer calls [runtime.SetFinalizer] with `p`.
58-
func (p *proposal) setFinalizer() {
59-
p.finalized = make(chan struct{})
60-
runtime.SetFinalizer(p, (*proposal).finalizer)
64+
func (h *handle) setFinalizer() {
65+
h.finalized = make(chan struct{})
66+
runtime.SetFinalizer(h, (*handle).finalizer)
6167
}
6268

6369
// finalizer is expected to be passed to [runtime.SetFinalizer], abstracted as a
6470
// method to guarantee that it doesn't accidentally capture the value being
6571
// collected, thus resurrecting it.
66-
func (p *proposal) finalizer() {
67-
p.free()
68-
close(p.finalized)
72+
func (h *handle) finalizer() {
73+
h.free()
74+
close(h.finalized)
6975
}
7076

7177
// free is called when the [proposal] is no longer reachable.
72-
func (p *proposal) free() {
78+
func (h *handle) free() {
7379
// TODO(alarso16) free the Rust object(s).
7480
}
7581

7682
// AfterMergeNodeSet implements [trienode.MergedNodeSetHooks], copying at most
7783
// one proposal handle into the merged set.
78-
func (h *proposal) AfterMergeNodeSet(into *trienode.MergedNodeSet, ns *trienode.NodeSet) error {
84+
func (p *proposals) AfterMergeNodeSet(into *trienode.MergedNodeSet, ns *trienode.NodeSet) error {
7985
if p := extras.MergedNodeSet.Get(into); p.root != (common.Hash{}) {
8086
return fmt.Errorf(">1 %T carrying non-zero %T", ns, p)
8187
}
@@ -86,4 +92,4 @@ func (h *proposal) AfterMergeNodeSet(into *trienode.MergedNodeSet, ns *trienode.
8692
}
8793

8894
// AfterAddNode implements [trienode.NodeSetHooks] as a noop.
89-
func (h *proposal) AfterAddNode(*trienode.NodeSet, []byte, *trienode.Node) {}
95+
func (p *proposals) AfterAddNode(*trienode.NodeSet, []byte, *trienode.Node) {}

libevm/triedb/firewood/proposals_test.go

Lines changed: 32 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,15 @@
1717
package firewood
1818

1919
import (
20+
"context"
21+
"fmt"
2022
"os"
2123
"runtime"
2224
"testing"
2325
"time"
2426

2527
"github.com/stretchr/testify/require"
28+
"golang.org/x/sync/errgroup"
2629

2730
"github.com/ava-labs/libevm/common"
2831
"github.com/ava-labs/libevm/core/rawdb"
@@ -72,8 +75,13 @@ func (t *trieWithDummyProposals) Commit(collectLeaf bool) (common.Hash, *trienod
7275
// happens. We use the existing geth plumbing to carry the proposal back to
7376
// [hashDBWithDummyProposals.Update], knowing that the Go GC will trigger
7477
// the FFI call to free the Rust memory.
75-
p := &proposal{root: root}
76-
p.setFinalizer()
78+
p := &proposals{
79+
root: root,
80+
handles: []*handle{{}, {}},
81+
}
82+
for _, h := range p.handles {
83+
h.setFinalizer()
84+
}
7785
p.injectInto(set)
7886

7987
return root, set, nil
@@ -83,7 +91,7 @@ func (t *trieWithDummyProposals) Commit(collectLeaf bool) (common.Hash, *trienod
8391
// propagated from [trieWithDummyProposals.Commit].
8492
type hashDBWithDummyProposals struct {
8593
*hashdb.Database
86-
got *proposal
94+
got *proposals
8795
}
8896

8997
func (db *hashDBWithDummyProposals) Reader(root common.Hash) (databasepkg.Reader, error) {
@@ -120,8 +128,11 @@ func TestProposalPropagation(t *testing.T) {
120128
t.Errorf("got %v; want %v", got, want)
121129
}
122130

123-
t.Run("GC_finalizer_invoked", func(t *testing.T) {
124-
finalized := backend.got.finalized
131+
t.Run("GC_finalizers_invoked", func(t *testing.T) {
132+
var finalized []chan struct{}
133+
for _, h := range backend.got.handles {
134+
finalized = append(finalized, h.finalized)
135+
}
125136

126137
// Everything that might still hold a reference to the `proposal`,
127138
// stopping it from being garbage collected.
@@ -133,10 +144,22 @@ func TestProposalPropagation(t *testing.T) {
133144
// Note that [runtime.GC] doesn't block on finalizers; see
134145
// https://go.dev/doc/gc-guide#Testing_object_death
135146
runtime.GC()
136-
select {
137-
case <-finalized:
138-
case <-time.After(5 * time.Second):
139-
t.Errorf("%T finalizer did not run", &proposal{})
147+
148+
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
149+
defer cancel()
150+
g, ctx := errgroup.WithContext(ctx)
151+
152+
for i, ch := range finalized {
153+
g.Go(func() error {
154+
select {
155+
case <-ch:
156+
return nil
157+
case <-ctx.Done():
158+
return fmt.Errorf("%T[%d] finalizer didn't run", &handle{}, i)
159+
}
160+
})
140161
}
162+
163+
require.NoError(t, g.Wait())
141164
})
142165
}

0 commit comments

Comments
 (0)