Skip to content

Commit ed246fd

Browse files
committed
feat: Add x/sync for Firewood
1 parent 5950ba3 commit ed246fd

File tree

6 files changed

+416
-3
lines changed

6 files changed

+416
-3
lines changed

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ require (
8989
require (
9090
github.com/Microsoft/go-winio v0.6.1 // indirect
9191
github.com/VictoriaMetrics/fastcache v1.12.1 // indirect
92-
github.com/ava-labs/firewood-go-ethhash/ffi v0.0.15 // indirect
92+
github.com/ava-labs/firewood-go-ethhash/ffi v0.0.15
9393
github.com/ava-labs/simplex v0.0.0-20250919142550-9cdfff10fd19
9494
github.com/beorn7/perks v1.0.1 // indirect
9595
github.com/bits-and-blooms/bitset v1.20.0 // indirect

x/firewood/proof.go

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
// Copyright (C) 2019-2025, Ava Labs, Inc. All rights reserved.
2+
// See the file LICENSE for licensing terms.
3+
4+
package firewood
5+
6+
import (
7+
"runtime"
8+
9+
"github.com/ava-labs/firewood-go-ethhash/ffi"
10+
11+
"github.com/ava-labs/avalanchego/ids"
12+
"github.com/ava-labs/avalanchego/utils/maybe"
13+
14+
xsync "github.com/ava-labs/avalanchego/x/sync"
15+
)
16+
17+
var (
18+
_ xsync.Marshaler[*RangeProof] = RangeProofMarshaler{}
19+
_ xsync.Marshaler[*ChangeProof] = ChangeProofMarshaler{}
20+
)
21+
22+
type RangeProofMarshaler struct{}
23+
24+
func (RangeProofMarshaler) Marshal(r *RangeProof) ([]byte, error) {
25+
if r == nil {
26+
return nil, errNilProof
27+
}
28+
if r.ffi == nil {
29+
return nil, nil
30+
}
31+
32+
data, err := r.ffi.MarshalBinary()
33+
return data, err
34+
}
35+
36+
func (RangeProofMarshaler) Unmarshal(data []byte) (*RangeProof, error) {
37+
proof := new(ffi.RangeProof)
38+
if err := proof.UnmarshalBinary(data); err != nil {
39+
return nil, err
40+
}
41+
return newRangeProof(proof), nil
42+
}
43+
44+
type RangeProof struct {
45+
ffi *ffi.RangeProof
46+
root ids.ID
47+
maxLength int
48+
}
49+
50+
// Wrap the ffi proof in our proof type.
51+
func newRangeProof(proof *ffi.RangeProof) *RangeProof {
52+
return &RangeProof{
53+
ffi: proof,
54+
}
55+
}
56+
57+
func (r *RangeProof) FindNextKey() (maybe.Maybe[[]byte], error) {
58+
// We can now get the FindNextKey iterator.
59+
nextKeyRange, err := r.ffi.FindNextKey()
60+
if err != nil {
61+
return maybe.Nothing[[]byte](), err
62+
}
63+
64+
// TODO: this panics
65+
startKey := maybe.Some(nextKeyRange.StartKey())
66+
67+
// Done using nextKeyRange
68+
if err := nextKeyRange.Free(); err != nil {
69+
return maybe.Nothing[[]byte](), err
70+
}
71+
72+
return startKey, nil
73+
}
74+
75+
type ChangeProofMarshaler struct{}
76+
77+
func (ChangeProofMarshaler) Marshal(r *ChangeProof) ([]byte, error) {
78+
if r == nil {
79+
return nil, errNilProof
80+
}
81+
if r.proof == nil {
82+
return nil, nil
83+
}
84+
85+
data, err := r.proof.MarshalBinary()
86+
return data, err
87+
}
88+
89+
func (ChangeProofMarshaler) Unmarshal(data []byte) (*ChangeProof, error) {
90+
proof := new(ffi.ChangeProof)
91+
if err := proof.UnmarshalBinary(data); err != nil {
92+
return nil, err
93+
}
94+
return newChangeProof(proof), nil
95+
}
96+
97+
type ChangeProof struct {
98+
proof *ffi.ChangeProof
99+
startRoot ids.ID
100+
endRoot ids.ID
101+
startKey maybe.Maybe[[]byte]
102+
maxLength int
103+
}
104+
105+
// Wrap the ffi proof in our proof type.
106+
func newChangeProof(proof *ffi.ChangeProof) *ChangeProof {
107+
changeProof := &ChangeProof{
108+
proof: proof,
109+
}
110+
111+
// Once this struct is out of scope, free the underlying proof.
112+
runtime.AddCleanup(changeProof, func(ffiProof *ffi.ChangeProof) {
113+
if ffiProof != nil {
114+
_ = ffiProof.Free()
115+
}
116+
}, changeProof.proof)
117+
118+
return changeProof
119+
}
120+
121+
func (c *ChangeProof) FindNextKey() (maybe.Maybe[[]byte], error) {
122+
// We can now get the FindNextKey iterator.
123+
nextKeyRange, err := c.proof.FindNextKey()
124+
if err != nil {
125+
return maybe.Nothing[[]byte](), err
126+
}
127+
128+
// TODO: this panics
129+
startKey := maybe.Some(nextKeyRange.StartKey())
130+
131+
// Done using nextKeyRange
132+
if err := nextKeyRange.Free(); err != nil {
133+
return maybe.Nothing[[]byte](), err
134+
}
135+
136+
return startKey, nil
137+
}

x/firewood/sync_db.go

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
// Copyright (C) 2019-2025, Ava Labs, Inc. All rights reserved.
2+
// See the file LICENSE for licensing terms.
3+
4+
package firewood
5+
6+
import (
7+
"context"
8+
"errors"
9+
"sync"
10+
11+
"github.com/ava-labs/firewood-go-ethhash/ffi"
12+
13+
"github.com/ava-labs/avalanchego/ids"
14+
"github.com/ava-labs/avalanchego/utils/maybe"
15+
16+
xsync "github.com/ava-labs/avalanchego/x/sync"
17+
)
18+
19+
var (
20+
_ xsync.DB[*RangeProof, *ChangeProof] = (*syncDB)(nil)
21+
22+
errNilProof = errors.New("nil proof")
23+
)
24+
25+
type syncDB struct {
26+
fw *ffi.Database
27+
lock sync.Mutex
28+
}
29+
30+
func New(db *ffi.Database) *syncDB {
31+
return &syncDB{fw: db}
32+
}
33+
34+
func (db *syncDB) GetMerkleRoot(context.Context) (ids.ID, error) {
35+
root, err := db.fw.Root()
36+
if err != nil {
37+
return ids.ID{}, err
38+
}
39+
return ids.ID(root), nil
40+
}
41+
42+
// TODO: implement
43+
func (db *syncDB) CommitChangeProof(_ context.Context, end maybe.Maybe[[]byte], proof *ChangeProof) (nextKey maybe.Maybe[[]byte], err error) {
44+
// Set up cleanup.
45+
var nextKeyRange *ffi.NextKeyRange
46+
defer func() {
47+
// If we got a nextKeyRange, free it too.
48+
if nextKeyRange != nil {
49+
err = errors.Join(err, nextKeyRange.Free())
50+
}
51+
}()
52+
53+
// Verify and commit the proof in a single step (TODO: separate these steps).
54+
// CommitRangeProof will verify the proof as part of committing it.
55+
db.lock.Lock()
56+
defer db.lock.Unlock()
57+
newRoot, err := db.fw.VerifyAndCommitChangeProof(proof.proof, ffi.Hash(proof.startRoot), ffi.Hash(proof.endRoot), proof.startKey, end, uint32(proof.maxLength))
58+
if err != nil {
59+
return maybe.Nothing[[]byte](), err
60+
}
61+
62+
// TODO: This case should be handled by `FindNextKey`.
63+
if ids.ID(newRoot) == proof.endRoot {
64+
return maybe.Nothing[[]byte](), nil
65+
}
66+
67+
return proof.FindNextKey()
68+
}
69+
70+
// Commit the range proof to the database.
71+
// TODO: This should only commit the range proof, not verify it.
72+
// This will be resolved once the Firewood supports that.
73+
// This is the last call to the proof, so it and any resources should be freed.
74+
func (db *syncDB) CommitRangeProof(_ context.Context, start, end maybe.Maybe[[]byte], proof *RangeProof) (nextKey maybe.Maybe[[]byte], err error) {
75+
// Verify and commit the proof in a single step (TODO: separate these steps).
76+
// CommitRangeProof will verify the proof as part of committing it.
77+
db.lock.Lock()
78+
defer db.lock.Unlock()
79+
newRoot, err := db.fw.VerifyAndCommitRangeProof(proof.ffi, start, end, ffi.Hash(proof.root), uint32(proof.maxLength))
80+
if err != nil {
81+
return maybe.Nothing[[]byte](), err
82+
}
83+
84+
// TODO: This case should be handled by `FindNextKey`.
85+
if ids.ID(newRoot) == proof.root {
86+
return maybe.Nothing[[]byte](), nil
87+
}
88+
89+
return proof.FindNextKey()
90+
}
91+
92+
// TODO: implement
93+
func (db *syncDB) GetChangeProof(_ context.Context, startRootID ids.ID, endRootID ids.ID, start maybe.Maybe[[]byte], end maybe.Maybe[[]byte], maxLength int) (*ChangeProof, error) {
94+
proof, err := db.fw.ChangeProof(ffi.Hash(startRootID), ffi.Hash(endRootID), start, end, uint32(maxLength))
95+
if err != nil {
96+
return nil, err
97+
}
98+
99+
return newChangeProof(proof), nil
100+
}
101+
102+
// Get the range proof between [start, end].
103+
// The returned proof must be freed when no longer needed.
104+
// Since this method is only called prior to marshalling the proof for sending over the
105+
// network, the proof will be freed when marshalled.
106+
func (db *syncDB) GetRangeProofAtRoot(_ context.Context, rootID ids.ID, start maybe.Maybe[[]byte], end maybe.Maybe[[]byte], maxLength int) (*RangeProof, error) {
107+
proof, err := db.fw.RangeProof(ffi.Hash(rootID), start, end, uint32(maxLength))
108+
if err != nil {
109+
return nil, err
110+
}
111+
112+
return newRangeProof(proof), nil
113+
}
114+
115+
// TODO: implement
116+
// Right now, we verify the proof as part of committing it, making this function a no-op.
117+
// We must only pass the necessary data to CommitChangeProof so it can verify the proof.
118+
//
119+
//nolint:revive
120+
func (db *syncDB) VerifyChangeProof(_ context.Context, proof *ChangeProof, start maybe.Maybe[[]byte], end maybe.Maybe[[]byte], expectedEndRootID ids.ID, maxLength int) error {
121+
if proof.proof == nil {
122+
return errNilProof
123+
}
124+
125+
// TODO: once firewood can verify separately from committing, do that here.
126+
// For now, pass any necessary data to be done in CommitChangeProof.
127+
// Namely, the start root, end root, and max length.
128+
proof.startRoot = expectedEndRootID
129+
proof.endRoot = expectedEndRootID
130+
proof.maxLength = maxLength
131+
return nil
132+
}
133+
134+
// TODO: implement
135+
// Right now, we verify the proof as part of committing it, making this function a no-op.
136+
// We must only pass the necessary data to CommitRangeProof so it can verify the proof.
137+
func (db *syncDB) VerifyRangeProof(_ context.Context, proof *RangeProof, start maybe.Maybe[[]byte], end maybe.Maybe[[]byte], expectedEndRootID ids.ID, maxLength int) error {
138+
if proof.ffi == nil {
139+
return errNilProof
140+
}
141+
142+
proof.root = expectedEndRootID
143+
proof.maxLength = maxLength
144+
145+
return proof.ffi.Verify(ffi.Hash(expectedEndRootID), start, end, uint32(maxLength))
146+
}
147+
148+
// TODO: implement
149+
// No error is returned to ensure some tests pass.
150+
//
151+
//nolint:revive
152+
func (db *syncDB) Clear() error {
153+
return nil
154+
}

0 commit comments

Comments
 (0)