Skip to content

Commit 289d442

Browse files
Update FFI tests to test both default firewood + ethhash (#891)
1 parent abdc230 commit 289d442

File tree

6 files changed

+134
-18
lines changed

6 files changed

+134
-18
lines changed

.github/workflows/attach-static-libs.yaml

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,7 @@ jobs:
4747
- uses: Swatinem/rust-cache@v2
4848

4949
- name: Build for ${{ matrix.target }}
50-
# TODO: add ethhash feature flag after updating FFI tests
51-
run: cargo build --profile maxperf --features logger --target ${{ matrix.target }} -p firewood-ffi
50+
run: cargo build --profile maxperf --features ethhash,logger --target ${{ matrix.target }} -p firewood-ffi
5251

5352
- name: Upload binary
5453
uses: actions/upload-artifact@v4
@@ -152,7 +151,7 @@ jobs:
152151
- name: Test Go FFI bindings
153152
working-directory: ffi
154153
# cgocheck2 is expensive but provides complete pointer checks
155-
run: GOEXPERIMENT=cgocheck2 go test ./...
154+
run: GOEXPERIMENT=cgocheck2 TEST_FIREWOOD_HASH_MODE=ethhash go test ./...
156155

157156
remove-if-pr-only:
158157
runs-on: ubuntu-latest

.github/workflows/ci.yaml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,7 @@ jobs:
135135
- name: Test Go FFI bindings
136136
working-directory: ffi
137137
# cgocheck2 is expensive but provides complete pointer checks
138-
run: GOEXPERIMENT=cgocheck2 go test ./...
138+
run: GOEXPERIMENT=cgocheck2 TEST_FIREWOOD_HASH_MODE=firewood go test ./...
139139

140140
ethhash:
141141
runs-on: ubuntu-latest
@@ -153,6 +153,10 @@ jobs:
153153
with:
154154
go-version-file: "ffi/tests/go.mod"
155155
cache-dependency-path: "ffi/tests/go.sum"
156+
- name: Test Go FFI bindings
157+
working-directory: ffi
158+
# cgocheck2 is expensive but provides complete pointer checks
159+
run: GOEXPERIMENT=cgocheck2 TEST_FIREWOOD_HASH_MODE=ethhash go test ./...
156160
- name: Test Ethereum hash compatability
157161
working-directory: ffi/tests
158162
run: go test ./...

ffi/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ tikv-jemallocator = "0.6.0"
2020

2121
[features]
2222
logger = ["dep:env_logger", "firewood/logger"]
23+
ethhash = ["firewood/ethhash"]
2324

2425
[build-dependencies]
2526
cbindgen = "0.28.0"

ffi/README.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,18 @@ This enables consumers to utilize it directly without forcing them to compile Fi
4848

4949
To trigger this build, [attach-static-libs](../.github/workflows/attach-static-libs.yaml) supports triggers for both manual GitHub Actions and tags, so you can create a mirror branch/tag on [firewood-go](https://github.com/ava-labs/firewood-go) by either trigger a manual GitHub Action and selecting your branch or pushing a tag to Firewood.
5050

51+
### Hash Mode
52+
53+
Firewood implemented its own optimized merkle trie structure. To support Ethereum Merkle Trie hash compatibility, it also provides a feature flag `ethhash`.
54+
55+
This is an optional feature (disabled by default). To enable it for a local build, compile with:
56+
57+
```
58+
cargo build -p firewood-ffi --features ethhash
59+
```
60+
61+
To support development in [Coreth](https://github.com/ava-labs/coreth), Firewood pushes static libraries to [firewood-go](https://github.com/ava-labs/firewood-go) with `ethhash` enabled by default.
62+
5163
## Development
5264
Iterative building is unintuitive for the ffi and some common sources of confusion are listed below.
5365

ffi/firewood_test.go

Lines changed: 113 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package firewood
22

33
import (
44
"bytes"
5+
"encoding/hex"
56
"fmt"
67
"os"
78
"path/filepath"
@@ -12,6 +13,68 @@ import (
1213
"github.com/stretchr/testify/require"
1314
)
1415

16+
const (
17+
ethhashKey = "ethhash"
18+
firewoodKey = "firewood"
19+
emptyKey = "empty"
20+
insert100Key = "100"
21+
emptyEthhashRoot = "56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421"
22+
emptyFirewoodRoot = "0000000000000000000000000000000000000000000000000000000000000000"
23+
)
24+
25+
// expectedRoots contains the expected root hashes for different use cases across both default
26+
// firewood hashing and ethhash.
27+
// By default, TestMain infers which mode Firewood is operating in and selects the expected roots
28+
// accordingly (this does turn test empty database into an effective no-op).
29+
//
30+
// To test a specific hashing mode explicitly, set the environment variable:
31+
// TEST_FIREWOOD_HASH_MODE=ethhash or TEST_FIREWOOD_HASH_MODE=firewood
32+
// This will skip the inference step and enforce we use the expected roots for the specified mode.
33+
var (
34+
// expectedRoots contains a mapping of expected root hashes for different test
35+
// vectors.
36+
expectedRootModes = map[string]map[string]string{
37+
ethhashKey: {
38+
emptyKey: emptyEthhashRoot,
39+
insert100Key: "c25a0076e0337d7c982c3c9dfa445c8088242a0a607f9d9def3762765bcb0fde",
40+
},
41+
firewoodKey: {
42+
emptyKey: emptyFirewoodRoot,
43+
insert100Key: "f858b51ada79c4abeb6566ef1204a453030dba1cca3526d174e2cb3ce2aadc57",
44+
},
45+
}
46+
expectedEmptyRootToMode = map[string]string{
47+
emptyEthhashRoot: ethhashKey,
48+
emptyFirewoodRoot: firewoodKey,
49+
}
50+
expectedRoots map[string]string
51+
)
52+
53+
func inferHashingMode() (string, error) {
54+
dbFile := filepath.Join(os.TempDir(), "test.db")
55+
db, closeDB, err := newDatabase(dbFile)
56+
if err != nil {
57+
return "", err
58+
}
59+
defer func() {
60+
_ = closeDB()
61+
_ = os.Remove(dbFile)
62+
}()
63+
64+
actualEmptyRoot, err := db.Root()
65+
if err != nil {
66+
return "", fmt.Errorf("failed to get root of empty database: %w", err)
67+
}
68+
actualEmptyRootHex := hex.EncodeToString(actualEmptyRoot)
69+
70+
actualFwMode, ok := expectedEmptyRootToMode[actualEmptyRootHex]
71+
if !ok {
72+
return "", fmt.Errorf("unknown empty root %q, cannot infer mode", actualEmptyRootHex)
73+
}
74+
75+
return actualFwMode, nil
76+
}
77+
1578
func TestMain(m *testing.M) {
1679
// The cgocheck debugging flag checks that all pointers are pinned.
1780
// TODO(arr4n) why doesn't `//go:debug cgocheck=1` work? https://go.dev/doc/godebug
@@ -35,25 +98,49 @@ func TestMain(m *testing.M) {
3598
}
3699
}
37100

101+
// If TEST_FIREWOOD_HASH_MODE is set, use it to select the expected roots.
102+
// Otherwise, infer the hash mode from an empty database.
103+
hashMode := os.Getenv("TEST_FIREWOOD_HASH_MODE")
104+
if hashMode == "" {
105+
inferredHashMode, err := inferHashingMode()
106+
if err != nil {
107+
fmt.Fprintf(os.Stderr, "failed to infer hash mode %v\n", err)
108+
os.Exit(1)
109+
}
110+
hashMode = inferredHashMode
111+
}
112+
selectedExpectedRoots, ok := expectedRootModes[hashMode]
113+
if !ok {
114+
fmt.Fprintf(os.Stderr, "unknown hash mode %q\n", hashMode)
115+
os.Exit(1)
116+
}
117+
expectedRoots = selectedExpectedRoots
118+
38119
os.Exit(m.Run())
39120
}
40121

41122
func newTestDatabase(t *testing.T) *Database {
42123
t.Helper()
43124

125+
dbFile := filepath.Join(t.TempDir(), "test.db")
126+
db, closeDB, err := newDatabase(dbFile)
127+
require.NoError(t, err)
128+
t.Cleanup(func() {
129+
require.NoError(t, closeDB())
130+
})
131+
return db
132+
}
133+
134+
func newDatabase(dbFile string) (*Database, func() error, error) {
44135
conf := DefaultConfig()
45136
conf.MetricsPort = 0
46137
conf.Create = true
47-
// The TempDir directory is automatically cleaned up so there's no need to
48-
// remove test.db.
49-
dbFile := filepath.Join(t.TempDir(), "test.db")
50138

51139
f, err := New(dbFile, conf)
52-
require.NoErrorf(t, err, "NewDatabase(%+v)", conf)
53-
// Close() always returns nil, its signature returning an error only to
54-
// conform with an externally required interface.
55-
t.Cleanup(func() { f.Close() })
56-
return f
140+
if err != nil {
141+
return nil, nil, fmt.Errorf("failed to create new database at filepath %q: %w", dbFile, err)
142+
}
143+
return f, f.Close, nil
57144
}
58145

59146
// Tests that a single key-value pair can be inserted and retrieved.
@@ -168,9 +255,15 @@ func TestInsert100(t *testing.T) {
168255

169256
hash, err := db.Root()
170257
require.NoError(t, err, "%T.Root()", db)
171-
require.Lenf(t, hash, 32, "%T.Root()", db)
172-
// we know the hash starts with 0xf8
173-
require.Equalf(t, byte(0xf8), hash[0], "First byte of %T.Root()", db)
258+
259+
// Assert the hash is exactly as expected. Test failure indicates a
260+
// non-hash compatible change has been made since the string was set.
261+
// If that's expected, update the string at the top of the file to
262+
// fix this test.
263+
expectedHashHex := expectedRoots[insert100Key]
264+
expectedHash, err := hex.DecodeString(expectedHashHex)
265+
require.NoError(t, err, "failed to decode expected hash")
266+
require.Equal(t, expectedHash, hash, "Root hash mismatch.\nExpected (hex): %x\nActual (hex): %x", expectedHash, hash)
174267
require.Equalf(t, rootFromInsert, hash, "%T.Root() matches value returned by insertion", db)
175268
})
176269
}
@@ -211,7 +304,11 @@ func TestInvariants(t *testing.T) {
211304
db := newTestDatabase(t)
212305
hash, err := db.Root()
213306
require.NoError(t, err, "%T.Root()", db)
214-
require.Equalf(t, make([]byte, 32), hash, "%T.Root() of empty trie", db)
307+
308+
emptyRootStr := expectedRoots[emptyKey]
309+
expectedHash, err := hex.DecodeString(emptyRootStr)
310+
require.NoError(t, err)
311+
require.Equalf(t, expectedHash, hash, "expected %x, got %x", expectedHash, hash)
215312

216313
got, err := db.Get([]byte("non-existent"))
217314
require.NoError(t, err)
@@ -327,7 +424,10 @@ func TestDeleteAll(t *testing.T) {
327424
// Check that the database is empty.
328425
hash, err := db.Root()
329426
require.NoError(t, err, "%T.Root()", db)
330-
require.Equalf(t, make([]byte, 32), hash, "%T.Root() of empty trie", db)
427+
expectedHashHex := expectedRoots[emptyKey]
428+
expectedHash, err := hex.DecodeString(expectedHashHex)
429+
require.NoError(t, err)
430+
require.Equalf(t, expectedHash, hash, "%T.Root() of empty trie", db)
331431
}
332432

333433
// Tests that a proposal with an invalid ID cannot be committed.

ffi/tests/eth_compatibility_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ func TestInsert(t *testing.T) {
3737
key common.Hash
3838
}
3939

40-
rand.Seed(0)
40+
rand := rand.New(rand.NewSource(0))
4141

4242
addrs := make([]common.Address, 0)
4343
storages := make([]storageKey, 0)

0 commit comments

Comments
 (0)