Skip to content

Commit f235c72

Browse files
authored
cmd: p2p store info (#2835)
<!-- Please read and fill out this form before submitting your PR. Please make sure you have reviewed our contributors guide before submitting your first PR. NOTE: PR titles should follow semantic commits: https://www.conventionalcommits.org/en/v1.0.0/ --> ## Overview <!-- Please provide an explanation of the PR, including the appropriate context, background, goal, and rationale. If there is an issue with this information, please provide a tl;dr and link the issue. Ex: Closes #<issue number> --> add store-info command to inspect p2p store to see what is present
1 parent 3327cf6 commit f235c72

5 files changed

Lines changed: 299 additions & 0 deletions

File tree

apps/evm/single/main.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ func main() {
3131
rollcmd.VersionCmd,
3232
rollcmd.NetInfoCmd,
3333
rollcmd.StoreUnsafeCleanCmd,
34+
rollcmd.StoreP2PInspectCmd,
3435
rollcmd.KeysCmd(),
3536
)
3637

apps/grpc/single/main.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ the Evolve execution gRPC interface.`,
3030
evcmd.VersionCmd,
3131
evcmd.NetInfoCmd,
3232
evcmd.StoreUnsafeCleanCmd,
33+
evcmd.StoreP2PInspectCmd,
3334
evcmd.KeysCmd(),
3435
)
3536

apps/testapp/main.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ func main() {
1919
rollcmd.VersionCmd,
2020
rollcmd.NetInfoCmd,
2121
rollcmd.StoreUnsafeCleanCmd,
22+
rollcmd.StoreP2PInspectCmd,
2223
rollcmd.KeysCmd(),
2324
cmds.NewRollbackCmd(),
2425
initCmd,

pkg/cmd/store.go

Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,23 @@
11
package cmd
22

33
import (
4+
"context"
5+
"errors"
46
"fmt"
57
"os"
68
"path/filepath"
9+
"time"
710

11+
goheader "github.com/celestiaorg/go-header"
12+
goheaderstore "github.com/celestiaorg/go-header/store"
13+
ds "github.com/ipfs/go-datastore"
14+
kt "github.com/ipfs/go-datastore/keytransform"
815
"github.com/spf13/cobra"
16+
17+
"github.com/evstack/ev-node/node"
18+
"github.com/evstack/ev-node/pkg/config"
19+
"github.com/evstack/ev-node/pkg/store"
20+
"github.com/evstack/ev-node/types"
921
)
1022

1123
// UnsafeCleanDataDir removes all contents of the specified data directory.
@@ -53,3 +65,191 @@ This operation is unsafe and cannot be undone. Use with caution!`,
5365
return nil
5466
},
5567
}
68+
69+
// StoreP2PInspectCmd reports head/tail information for the go-header stores used by P2P sync.
70+
var StoreP2PInspectCmd = &cobra.Command{
71+
Use: "store-info",
72+
Short: "Inspect the go-header (P2P) stores and display their tail/head entries",
73+
Long: `Opens the datastore used by the node's go-header services and reports
74+
the current height, head, and tail information for both the header and data stores.`,
75+
RunE: func(cmd *cobra.Command, args []string) error {
76+
nodeConfig, err := ParseConfig(cmd)
77+
if err != nil {
78+
return fmt.Errorf("error parsing config: %w", err)
79+
}
80+
81+
ctx := cmd.Context()
82+
if ctx == nil {
83+
ctx = context.Background()
84+
}
85+
86+
dbName := resolveDBName(cmd)
87+
88+
rawStore, err := store.NewDefaultKVStore(nodeConfig.RootDir, nodeConfig.DBPath, dbName)
89+
if err != nil {
90+
return fmt.Errorf("failed to open datastore: %w", err)
91+
}
92+
defer func() {
93+
if closeErr := rawStore.Close(); closeErr != nil {
94+
cmd.PrintErrf("warning: failed to close datastore: %v\n", closeErr)
95+
}
96+
}()
97+
98+
mainStore := kt.Wrap(rawStore, &kt.PrefixTransform{
99+
Prefix: ds.NewKey(node.EvPrefix),
100+
})
101+
102+
headerSnapshot, err := inspectP2PStore[*types.SignedHeader](ctx, mainStore, headerStorePrefix, "Header Store")
103+
if err != nil {
104+
return fmt.Errorf("failed to inspect header store: %w", err)
105+
}
106+
107+
dataSnapshot, err := inspectP2PStore[*types.Data](ctx, mainStore, dataStorePrefix, "Data Store")
108+
if err != nil {
109+
return fmt.Errorf("failed to inspect data store: %w", err)
110+
}
111+
112+
storePath := resolveStorePath(nodeConfig.RootDir, nodeConfig.DBPath, dbName)
113+
114+
out := cmd.OutOrStdout()
115+
fmt.Fprintf(out, "Inspecting go-header stores at %s\n", storePath)
116+
printP2PStoreSnapshot(cmd, headerSnapshot)
117+
printP2PStoreSnapshot(cmd, dataSnapshot)
118+
119+
return nil
120+
},
121+
}
122+
123+
const (
124+
headerStorePrefix = "headerSync"
125+
dataStorePrefix = "dataSync"
126+
)
127+
128+
type p2pStoreSnapshot struct {
129+
Label string
130+
Prefix string
131+
Height uint64
132+
HeadHeight uint64
133+
HeadHash string
134+
HeadTime time.Time
135+
TailHeight uint64
136+
TailHash string
137+
TailTime time.Time
138+
HeadPresent bool
139+
TailPresent bool
140+
Empty bool
141+
}
142+
143+
func inspectP2PStore[H goheader.Header[H]](
144+
ctx context.Context,
145+
datastore ds.Batching,
146+
prefix string,
147+
label string,
148+
) (p2pStoreSnapshot, error) {
149+
storeImpl, err := goheaderstore.NewStore[H](
150+
datastore,
151+
goheaderstore.WithStorePrefix(prefix),
152+
goheaderstore.WithMetrics(),
153+
)
154+
if err != nil {
155+
return p2pStoreSnapshot{}, fmt.Errorf("failed to open %s: %w", label, err)
156+
}
157+
158+
if err := storeImpl.Start(ctx); err != nil {
159+
return p2pStoreSnapshot{}, fmt.Errorf("failed to start %s: %w", label, err)
160+
}
161+
defer func() {
162+
_ = storeImpl.Stop(context.Background())
163+
}()
164+
165+
snapshot := p2pStoreSnapshot{
166+
Label: label,
167+
Prefix: prefix,
168+
Height: storeImpl.Height(),
169+
}
170+
171+
if err := populateSnapshot(ctx, storeImpl, &snapshot); err != nil {
172+
return p2pStoreSnapshot{}, err
173+
}
174+
175+
return snapshot, nil
176+
}
177+
178+
func populateSnapshot[H goheader.Header[H]](
179+
ctx context.Context,
180+
storeImpl *goheaderstore.Store[H],
181+
snapshot *p2pStoreSnapshot,
182+
) error {
183+
head, err := storeImpl.Head(ctx)
184+
switch {
185+
case err == nil:
186+
snapshot.HeadPresent = true
187+
snapshot.HeadHeight = head.Height()
188+
snapshot.HeadHash = head.Hash().String()
189+
snapshot.HeadTime = head.Time()
190+
case errors.Is(err, goheader.ErrEmptyStore), errors.Is(err, goheader.ErrNotFound):
191+
// store not initialized yet
192+
default:
193+
return fmt.Errorf("failed to read %s head: %w", snapshot.Label, err)
194+
}
195+
196+
tail, err := storeImpl.Tail(ctx)
197+
switch {
198+
case err == nil:
199+
snapshot.TailPresent = true
200+
snapshot.TailHeight = tail.Height()
201+
snapshot.TailHash = tail.Hash().String()
202+
snapshot.TailTime = tail.Time()
203+
case errors.Is(err, goheader.ErrEmptyStore), errors.Is(err, goheader.ErrNotFound):
204+
default:
205+
return fmt.Errorf("failed to read %s tail: %w", snapshot.Label, err)
206+
}
207+
208+
snapshot.Empty = !(snapshot.HeadPresent || snapshot.TailPresent)
209+
210+
return nil
211+
}
212+
213+
func printP2PStoreSnapshot(cmd *cobra.Command, snapshot p2pStoreSnapshot) {
214+
out := cmd.OutOrStdout()
215+
fmt.Fprintf(out, "\n[%s]\n", snapshot.Label)
216+
fmt.Fprintf(out, "prefix: %s\n", snapshot.Prefix)
217+
fmt.Fprintf(out, "height: %d\n", snapshot.Height)
218+
if snapshot.Empty {
219+
fmt.Fprintln(out, "status: empty (no entries found)")
220+
return
221+
}
222+
223+
if snapshot.TailPresent {
224+
fmt.Fprintf(out, "tail: height=%d hash=%s%s\n", snapshot.TailHeight, snapshot.TailHash, formatTime(snapshot.TailTime))
225+
}
226+
if snapshot.HeadPresent {
227+
fmt.Fprintf(out, "head: height=%d hash=%s%s\n", snapshot.HeadHeight, snapshot.HeadHash, formatTime(snapshot.HeadTime))
228+
}
229+
}
230+
231+
func formatTime(t time.Time) string {
232+
if t.IsZero() {
233+
return ""
234+
}
235+
return fmt.Sprintf(" time=%s", t.UTC().Format(time.RFC3339))
236+
}
237+
238+
func resolveDBName(cmd *cobra.Command) string {
239+
if cmd == nil {
240+
return config.ConfigFileName
241+
}
242+
root := cmd.Root()
243+
if root == nil || root.Name() == "" {
244+
return config.ConfigFileName
245+
}
246+
return root.Name()
247+
}
248+
249+
func resolveStorePath(rootDir, dbPath, dbName string) string {
250+
base := dbPath
251+
if !filepath.IsAbs(dbPath) {
252+
base = filepath.Join(rootDir, dbPath)
253+
}
254+
return filepath.Join(base, dbName)
255+
}

