Skip to content

Commit 48d0ab2

Browse files
authored
feat: automatically detect if genesis CAR is compressed (#12885)
Automatically detect if genesis CAR is compressed When `--genesis` path is set, automatically detect if the genesis file is ZSTD compressed and decompress it.
1 parent dee78a3 commit 48d0ab2

File tree

5 files changed

+167
-12
lines changed

5 files changed

+167
-12
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515

1616
- chore: switch to pure-go zstd decoder for snapshot imports. ([filecoin-project/lotus#12857](https://github.com/filecoin-project/lotus/pull/12857))
1717

18+
- feat: automatically detect if the genesis is zstd compressed. ([filecoin-project/lotus#12885](https://github.com/filecoin-project/lotus/pull/12885)
19+
1820
# UNRELEASED v.1.32.0
1921

2022
See https://github.com/filecoin-project/lotus/blob/release/v1.32.0/CHANGELOG.md

build/genesis.go

Lines changed: 34 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
package build
22

33
import (
4+
"bytes"
45
"embed"
56
"io"
67
"path"
78

89
logging "github.com/ipfs/go-log/v2"
910
"github.com/klauspost/compress/zstd"
11+
"golang.org/x/xerrors"
1012

1113
"github.com/filecoin-project/lotus/build/buildconstants"
1214
)
@@ -17,26 +19,51 @@ var log = logging.Logger("build")
1719
//go:embed genesis/*.car.zst
1820
var genesisCars embed.FS
1921

22+
var zstdHeader = []byte{0x28, 0xb5, 0x2f, 0xfd}
23+
2024
func MaybeGenesis() []byte {
2125
file, err := genesisCars.Open(path.Join("genesis", buildconstants.GenesisFile))
2226
if err != nil {
2327
log.Warnf("opening built-in genesis: %s", err)
2428
return nil
2529
}
2630
defer file.Close() //nolint
27-
28-
decoder, err := zstd.NewReader(file)
31+
decompressed, err := DecompressAsZstd(file)
2932
if err != nil {
30-
log.Warnf("creating zstd decoder: %s", err)
33+
log.Warnf("decompressing genesis: %s", err)
3134
return nil
3235
}
36+
return decompressed
37+
}
38+
39+
func DecompressAsZstd(target io.Reader) ([]byte, error) {
40+
decoder, err := zstd.NewReader(target)
41+
if err != nil {
42+
return nil, xerrors.Errorf("creating zstd decoder: %w", err)
43+
}
3344
defer decoder.Close() //nolint
3445

35-
decompressedBytes, err := io.ReadAll(decoder)
46+
decompressed, err := io.ReadAll(decoder)
3647
if err != nil {
37-
log.Warnf("reading decompressed genesis file: %s", err)
38-
return nil
48+
return nil, xerrors.Errorf("reading decompressed genesis file: %w", err)
3949
}
50+
return decompressed, nil
51+
}
4052

41-
return decompressedBytes
53+
func IsZstdCompressed(file io.ReadSeeker) (_ bool, _err error) {
54+
pos, err := file.Seek(0, io.SeekCurrent)
55+
if err != nil {
56+
return false, xerrors.Errorf("getting current position: %w", err)
57+
}
58+
defer func() {
59+
_, err := file.Seek(pos, io.SeekStart)
60+
if _err == nil && err != nil {
61+
_err = xerrors.Errorf("seeking back to original offset: %w", err)
62+
}
63+
}()
64+
header := make([]byte, 4)
65+
if _, err := file.Read(header); err != nil {
66+
return false, xerrors.Errorf("failed to read file header: %w", err)
67+
}
68+
return bytes.Equal(header, zstdHeader), nil
4269
}

build/genesis_test.go

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
package build_test
2+
3+
import (
4+
"bytes"
5+
"errors"
6+
"io"
7+
"os"
8+
"testing"
9+
10+
"github.com/klauspost/compress/zstd"
11+
"github.com/stretchr/testify/require"
12+
13+
"github.com/filecoin-project/lotus/build"
14+
)
15+
16+
func TestGenesis(t *testing.T) {
17+
for _, test := range []struct {
18+
path string
19+
}{
20+
{path: "genesis/butterflynet.car.zst"},
21+
{path: "genesis/calibnet.car.zst"},
22+
{path: "genesis/interopnet.car.zst"},
23+
{path: "genesis/mainnet.car.zst"},
24+
} {
25+
t.Run(test.path, func(t *testing.T) {
26+
subject, err := os.Open(test.path)
27+
require.NoError(t, err)
28+
29+
gotIsCompressed, err := build.IsZstdCompressed(subject)
30+
require.NoError(t, err)
31+
require.True(t, gotIsCompressed)
32+
33+
gotDecompressed, err := build.DecompressAsZstd(subject)
34+
require.NoError(t, err)
35+
require.NotEmpty(t, gotDecompressed)
36+
37+
gotIsCompressed, err = build.IsZstdCompressed(bytes.NewReader(gotDecompressed))
38+
require.NoError(t, err)
39+
require.False(t, gotIsCompressed)
40+
})
41+
}
42+
}
43+
44+
func TestGenesis_ZstdCheck(t *testing.T) {
45+
for _, test := range []struct {
46+
name string
47+
given func(t *testing.T) io.ReadSeeker
48+
wantCompressed bool
49+
wantErr bool
50+
}{
51+
{
52+
name: "arbitraryLongEnough",
53+
given: func(t *testing.T) io.ReadSeeker {
54+
return bytes.NewReader([]byte("fish"))
55+
},
56+
},
57+
{
58+
name: "arbitraryShort",
59+
given: func(t *testing.T) io.ReadSeeker {
60+
return bytes.NewReader([]byte("🐠"))
61+
},
62+
},
63+
{
64+
name: "arbitraryZstdCompressed",
65+
given: func(t *testing.T) io.ReadSeeker {
66+
var buf bytes.Buffer
67+
writer, err := zstd.NewWriter(&buf)
68+
require.NoError(t, err)
69+
written, err := writer.Write([]byte("fish"))
70+
require.NoError(t, err)
71+
require.NotZero(t, written)
72+
require.NoError(t, writer.Close())
73+
return bytes.NewReader(buf.Bytes())
74+
},
75+
wantCompressed: true,
76+
},
77+
{
78+
name: "failingPositionReset",
79+
given: func(t *testing.T) io.ReadSeeker { return failOnSeekStart{} },
80+
wantErr: true,
81+
},
82+
} {
83+
t.Run(test.name, func(t *testing.T) {
84+
target := test.given(t)
85+
gotCompressed, gotErr := build.IsZstdCompressed(target)
86+
require.Equal(t, test.wantCompressed, gotCompressed)
87+
require.Equal(t, test.wantErr, gotErr != nil)
88+
89+
if !test.wantErr {
90+
gotPosition, err := target.Seek(0, io.SeekCurrent)
91+
require.NoError(t, err)
92+
require.Zero(t, gotPosition)
93+
}
94+
})
95+
}
96+
}
97+
98+
var _ io.ReadSeeker = (*failOnSeekStart)(nil)
99+
100+
type failOnSeekStart struct{}
101+
102+
func (failOnSeekStart) Read([]byte) (int, error) { return 0, nil }
103+
104+
func (failOnSeekStart) Seek(_ int64, whence int) (int64, error) {
105+
if whence == io.SeekStart {
106+
return 0, errors.New("pursue the horizon; forsake the dawn; the start is long gone")
107+
}
108+
return 0, nil
109+
}

cli/lotus/daemon.go

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ var DaemonCmd = &cli.Command{
107107
},
108108
&cli.StringFlag{
109109
Name: "genesis",
110-
Usage: "genesis file to use for first node run",
110+
Usage: "genesis file to use for first node run, which may be a zstd compressed CAR or an uncompressed CAR file.",
111111
},
112112
&cli.BoolFlag{
113113
Name: "bootstrap",
@@ -253,9 +253,9 @@ var DaemonCmd = &cli.Command{
253253
}
254254

255255
var genBytes []byte
256-
if cctx.String("genesis") != "" {
257-
genBytes, err = os.ReadFile(cctx.String("genesis"))
258-
if err != nil {
256+
genesisPath := cctx.String("genesis")
257+
if genesisPath != "" {
258+
if genBytes, err = readGenesis(genesisPath); err != nil {
259259
return xerrors.Errorf("reading genesis: %w", err)
260260
}
261261
} else {
@@ -470,6 +470,23 @@ var DaemonCmd = &cli.Command{
470470
},
471471
}
472472

473+
// readGenesis detects if the path points to a zstd compressed file and if so
474+
// automatically decompress it. Otherwise, defaults to reading the path as is.
475+
func readGenesis(path string) ([]byte, error) {
476+
genesisFile, err := os.Open(filepath.Clean(path))
477+
if err != nil {
478+
return nil, xerrors.Errorf("opening genesis file: %w", err)
479+
}
480+
defer func() { _ = genesisFile.Close() }()
481+
482+
if compressed, err := build.IsZstdCompressed(genesisFile); err != nil {
483+
return nil, xerrors.Errorf("checking genesis header: %w", err)
484+
} else if compressed {
485+
return build.DecompressAsZstd(genesisFile)
486+
}
487+
return io.ReadAll(genesisFile)
488+
}
489+
473490
func importKey(ctx context.Context, api lapi.FullNode, f string) error {
474491
f, err := homedir.Expand(f)
475492
if err != nil {

documentation/en/cli-lotus.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ COMMANDS:
6464
6565
OPTIONS:
6666
--api value (default: "1234")
67-
--genesis value genesis file to use for first node run
67+
--genesis value genesis file to use for first node run, which may be a zstd compressed CAR or an uncompressed CAR file.
6868
--bootstrap (default: true)
6969
--import-chain value on first run, load chain from given file or url and validate
7070
--import-snapshot value import chain state from a given chain export file or url

0 commit comments

Comments
 (0)