-
Notifications
You must be signed in to change notification settings - Fork 840
Add firewood wrapper #4661
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: x-chain-merkle-db-shared-memory
Are you sure you want to change the base?
Add firewood wrapper #4661
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,191 @@ | ||||||
| // Copyright (C) 2019-2025, Ava Labs, Inc. All rights reserved. | ||||||
| // See the file LICENSE for licensing terms. | ||||||
|
|
||||||
| package firewood | ||||||
|
|
||||||
| import ( | ||||||
| "context" | ||||||
| "fmt" | ||||||
|
|
||||||
| "github.com/ava-labs/firewood-go-ethhash/ffi" | ||||||
|
|
||||||
| "github.com/ava-labs/avalanchego/database" | ||||||
| "github.com/ava-labs/avalanchego/ids" | ||||||
| ) | ||||||
|
|
||||||
| const PrefixDelimiter = '/' | ||||||
|
|
||||||
| var ( | ||||||
| reservedPrefix = []byte("reserved") | ||||||
| heightKey = []byte("height") | ||||||
| appPrefix = []byte("app") | ||||||
| ) | ||||||
|
|
||||||
| type changes struct { | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I also use this sort of structure in
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah this branch was made prior to the monorepo so I need to do a pass of the evm wrapper and see what we can share here |
||||||
| Keys [][]byte | ||||||
| Vals [][]byte | ||||||
|
|
||||||
| kv map[string][]byte | ||||||
| } | ||||||
|
|
||||||
| func (c *changes) Put(key []byte, val []byte) { | ||||||
| c.put(key, val, false) | ||||||
| } | ||||||
|
|
||||||
| func (c *changes) Delete(key []byte) { | ||||||
| c.put(key, nil, true) | ||||||
| } | ||||||
|
|
||||||
| // del is true if this is a deletion | ||||||
| func (c *changes) put(key []byte, val []byte, del bool) { | ||||||
| if val == nil && !del { | ||||||
| // Firewood treats nil values as deletions, so we use a workaround to use | ||||||
| // an empty slice until firewood supports this. | ||||||
| val = []byte{} | ||||||
| } | ||||||
|
|
||||||
| c.Keys = append(c.Keys, key) | ||||||
| c.Vals = append(c.Vals, val) | ||||||
|
|
||||||
| if c.kv == nil { | ||||||
| c.kv = make(map[string][]byte) | ||||||
| } | ||||||
|
|
||||||
| c.kv[string(key)] = val | ||||||
| } | ||||||
|
|
||||||
| func (c *changes) Get(key []byte) ([]byte, bool) { | ||||||
| v, ok := c.kv[string(key)] | ||||||
| return v, ok | ||||||
| } | ||||||
|
|
||||||
| type DB struct { | ||||||
| db *ffi.Database | ||||||
| height uint64 | ||||||
| heightInitialized bool | ||||||
| heightKey []byte | ||||||
| pending changes | ||||||
| } | ||||||
|
|
||||||
| func New(path string) (*DB, error) { | ||||||
| db, err := ffi.New(path, ffi.DefaultConfig()) | ||||||
| if err != nil { | ||||||
| return nil, fmt.Errorf("opening firewood db: %w", err) | ||||||
| } | ||||||
|
|
||||||
| heightKey := Prefix(reservedPrefix, heightKey) | ||||||
| heightBytes, err := db.Get(heightKey) | ||||||
| if err != nil { | ||||||
| return nil, fmt.Errorf("getting height: %w", err) | ||||||
| } | ||||||
|
|
||||||
| var height uint64 | ||||||
| if heightBytes != nil { | ||||||
| height, err = database.ParseUInt64(heightBytes) | ||||||
| if err != nil { | ||||||
| return nil, fmt.Errorf("parsing height: %w", err) | ||||||
| } | ||||||
| } | ||||||
|
|
||||||
| return &DB{ | ||||||
| db: db, | ||||||
| height: height, | ||||||
| heightInitialized: heightBytes != nil, | ||||||
| heightKey: heightKey, | ||||||
| }, nil | ||||||
| } | ||||||
|
|
||||||
| // Get returns a key value pair or [database.ErrNotFound] if `key` is not in the | ||||||
| // [DB]. | ||||||
| func (db *DB) Get(key []byte) ([]byte, error) { | ||||||
| key = Prefix(appPrefix, key) | ||||||
|
|
||||||
| if val, ok := db.pending.Get(key); ok { | ||||||
| if val == nil { | ||||||
| return nil, database.ErrNotFound | ||||||
| } | ||||||
|
|
||||||
| return val, nil | ||||||
| } | ||||||
|
|
||||||
| val, err := db.db.Get(key) | ||||||
| if val == nil && err == nil { | ||||||
| return nil, database.ErrNotFound | ||||||
| } | ||||||
|
|
||||||
| return val, err | ||||||
| } | ||||||
|
|
||||||
| // Put inserts a key value pair into [DB]. | ||||||
| func (db *DB) Put(key []byte, val []byte) { | ||||||
| db.pending.Put(Prefix(appPrefix, key), val) | ||||||
| } | ||||||
|
|
||||||
| func (db *DB) Delete(key []byte) { | ||||||
| db.pending.Delete(Prefix(appPrefix, key)) | ||||||
| } | ||||||
|
|
||||||
| // Height returns the last height of [DB] written to by [DB.Flush]. | ||||||
| // | ||||||
| // If this returns false, the height has not been initialized yet. | ||||||
| func (db *DB) Height() (uint64, bool) { | ||||||
| if !db.heightInitialized { | ||||||
| return 0, false | ||||||
| } | ||||||
|
|
||||||
| return db.height, true | ||||||
| } | ||||||
|
|
||||||
| // Root returns the merkle root of the state on disk ignoring pending writes. | ||||||
| func (db *DB) Root() (ids.ID, error) { | ||||||
| root, err := db.db.Root() | ||||||
| if err != nil { | ||||||
| return ids.ID{}, err | ||||||
| } | ||||||
|
|
||||||
| return ids.ID(root[:]), nil | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
Why isn't this just a typecast? |
||||||
| } | ||||||
|
|
||||||
| // Abort cancels all pending writes. | ||||||
| func (db *DB) Abort() { | ||||||
| db.pending = changes{} | ||||||
| } | ||||||
|
|
||||||
| // Flush flushes pending writes to disk and increments [DB.Height]. | ||||||
| func (db *DB) Flush() error { | ||||||
| if !db.heightInitialized { | ||||||
| db.heightInitialized = true | ||||||
| } else { | ||||||
| db.height++ | ||||||
| } | ||||||
|
|
||||||
| db.pending.Put(db.heightKey, database.PackUInt64(db.height)) | ||||||
|
|
||||||
| p, err := db.db.Propose(db.pending.Keys, db.pending.Vals) | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If you will always be proposing and immediately committing, then there is an operation |
||||||
| if err != nil { | ||||||
| return fmt.Errorf("proposing changes: %w", err) | ||||||
| } | ||||||
|
|
||||||
| if err := p.Commit(); err != nil { | ||||||
| return fmt.Errorf("committing changes: %w", err) | ||||||
| } | ||||||
|
|
||||||
| db.pending = changes{} | ||||||
|
|
||||||
| return nil | ||||||
| } | ||||||
|
|
||||||
| func (db *DB) Close(ctx context.Context) error { | ||||||
| return db.db.Close(ctx) | ||||||
| } | ||||||
|
|
||||||
| // Prefix prefixes `key` with `prefix` + [PrefixDelimiter]. | ||||||
| func Prefix(prefix []byte, key []byte) []byte { | ||||||
| k := make([]byte, len(prefix)+1+len(key)) | ||||||
|
|
||||||
| copy(k, prefix) | ||||||
| k[len(prefix)] = PrefixDelimiter | ||||||
| copy(k[len(prefix)+1:], key) | ||||||
|
|
||||||
| return k | ||||||
| } | ||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This location is weird to me, for a lot of reasons.
database/folder, which seems to currently be only key-value databases. Either way, this does seem like a databaseThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I left it here because I wasn't sure what I wanted to do with this... we need to figure out what to do before merging because this is in an admittedly weird spot right now. Some thoughts:
databaseseems like a good spot to put this at first, but the problem is that this type doesn't implement the database interface. We could implement the interface if we wanted to... but it would also mean that we can do wrapping with the other database types (likeprefixdb) and I don't think we want to use those wrappers (e.gprefixdbhashes prefixes, leading to unpredictable prefixes). Against my point however - we are usingdatabase.ErrNotFoundto avoid unintended behavior changes which makes it seem like we are adatabase.Database- so we need to pick a lane.I need to review the EVM wrapper over firewood to see if there's a way we can share test coverage + code and do some thinking over where this lives and its relationship to
database.