Skip to content

Commit 8832ed5

Browse files
authored
tools: add -d and -o args for catchpointdump net, like file, new info command (#6235)
1 parent 8d01230 commit 8832ed5

File tree

3 files changed

+284
-2
lines changed

3 files changed

+284
-2
lines changed

cmd/catchpointdump/commands.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ func init() {
4444
rootCmd.AddCommand(fileCmd)
4545
rootCmd.AddCommand(netCmd)
4646
rootCmd.AddCommand(databaseCmd)
47+
rootCmd.AddCommand(infoCmd)
4748
}
4849

4950
var rootCmd = &cobra.Command{

cmd/catchpointdump/info.go

Lines changed: 274 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,274 @@
1+
// Copyright (C) 2019-2025 Algorand, Inc.
2+
// This file is part of go-algorand
3+
//
4+
// go-algorand is free software: you can redistribute it and/or modify
5+
// it under the terms of the GNU Affero General Public License as
6+
// published by the Free Software Foundation, either version 3 of the
7+
// License, or (at your option) any later version.
8+
//
9+
// go-algorand is distributed in the hope that it will be useful,
10+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
// GNU Affero General Public License for more details.
13+
//
14+
// You should have received a copy of the GNU Affero General Public License
15+
// along with go-algorand. If not, see <https://www.gnu.org/licenses/>.
16+
17+
package main
18+
19+
import (
20+
"bufio"
21+
"context"
22+
"fmt"
23+
"io"
24+
"net/http"
25+
"os"
26+
"strconv"
27+
"strings"
28+
"time"
29+
30+
"github.com/spf13/cobra"
31+
32+
"github.com/algorand/go-algorand/ledger"
33+
"github.com/algorand/go-algorand/network"
34+
"github.com/algorand/go-algorand/protocol"
35+
"github.com/algorand/go-algorand/util"
36+
)
37+
38+
var infoFile string
39+
40+
func init() {
41+
infoCmd.Flags().StringVarP(&infoFile, "tar", "t", "", "Specify the catchpoint file (.tar or .tar.gz) to read")
42+
infoCmd.Flags().StringVarP(&networkName, "net", "n", "", "Specify the network name (e.g. mainnet.algorand.network)")
43+
infoCmd.Flags().IntVarP(&round, "round", "r", 0, "Specify the round number (e.g. 7700000). Only used if --relay/-p is given.")
44+
infoCmd.Flags().StringVarP(&relayAddress, "relay", "p", "", "Relay address to download from (e.g. r-ru.algorand-mainnet.network:4160). If specified, fetch instead of reading local --tar.")
45+
}
46+
47+
// infoCmd defines a new cobra command that only loads and prints the CatchpointFileHeader.
48+
var infoCmd = &cobra.Command{
49+
Use: "info",
50+
Short: "Show header info from a catchpoint tar file",
51+
Long: "Reads the specified catchpoint tar (or tar.gz) file, locates the content.json block, and prints the CatchpointFileHeader fields without loading the entire ledger.",
52+
Args: validateNoPosArgsFn,
53+
Run: func(cmd *cobra.Command, args []string) {
54+
// If user gave us a relay, stream from the network:
55+
if relayAddress != "" {
56+
// If they gave a relay, they must also give us a valid network and round
57+
if networkName == "" || round == 0 {
58+
cmd.HelpFunc()(cmd, args)
59+
reportErrorf("Must specify --net and --round when using --relay")
60+
}
61+
// Attempt to read the CatchpointFileHeader from the network stream
62+
fileHeader, err := loadCatchpointFileHeaderFromRelay(relayAddress, networkName, round)
63+
if err != nil {
64+
reportErrorf("Error streaming CatchpointFileHeader from relay %s: %v", relayAddress, err)
65+
}
66+
if fileHeader.Version == 0 {
67+
fmt.Printf("No valid header was found streaming from relay '%s'.\n", relayAddress)
68+
return
69+
}
70+
fmt.Printf("Relay: %s\n", relayAddress)
71+
printHeaderFields(fileHeader)
72+
return
73+
}
74+
75+
// Otherwise, fallback to local file usage:
76+
if infoFile == "" {
77+
cmd.HelpFunc()(cmd, args)
78+
return
79+
}
80+
fi, err := os.Stat(infoFile)
81+
if err != nil {
82+
reportErrorf("Unable to stat file '%s': %v", infoFile, err)
83+
}
84+
if fi.Size() == 0 {
85+
reportErrorf("File '%s' is empty.", infoFile)
86+
}
87+
88+
// Open the catchpoint file
89+
f, err := os.Open(infoFile)
90+
if err != nil {
91+
reportErrorf("Unable to open file '%s': %v", infoFile, err)
92+
}
93+
defer f.Close()
94+
95+
// Extract just the file header
96+
fileHeader, err := loadCatchpointFileHeader(f, fi.Size())
97+
if err != nil {
98+
reportErrorf("Error reading CatchpointFileHeader from '%s': %v", infoFile, err)
99+
}
100+
101+
// Print out the fields (mimicking the logic in printAccountsDatabase, but simpler)
102+
if fileHeader.Version == 0 {
103+
fmt.Printf("No valid header was found.\n")
104+
return
105+
}
106+
107+
printHeaderFields(fileHeader)
108+
},
109+
}
110+
111+
func printHeaderFields(fileHeader ledger.CatchpointFileHeader) {
112+
fmt.Printf("Version: %d\n", fileHeader.Version)
113+
fmt.Printf("Balances Round: %d\n", fileHeader.BalancesRound)
114+
fmt.Printf("Block Round: %d\n", fileHeader.BlocksRound)
115+
fmt.Printf("Block Header Digest: %s\n", fileHeader.BlockHeaderDigest.String())
116+
fmt.Printf("Catchpoint: %s\n", fileHeader.Catchpoint)
117+
fmt.Printf("Total Accounts: %d\n", fileHeader.TotalAccounts)
118+
fmt.Printf("Total KVs: %d\n", fileHeader.TotalKVs)
119+
fmt.Printf("Total Online Accounts: %d\n", fileHeader.TotalOnlineAccounts)
120+
fmt.Printf("Total Online Round Params: %d\n", fileHeader.TotalOnlineRoundParams)
121+
fmt.Printf("Total Chunks: %d\n", fileHeader.TotalChunks)
122+
123+
totals := fileHeader.Totals
124+
fmt.Printf("AccountTotals - Online Money: %d\n", totals.Online.Money.Raw)
125+
fmt.Printf("AccountTotals - Online RewardUnits: %d\n", totals.Online.RewardUnits)
126+
fmt.Printf("AccountTotals - Offline Money: %d\n", totals.Offline.Money.Raw)
127+
fmt.Printf("AccountTotals - Offline RewardUnits: %d\n", totals.Offline.RewardUnits)
128+
fmt.Printf("AccountTotals - Not Participating Money: %d\n", totals.NotParticipating.Money.Raw)
129+
fmt.Printf("AccountTotals - Not Participating RewardUnits: %d\n", totals.NotParticipating.RewardUnits)
130+
fmt.Printf("AccountTotals - Rewards Level: %d\n", totals.RewardsLevel)
131+
}
132+
133+
// loadCatchpointFileHeader reads only enough of the tar (or tar.gz) to
134+
// decode the ledger.CatchpointFileHeader from the "content.json" chunk.
135+
func loadCatchpointFileHeader(catchpointFile io.Reader, catchpointFileSize int64) (ledger.CatchpointFileHeader, error) {
136+
var fileHeader ledger.CatchpointFileHeader
137+
fmt.Printf("Scanning for CatchpointFileHeader in tar...\n\n")
138+
139+
catchpointReader := bufio.NewReader(catchpointFile)
140+
tarReader, _, err := getCatchpointTarReader(catchpointReader, catchpointFileSize)
141+
if err != nil {
142+
return fileHeader, err
143+
}
144+
145+
for {
146+
hdr, err := tarReader.Next()
147+
if err != nil {
148+
if err == io.EOF {
149+
// We reached the end without finding content.json
150+
break
151+
}
152+
return fileHeader, err
153+
}
154+
155+
// We only need the "content.json" file
156+
if hdr.Name == ledger.CatchpointContentFileName {
157+
// Read exactly hdr.Size bytes
158+
buf := make([]byte, hdr.Size)
159+
_, readErr := io.ReadFull(tarReader, buf)
160+
if readErr != nil && readErr != io.EOF {
161+
return fileHeader, readErr
162+
}
163+
164+
// Decode into fileHeader
165+
readErr = protocol.Decode(buf, &fileHeader)
166+
if readErr != nil {
167+
return fileHeader, readErr
168+
}
169+
// Once we have the fileHeader, we can break out.
170+
// If you wanted to keep scanning, you could keep going,
171+
// but it’s not needed just for the header.
172+
return fileHeader, nil
173+
}
174+
175+
// Otherwise skip this chunk
176+
skipBytes := hdr.Size
177+
n, err := io.Copy(io.Discard, tarReader)
178+
if err != nil {
179+
return fileHeader, err
180+
}
181+
182+
// skip any leftover in case we didn't read the entire chunk
183+
if skipBytes > n {
184+
// keep discarding until we've skipped skipBytes total
185+
_, err := io.CopyN(io.Discard, tarReader, skipBytes-n)
186+
if err != nil {
187+
return fileHeader, err
188+
}
189+
}
190+
}
191+
// If we get here, we never found the content.json entry
192+
return fileHeader, nil
193+
}
194+
195+
// loadCatchpointFileHeaderFromRelay opens a streaming HTTP connection to the
196+
// given relay for the given round, then scans the (possibly gzip) tar stream
197+
// until it finds `content.json`, decodes the ledger.CatchpointFileHeader, and
198+
// immediately closes the network connection (so we don't download the entire file).
199+
func loadCatchpointFileHeaderFromRelay(relay string, netName string, round int) (ledger.CatchpointFileHeader, error) {
200+
var fileHeader ledger.CatchpointFileHeader
201+
202+
// Create an HTTP GET to the relay
203+
genesisID := strings.Split(netName, ".")[0] + "-v1.0"
204+
urlTemplate := "http://" + relay + "/v1/" + genesisID + "/%s/" + strconv.FormatUint(uint64(round), 36)
205+
catchpointURL := fmt.Sprintf(urlTemplate, "ledger")
206+
207+
req, err := http.NewRequest(http.MethodGet, catchpointURL, nil)
208+
if err != nil {
209+
return fileHeader, err
210+
}
211+
// Add a short-ish timeout or rely on default
212+
ctx, cancelFn := context.WithTimeout(context.Background(), 60*time.Second)
213+
defer cancelFn()
214+
req = req.WithContext(ctx)
215+
network.SetUserAgentHeader(req.Header)
216+
217+
resp, err := http.DefaultClient.Do(req)
218+
if err != nil {
219+
return fileHeader, err
220+
}
221+
if resp.StatusCode != http.StatusOK {
222+
// e.g. 404 if not found
223+
return fileHeader, fmt.Errorf("HTTP status code %d from relay", resp.StatusCode)
224+
}
225+
defer resp.Body.Close()
226+
227+
// Wrap with a small "watchdog" so we don't hang if data stops flowing
228+
wdReader := util.MakeWatchdogStreamReader(resp.Body, 4096, 4096, 5*time.Second)
229+
defer wdReader.Close()
230+
231+
// Use isGzip logic from file.go
232+
// We have to peek the first 2 bytes to see if it's gz
233+
peekReader := bufio.NewReader(wdReader)
234+
// We'll fake a size of "unknown" since we don't truly know the length
235+
tarReader, _, err := getCatchpointTarReader(peekReader, -1 /* unknown size */)
236+
if err != nil {
237+
return fileHeader, err
238+
}
239+
240+
// Now read each tar entry, ignoring everything except "content.json"
241+
for {
242+
hdr, err := tarReader.Next()
243+
if err != nil {
244+
if err == io.EOF {
245+
// finished the entire tar stream
246+
break
247+
}
248+
return fileHeader, err
249+
}
250+
if hdr.Name == ledger.CatchpointContentFileName {
251+
// We only need "content.json"
252+
buf := make([]byte, hdr.Size)
253+
_, readErr := io.ReadFull(tarReader, buf)
254+
if readErr != nil && readErr != io.EOF {
255+
return fileHeader, readErr
256+
}
257+
258+
// decode
259+
decodeErr := protocol.Decode(buf, &fileHeader)
260+
if decodeErr != nil {
261+
return fileHeader, decodeErr
262+
}
263+
// Done! We can return immediately.
264+
return fileHeader, nil
265+
}
266+
// If not content.json, skip over this tar chunk
267+
_, err = io.Copy(io.Discard, tarReader)
268+
if err != nil {
269+
return fileHeader, err
270+
}
271+
}
272+
// If we exit the loop, we never found content.json
273+
return fileHeader, nil
274+
}

cmd/catchpointdump/net.go

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,8 @@ func init() {
6060
netCmd.Flags().BoolVarP(&singleCatchpoint, "single", "s", true, "Download/process only from a single relay")
6161
netCmd.Flags().BoolVarP(&loadOnly, "load", "l", false, "Load only, do not dump")
6262
netCmd.Flags().VarP(excludedFields, "exclude-fields", "e", "List of fields to exclude from the dump: ["+excludedFields.AllowedString()+"]")
63+
netCmd.Flags().StringVarP(&outFileName, "output", "o", "", "Specify an outfile for the dump ( i.e. tracker.dump.txt )")
64+
netCmd.Flags().BoolVarP(&printDigests, "digest", "d", false, "Print balances and spver digests")
6365
}
6466

6567
var netCmd = &cobra.Command{
@@ -103,7 +105,7 @@ var netCmd = &cobra.Command{
103105
reportInfof("failed to load/dump from tar file for '%s' : %v", addr, err)
104106
continue
105107
}
106-
// clear possible errors from previous run: at this point we've been succeed
108+
// clear possible errors from previous run: at this point we've succeeded
107109
err = nil
108110
if singleCatchpoint {
109111
// a catchpoint processes successfully, exit if needed
@@ -348,7 +350,12 @@ func loadAndDump(addr string, tarFile string, genesisInitState ledgercore.InitSt
348350

349351
if !loadOnly {
350352
dirName := "./" + strings.Split(networkName, ".")[0] + "/" + strings.Split(addr, ".")[0]
351-
outFile, err := os.OpenFile(dirName+"/"+strconv.FormatUint(uint64(round), 10)+".dump", os.O_RDWR|os.O_TRUNC|os.O_CREATE, 0755)
353+
// If user provided -o <filename>, use that; otherwise use <dirName>/<round>.dump
354+
dumpFilename := outFileName
355+
if dumpFilename == "" {
356+
dumpFilename = dirName + "/" + strconv.FormatUint(uint64(round), 10) + ".dump"
357+
}
358+
outFile, err := os.OpenFile(dumpFilename, os.O_RDWR|os.O_TRUNC|os.O_CREATE, 0755)
352359
if err != nil {
353360
return err
354361
}

0 commit comments

Comments
 (0)