diff --git a/database/merkle/firewood/syncer/evm_syncer.go b/database/merkle/firewood/syncer/evm_syncer.go index fe65ae67b2dc..5c0659ce8c52 100644 --- a/database/merkle/firewood/syncer/evm_syncer.go +++ b/database/merkle/firewood/syncer/evm_syncer.go @@ -5,7 +5,6 @@ package syncer import ( "context" - "errors" "github.com/ava-labs/firewood-go-ethhash/ffi" "github.com/ava-labs/libevm/common" @@ -16,7 +15,7 @@ import ( "github.com/ava-labs/avalanchego/utils/maybe" ) -var _ sync.DB[*RangeProof, struct{}] = (*evmDB)(nil) +var _ sync.DB[*RangeProof, *ChangeProof] = (*evmDB)(nil) type CodeQueue interface { AddCode(context.Context, []common.Hash) error @@ -33,11 +32,11 @@ func NewEVM( codeQueue CodeQueue, targetRoot ids.ID, proofClient *p2p.Client, -) (*sync.Syncer[*RangeProof, struct{}], error) { +) (*sync.Syncer[*RangeProof, *ChangeProof], error) { return newWithDB( config, &evmDB{ - db: &database{db}, + db: &database{db: db}, codeQueue: codeQueue, }, targetRoot, @@ -66,8 +65,23 @@ func (e *evmDB) CommitRangeProof(ctx context.Context, start, end maybe.Maybe[[]b return nextKey, nil } -func (*evmDB) CommitChangeProof(context.Context, maybe.Maybe[[]byte], struct{}) (maybe.Maybe[[]byte], error) { - return maybe.Nothing[[]byte](), errors.New("change proof code hashes not implemented") +func (e *evmDB) CommitChangeProof(ctx context.Context, end maybe.Maybe[[]byte], proof *ChangeProof) (maybe.Maybe[[]byte], error) { + // First enqueue any code hashes found in the proof. + // If an error occurs here, we don't want to commit the proof to the database, because the + // code hashes will never be requested. + // Note: ffi.ChangeProof.CodeHashes() is currently a no-op in firewood; the loop is + // kept so we pick up real code hashes automatically once firewood implements it. + var codeHashes []common.Hash + for h, err := range proof.cp.CodeHashes() { + if err != nil { + return maybe.Nothing[[]byte](), err + } + codeHashes = append(codeHashes, common.Hash(h)) + } + if err := e.codeQueue.AddCode(ctx, codeHashes); err != nil { + return maybe.Nothing[[]byte](), err + } + return e.db.CommitChangeProof(ctx, end, proof) } func (e *evmDB) GetMerkleRoot(ctx context.Context) (ids.ID, error) { @@ -78,7 +92,7 @@ func (e *evmDB) Clear() error { return e.db.Clear() } -func (e *evmDB) GetChangeProof(ctx context.Context, startRootID ids.ID, endRootID ids.ID, start maybe.Maybe[[]byte], end maybe.Maybe[[]byte], maxLength int) (struct{}, error) { +func (e *evmDB) GetChangeProof(ctx context.Context, startRootID ids.ID, endRootID ids.ID, start maybe.Maybe[[]byte], end maybe.Maybe[[]byte], maxLength int) (*ChangeProof, error) { return e.db.GetChangeProof(ctx, startRootID, endRootID, start, end, maxLength) } @@ -86,7 +100,7 @@ func (e *evmDB) GetRangeProofAtRoot(ctx context.Context, rootID ids.ID, start ma return e.db.GetRangeProofAtRoot(ctx, rootID, start, end, maxLength) } -func (e *evmDB) VerifyChangeProof(ctx context.Context, proof struct{}, start maybe.Maybe[[]byte], end maybe.Maybe[[]byte], expectedEndRootID ids.ID, maxLength int) error { +func (e *evmDB) VerifyChangeProof(ctx context.Context, proof *ChangeProof, start maybe.Maybe[[]byte], end maybe.Maybe[[]byte], expectedEndRootID ids.ID, maxLength int) error { return e.db.VerifyChangeProof(ctx, proof, start, end, expectedEndRootID, maxLength) } diff --git a/database/merkle/firewood/syncer/handler.go b/database/merkle/firewood/syncer/handler.go index a98a23b253dd..b70a0f0d06c5 100644 --- a/database/merkle/firewood/syncer/handler.go +++ b/database/merkle/firewood/syncer/handler.go @@ -11,6 +11,6 @@ import ( // NewGetProofHandler returns a handler that services proof requests // using the provided Firewood database for p2p connections. -func NewGetProofHandler(db *ffi.Database) *sync.ProofHandler[*RangeProof, struct{}] { +func NewGetProofHandler(db *ffi.Database) *sync.ProofHandler[*RangeProof, *ChangeProof] { return sync.NewProofHandler(&database{db: db}, rangeProofMarshaler{}, changeProofMarshaler{}) } diff --git a/database/merkle/firewood/syncer/proof.go b/database/merkle/firewood/syncer/proof.go index 1f2065d61067..4d99fea2545d 100644 --- a/database/merkle/firewood/syncer/proof.go +++ b/database/merkle/firewood/syncer/proof.go @@ -4,8 +4,6 @@ package syncer import ( - "errors" - "github.com/ava-labs/firewood-go-ethhash/ffi" "github.com/ava-labs/avalanchego/database/merkle/sync" @@ -13,8 +11,8 @@ import ( ) var ( - _ sync.Marshaler[*RangeProof] = rangeProofMarshaler{} - _ sync.Marshaler[struct{}] = changeProofMarshaler{} + _ sync.Marshaler[*RangeProof] = rangeProofMarshaler{} + _ sync.Marshaler[*ChangeProof] = changeProofMarshaler{} ) type rangeProofMarshaler struct{} @@ -39,13 +37,24 @@ type RangeProof struct { maxLength int } -// TODO: implement an actual ChangeProof marshaler. type changeProofMarshaler struct{} -func (changeProofMarshaler) Marshal(struct{}) ([]byte, error) { - return nil, errors.New("not implemented") +func (changeProofMarshaler) Marshal(p *ChangeProof) ([]byte, error) { + return p.cp.MarshalBinary() +} + +func (changeProofMarshaler) Unmarshal(data []byte) (*ChangeProof, error) { + proof := new(ffi.ChangeProof) + if err := proof.UnmarshalBinary(data); err != nil { + return nil, err + } + return &ChangeProof{cp: proof}, nil } -func (changeProofMarshaler) Unmarshal([]byte) (struct{}, error) { - return struct{}{}, errors.New("not implemented") +type ChangeProof struct { + cp *ffi.ChangeProof + // proposal is populated by VerifyChangeProof on the client side and + // consumed by CommitChangeProof. It is nil for proofs returned from + // GetChangeProof on the server side. + proposal *ffi.Proposal } diff --git a/database/merkle/firewood/syncer/syncer.go b/database/merkle/firewood/syncer/syncer.go index c8d499c0455a..9b9759f7f95f 100644 --- a/database/merkle/firewood/syncer/syncer.go +++ b/database/merkle/firewood/syncer/syncer.go @@ -20,7 +20,7 @@ import ( ) var ( - _ sync.DB[*RangeProof, struct{}] = (*database)(nil) + _ sync.DB[*RangeProof, *ChangeProof] = (*database)(nil) defaultSimultaneousWorkLimit = 8 ) @@ -37,16 +37,16 @@ type Config struct { Registerer prometheus.Registerer } -func New(config Config, db *ffi.Database, targetRoot ids.ID, proofClient *p2p.Client) (*sync.Syncer[*RangeProof, struct{}], error) { +func New(config Config, db *ffi.Database, targetRoot ids.ID, proofClient *p2p.Client) (*sync.Syncer[*RangeProof, *ChangeProof], error) { return newWithDB( config, - &database{db}, + &database{db: db}, targetRoot, proofClient, ) } -func newWithDB(config Config, db sync.DB[*RangeProof, struct{}], targetRoot ids.ID, proofClient *p2p.Client) (*sync.Syncer[*RangeProof, struct{}], error) { +func newWithDB(config Config, db sync.DB[*RangeProof, *ChangeProof], targetRoot ids.ID, proofClient *p2p.Client) (*sync.Syncer[*RangeProof, *ChangeProof], error) { if config.Registerer == nil { config.Registerer = prometheus.NewRegistry() } @@ -58,7 +58,7 @@ func newWithDB(config Config, db sync.DB[*RangeProof, struct{}], targetRoot ids. } return sync.NewSyncer( db, - sync.Config[*RangeProof, struct{}]{ + sync.Config[*RangeProof, *ChangeProof]{ RangeProofMarshaler: rangeProofMarshaler{}, ChangeProofMarshaler: changeProofMarshaler{}, EmptyRoot: ids.ID(types.EmptyRootHash), @@ -124,20 +124,53 @@ func (db *database) CommitRangeProof(_ context.Context, start, end maybe.Maybe[[ return maybe.Some(nextKey), nil } -// TODO: Use change proofs to optimize syncing. -// Returning the sentinel error suggests to the server handler to serve a full range proof instead. -func (*database) GetChangeProof(context.Context, ids.ID, ids.ID, maybe.Maybe[[]byte], maybe.Maybe[[]byte], int) (struct{}, error) { - return struct{}{}, sync.ErrInsufficientHistory +func (db *database) GetChangeProof(_ context.Context, startRootID ids.ID, endRootID ids.ID, start maybe.Maybe[[]byte], end maybe.Maybe[[]byte], maxLength int) (*ChangeProof, error) { + proof, err := db.db.ChangeProof(ffi.Hash(startRootID), ffi.Hash(endRootID), start, end, uint32(maxLength)) + switch { + case errors.Is(err, ffi.ErrStartRevisionNotFound): + return nil, sync.ErrInsufficientHistory + case errors.Is(err, ffi.ErrEndRevisionNotFound): + return nil, sync.ErrNoEndRoot + case err != nil: + return nil, err + } + return &ChangeProof{cp: proof}, nil } -// TODO: implement this method. -func (*database) VerifyChangeProof(context.Context, struct{}, maybe.Maybe[[]byte], maybe.Maybe[[]byte], ids.ID, int) error { - return errors.New("change proofs are not implemented") +func (db *database) VerifyChangeProof(_ context.Context, proof *ChangeProof, start maybe.Maybe[[]byte], end maybe.Maybe[[]byte], expectedEndRootID ids.ID, maxLength int) error { + proposal, err := db.db.VerifyChangeProof(proof.cp, ffi.Hash(expectedEndRootID), start, end, uint32(maxLength)) + if err != nil { + return err + } + proof.proposal = proposal + return nil } -// TODO: implement this method. -func (*database) CommitChangeProof(context.Context, maybe.Maybe[[]byte], struct{}) (maybe.Maybe[[]byte], error) { - return maybe.Nothing[[]byte](), errors.New("change proofs are not implemented") +func (*database) CommitChangeProof(_ context.Context, end maybe.Maybe[[]byte], proof *ChangeProof) (maybe.Maybe[[]byte], error) { + if proof.proposal == nil { + return maybe.Nothing[[]byte](), errors.New("change proof was not verified before commit") + } + if _, err := proof.proposal.CommitWithRebase(); err != nil { + return maybe.Nothing[[]byte](), err + } + + nextKeyRange, err := proof.cp.FindNextKey(end) + if err != nil { + return maybe.Nothing[[]byte](), err + } + if nextKeyRange == nil { + return maybe.Nothing[[]byte](), nil + } + + nextKey := nextKeyRange.StartKey() + if err := nextKeyRange.Free(); err != nil { + return maybe.Nothing[[]byte](), err + } + + if end.HasValue() && bytes.Compare(nextKey, end.Value()) > 0 { + return maybe.Nothing[[]byte](), nil + } + return maybe.Some(nextKey), nil } func (db *database) Clear() error { diff --git a/database/merkle/firewood/syncer/syncer_test.go b/database/merkle/firewood/syncer/syncer_test.go index b2276fa55860..62261fbe3f03 100644 --- a/database/merkle/firewood/syncer/syncer_test.go +++ b/database/merkle/firewood/syncer/syncer_test.go @@ -151,7 +151,7 @@ func testSyncWithUpdate(t *testing.T, clientKeys int, serverKeys int, numRequest }() wantRoot := fillDB(t, r, serverDB, serverKeys) - var syncer *sync.Syncer[*RangeProof, struct{}] + var syncer *sync.Syncer[*RangeProof, *ChangeProof] ctx, cancel := context.WithCancelCause(t.Context()) defer cancel(nil) diff --git a/go.work.sum b/go.work.sum index c1cb374e0c0c..e2a7077c5fe1 100644 --- a/go.work.sum +++ b/go.work.sum @@ -338,6 +338,7 @@ github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310 h1:BUAU3CGlLvorLI26 github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a h1:idn718Q4B6AGu/h5Sxe66HYVdqdGu2l9Iebqhi/AEoA= github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= github.com/ava-labs/firewood-go-ethhash/ffi v0.3.0/go.mod h1:71C76bo47zlDX9gWnn3p/0QZQXkTQ/GNqUNI6fchvjs= +github.com/ava-labs/firewood-go-ethhash/ffi v0.5.0/go.mod h1:MoUHYlFrwaflfLpHt8nmQUHoLy2CCHaFNH/n7vazUuI= github.com/ava-labs/simplex v0.0.0-20260320130759-afe09323fdd1 h1:Y9UdRQj28/t+GNzVSlP6nvoNLtzNRo3fNXLUc/NscVM= github.com/ava-labs/simplex v0.0.0-20260320130759-afe09323fdd1/go.mod h1:GVzumIo3zR23/qGRN2AdnVkIPHcKMq/D89EGWZfMGQ0= github.com/aws/aws-sdk-go-v2 v1.21.2 h1:+LXZ0sgo8quN9UOKXXzAWRT3FWd4NxeXWOZom9pE7GA= diff --git a/graft/evm/sync/evmstate/firewood_syncer.go b/graft/evm/sync/evmstate/firewood_syncer.go index 1671c3b39776..e06f2207b245 100644 --- a/graft/evm/sync/evmstate/firewood_syncer.go +++ b/graft/evm/sync/evmstate/firewood_syncer.go @@ -26,7 +26,7 @@ var ( ) type FirewoodSyncer struct { - s *merklesync.Syncer[*syncer.RangeProof, struct{}] + s *merklesync.Syncer[*syncer.RangeProof, *syncer.ChangeProof] cancel context.CancelFunc codeQueue *code.Queue // finalizeOnce is initialized in the constructor to make Finalize idempotent.