Fix validator export failing when validator.db is in nested directory (#15351)

* files changed

* added changelog file

* added tests and formatting changes

* fixed bugs

* fixed formatting
This commit is contained in:
Rose Jethani
2025-05-30 20:23:19 +05:30
committed by GitHub
parent 711984d942
commit 3300866572
12 changed files with 162 additions and 115 deletions

View File

@@ -58,10 +58,11 @@ func exportSlashingProtectionJSON(cliCtx *cli.Context) error {
}
// Ensure that the database is found under the specified dir or its subdirectories
var matchPath string
if isDatabaseMinimal {
found, _, err = file.RecursiveDirFind(filesystem.DatabaseDirName, dataDir)
found, matchPath, err = file.RecursiveDirFind(filesystem.DatabaseDirName, dataDir)
} else {
found, _, err = file.RecursiveFileFind(kv.ProtectionDbFileName, dataDir)
found, matchPath, err = file.RecursiveFileFind(kv.ProtectionDbFileName, dataDir)
}
if err != nil {
@@ -74,6 +75,12 @@ func exportSlashingProtectionJSON(cliCtx *cli.Context) error {
databaseFileDir = filesystem.DatabaseDirName
}
return fmt.Errorf("%s (validator database) was not found at path %s, so nothing to export", databaseFileDir, dataDir)
} else {
if !isDatabaseMinimal {
matchPath = filepath.Dir(matchPath) // strip the file name
}
dataDir = matchPath
log.Infof("Found validator database at path %s", dataDir)
}
// Open the validator database.

View File

@@ -3,6 +3,7 @@ package historycmd
import (
"bytes"
"fmt"
"path/filepath"
"github.com/OffchainLabs/prysm/v6/cmd"
"github.com/OffchainLabs/prysm/v6/cmd/validator/flags"
@@ -45,16 +46,29 @@ func importSlashingProtectionJSON(cliCtx *cli.Context) error {
}
// Ensure that the database is found under the specified directory or its subdirectories
var matchPath string
if isDatabaseMinimal {
found, _, err = file.RecursiveDirFind(filesystem.DatabaseDirName, dataDir)
found, matchPath, err = file.RecursiveDirFind(filesystem.DatabaseDirName, dataDir)
} else {
found, _, err = file.RecursiveFileFind(kv.ProtectionDbFileName, dataDir)
found, matchPath, err = file.RecursiveFileFind(kv.ProtectionDbFileName, dataDir)
}
if err != nil {
return errors.Wrapf(err, "error finding validator database at path %s", dataDir)
}
if !found {
databaseFileDir := kv.ProtectionDbFileName
if isDatabaseMinimal {
databaseFileDir = filesystem.DatabaseDirName
}
return fmt.Errorf("%s (validator database) was not found at path %s, so nothing to export", databaseFileDir, dataDir)
} else {
if !isDatabaseMinimal {
matchPath = filepath.Dir(matchPath) // strip the file name
}
dataDir = matchPath
log.Infof("Found validator database at path %s", dataDir)
}
message := "Found existing database inside of %s"
if !found {
message = "Did not find existing database inside of %s, creating a new one"

View File

@@ -3,9 +3,6 @@ package historycmd
import (
"encoding/json"
"flag"
"path/filepath"
"testing"
"github.com/OffchainLabs/prysm/v6/cmd"
"github.com/OffchainLabs/prysm/v6/cmd/validator/flags"
"github.com/OffchainLabs/prysm/v6/io/file"
@@ -16,6 +13,8 @@ import (
"github.com/OffchainLabs/prysm/v6/validator/slashing-protection-history/format"
mocks "github.com/OffchainLabs/prysm/v6/validator/testing"
"github.com/urfave/cli/v2"
"path/filepath"
"testing"
)
func setupCliCtx(
@@ -41,76 +40,100 @@ func setupCliCtx(
// slashing protection history database will keep only the latest signed block slot / attestations,
// and thus will not be able to export the same data as the original file.
func TestImportExportSlashingProtectionCli_RoundTrip(t *testing.T) {
numValidators := 10
outputPath := filepath.Join(t.TempDir(), "slashing-exports")
err := file.MkdirAll(outputPath)
require.NoError(t, err)
protectionFileName := "slashing_history_import.json"
// Create some mock slashing protection history. and JSON file
pubKeys, err := mocks.CreateRandomPubKeys(numValidators)
require.NoError(t, err)
attestingHistory, proposalHistory := mocks.MockAttestingAndProposalHistories(pubKeys)
require.NoError(t, err)
mockJSON, err := mocks.MockSlashingProtectionJSON(pubKeys, attestingHistory, proposalHistory)
require.NoError(t, err)
// We JSON encode the protection file and save it to disk as a JSON file.
encoded, err := json.Marshal(mockJSON)
require.NoError(t, err)
protectionFilePath := filepath.Join(outputPath, protectionFileName)
err = file.WriteFile(protectionFilePath, encoded)
require.NoError(t, err)
// We create a CLI context with the required values, such as the database datadir and output directory.
isSlashingProtectionMinimal := false
validatorDB := dbTest.SetupDB(t, pubKeys, isSlashingProtectionMinimal)
dbPath := validatorDB.DatabasePath()
require.NoError(t, validatorDB.Close())
cliCtx := setupCliCtx(t, dbPath, protectionFilePath, outputPath)
// We import the slashing protection history file via CLI.
err = importSlashingProtectionJSON(cliCtx)
require.NoError(t, err)
// We export the slashing protection history file via CLI.
err = exportSlashingProtectionJSON(cliCtx)
require.NoError(t, err)
// Attempt to read the exported file from the output directory.
enc, err := file.ReadFileAsBytes(filepath.Join(outputPath, jsonExportFileName))
require.NoError(t, err)
receivedJSON := &format.EIPSlashingProtectionFormat{}
err = json.Unmarshal(enc, receivedJSON)
require.NoError(t, err)
// We verify the parsed JSON file matches. Given there is no guarantee of order,
// we will have to carefully compare and sort values as needed.
//
// First, we compare basic data such as the MetadataV0 value in the JSON file.
require.DeepEqual(t, mockJSON.Metadata, receivedJSON.Metadata)
wantedHistoryByPublicKey := make(map[string]*format.ProtectionData)
for _, item := range mockJSON.Data {
wantedHistoryByPublicKey[item.Pubkey] = item
layouts := []struct {
name string
mutate func(t *testing.T) (string, string)
}{
{
name: "flat",
mutate: func(t *testing.T) (string, string) {
dbPath := t.TempDir()
return dbPath, dbPath
},
},
{
name: "nested",
mutate: func(t *testing.T) (string, string) {
dbPath := t.TempDir()
nestedDir := filepath.Join(dbPath, "data", "direct")
return dbPath, nestedDir
},
},
}
for _, tc := range layouts {
t.Run(tc.name, func(t *testing.T) {
numValidators := 10
outputPath := filepath.Join(t.TempDir(), "slashing-exports")
err := file.MkdirAll(outputPath)
require.NoError(t, err)
protectionFileName := "slashing_history_import.json"
// Next, we compare all the data for each validator public key.
for _, item := range receivedJSON.Data {
wanted, ok := wantedHistoryByPublicKey[item.Pubkey]
require.Equal(t, true, ok)
wantedAttsByRoot := make(map[string]*format.SignedAttestation)
for _, att := range wanted.SignedAttestations {
wantedAttsByRoot[att.SigningRoot] = att
}
for _, att := range item.SignedAttestations {
wantedAtt, ok := wantedAttsByRoot[att.SigningRoot]
require.Equal(t, true, ok)
require.DeepEqual(t, wantedAtt, att)
}
require.Equal(t, len(wanted.SignedBlocks), len(item.SignedBlocks))
require.DeepEqual(t, wanted.SignedBlocks, item.SignedBlocks)
// Create some mock slashing protection history. and JSON file
pubKeys, err := mocks.CreateRandomPubKeys(numValidators)
require.NoError(t, err)
attestingHistory, proposalHistory := mocks.MockAttestingAndProposalHistories(pubKeys)
require.NoError(t, err)
mockJSON, err := mocks.MockSlashingProtectionJSON(pubKeys, attestingHistory, proposalHistory)
require.NoError(t, err)
// We JSON encode the protection file and save it to disk as a JSON file.
encoded, err := json.Marshal(mockJSON)
require.NoError(t, err)
protectionFilePath := filepath.Join(outputPath, protectionFileName)
err = file.WriteFile(protectionFilePath, encoded)
require.NoError(t, err)
// We create a CLI context with the required values, such as the database datadir and output directory.
isSlashingProtectionMinimal := false
dataDir, dataPath := tc.mutate(t)
validatorDB := dbTest.SetupDB(t, dataPath, pubKeys, isSlashingProtectionMinimal)
require.NoError(t, validatorDB.Close())
cliCtx := setupCliCtx(t, dataDir, protectionFilePath, outputPath)
// We import the slashing protection history file via CLI.
err = importSlashingProtectionJSON(cliCtx)
require.NoError(t, err)
// We export the slashing protection history file via CLI.
err = exportSlashingProtectionJSON(cliCtx)
require.NoError(t, err)
// Attempt to read the exported file from the output directory.
enc, err := file.ReadFileAsBytes(filepath.Join(outputPath, jsonExportFileName))
require.NoError(t, err)
receivedJSON := &format.EIPSlashingProtectionFormat{}
err = json.Unmarshal(enc, receivedJSON)
require.NoError(t, err)
// We verify the parsed JSON file matches. Given there is no guarantee of order,
// we will have to carefully compare and sort values as needed.
//
// First, we compare basic data such as the MetadataV0 value in the JSON file.
require.DeepEqual(t, mockJSON.Metadata, receivedJSON.Metadata)
wantedHistoryByPublicKey := make(map[string]*format.ProtectionData)
for _, item := range mockJSON.Data {
wantedHistoryByPublicKey[item.Pubkey] = item
}
// Next, we compare all the data for each validator public key.
for _, item := range receivedJSON.Data {
wanted, ok := wantedHistoryByPublicKey[item.Pubkey]
require.Equal(t, true, ok)
wantedAttsByRoot := make(map[string]*format.SignedAttestation)
for _, att := range wanted.SignedAttestations {
wantedAttsByRoot[att.SigningRoot] = att
}
for _, att := range item.SignedAttestations {
wantedAtt, ok := wantedAttsByRoot[att.SigningRoot]
require.Equal(t, true, ok)
require.DeepEqual(t, wantedAtt, att)
}
require.Equal(t, len(wanted.SignedBlocks), len(item.SignedBlocks))
require.DeepEqual(t, wanted.SignedBlocks, item.SignedBlocks)
}
})
}
}
@@ -147,7 +170,7 @@ func TestImportExportSlashingProtectionCli_EmptyData(t *testing.T) {
// We create a CLI context with the required values, such as the database datadir and output directory.
isSlashingProtectionMinimal := false
validatorDB := dbTest.SetupDB(t, pubKeys, isSlashingProtectionMinimal)
validatorDB := dbTest.SetupDB(t, t.TempDir(), pubKeys, isSlashingProtectionMinimal)
dbPath := validatorDB.DatabasePath()
require.NoError(t, validatorDB.Close())
cliCtx := setupCliCtx(t, dbPath, protectionFilePath, outputPath)