Repository: ava-labs/firewood
Pull request #893: ci(ffi): add linter
Author: login: alarso16
Labels: name: testing
name: go
description: Pull requests that update go code
Created: 2025-05-15T20:45:28Z | Merged: 2025-05-16T18:58:30Z

We should have some set of standards for the Go code made in this folder. I "adapted" the `avalanchego` linter to use here. This is my first time messing with CI and linting, so nit reviews are especially helpful.

Any edits outside of the `.yaml` files are nit changes for linting - nothing significant.


Diff:
diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml
index 461b4df1b5..6020204472 100644
--- a/.github/workflows/ci.yaml
+++ b/.github/workflows/ci.yaml
@@ -127,6 +127,11 @@ jobs:
         with:
           go-version-file: "ffi/go.mod"
           cache-dependency-path: "ffi/go.sum"
+      - name: Run golanci-lint
+        uses: golangci/golangci-lint-action@v3
+        with:
+          version: latest
+          working-directory: ffi
       - name: Test Go FFI bindings
         working-directory: ffi
         # cgocheck2 is expensive but provides complete pointer checks
diff --git a/ffi/.golangci.yaml b/ffi/.golangci.yaml
new file mode 100644
index 0000000000..34e7ff1e6e
--- /dev/null
+++ b/ffi/.golangci.yaml
@@ -0,0 +1,170 @@
+# https://golangci-lint.run/usage/configuration/
+run:
+  timeout: 10m
+
+  # If set we pass it to "go list -mod={option}". From "go help modules":
+  # If invoked with -mod=readonly, the go command is disallowed from the implicit
+  # automatic updating of go.mod described above. Instead, it fails when any changes
+  # to go.mod are needed. This setting is most useful to check that go.mod does
+  # not need updates, such as in a continuous integration and testing system.
+  # If invoked with -mod=vendor, the go command assumes that the vendor
+  # directory holds the correct copies of dependencies and ignores
+  # the dependency descriptions in go.mod.
+  #
+  # Allowed values: readonly|vendor|mod
+  # By default, it isn't set.
+  modules-download-mode: readonly
+
+issues:
+  # Make issues output unique by line.
+  # Default: true
+  uniq-by-line: false
+
+  # Maximum issues count per one linter.
+  # Set to 0 to disable.
+  # Default: 50
+  max-issues-per-linter: 0
+
+  # Maximum count of issues with the same text.
+  # Set to 0 to disable.
+  # Default: 3
+  max-same-issues: 0
+
+  # Enables skipping of directories:
+  # - vendor$, third_party$, testdata$, examples$, Godeps$, builtin$
+  # Default: true
+  exclude-dirs-use-default: false
+
+linters:
+  disable-all: true
+  enable:
+    - asciicheck
+    - bodyclose
+    - copyloopvar
+    - depguard
+    - dupword
+    - dupl
+    - errcheck
+    - errname
+    - errorlint
+    - forbidigo
+    - gci
+    - goconst
+    - gocritic
+    # - err113 - encourages wrapping static errors
+    - gofmt
+    - gofumpt
+    # - mnd - unnecessary magic numbers
+    - goprintffuncname
+    - gosec
+    - gosimple
+    - govet
+    - importas
+    - ineffassign
+    # - lll line length linter
+    - misspell
+    - nakedret
+    - nilerr
+    - noctx
+    - nolintlint
+    - perfsprint
+    - prealloc
+    - predeclared
+    - revive
+    - spancheck
+    - staticcheck
+    - stylecheck
+    - tagalign
+    - testifylint
+    - typecheck
+    - unconvert
+    - unparam
+    - unused
+    - usestdlibvars
+    - whitespace
+
+linters-settings:
+  depguard:
+    rules:
+      packages:
+        deny:
+          - pkg: "github.com/golang/mock/gomock"
+            desc: go.uber.org/mock/gomock should be used instead.
+          - pkg: "github.com/stretchr/testify/assert"
+            desc: github.com/stretchr/testify/require should be used instead.
+          - pkg: "io/ioutil"
+            desc: io/ioutil is deprecated. Use package io or os instead.
+  errorlint:
+    # Check for plain type assertions and type switches.
+    asserts: false
+    # Check for plain error comparisons.
+    comparison: false
+  forbidigo:
+    # Forbid the following identifiers (list of regexp).
+    forbid:
+      - 'require\.Error$(# ErrorIs should be used instead)?'
+      - 'require\.ErrorContains$(# ErrorIs should be used instead)?'
+      - 'require\.EqualValues$(# Equal should be used instead)?'
+      - 'require\.NotEqualValues$(# NotEqual should be used instead)?'
+      - '^(t|b|tb|f)\.(Fatal|Fatalf|Error|Errorf)$(# the require library should be used instead)?'
+  revive:
+    rules:
+      # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#bool-literal-in-expr
+      - name: bool-literal-in-expr
+        disabled: false
+      # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#early-return
+      - name: early-return
+        disabled: false
+      # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#empty-lines
+      - name: empty-lines
+        disabled: false
+      # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#string-format
+      - name: string-format
+        disabled: false
+        arguments:
+        - ["b.Logf[0]", "/.*%.*/", "no format directive, use b.Log instead"]
+        - ["fmt.Errorf[0]", "/.*%.*/", "no format directive, use errors.New instead"]
+        - ["fmt.Fprintf[1]", "/.*%.*/", "no format directive, use fmt.Fprint instead"]
+        - ["fmt.Printf[0]", "/.*%.*/", "no format directive, use fmt.Print instead"]
+        - ["fmt.Sprintf[0]", "/.*%.*/", "no format directive, use fmt.Sprint instead"]
+        - ["log.Fatalf[0]", "/.*%.*/", "no format directive, use log.Fatal instead"]
+        - ["log.Printf[0]", "/.*%.*/", "no format directive, use log.Print instead"]
+        - ["t.Logf[0]", "/.*%.*/", "no format directive, use t.Log instead"]
+      # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#struct-tag
+      - name: struct-tag
+        disabled: false
+      # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#unexported-naming
+      - name: unexported-naming
+        disabled: false
+      # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#unhandled-error
+      - name: unhandled-error
+        # prefer the errcheck linter since it can be disabled directly with nolint directive
+        # but revive's disable directive (e.g. //revive:disable:unhandled-error) is not
+        # supported when run under golangci_lint
+        disabled: true
+      # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#unused-parameter
+      - name: unused-parameter
+        disabled: false
+      # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#unused-receiver
+      - name: unused-receiver
+        disabled: false
+      # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#useless-break
+      - name: useless-break
+        disabled: false
+  tagalign:
+    align: true
+    sort: true
+    strict: true
+    order:
+      - serialize
+  testifylint:
+    # Enable all checkers (https://github.com/Antonboom/testifylint#checkers).
+    # Default: false
+    enable-all: true
+    # Disable checkers by name
+    # (in addition to default
+    #   suite-thelper
+    # ).
+    disable:
+      - go-require
+      - float-compare
diff --git a/ffi/firewood.go b/ffi/firewood.go
index 9f85e64549..6b58ce2b04 100644
--- a/ffi/firewood.go
+++ b/ffi/firewood.go
@@ -33,7 +33,7 @@ const (
 	keyNotFound      = "key not found"
 )
 