pkg/cmd/store_test.go

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,25 @@ package cmd
22

33
import (
44
"bytes"
5+
"context"
6+
cryptoRand "crypto/rand"
57
"fmt"
68
"os"
79
"path/filepath"
810
"testing"
911

12+
goheaderstore "github.com/celestiaorg/go-header/store"
13+
ds "github.com/ipfs/go-datastore"
14+
kt "github.com/ipfs/go-datastore/keytransform"
15+
"github.com/libp2p/go-libp2p/core/crypto"
1016
"github.com/spf13/cobra"
1117
"github.com/stretchr/testify/require"
18+
19+
"github.com/evstack/ev-node/node"
20+
"github.com/evstack/ev-node/pkg/config"
21+
"github.com/evstack/ev-node/pkg/signer/noop"
22+
"github.com/evstack/ev-node/pkg/store"
23+
"github.com/evstack/ev-node/types"
1224
)
1325

1426
func TestUnsafeCleanDataDir(t *testing.T) {
@@ -85,3 +97,87 @@ func TestStoreUnsafeCleanCmd(t *testing.T) {
8597
// Check output message (optional)
8698
require.Contains(t, buf.String(), fmt.Sprintf("All contents of the data directory at %s have been removed.", dataDir))
8799
}
100+
101+
func TestStoreP2PInspectCmd(t *testing.T) {
102+
tempDir := t.TempDir()
103+
const appName = "testapp"
104+
105+
// Seed the header store with a couple of entries.
106+
seedHeaderStore(t, tempDir, appName)
107+
108+
rootCmd := &cobra.Command{Use: appName}
109+
rootCmd.PersistentFlags().String(config.FlagRootDir, tempDir, "root directory")
110+
rootCmd.AddCommand(StoreP2PInspectCmd)
111+
112+
buf := new(bytes.Buffer)
113+
rootCmd.SetOut(buf)
114+
rootCmd.SetErr(buf)
115+
rootCmd.SetArgs([]string{"store-info"})
116+
117+
err := rootCmd.Execute()
118+
require.NoError(t, err)
119+
120+
output := buf.String()
121+
require.Contains(t, output, "Inspecting go-header stores")
122+
require.Contains(t, output, "[Header Store]")
123+
require.Contains(t, output, "tail: height=1")
124+
require.Contains(t, output, "head: height=2")
125+
require.Contains(t, output, "[Data Store]")
126+
require.Contains(t, output, "status: empty")
127+
}
128+
129+
func seedHeaderStore(t *testing.T, rootDir, dbName string) {
130+
t.Helper()
131+
132+
rawStore, err := store.NewDefaultKVStore(rootDir, "data", dbName)
133+
require.NoError(t, err)
134+
135+
mainStore := kt.Wrap(rawStore, &kt.PrefixTransform{
136+
Prefix: ds.NewKey(node.EvPrefix),
137+
})
138+
139+
headerStore, err := goheaderstore.NewStore[*types.SignedHeader](
140+
mainStore,
141+
goheaderstore.WithStorePrefix(headerStorePrefix),
142+
goheaderstore.WithMetrics(),
143+
)
144+
require.NoError(t, err)
145+
146+
ctx := context.Background()
147+
require.NoError(t, headerStore.Start(ctx))
148+
149+
defer func() {
150+
require.NoError(t, headerStore.Stop(ctx))
151+
require.NoError(t, rawStore.Close())
152+
}()
153+
154+
pk, _, err := crypto.GenerateEd25519Key(cryptoRand.Reader)
155+
require.NoError(t, err)
156+
noopSigner, err := noop.NewNoopSigner(pk)
157+
require.NoError(t, err)
158+
159+
chainID := "test-chain"
160+
headerCfg := types.HeaderConfig{
161+
Height: 1,
162+
DataHash: types.GetRandomBytes(32),
163+
AppHash: types.GetRandomBytes(32),
164+
Signer: noopSigner,
165+
}
166+
167+
first, err := types.GetRandomSignedHeaderCustom(&headerCfg, chainID)
168+
require.NoError(t, err)
169+
require.NoError(t, headerStore.Append(ctx, first))
170+
171+
next := &types.SignedHeader{
172+
Header: types.GetRandomNextHeader(first.Header, chainID),
173+
Signer: first.Signer,
174+
}
175+
payload, err := next.Header.MarshalBinary()
176+
require.NoError(t, err)
177+
signature, err := noopSigner.Sign(payload)
178+
require.NoError(t, err)
179+
next.Signature = signature
180+
181+
require.NoError(t, headerStore.Append(ctx, next))
182+
require.NoError(t, headerStore.Sync(ctx))
183+
}

0 commit comments

Comments
 (0)