Skip to content
Closed
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
7 changes: 5 additions & 2 deletions accounts/keystore/account_cache.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,10 @@ import (
"sync"
"time"

mapset "github.com/deckarep/golang-set/v2"
"github.com/ava-labs/libevm/accounts"
"github.com/ava-labs/libevm/common"
"github.com/ava-labs/libevm/log"
mapset "github.com/deckarep/golang-set/v2"
"golang.org/x/exp/slices"
)

Expand Down Expand Up @@ -79,7 +79,10 @@ func newAccountCache(keydir string) (*accountCache, chan struct{}) {
keydir: keydir,
byAddr: make(map[common.Address][]accounts.Account),
notify: make(chan struct{}, 1),
fileC: fileCache{all: mapset.NewThreadUnsafeSet[string]()},
fileC: fileCache{
all: mapset.NewThreadUnsafeSet[string](),
fileStat: make(map[string]fileStat),
},
}
ac.watcher = newWatcher(ac)
return ac, ac.notify
Expand Down
32 changes: 26 additions & 6 deletions accounts/keystore/file_cache.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,15 +23,22 @@ import (
"sync"
"time"

mapset "github.com/deckarep/golang-set/v2"
"github.com/ava-labs/libevm/log"
mapset "github.com/deckarep/golang-set/v2"
)

// fileStat is metadata from the last scan used to detect in-place updates.
type fileStat struct {
mod time.Time
size int64
}

// fileCache is a cache of files seen during scan of keystore.
type fileCache struct {
all mapset.Set[string] // Set of all files from the keystore folder
lastMod time.Time // Last time instance when a file was modified
mu sync.Mutex
all mapset.Set[string] // Set of all files from the keystore folder
lastMod time.Time // Latest ModTime among key files in the last scan
fileStat map[string]fileStat // path -> size and mod time at last scan
mu sync.Mutex
}

