Skip to content

Commit

Permalink
services: add new service for fetching blocks from NeoFS
Browse files Browse the repository at this point in the history
Close #3496

Signed-off-by: Ekaterina Pavlova <[email protected]>
  • Loading branch information
AliceInHunterland committed Aug 29, 2024
1 parent dc6c195 commit d37cc2c
Show file tree
Hide file tree
Showing 14 changed files with 970 additions and 70 deletions.
91 changes: 91 additions & 0 deletions cli/server/dump_bin.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
package server

import (
"fmt"
"os"
"path/filepath"

"github.com/nspcc-dev/neo-go/cli/cmdargs"
"github.com/nspcc-dev/neo-go/cli/options"
"github.com/nspcc-dev/neo-go/pkg/core/block"
"github.com/nspcc-dev/neo-go/pkg/io"
"github.com/urfave/cli/v2"
)

func dumpBin(ctx *cli.Context) error {
if err := cmdargs.EnsureNone(ctx); err != nil {
return err

Check warning on line 17 in cli/server/dump_bin.go

View check run for this annotation

Codecov / codecov/patch

cli/server/dump_bin.go#L17

Added line #L17 was not covered by tests
}
cfg, err := options.GetConfigFromContext(ctx)
if err != nil {
return cli.Exit(err, 1)
}
log, _, logCloser, err := options.HandleLoggingParams(ctx.Bool("debug"), cfg.ApplicationConfiguration)
if err != nil {
return cli.Exit(err, 1)

Check warning on line 25 in cli/server/dump_bin.go

View check run for this annotation

Codecov / codecov/patch

cli/server/dump_bin.go#L25

Added line #L25 was not covered by tests
}
if logCloser != nil {
defer func() { _ = logCloser() }()

Check warning on line 28 in cli/server/dump_bin.go

View check run for this annotation

Codecov / codecov/patch

cli/server/dump_bin.go#L28

Added line #L28 was not covered by tests
}
count := uint32(ctx.Uint("count"))
start := uint32(ctx.Uint("start"))

chain, prometheus, pprof, err := initBCWithMetrics(cfg, log)
if err != nil {
return err

Check warning on line 35 in cli/server/dump_bin.go

View check run for this annotation

Codecov / codecov/patch

cli/server/dump_bin.go#L35

Added line #L35 was not covered by tests
}
defer func() {
pprof.ShutDown()
prometheus.ShutDown()
chain.Close()
}()

blocksCount := chain.BlockHeight() + 1
if start+count > blocksCount {
return cli.Exit(fmt.Errorf("chain is not that high (%d) to dump %d blocks starting from %d", blocksCount-1, count, start), 1)
}
if count == 0 {
count = blocksCount - start
}

out := ctx.String("out")
if out == "" {
return cli.Exit("output directory is not specified", 1)
}
if _, err = os.Stat(out); os.IsNotExist(err) {
if err = os.MkdirAll(out, os.ModePerm); err != nil {
return cli.Exit(fmt.Sprintf("failed to create directory %s: %s", out, err), 1)

Check warning on line 57 in cli/server/dump_bin.go

View check run for this annotation

Codecov / codecov/patch

cli/server/dump_bin.go#L57

Added line #L57 was not covered by tests
}
}
if err != nil {
return cli.Exit(fmt.Sprintf("failed to check directory %s: %s", out, err), 1)

Check warning on line 61 in cli/server/dump_bin.go

View check run for this annotation

Codecov / codecov/patch

cli/server/dump_bin.go#L61

Added line #L61 was not covered by tests
}

for i := start; i < start+count; i++ {
bh := chain.GetHeaderHash(i)
blk, err := chain.GetBlock(bh)
if err != nil {
return cli.Exit(fmt.Sprintf("failed to get block %d: %s", i, err), 1)

Check warning on line 68 in cli/server/dump_bin.go

View check run for this annotation

Codecov / codecov/patch

cli/server/dump_bin.go#L68

Added line #L68 was not covered by tests
}
filePath := filepath.Join(out, fmt.Sprintf("block-%d.bin", i))
if err = saveBlockToFile(blk, filePath); err != nil {
return cli.Exit(fmt.Sprintf("failed to save block %d to file %s: %s", i, filePath, err), 1)
}
}
return nil
}

