Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
191 changes: 191 additions & 0 deletions firewood/firewood.go
Copy link
Contributor

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.

  1. I think Firewood is eventually joining the monorepo, and this is definitely the logical spot to put it
  2. This won't be the only firewood implementation (since there's also the C-Chain). Should that go here as well? I would expect not, but maybe.
  3. There's a whole database/ folder, which seems to currently be only key-value databases. Either way, this does seem like a database

Copy link
Contributor Author

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:

  1. If firewood is joining the monorepo then this needs to move
  2. database seems 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 (like prefixdb) and I don't think we want to use those wrappers (e.g prefixdb hashes prefixes, leading to unpredictable prefixes). Against my point however - we are using database.ErrNotFound to avoid unintended behavior changes which makes it seem like we are a database.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.

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 {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I also use this sort of structure in graft/coreth/triedb/firewood/account_trie.go. I have an issue open at ava-labs/firewood#1433, so if you think it should be, will you contribute any technical requirements/suggestions there?

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
return ids.ID(root[:]), nil
return ids.ID(root), nil

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)
Copy link
Contributor

Choose a reason for hiding this comment

The 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 db.Update which combines these into a single FFI

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
}
Loading