-var errDbClosed = errors.New("firewood database already closed")
+var errDBClosed = errors.New("firewood database already closed")
 
 // A Database is a handle to a Firewood database.
 // It is not safe to call these methods with a nil handle.
@@ -138,14 +138,14 @@ func (db *Database) Batch(ops []KeyValue) ([]byte, error) {
 	hash := C.fwd_batch(
 		db.handle,
 		C.size_t(len(ffiOps)),
-		(*C.struct_KeyValue)(unsafe.SliceData(ffiOps)), // implicitly pinned
+		unsafe.SliceData(ffiOps), // implicitly pinned
 	)
 	return extractBytesThenFree(&hash)
 }
 
 func (db *Database) Propose(keys, vals [][]byte) (*Proposal, error) {
 	if db.handle == nil {
-		return nil, errDbClosed
+		return nil, errDBClosed
 	}
 
 	values, cleanup := newValueFactory()
@@ -158,13 +158,12 @@ func (db *Database) Propose(keys, vals [][]byte) (*Proposal, error) {
 			value: values.from(vals[i]),
 		}
 	}
-	id_or_err := C.fwd_propose_on_db(
+	idOrErr := C.fwd_propose_on_db(
 		db.handle,
 		C.size_t(len(ffiOps)),
-		(*C.struct_KeyValue)(unsafe.SliceData(ffiOps)), // implicitly pinned
+		unsafe.SliceData(ffiOps), // implicitly pinned
 	)
-	id, err := extractIdThenFree(&id_or_err)
-
+	id, err := extractUintThenFree(&idOrErr)
 	if err != nil {
 		return nil, err
 	}
@@ -180,7 +179,7 @@ func (db *Database) Propose(keys, vals [][]byte) (*Proposal, error) {
 // If the key is not found, the return value will be (nil, nil).
 func (db *Database) Get(key []byte) ([]byte, error) {
 	if db.handle == nil {
-		return nil, errDbClosed
+		return nil, errDBClosed
 	}
 
 	values, cleanup := newValueFactory()
@@ -200,7 +199,7 @@ func (db *Database) Get(key []byte) ([]byte, error) {
 // Empty trie must return common.Hash{}.
 func (db *Database) Root() ([]byte, error) {
 	if db.handle == nil {
-		return nil, errDbClosed
+		return nil, errDBClosed
 	}
 	hash := C.fwd_root_hash(db.handle)
 	bytes, err := extractBytesThenFree(&hash)
@@ -222,7 +221,7 @@ func (db *Database) Revision(root []byte) (*Revision, error) {
 // Returns an error if already closed.
 func (db *Database) Close() error {
 	if db.handle == nil {
-		return errDbClosed
+		return errDBClosed
 	}
 	C.fwd_close_db(db.handle)
 	db.handle = nil
diff --git a/ffi/firewood_test.go b/ffi/firewood_test.go
index 150287ee58..8b783e4db3 100644
--- a/ffi/firewood_test.go
+++ b/ffi/firewood_test.go
@@ -9,7 +9,6 @@ import (
 	"strings"
 	"testing"
 
-	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
 )
 
@@ -22,7 +21,6 @@ func TestMain(m *testing.M) {
 		switch strings.TrimSpace(kv) {
 		case "cgocheck=1":
 			hasCgoCheck = true
-			break
 		case "cgocheck=0":
 			fmt.Fprint(os.Stderr, "GODEBUG=cgocheck=0; MUST be 1 for Firewood cgo tests")
 			os.Exit(1)
@@ -66,13 +64,15 @@ func TestInsert(t *testing.T) {
 		key = "abc"
 		val = "def"
 	)
-	db.Batch([]KeyValue{
+
+	_, err := db.Batch([]KeyValue{
 		{[]byte(key), []byte(val)},
 	})
+	require.NoError(t, err, "Batch(%q)", key)
 
 	got, err := db.Get([]byte(key))
 	require.NoErrorf(t, err, "%T.Get(%q)", db, key)
-	assert.Equal(t, val, string(got), "Recover lone batch-inserted value")
+	require.Equal(t, val, string(got), "Recover lone batch-inserted value")
 }
 
 // Attempt to make a call to a nil or invalid handle.
@@ -82,21 +82,21 @@ func TestGetBadHandle(t *testing.T) {
 
 	// This ignores error, but still shouldn't panic.
 	_, err := db.Get([]byte("non-existent"))
-	assert.ErrorIs(t, err, errDbClosed)
+	require.ErrorIs(t, err, errDBClosed)
 
 	// We ignore the error, but it shouldn't panic.
 	_, err = db.Root()
-	assert.ErrorIs(t, err, errDbClosed)
+	require.ErrorIs(t, err, errDBClosed)
 
 	root, err := db.Update(
 		[][]byte{[]byte("key")},
 		[][]byte{[]byte("value")},
 	)
-	assert.Empty(t, root)
-	assert.ErrorIs(t, err, errDbClosed)
+	require.Empty(t, root)
+	require.ErrorIs(t, err, errDBClosed)
 
 	err = db.Close()
-	require.ErrorIs(t, err, errDbClosed)
+	require.ErrorIs(t, err, errDBClosed)
 }
 
 func keyForTest(i int) []byte {
@@ -163,15 +163,15 @@ func TestInsert100(t *testing.T) {
 				require.NoErrorf(t, err, "%T.Get(%q)", db, keys[i])
 				// Cast as strings to improve debug messages.
 				want := string(vals[i])
-				assert.Equal(t, want, string(got), "Recover nth batch-inserted value")
+				require.Equal(t, want, string(got), "Recover nth batch-inserted value")
 			}
 
 			hash, err := db.Root()
-			assert.NoError(t, err, "%T.Root()", db)
-			assert.Lenf(t, hash, 32, "%T.Root()", db)
+			require.NoError(t, err, "%T.Root()", db)
+			require.Lenf(t, hash, 32, "%T.Root()", db)
 			// we know the hash starts with 0xf8
-			assert.Equalf(t, byte(0xf8), hash[0], "First byte of %T.Root()", db)
-			assert.Equalf(t, rootFromInsert, hash, "%T.Root() matches value returned by insertion", db)
+			require.Equalf(t, byte(0xf8), hash[0], "First byte of %T.Root()", db)
+			require.Equalf(t, rootFromInsert, hash, "%T.Root() matches value returned by insertion", db)
 		})
 	}
 }
@@ -183,23 +183,25 @@ func TestRangeDelete(t *testing.T) {
 	for i := range ops {
 		ops[i] = kvForTest(i)
 	}
-	db.Batch(ops)
+	_, err := db.Batch(ops)
+	require.NoError(t, err, "Batch")
 
 	const deletePrefix = 1
-	db.Batch([]KeyValue{{
+	_, err = db.Batch([]KeyValue{{
 		Key: keyForTest(deletePrefix),
 		// delete all keys that start with "key1"
 		Value: nil,
 	}})
+	require.NoError(t, err, "Batch")
 
 	for _, op := range ops {
 		got, err := db.Get(op.Key)
 		require.NoError(t, err)
 
 		if deleted := bytes.HasPrefix(op.Key, keyForTest(deletePrefix)); deleted {
-			assert.Empty(t, err, got)
+			require.NoError(t, err, got)
 		} else {
-			assert.Equal(t, op.Value, got)
+			require.Equal(t, op.Value, got)
 		}
 	}
 }
@@ -209,11 +211,11 @@ func TestInvariants(t *testing.T) {
 	db := newTestDatabase(t)
 	hash, err := db.Root()
 	require.NoError(t, err, "%T.Root()", db)
-	assert.Equalf(t, make([]byte, 32), hash, "%T.Root() of empty trie")
+	require.Equalf(t, make([]byte, 32), hash, "%T.Root() of empty trie", db)
 
 	got, err := db.Get([]byte("non-existent"))
 	require.NoError(t, err)
-	assert.Emptyf(t, got, "%T.Get([non-existent key])", db)
+	require.Emptyf(t, got, "%T.Get([non-existent key])", db)
 }
 
 func TestParallelProposals(t *testing.T) {
@@ -240,7 +242,7 @@ func TestParallelProposals(t *testing.T) {
 		for j := 0; j < numKeys; j++ {
 			got, err := p.Get(keyForTest(i*numKeys + j))
 			require.NoError(t, err, "Get(%d)", i*numKeys+j)
-			assert.Equal(t, valForTest(i*numKeys+j), got, "Get(%d)", i*numKeys+j)
+			require.Equal(t, valForTest(i*numKeys+j), got, "Get(%d)", i*numKeys+j)
 		}
 	}
 
@@ -251,14 +253,14 @@ func TestParallelProposals(t *testing.T) {
 	for j := 0; j < numKeys; j++ {
 		got, err := db.Get(keyForTest(j))
 		require.NoError(t, err, "Get(%d)", j)
-		assert.Equal(t, valForTest(j), got, "Get(%d)", j)
+		require.Equal(t, valForTest(j), got, "Get(%d)", j)
 	}
 	// Check that the other proposals' keys are not present.
 	for i := 1; i < numProposals; i++ {
 		for j := 0; j < numKeys; j++ {
 			got, err := db.Get(keyForTest(i*numKeys + j))
 			require.NoError(t, err, "Get(%d)", i*numKeys+j)
-			assert.Empty(t, got, "Get(%d)", i*numKeys+j)
+			require.Empty(t, got, "Get(%d)", i*numKeys+j)
 		}
 	}
 
@@ -267,7 +269,7 @@ func TestParallelProposals(t *testing.T) {
 		for j := 0; j < numKeys; j++ {
 			got, err := proposals[i].Get(keyForTest(i*numKeys + j))
 			require.NoError(t, err, "Get(%d)", i*numKeys+j)
-			assert.Equal(t, valForTest(i*numKeys+j), got, "Get(%d)", i*numKeys+j)
+			require.Equal(t, valForTest(i*numKeys+j), got, "Get(%d)", i*numKeys+j)
 		}
 	}
 
@@ -288,7 +290,7 @@ func TestParallelProposals(t *testing.T) {
 		for j := 0; j < numKeys; j++ {
 			got, err := proposals[i].Get(keyForTest(i*numKeys + j))
 			require.ErrorIs(t, err, errDroppedProposal, "Get(%d)", i*numKeys+j)
-			assert.Empty(t, got, "Get(%d)", i*numKeys+j)
+			require.Empty(t, got, "Get(%d)", i*numKeys+j)
 		}
 	}
 }
@@ -304,7 +306,8 @@ func TestDeleteAll(t *testing.T) {
 		vals[i] = valForTest(i)
 	}
 	// Insert 10 key-value pairs.
-	db.Update(keys, vals)
+	_, err := db.Update(keys, vals)
+	require.NoError(t, err, "Update")
 
 	// Create a proposal that deletes all keys.
 	proposal, err := db.Propose([][]byte{[]byte("key")}, [][]byte{nil})
@@ -314,7 +317,7 @@ func TestDeleteAll(t *testing.T) {
 	for i := range keys {
 		got, err := proposal.Get(keys[i])
 		require.NoError(t, err, "Get(%d)", i)
-		assert.Empty(t, got, "Get(%d)", i)
+		require.Empty(t, got, "Get(%d)", i)
 	}
 
 	// Commit the proposal.
@@ -324,7 +327,7 @@ func TestDeleteAll(t *testing.T) {
 	// Check that the database is empty.
 	hash, err := db.Root()
 	require.NoError(t, err, "%T.Root()", db)
-	assert.Equalf(t, make([]byte, 32), hash, "%T.Root() of empty trie")
+	require.Equalf(t, make([]byte, 32), hash, "%T.Root() of empty trie", db)
 }
 
 // Tests that a proposal with an invalid ID cannot be committed.
@@ -415,13 +418,13 @@ func TestProposeFromProposal(t *testing.T) {
 	for i := range keys2 {
 		got, err := proposal1.Get(keys2[i])
 		require.NoError(t, err, "Get(%d)", i)
-		assert.Empty(t, got, "Get(%d)", i)
+		require.Empty(t, got, "Get(%d)", i)
 	}
 	// Assert that the second proposal has keys from the first.
 	for i := range keys1 {
 		got, err := proposal2.Get(keys1[i])
 		require.NoError(t, err, "Get(%d)", i)
-		assert.Equal(t, vals1[i], got, "Get(%d)", i)
+		require.Equal(t, vals1[i], got, "Get(%d)", i)
 	}
 
 	// Commit the first proposal.
@@ -432,12 +435,12 @@ func TestProposeFromProposal(t *testing.T) {
 	for i := range keys1 {
 		got, err := db.Get(keys1[i])
 		require.NoError(t, err, "Get(%d)", i)
-		assert.Equal(t, vals1[i], got, "Get(%d)", i)
+		require.Equal(t, vals1[i], got, "Get(%d)", i)
 	}
 	for i := range keys2 {
 		got, err := proposal2.Get(keys2[i])
 		require.NoError(t, err, "Get(%d)", i)
-		assert.Equal(t, vals2[i], got, "Get(%d)", i)
+		require.Equal(t, vals2[i], got, "Get(%d)", i)
 	}
 
 	// Commit the second proposal.
@@ -448,12 +451,12 @@ func TestProposeFromProposal(t *testing.T) {
 	for i := range keys1 {
 		got, err := db.Get(keys1[i])
 		require.NoError(t, err, "Get(%d)", i)
-		assert.Equal(t, vals1[i], got, "Get(%d)", i)
+		require.Equal(t, vals1[i], got, "Get(%d)", i)
 	}
 	for i := range keys2 {
 		got, err := db.Get(keys2[i])
 		require.NoError(t, err, "Get(%d)", i)
-		assert.Equal(t, vals2[i], got, "Get(%d)", i)
+		require.Equal(t, vals2[i], got, "Get(%d)", i)
 	}
 }
 
@@ -490,7 +493,7 @@ func TestDeepPropose(t *testing.T) {
 	for i := range keys {
 		got, err := proposals[numProposals-1].Get(keys[i])
 		require.NoError(t, err, "Get(%d)", i)
-		assert.Equal(t, vals[i], got, "Get(%d)", i)
+		require.Equal(t, vals[i], got, "Get(%d)", i)
 	}
 
 	// Commit each proposal sequentially, and ensure that the values are
@@ -502,7 +505,7 @@ func TestDeepPropose(t *testing.T) {
 		for j := i * numKeys; j < (i+1)*numKeys; j++ {
 			got, err := db.Get(keys[j])
 			require.NoError(t, err, "Get(%d)", j)
-			assert.Equal(t, vals[j], got, "Get(%d)", j)
+			require.Equal(t, vals[j], got, "Get(%d)", j)
 		}
 	}
 }
@@ -552,7 +555,7 @@ func TestDropProposalAndCommit(t *testing.T) {
 	for i := range keys {
 		got, err := proposals[numProposals-1].Get(keys[i])
 		require.NoError(t, err, "Get(%d)", i)
-		assert.Equal(t, vals[i], got, "Get(%d)", i)
+		require.Equal(t, vals[i], got, "Get(%d)", i)
 	}
 }
 
@@ -577,53 +580,53 @@ func TestProposeSameRoot(t *testing.T) {
 	// Create the first proposal chain.
 	proposal1, err := db.Propose(keys[0:5], vals[0:5])
 	require.NoError(t, err, "Propose")
-	proposal3_top, err := proposal1.Propose(keys[5:10], vals[5:10])
+	proposal3Top, err := proposal1.Propose(keys[5:10], vals[5:10])
 	require.NoError(t, err, "Propose")
 	// Create the second proposal chain.
 	proposal2, err := db.Propose(keys[5:10], vals[5:10])
 	require.NoError(t, err, "Propose")
-	proposal3_bottom, err := proposal2.Propose(keys[0:5], vals[0:5])
+	proposal3Bottom, err := proposal2.Propose(keys[0:5], vals[0:5])
 	require.NoError(t, err, "Propose")
 	// Because the proposals are identical, they should have the same root.
 
 	// Create a unique proposal from each of the two chains.
-	top_keys := make([][]byte, 5)
-	top_vals := make([][]byte, 5)
-	for i := range top_keys {
-		top_keys[i] = keyForTest(i + 10)
-		top_vals[i] = valForTest(i + 10)
-	}
-	bot_keys := make([][]byte, 5)
-	bot_vals := make([][]byte, 5)
-	for i := range bot_keys {
-		bot_keys[i] = keyForTest(i + 20)
-		bot_vals[i] = valForTest(i + 20)
-	}
-	proposal4, err := proposal3_top.Propose(top_keys, top_vals)
+	topKeys := make([][]byte, 5)
+	topVals := make([][]byte, 5)
+	for i := range topKeys {
+		topKeys[i] = keyForTest(i + 10)
+		topVals[i] = valForTest(i + 10)
+	}
+	bottomKeys := make([][]byte, 5)
+	bottomVals := make([][]byte, 5)
+	for i := range bottomKeys {
+		bottomKeys[i] = keyForTest(i + 20)
+		bottomVals[i] = valForTest(i + 20)
+	}
+	proposal4, err := proposal3Top.Propose(topKeys, topVals)
 	require.NoError(t, err, "Propose")
-	proposal5, err := proposal3_bottom.Propose(bot_keys, bot_vals)
+	proposal5, err := proposal3Bottom.Propose(bottomKeys, bottomVals)
 	require.NoError(t, err, "Propose")
 
 	// Now we will commit the top chain, and check that the bottom chain is still valid.
 	err = proposal1.Commit()
 	require.NoError(t, err, "Commit")
-	err = proposal3_top.Commit()
+	err = proposal3Top.Commit()
 	require.NoError(t, err, "Commit")
 
 	// Check that both final proposals are valid.
 	for i := range keys {
 		got, err := proposal4.Get(keys[i])
 		require.NoError(t, err, "P4 Get(%d)", i)
-		assert.Equal(t, vals[i], got, "P4 Get(%d)", i)
+		require.Equal(t, vals[i], got, "P4 Get(%d)", i)
 		got, err = proposal5.Get(keys[i])
 		require.NoError(t, err, "P5 Get(%d)", i)
-		assert.Equal(t, vals[i], got, "P5 Get(%d)", i)
+		require.Equal(t, vals[i], got, "P5 Get(%d)", i)
 	}
 
 	// Attempt to commit P5. Since this isn't in the canonical chain, it should
 	// fail.
 	err = proposal5.Commit()
-	require.Error(t, err, "Commit P5") // this error is internal to firewood
+	require.Contains(t, err.Error(), "commit the parents of this proposal first", "Commit P5") // this error is internal to firewood
 
 	// We should be able to commit P4, since it is in the canonical chain.
 	err = proposal4.Commit()
@@ -659,7 +662,7 @@ func TestRevision(t *testing.T) {
 	for i := range keys {
 		got, err := revision.Get(keys[i])
 		require.NoError(t, err, "Get(%d)", i)
-		assert.Equal(t, valForTest(i), got, "Get(%d)", i)
+		require.Equal(t, valForTest(i), got, "Get(%d)", i)
 	}
 
 	// Create a second proposal with 10 key-value pairs.
@@ -682,7 +685,7 @@ func TestRevision(t *testing.T) {
 	for i := range keys {
 		got, err := revision.Get(keys[i])
 		require.NoError(t, err, "Get(%d)", i)
-		assert.Equal(t, valForTest(i), got, "Get(%d)", i)
+		require.Equal(t, valForTest(i), got, "Get(%d)", i)
 	}
 }
 
@@ -700,7 +703,7 @@ func TestFakeRevision(t *testing.T) {
 
 	// Create a fake revision with an valid root.
 	validRoot := []byte("counting 32 bytes to make a hash")
-	assert.Len(t, validRoot, 32, "valid root")
+	require.Len(t, validRoot, 32, "valid root")
 	_, err = db.Revision(validRoot)
 	require.ErrorIs(t, err, errRevisionNotFound, "Revision(valid root)")
 }
@@ -733,14 +736,14 @@ func TestGetNilCases(t *testing.T) {
 	for _, k := range specialKeys {
 		got, err := db.Get(k)
 		require.NoError(t, err, "db.Get(%q)", k)
-		assert.Empty(t, got, "db.Get(%q)", k)
+		require.Empty(t, got, "db.Get(%q)", k)
 
 		got, err = revision.Get(k)
 		require.NoError(t, err, "Revision.Get(%q)", k)
-		assert.Empty(t, got, "Revision.Get(%q)", k)
+		require.Empty(t, got, "Revision.Get(%q)", k)
 
 		got, err = proposal.Get(k)
 		require.NoError(t, err, "Proposal.Get(%q)", k)
-		assert.Empty(t, got, "Proposal.Get(%q)", k)
+		require.Empty(t, got, "Proposal.Get(%q)", k)
 	}
 }
diff --git a/ffi/kvbackend.go b/ffi/kvbackend.go
index 05fff47407..6c49a81d4e 100644
--- a/ffi/kvbackend.go
+++ b/ffi/kvbackend.go
@@ -41,18 +41,18 @@ type kVBackend interface {
 }
 
 // Prefetch is a no-op since we don't need to prefetch for Firewood.
-func (db *Database) Prefetch(key []byte) ([]byte, error) {
+func (db *Database) Prefetch(_ []byte) ([]byte, error) {
 	if db.handle == nil {
-		return nil, errDbClosed
+		return nil, errDBClosed
 	}
 
 	return nil, nil
 }
 
 // Commit is a no-op, since [Database.Update] already persists changes.
-func (db *Database) Commit(root []byte) error {
+func (db *Database) Commit(_ []byte) error {
 	if db.handle == nil {
-		return errDbClosed
+		return errDBClosed
 	}
 
 	return nil
@@ -62,7 +62,7 @@ func (db *Database) Commit(root []byte) error {
 // database.
 func (db *Database) Update(keys, vals [][]byte) ([]byte, error) {
 	if db.handle == nil {
-		return nil, errDbClosed
+		return nil, errDBClosed
 	}
 
 	ops := make([]KeyValue, len(keys))
diff --git a/ffi/memory.go b/ffi/memory.go
index 5bb22c5f0e..a803a38230 100644
--- a/ffi/memory.go
+++ b/ffi/memory.go
@@ -8,6 +8,7 @@ package firewood
 // #include <stdlib.h>
 // #include "firewood.h"
 import "C"
+
 import (
 	"errors"
 	"fmt"
@@ -57,15 +58,14 @@ func extractErrorThenFree(v *C.struct_Value) error {
 	// We should still attempt to free the value.
 	C.fwd_free_value(v)
 	return errBadValue
-
 }
 
-// extractIdThenFree converts the cgo `Value` payload to either:
+// extractUintThenFree converts the cgo `Value` payload to either:
 // 1. a nonzero uint32 and nil error, indicating a valid int
 // 2. a zero uint32 and a non-nil error, indicating an error occurred.
 // This should only be called when the `Value` is expected to only contain an error or an ID.
 // Otherwise, an error is returned.
-func extractIdThenFree(v *C.struct_Value) (uint32, error) {
+func extractUintThenFree(v *C.struct_Value) (uint32, error) {
 	// Pin the returned value to prevent it from being garbage collected.
 	defer runtime.KeepAlive(v)
 
@@ -129,7 +129,6 @@ func extractBytesThenFree(v *C.struct_Value) ([]byte, error) {
 	// We should still attempt to free the value.
 	C.fwd_free_value(v)
 	return nil, errBadValue
-
 }
 
 // newValueFactory returns a factory for converting byte slices into cgo `Value`
diff --git a/ffi/proposal.go b/ffi/proposal.go
index ad058c548f..8b086b3cbb 100644
--- a/ffi/proposal.go
+++ b/ffi/proposal.go
@@ -8,6 +8,7 @@ package firewood
 // #include <stdlib.h>
 // #include "firewood.h"
 import "C"
+
 import (
 	"errors"
 	"unsafe"
@@ -30,7 +31,7 @@ type Proposal struct {
 // If the key does not exist, it returns (nil, nil).
 func (p *Proposal) Get(key []byte) ([]byte, error) {
 	if p.handle == nil {
-		return nil, errDbClosed
+		return nil, errDBClosed
 	}
 
 	if p.id == 0 {
@@ -48,7 +49,7 @@ func (p *Proposal) Get(key []byte) ([]byte, error) {
 // The proposal is not committed until Commit is called.
 func (p *Proposal) Propose(keys, vals [][]byte) (*Proposal, error) {
 	if p.handle == nil {
-		return nil, errDbClosed
+		return nil, errDBClosed
 	}
 
 	if p.id == 0 {
@@ -74,9 +75,9 @@ func (p *Proposal) Propose(keys, vals [][]byte) (*Proposal, error) {
 	// Propose the keys and values.
 	val := C.fwd_propose_on_proposal(p.handle, C.uint32_t(p.id),
 		C.size_t(len(ffiOps)),
-		(*C.struct_KeyValue)(unsafe.SliceData(ffiOps)),
+		unsafe.SliceData(ffiOps),
 	)
-	id, err := extractIdThenFree(&val)
+	id, err := extractUintThenFree(&val)
 	if err != nil {
 		return nil, err
 	}
@@ -91,7 +92,7 @@ func (p *Proposal) Propose(keys, vals [][]byte) (*Proposal, error) {
 // If an error occurs, the proposal is dropped and no longer valid.
 func (p *Proposal) Commit() error {
 	if p.handle == nil {
-		return errDbClosed
+		return errDBClosed
 	}
 
 	if p.id == 0 {
@@ -99,8 +100,8 @@ func (p *Proposal) Commit() error {
 	}
 
 	// Commit the proposal and return the hash.
-	err_val := C.fwd_commit(p.handle, C.uint32_t(p.id))
-	err := extractErrorThenFree(&err_val)
+	errVal := C.fwd_commit(p.handle, C.uint32_t(p.id))
+	err := extractErrorThenFree(&errVal)
 	if err != nil {
 		// this is unrecoverable due to Rust's ownership model
 		// The underlying proposal is no longer valid.
@@ -114,7 +115,7 @@ func (p *Proposal) Commit() error {
 // An error is returned if the proposal was already dropped.
 func (p *Proposal) Drop() error {
 	if p.handle == nil {
-		return errDbClosed
+		return errDBClosed
 	}
 
 	if p.id == 0 {
diff --git a/ffi/revision.go b/ffi/revision.go
index c1fdac4ae4..20603b6a27 100644
--- a/ffi/revision.go
+++ b/ffi/revision.go
@@ -8,6 +8,7 @@ package firewood
 // #include <stdlib.h>
 // #include "firewood.h"
 import "C"
+
 import (
 	"errors"
 	"fmt"
@@ -58,7 +59,7 @@ func newRevision(handle *C.DatabaseHandle, root []byte) (*Revision, error) {
 
 func (r *Revision) Get(key []byte) ([]byte, error) {
 	if r.handle == nil {
-		return nil, errDbClosed
+		return nil, errDBClosed
 	}
 	if r.root == nil {
 		return nil, errRevisionNotFound


Reviews:
user: login: alarso16
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-15T20:46:30Z
user: login: alarso16
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-15T20:47:00Z
user: login: aaronbuchwald
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-16T14:19:54Z

Review comments:
diff_hunk: @@ -127,6 +127,11 @@ jobs:
         with:
           go-version-file: "ffi/go.mod"
           cache-dependency-path: "ffi/go.sum"
+      - name: Run golanci-lint
path: .github/workflows/ci.yaml
user: login: alarso16
body: Should this go with the other linting, or with the ffi stuff like it is here?
created_at: 2025-05-15T20:46:30Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,170 @@
+# https://golangci-lint.run/usage/configuration/
+run:
+  timeout: 10m
+
+  # If set we pass it to "go list -mod={option}". From "go help modules":
+  # If invoked with -mod=readonly, the go command is disallowed from the implicit
+  # automatic updating of go.mod described above. Instead, it fails when any changes
+  # to go.mod are needed. This setting is most useful to check that go.mod does
+  # not need updates, such as in a continuous integration and testing system.
+  # If invoked with -mod=vendor, the go command assumes that the vendor
+  # directory holds the correct copies of dependencies and ignores
+  # the dependency descriptions in go.mod.
+  #
+  # Allowed values: readonly|vendor|mod
+  # By default, it isn't set.
+  modules-download-mode: readonly
+
+issues:
+  # Make issues output unique by line.
+  # Default: true
+  uniq-by-line: false
+
+  # Maximum issues count per one linter.
+  # Set to 0 to disable.
+  # Default: 50
+  max-issues-per-linter: 0
+
+  # Maximum count of issues with the same text.
+  # Set to 0 to disable.
+  # Default: 3
+  max-same-issues: 0
+
+  # Enables skipping of directories:
+  # - vendor$, third_party$, testdata$, examples$, Godeps$, builtin$
+  # Default: true
+  exclude-dirs-use-default: false
+
+linters:
+  disable-all: true
+  enable:
+    - asciicheck
path: ffi/.golangci.yaml
user: login: alarso16
body: Any other linters worth adding?
created_at: 2025-05-15T20:47:00Z
author_association: CONTRIBUTOR
diff_hunk: @@ -127,6 +127,11 @@ jobs:
         with:
           go-version-file: "ffi/go.mod"
           cache-dependency-path: "ffi/go.sum"
+      - name: Run golanci-lint
path: .github/workflows/ci.yaml
user: login: aaronbuchwald
body: I think it makes sense here the way it's currently set up because we have a separate job for build, lint, and testing that's all separate.
created_at: 2025-05-16T14:09:06Z
author_association: CONTRIBUTOR

PR comments:
user: login: aaronbuchwald
created_at: 2025-05-16T14:19:45Z
body: Looking at the diff from AvalancheGo, what's the reason for:

- adding `dupl`
- adding `errname`

Both seem reasonable, just curious
author_association: CONTRIBUTOR
user: login: alarso16
created_at: 2025-05-16T14:41:56Z
body: > Looking at the diff from AvalancheGo, what's the reason for:
> 
> * adding `dupl`
> * adding `errname`
> 
> Both seem reasonable, just curious

I added `dupl` and `errname` because I like the idea of `dupl`, and I made the `errname` mistake in a previous PR.
author_association: CONTRIBUTOR
user: login: rkuris
created_at: 2025-05-16T16:25:20Z
body: No opinion here, but whatever we do on avalanche-go should be done here if possible.
author_association: COLLABORATOR