diff --git a/libevm/triedb/firewood/firewood.go b/libevm/triedb/firewood/firewood.go
new file mode 100644
index 00000000000..5448dbde738
--- /dev/null
+++ b/libevm/triedb/firewood/firewood.go
@@ -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
+// along with the go-ethereum library. If not, see
+// .
+
+// 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 _ *proposals = 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))
+}
diff --git a/libevm/triedb/firewood/proposals.go b/libevm/triedb/firewood/proposals.go
new file mode 100644
index 00000000000..5918381391a
--- /dev/null
+++ b/libevm/triedb/firewood/proposals.go
@@ -0,0 +1,95 @@
+// 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
+// .
+
+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[proposals, proposals, struct{}]()
+}
+
+var extras trienode.ExtraPayloads[*proposals, *proposals, *struct{}]
+
+// A proposals carrier is embedded as a payload in the [trienode.NodeSet] object
+// returned by trie `Commit()`. A preceding call to [RegisterExtras] ensures
+// that the proposals will be propagated to [Database.Update].
+type proposals struct {
+ // root MUST match the argument returned by the trie's `Commit()` method.
+ root common.Hash
+ // handles MAY carry >=1 handle, based off different parents, but all MUST
+ // result in the same root (i.e. the one specified in the other field).
+ handles []*handle
+}
+
+func (p *proposals) injectInto(ns *trienode.NodeSet) {
+ extras.NodeSet.Set(ns, p)
+}
+
+// A handle carries a Firewood FFI proposal handle (i.e. Rust-owned memory).
+// After construction, [handle.setFinalizer] SHOULD be called to ensure release
+// of resources via [handle.free] once the handle is garbage collected.
+type handle struct {
+ // TODO(alarso16) place the FFI handle here
+
+ // finalized is set by [handle.setFinalizer] to signal when said finalizer
+ // has run; see https://go.dev/doc/gc-guide#Testing_object_death
+ finalized chan struct{}
+}
+
+// setFinalizer calls [runtime.SetFinalizer] with `p`.
+func (h *handle) setFinalizer() {
+ h.finalized = make(chan struct{})
+ runtime.SetFinalizer(h, (*handle).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 (h *handle) finalizer() {
+ h.free()
+ close(h.finalized)
+}
+
+// free is called when the [proposal] is no longer reachable.
+func (h *handle) free() {
+ // TODO(alarso16) free the Rust object(s).
+}
+
+// AfterMergeNodeSet implements [trienode.MergedNodeSetHooks], copying at most
+// one proposal handle into the merged set.
+func (p *proposals) 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 (p *proposals) AfterAddNode(*trienode.NodeSet, []byte, *trienode.Node) {}
diff --git a/libevm/triedb/firewood/proposals_test.go b/libevm/triedb/firewood/proposals_test.go
new file mode 100644
index 00000000000..1f96cec5987
--- /dev/null
+++ b/libevm/triedb/firewood/proposals_test.go
@@ -0,0 +1,165 @@
+// 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
+// .
+
+package firewood
+
+import (
+ "context"
+ "fmt"
+ "os"
+ "runtime"
+ "testing"
+ "time"
+
+ "github.com/stretchr/testify/require"
+ "golang.org/x/sync/errgroup"
+
+ "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 := &proposals{
+ root: root,
+ handles: []*handle{{}, {}},
+ }
+ for _, h := range p.handles {
+ h.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 *proposals
+}
+
+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_finalizers_invoked", func(t *testing.T) {
+ var finalized []chan struct{}
+ for _, h := range backend.got.handles {
+ finalized = append(finalized, h.finalized)
+ }
+
+ // Everything that might still hold a reference to the `proposal`,
+ // stopping it from being garbage collected.
+ 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()
+
+ ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
+ defer cancel()
+ g, ctx := errgroup.WithContext(ctx)
+
+ for i, ch := range finalized {
+ g.Go(func() error {
+ select {
+ case <-ch:
+ return nil
+ case <-ctx.Done():
+ return fmt.Errorf("%T[%d] finalizer didn't run", &handle{}, i)
+ }
+ })
+ }
+
+ require.NoError(t, g.Wait())
+ })
+}
diff --git a/trie/trienode/node.go b/trie/trienode/node.go
index 8bd0a18ba38..939d042efe7 100644
--- a/trie/trienode/node.go
+++ b/trie/trienode/node.go
@@ -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
@@ -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
}
// Size returns the total memory size used by this node.
@@ -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
}
// NewNodeSet initializes a node set. The owner is zero for the account trie and
@@ -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.
@@ -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
}
// NewMergedNodeSet initializes an empty merged set.
@@ -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)
diff --git a/trie/trienode/node.libevm.go b/trie/trienode/node.libevm.go
new file mode 100644
index 00000000000..b45c0436640
--- /dev/null
+++ b/trie/trienode/node.libevm.go
@@ -0,0 +1,166 @@
+// 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
+// .
+
+package trienode
+
+import (
+ "github.com/ava-labs/libevm/libevm/pseudo"
+ "github.com/ava-labs/libevm/libevm/register"
+)
+
+// MergedNodeSetHooks are called as part of standard [MergedNodeSet] behaviour.
+type MergedNodeSetHooks interface {
+ // AfterMergeNodeSet is called at the end of [MergedNodeSet.Merge], with the
+ // method receiver and argument propagated.
+ AfterMergeNodeSet(receiver *MergedNodeSet, _ *NodeSet) error
+}
+
+// NodeSetHooks are called as part of standard [NodeSet] behaviour.
+type NodeSetHooks interface {
+ // AfterAddNode is called at the end of [NodeSet.AddNode], with the method
+ // receiver and arguments propagated.
+ AfterAddNode(receiver *NodeSet, path []byte, _ *Node)
+}
+
+// RegisterExtras registers types `MNSPtr`, `NSPtr`, and `N` to be carried as
+// extra payloads in [MergedNodeSet], [NodeSet], and [Node] objects
+// respectively. It MUST NOT be called more than once.
+func RegisterExtras[
+ MNS, NS, N any,
+ MNSPtr interface {
+ MergedNodeSetHooks
+ *MNS
+ },
+ NSPtr interface {
+ NodeSetHooks
+ *NS
+ },
+ NPtr interface{ *N },
+]() ExtraPayloads[MNSPtr, NSPtr, NPtr] {
+ payloads := ExtraPayloads[MNSPtr, NSPtr, NPtr]{
+ MergedNodeSet: pseudo.NewAccessor[*MergedNodeSet, MNSPtr](
+ (*MergedNodeSet).extraPayload,
+ func(s *MergedNodeSet, t *pseudo.Type) { s.extra = t },
+ ),
+ NodeSet: pseudo.NewAccessor[*NodeSet, NSPtr](
+ (*NodeSet).extraPayload,
+ func(s *NodeSet, t *pseudo.Type) { s.extra = t },
+ ),
+ Node: pseudo.NewAccessor[*Node, NPtr](
+ (*Node).extraPayload,
+ func(n *Node, t *pseudo.Type) { n.extra = t },
+ ),
+ }
+
+ registeredExtras.MustRegister(&extraConstructors{
+ newMergedNodeSet: pseudo.NewConstructor[MNS]().NewPointer, // i.e. non-nil MNSPtr
+ newNodeSet: pseudo.NewConstructor[NS]().NewPointer, // i.e. non-nil NSPtr
+ newNode: pseudo.NewConstructor[N]().NewPointer, // i.e. non-nil N
+ hooks: payloads,
+ })
+
+ return payloads
+}
+
+// TestOnlyClearRegisteredExtras clears any previous call to [RegisterExtras].
+// It panics if called from a non-testing call stack.
+func TestOnlyClearRegisteredExtras() {
+ registeredExtras.TestOnlyClear()
+}
+
+var registeredExtras register.AtMostOnce[*extraConstructors]
+
+type extraConstructors struct {
+ newMergedNodeSet func() *pseudo.Type
+ newNodeSet func() *pseudo.Type
+ newNode func() *pseudo.Type
+ hooks interface {
+ hooksFromMNS(*MergedNodeSet) MergedNodeSetHooks
+ hooksFromNS(*NodeSet) NodeSetHooks
+ }
+}
+
+// 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 {
+ if err := set.merge(other); err != nil {
+ return err
+ }
+ if r := registeredExtras; r.Registered() {
+ return r.Get().hooks.hooksFromMNS(set).AfterMergeNodeSet(set, other)
+ }
+ return nil
+}
+
+func (set *NodeSet) mergePayload(path []byte, n *Node) {
+ if r := registeredExtras; r.Registered() {
+ r.Get().hooks.hooksFromNS(set).AfterAddNode(set, path, n)
+ }
+}
+
+// ExtraPayloads provides strongly typed access to the extra payloads carried by
+// [MergedNodeSet], [NodeSet], and [Node] ojects. The only valid way to
+// construct an instance is by a call to [RegisterExtras]. The default `MNSPtr`
+// and `NSPtr` default values, returned by [pseudo.Accessor.Get] are guaranteed
+// to be non-nil pointers to zero values, equivalent to, e.g. `new(MNS)`.
+type ExtraPayloads[
+ MNSPtr MergedNodeSetHooks,
+ NSPtr NodeSetHooks,
+ NPtr any,
+] struct {
+ MergedNodeSet pseudo.Accessor[*MergedNodeSet, MNSPtr]
+ NodeSet pseudo.Accessor[*NodeSet, NSPtr]
+ Node pseudo.Accessor[*Node, NPtr]
+}
+
+func (e ExtraPayloads[MNS, NS, N]) hooksFromMNS(s *MergedNodeSet) MergedNodeSetHooks {
+ return e.MergedNodeSet.Get(s)
+}
+
+func (e ExtraPayloads[MNS, NS, N]) hooksFromNS(s *NodeSet) NodeSetHooks {
+ return e.NodeSet.Get(s)
+}
+
+func extraPayloadOrSetDefault(field **pseudo.Type, construct func(*extraConstructors) *pseudo.Type) *pseudo.Type {
+ r := registeredExtras
+ if !r.Registered() {
+ // See params.ChainConfig.extraPayload() for panic rationale.
+ panic(".extraPayload() called before RegisterExtras()")
+ }
+ if *field == nil {
+ *field = construct(r.Get())
+ }
+ return *field
+}
+
+func (set *MergedNodeSet) extraPayload() *pseudo.Type {
+ return extraPayloadOrSetDefault(&set.extra, func(c *extraConstructors) *pseudo.Type {
+ return c.newMergedNodeSet()
+ })
+}
+
+func (set *NodeSet) extraPayload() *pseudo.Type {
+ return extraPayloadOrSetDefault(&set.extra, func(c *extraConstructors) *pseudo.Type {
+ return c.newNodeSet()
+ })
+}
+
+func (n *Node) extraPayload() *pseudo.Type {
+ return extraPayloadOrSetDefault(&n.extra, func(c *extraConstructors) *pseudo.Type {
+ return c.newNode()
+ })
+}
diff --git a/trie/trienode/node.libevm_test.go b/trie/trienode/node.libevm_test.go
new file mode 100644
index 00000000000..e6bc6e334eb
--- /dev/null
+++ b/trie/trienode/node.libevm_test.go
@@ -0,0 +1,86 @@
+// 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
+// .
+
+package trienode
+
+import (
+ "maps"
+ "testing"
+
+ "github.com/google/go-cmp/cmp"
+ "github.com/stretchr/testify/require"
+
+ "github.com/ava-labs/libevm/common"
+)
+
+type nodePayload struct {
+ x uint64
+}
+
+type setPayload struct {
+ added map[string]uint64
+}
+
+func (p *setPayload) AfterAddNode(_ *NodeSet, path []byte, n *Node) {
+ if p.added == nil {
+ p.added = make(map[string]uint64)
+ }
+ p.added[string(path)] = extras.Node.Get(n).x
+}
+
+type mergedSetPayload struct {
+ merged []map[string]uint64
+}
+
+func (p *mergedSetPayload) AfterMergeNodeSet(_ *MergedNodeSet, ns *NodeSet) error {
+ p.merged = append(p.merged, maps.Clone(extras.NodeSet.Get(ns).added))
+ return nil
+}
+
+var extras ExtraPayloads[*mergedSetPayload, *setPayload, *nodePayload]
+
+func TestExtras(t *testing.T) {
+ extras = RegisterExtras[mergedSetPayload, setPayload, nodePayload]()
+ t.Cleanup(TestOnlyClearRegisteredExtras)
+
+ n1 := New(common.Hash{0}, nil)
+ extras.Node.Set(n1, &nodePayload{x: 1})
+ n42 := New(common.Hash{1}, nil)
+ extras.Node.Set(n42, &nodePayload{x: 42})
+
+ set := NewNodeSet(common.Hash{})
+ merge := NewMergedNodeSet()
+ set.AddNode([]byte("n1"), n1)
+ require.NoError(t, merge.Merge(set))
+
+ set.AddNode([]byte("n42"), n42)
+ require.NoError(t, merge.Merge(set))
+
+ got := extras.MergedNodeSet.Get(merge).merged
+ want := []map[string]uint64{
+ {
+ "n1": 1,
+ },
+ {
+ "n1": 1,
+ "n42": 42,
+ },
+ }
+
+ if diff := cmp.Diff(want, got); diff != "" {
+ t.Errorf("%T payload diff (-want +got):\n%s", merge, diff)
+ }
+}