func saveBlockToFile(blk *block.Block, filePath string) error {
file, err := os.Create(filePath)
if err != nil {
return err
}
defer file.Close()

writer := io.NewBinWriterFromIO(file)
blk.EncodeBinary(writer)
if writer.Err != nil {
return writer.Err

Check warning on line 88 in cli/server/dump_bin.go

View check run for this annotation

Codecov / codecov/patch

cli/server/dump_bin.go#L88

Added line #L88 was not covered by tests
}
return nil
}
101 changes: 101 additions & 0 deletions cli/server/dump_bin_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
package server_test

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

"github.com/nspcc-dev/neo-go/internal/testcli"
"github.com/nspcc-dev/neo-go/pkg/config"
"github.com/nspcc-dev/neo-go/pkg/core/storage/dbconfig"
"github.com/stretchr/testify/require"
"gopkg.in/yaml.v3"
)

func TestDumpBin(t *testing.T) {
tmpDir := t.TempDir()

loadConfig := func(t *testing.T) config.Config {
chainPath := filepath.Join(tmpDir, "neogotestchain")
cfg, err := config.LoadFile(filepath.Join("..", "..", "config", "protocol.unit_testnet.yml"))
require.NoError(t, err, "could not load config")
cfg.ApplicationConfiguration.DBConfiguration.Type = dbconfig.LevelDB
cfg.ApplicationConfiguration.DBConfiguration.LevelDBOptions.DataDirectoryPath = chainPath
return cfg
}

cfg := loadConfig(t)
out, err := yaml.Marshal(cfg)
require.NoError(t, err)

cfgPath := filepath.Join(tmpDir, "protocol.unit_testnet.yml")
require.NoError(t, os.WriteFile(cfgPath, out, os.ModePerm))

e := testcli.NewExecutor(t, false)

restoreArgs := []string{"neo-go", "db", "restore",
"--config-file", cfgPath, "--in", inDump}
e.Run(t, restoreArgs...)

t.Run("missing output directory", func(t *testing.T) {
args := []string{"neo-go", "db", "dump-bin",
"--config-file", cfgPath, "--out", ""}
e.RunWithErrorCheck(t, "output directory is not specified", args...)
})

t.Run("successful dump", func(t *testing.T) {
outDir := filepath.Join(tmpDir, "blocks")
args := []string{"neo-go", "db", "dump-bin",
"--config-file", cfgPath, "--out", outDir, "--count", "5", "--start", "0"}

e.Run(t, args...)

require.DirExists(t, outDir)

for i := 0; i < 5; i++ {
blockFile := filepath.Join(outDir, "block-"+strconv.Itoa(i)+".bin")
require.FileExists(t, blockFile)
}
})

t.Run("invalid block range", func(t *testing.T) {
outDir := filepath.Join(tmpDir, "invalid-blocks")
args := []string{"neo-go", "db", "dump-bin",
"--config-file", cfgPath, "--out", outDir, "--count", "1000", "--start", "0"}

e.RunWithError(t, args...)
})

t.Run("output directory with no write permission", func(t *testing.T) {
outDir := filepath.Join(tmpDir, "no-write-permission")
require.NoError(t, os.Mkdir(outDir, 0400))

args := []string{"neo-go", "db", "dump-bin",
"--config-file", cfgPath, "--out", outDir, "--count", "5", "--start", "0"}

e.RunWithError(t, args...)
})

t.Run("zero blocks (full chain dump)", func(t *testing.T) {
outDir := filepath.Join(tmpDir, "full-dump")
args := []string{"neo-go", "db", "dump-bin",
"--config-file", cfgPath, "--out", outDir}

e.Run(t, args...)

require.DirExists(t, outDir)
for i := 0; i < 50; i++ {
blockFile := filepath.Join(outDir, "block-"+strconv.Itoa(i)+".bin")
require.FileExists(t, blockFile)
}
})

t.Run("invalid config file", func(t *testing.T) {
outDir := filepath.Join(tmpDir, "blocks")
args := []string{"neo-go", "db", "dump-bin",
"--config-file", "invalid-config-path", "--out", outDir}

e.RunWithError(t, args...)
})
}
7 changes: 7 additions & 0 deletions cli/server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,13 @@ func NewCommands() []*cli.Command {
Action: dumpDB,
Flags: cfgCountOutFlags,
},
{
Name: "dump-bin",
Usage: "Dump blocks (starting with the genesis or specified block) to the directory in binary format",
UsageText: "neo-go db dump-bin -o directory [-s start] [-c count] [--config-path path] [-p/-m/-t] [--config-file file]",
Action: dumpBin,
Flags: cfgCountOutFlags,
},
{
Name: "restore",
Usage: "Restore blocks from the file",
Expand Down
16 changes: 16 additions & 0 deletions config/protocol.testnet.yml
Original file line number Diff line number Diff line change
Expand Up @@ -100,3 +100,19 @@ ApplicationConfiguration:
Enabled: false
Addresses:
- ":2113"
# NeoFSBlockFetcher:
# Enabled: true
# UnlockWallet:
# Path: "./testnet_wallet.json"
# Password: "111"
# Addresses:
# - st1.t5.fs.neo.org:8080
# Timeout: 30s
# ContainerID: "9iVfUg8aDHKjPC4LhQXEkVUM4HDkR7UCXYLs8NQwYfSG"
# Mode: "oidSearch" # other options: 'indexSearch', 'oidSearch'
# BatchSize: 1000
# BlockAttribute: "blocks_index_1"
# OidAttribute: "block_oids_1"
# HeaderAttribute: "index_header"


54 changes: 54 additions & 0 deletions docs/node-configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ node-related settings described in the table below.
| GarbageCollectionPeriod | `uint32` | 10000 | Controls MPT garbage collection interval (in blocks) for configurations with `RemoveUntraceableBlocks` enabled and `KeepOnlyLatestState` disabled. In this mode the node stores a number of MPT trees (corresponding to `MaxTraceableBlocks` and `StateSyncInterval`), but the DB needs to be clean from old entries from time to time. Doing it too often will cause too much processing overhead, doing it too rarely will leave more useless data in the DB. |
| KeepOnlyLatestState | `bool` | `false` | Specifies if MPT should only store the latest state (or a set of latest states, see `P2PStateExchangeExtensions` section in the ProtocolConfiguration for details). If true, DB size will be smaller, but older roots won't be accessible. This value should remain the same for the same database. | |
| LogPath | `string` | "", so only console logging | File path where to store node logs. |
| NeoFSBlockFetcher | [NeoFSBlockFetcher Configuration](#NeoFSBlockFetcher-Configuration) | | NeoFSBlockFetcher module configuration. See the [NeoFSBlockFetcher Configuration](#Oracle-Configuration) section for details. |
| Oracle | [Oracle Configuration](#Oracle-Configuration) | | Oracle module configuration. See the [Oracle Configuration](#Oracle-Configuration) section for details. |
| P2P | [P2P Configuration](#P2P-Configuration) | | Configuration values for P2P network interaction. See the [P2P Configuration](#P2P-Configuration) section for details. |
| P2PNotary | [P2P Notary Configuration](#P2P-Notary-Configuration) | | P2P Notary module configuration. See the [P2P Notary Configuration](#P2P-Notary-Configuration) section for details. |
Expand Down Expand Up @@ -323,6 +324,59 @@ where:
- `Path` is a path to wallet.
- `Password` is a wallet password.

### NeoFSBlockFetcher Configuration
NeoFSBlockFetcher:
Enabled: true
UnlockWallet:
Path: "./testnet_wallet.json"
Password: "111"
Addresses:
- st1.t5.fs.neo.org:8080
Timeout: 30s
ContainerID: "9iVfUg8aDHKjPC4LhQXEkVUM4HDkR7UCXYLs8NQwYfSG"
Mode: "oidSearch" # other options: 'indexSearch', 'oidSearch'
BatchSize: 1000
BlockAttribute: "blocks_index_1"
OidAttribute: "block_oids_1"
HeaderAttribute: "index_header"


### NeoFSBlockFetcher Configuration
`NeoFSBlockFetcher` configuration section contains settings for NeoFS block fetcher
module and has the following structure:
```
NeoFSBlockFetcher:
Enabled: false
UnlockWallet:
Path: "./wallet.json"
Password: "pass"
Addresses:
- st1.t5.fs.neo.org:8080
Timeout: 30s
ContainerID: "9iVfUg8aDHKjPC4LhQXEkVUM4HDkR7UCXYLs8NQwYfSG"
Mode: "oidSearch"
BatchSize: 1000
BlockAttribute: "blocks_index_1"
OidAttribute: "block_oids_1"
HeaderAttribute: "index_header"
```
where:
- `Enabled` enables NeoFS block fetcher module.
- `UnlockWallet` contains wallet settings, see
[Unlock Wallet Configuration](#Unlock-Wallet-Configuration) section for
structure details.
- `Addresses` is a list of NeoFS storage nodes addresses.
- `Timeout` is a timeout for NeoFS storage nodes requests.
- `ContainerID` is a container ID to fetch blocks from.
- `Mode` is a mode of fetching blocks from NeoFS storage nodes. Available options:
- `oidSearch` - fetch blocks by their OIDs.
- `indexSearch` - fetch blocks by their indexes.
- `BatchSize` is a number of blocks to fetch in a single request.
- `BlockAttribute` is an attribute name in the container that contains blocks.
- `OidAttribute` is an attribute name in the container that contains block OIDs.
- `HeaderAttribute` is an attribute name in the container that contains block headers.


## Protocol Configuration

`ProtocolConfiguration` section of `yaml` node configuration file contains
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ require (
golang.org/x/term v0.18.0
golang.org/x/text v0.14.0
golang.org/x/tools v0.19.0
google.golang.org/grpc v1.62.0
gopkg.in/yaml.v3 v3.0.1
)

Expand Down Expand Up @@ -67,7 +68,6 @@ require (
golang.org/x/net v0.23.0 // indirect
golang.org/x/sys v0.18.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240221002015-b0ce06bbee7c // indirect
google.golang.org/grpc v1.62.0 // indirect
google.golang.org/protobuf v1.33.0 // indirect
rsc.io/tmplfunc v0.0.3 // indirect
)
23 changes: 17 additions & 6 deletions pkg/config/application_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,13 @@ type ApplicationConfiguration struct {
Pprof BasicService `yaml:"Pprof"`
Prometheus BasicService `yaml:"Prometheus"`

Relay bool `yaml:"Relay"`
Consensus Consensus `yaml:"Consensus"`
RPC RPC `yaml:"RPC"`
Oracle OracleConfiguration `yaml:"Oracle"`
P2PNotary P2PNotary `yaml:"P2PNotary"`
StateRoot StateRoot `yaml:"StateRoot"`
Relay bool `yaml:"Relay"`
Consensus Consensus `yaml:"Consensus"`
RPC RPC `yaml:"RPC"`
Oracle OracleConfiguration `yaml:"Oracle"`
P2PNotary P2PNotary `yaml:"P2PNotary"`
StateRoot StateRoot `yaml:"StateRoot"`
NeoFSBlockFetcher NeoFSBlockFetcher `yaml:"NeoFSBlockFetcher"`
}

// EqualsButServices returns true when the o is the same as a except for services
Expand Down Expand Up @@ -145,3 +146,13 @@ func (a *ApplicationConfiguration) GetAddresses() ([]AnnounceableAddress, error)
}
return addrs, nil
}

// Validate checks ApplicationConfiguration for internal consistency and returns
// an error if any invalid settings are found. This ensures that the application
// configuration is valid and safe to use for further operations.
func (a *ApplicationConfiguration) Validate() error {
if err := a.NeoFSBlockFetcher.Validate(); err != nil {
return fmt.Errorf("failed to validate NeoFSBlockFetcher section: %w", err)

Check warning on line 155 in pkg/config/application_config.go

View check run for this annotation

Codecov / codecov/patch

pkg/config/application_config.go#L155

Added line #L155 was not covered by tests
}
return nil
}
49 changes: 49 additions & 0 deletions pkg/config/blockfetcher_config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package config

import (
"errors"
"fmt"
"time"

cid "github.com/nspcc-dev/neofs-sdk-go/container/id"
)

// NeoFSBlockFetcher represents the configuration for the NeoFS block fetcher service.
type NeoFSBlockFetcher struct {
InternalService `yaml:",inline"`
Timeout time.Duration `yaml:"Timeout"`
ContainerID string `yaml:"ContainerID"`
Mode string `yaml:"Mode"`
Addresses []string `yaml:"Addresses"`
BatchSize int `yaml:"BatchSize"`
BlockAttribute string `yaml:"BlockAttribute"`
OidAttribute string `yaml:"OidAttribute"`
HeaderAttribute string `yaml:"HeaderAttribute"`
}

// Validate checks NeoFSBlockFetcher for internal consistency and ensures
// that all required fields are properly set. It returns an error if the
// configuration is invalid or if the ContainerID cannot be properly decoded.
func (cfg *NeoFSBlockFetcher) Validate() error {
if !cfg.Enabled {
return nil
}
if cfg.ContainerID == "" {
return errors.New("container ID is not set")

Check warning on line 32 in pkg/config/blockfetcher_config.go

View check run for this annotation

Codecov / codecov/patch

pkg/config/blockfetcher_config.go#L31-L32

Added lines #L31 - L32 were not covered by tests
}
var containerID cid.ID
err := containerID.DecodeString(cfg.ContainerID)
if err != nil {
return fmt.Errorf("invalid container ID: %w", err)

Check warning on line 37 in pkg/config/blockfetcher_config.go

View check run for this annotation

Codecov / codecov/patch

pkg/config/blockfetcher_config.go#L34-L37

Added lines #L34 - L37 were not covered by tests
}
if cfg.Timeout == 0 {
cfg.Timeout = 10 * time.Second

Check warning on line 40 in pkg/config/blockfetcher_config.go

View check run for this annotation

Codecov / codecov/patch

pkg/config/blockfetcher_config.go#L39-L40

Added lines #L39 - L40 were not covered by tests
}
if cfg.BatchSize == 0 {
cfg.BatchSize = 50

Check warning on line 43 in pkg/config/blockfetcher_config.go

View check run for this annotation

Codecov / codecov/patch

pkg/config/blockfetcher_config.go#L42-L43

Added lines #L42 - L43 were not covered by tests
}
if cfg.Mode == "" {
cfg.Mode = "indexSearch"

Check warning on line 46 in pkg/config/blockfetcher_config.go

View check run for this annotation

Codecov / codecov/patch

pkg/config/blockfetcher_config.go#L45-L46

Added lines #L45 - L46 were not covered by tests
}
return nil

Check warning on line 48 in pkg/config/blockfetcher_config.go

View check run for this annotation

Codecov / codecov/patch

pkg/config/blockfetcher_config.go#L48

Added line #L48 was not covered by tests
}
Loading

0 comments on commit d37cc2c

Please sign in to comment.