// scan performs a new scan on the given directory, compares against the already
Expand All @@ -54,6 +61,7 @@ func (fc *fileCache) scan(keyDir string) (mapset.Set[string], mapset.Set[string]
mods := mapset.NewThreadUnsafeSet[string]()

var newLastMod time.Time
newFileStat := make(map[string]fileStat)
for _, fi := range files {
path := filepath.Join(keyDir, fi.Name())
// Skip any non-key files from the folder
Expand All @@ -69,8 +77,20 @@ func (fc *fileCache) scan(keyDir string) (mapset.Set[string], mapset.Set[string]
return nil, nil, nil, err
}
modified := info.ModTime()
if modified.After(fc.lastMod) {
size := info.Size()
newFileStat[path] = fileStat{mod: modified, size: size}

switch {
case modified.After(fc.lastMod):
// Legacy coarse signal (new files and clock moved forward).
mods.Add(path)
case fc.all.Contains(path):
// In-place edits can keep the same coarse ModTime as fc.lastMod (e.g. same
// wall-clock second) while still changing content; compare per-file metadata.
prev := fc.fileStat[path]
if prev.size != size || !prev.mod.Equal(modified) {
mods.Add(path)
}
}
if modified.After(newLastMod) {
newLastMod = modified
Expand All @@ -83,7 +103,7 @@ func (fc *fileCache) scan(keyDir string) (mapset.Set[string], mapset.Set[string]
creates := all.Difference(fc.all) // Creates = current - previous
updates := mods.Difference(creates) // Updates = modified - creates

fc.all, fc.lastMod = all, newLastMod
fc.all, fc.lastMod, fc.fileStat = all, newLastMod, newFileStat
t3 := time.Now()

// Report on the scanning stats and return
Expand Down
90 changes: 90 additions & 0 deletions accounts/keystore/file_cache_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
// Copyright 2026 the libevm authors.
//
// The libevm additions to go-ethereum are free software: you can redistribute
// them and/or modify them under the terms of the GNU Lesser General Public License
// as published by the Free Software Foundation, either version 3 of the License,
// or (at your option) any later version.
//
// The libevm additions are distributed in the hope that they will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser
// General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with the go-ethereum library. If not, see
// <http://www.gnu.org/licenses/>.

package keystore

import (
"os"
"path/filepath"
"testing"
"time"

mapset "github.com/deckarep/golang-set/v2"
)

// TestFileCacheScan_inPlaceSizeChangeWithoutAfterLastMod ensures a key file is
// treated as updated when its size changes even if ModTime is not strictly after
// the previous scan's global lastMod (e.g. same wall-clock second, or tests that
// bump lastMod artificially).
func TestFileCacheScan_inPlaceSizeChangeWithoutAfterLastMod(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "aaa")
if err := os.WriteFile(path, []byte("short"), 0600); err != nil {
t.Fatal(err)
}

fc := &fileCache{
all: mapset.NewThreadUnsafeSet[string](),
fileStat: make(map[string]fileStat),
}
if _, _, _, err := fc.scan(dir); err != nil {
t.Fatal(err)
}

// Make the global lastMod lie in the future so ModTime.After(lastMod) is false,
// while the path is still known (in-place edit scenario).
fc.lastMod = time.Now().Add(1 * time.Hour)

if err := os.WriteFile(path, []byte("muuuuuuuuch longer content"), 0600); err != nil {
t.Fatal(err)
}

creates, deletes, updates, err := fc.scan(dir)
if err != nil {
t.Fatal(err)
}
if creates.Cardinality() != 0 || deletes.Cardinality() != 0 {
t.Fatalf("creates=%d deletes=%d; want 0,0", creates.Cardinality(), deletes.Cardinality())
}
if updates.Cardinality() != 1 || !updates.Contains(path) {
t.Fatalf("updates=%v; want single path %q", updates, path)
}
}

func TestFileCacheScan_noSpuriousUpdateWhenUnchanged(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "aaa")
content := []byte("fixed content")
if err := os.WriteFile(path, content, 0600); err != nil {
t.Fatal(err)
}

fc := &fileCache{
all: mapset.NewThreadUnsafeSet[string](),
fileStat: make(map[string]fileStat),
}
if _, _, _, err := fc.scan(dir); err != nil {
t.Fatal(err)
}
creates, deletes, updates, err := fc.scan(dir)
if err != nil {
t.Fatal(err)
}
if creates.Cardinality() != 0 || deletes.Cardinality() != 0 || updates.Cardinality() != 0 {
t.Fatalf("creates=%d deletes=%d updates=%d; want all 0",
creates.Cardinality(), deletes.Cardinality(), updates.Cardinality())
}
}
8 changes: 4 additions & 4 deletions cmd/geth/logging_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ func testConsoleLogging(t *testing.T, format string, tStart, tEnd int) {
have = censor(have, tStart, tEnd)
want = censor(want, tStart, tEnd)
if have != want {
t.Logf(nicediff([]byte(have), []byte(want)))
t.Log(nicediff([]byte(have), []byte(want)))
t.Fatalf("format %v, line %d\nhave %v\nwant %v", format, i, have, want)
}
}
Expand Down Expand Up @@ -140,7 +140,7 @@ func TestJsonLogging(t *testing.T) {
}
if !bytes.Equal(have, want) {
// show an intelligent diff
t.Logf(nicediff(have, want))
t.Log(nicediff(have, want))
t.Errorf("file content wrong")
}
}
Expand Down Expand Up @@ -210,7 +210,7 @@ func TestFileOut(t *testing.T) {
}
if !bytes.Equal(have, want) {
// show an intelligent diff
t.Logf(nicediff(have, want))
t.Log(nicediff(have, want))
t.Errorf("file content wrong")
}
}
Expand All @@ -231,7 +231,7 @@ func TestRotatingFileOut(t *testing.T) {
}
if !bytes.Equal(have, want) {
// show an intelligent diff
t.Logf(nicediff(have, want))
t.Log(nicediff(have, want))
t.Errorf("file content wrong")
}
}
21 changes: 19 additions & 2 deletions metrics/influxdb/influxdb_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ import (
"net/http/httptest"
"net/url"
"os"
"regexp"
"strconv"
"strings"
"testing"

Expand All @@ -31,6 +33,21 @@ import (
influxdb2 "github.com/influxdata/influxdb-client-go/v2"
)

// stddev and variance serialization can differ by one ULP across GOARCH / Go versions.
var influxStddevVarRE = regexp.MustCompile(`\b(stddev|variance)=([0-9.]+(?:[eE][+-]?[0-9]+)?)`)

func normalizeInfluxStddevVariance(s string) string {
return influxStddevVarRE.ReplaceAllStringFunc(s, func(m string) string {
eq := strings.Index(m, "=")
key, val := m[:eq], m[eq+1:]
f, err := strconv.ParseFloat(val, 64)
if err != nil {
return m
}
return fmt.Sprintf("%s=%.12e", key, f)
})
}

func TestMain(m *testing.M) {
metrics.Enabled = true
os.Exit(m.Run())
Expand Down Expand Up @@ -62,7 +79,7 @@ func TestExampleV1(t *testing.T) {
} else {
want = string(wantB)
}
if have != want {
if normalizeInfluxStddevVariance(have) != normalizeInfluxStddevVariance(want) {
t.Errorf("\nhave:\n%v\nwant:\n%v\n", have, want)
t.Logf("have vs want:\n%v", findFirstDiffPos(have, want))
}
Expand Down Expand Up @@ -94,7 +111,7 @@ func TestExampleV2(t *testing.T) {
} else {
want = string(wantB)
}
if have != want {
if normalizeInfluxStddevVariance(have) != normalizeInfluxStddevVariance(want) {
t.Errorf("\nhave:\n%v\nwant:\n%v\n", have, want)
t.Logf("have vs want:\n%v", findFirstDiffPos(have, want))
}
Expand Down