Repository: nspcc-dev/neo-go
Pull request #3833 (2025-03-07 10:29:42)

config: upgrade NeoFS networks configuration
1-second blocks are used now.

codecov[bot]: ## [Codecov](https://app.codecov.io/gh/nspcc-dev/neo-go/pull/3833?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=nspcc-dev) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Project coverage is 82.76%. Comparing base [(`d411337`)](https://app.codecov.io/gh/nspcc-dev/neo-go/commit/d411337648ee7c92182c907eab2483db56a0f63e?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=nspcc-dev) to head [(`0481089`)](https://app.codecov.io/gh/nspcc-dev/neo-go/commit/0481089d98687dcd59a090c36fe5b9e4eab659da?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=nspcc-dev).

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##           master    #3833      +/-   ##
==========================================
+ Coverage   82.74%   82.76%   +0.01%     
==========================================
  Files         338      338              
  Lines       47484    47484              
==========================================
+ Hits        39293    39301       +8     
+ Misses       6561     6554       -7     
+ Partials     1630     1629       -1     
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/nspcc-dev/neo-go/pull/3833?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=nspcc-dev).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=nspcc-dev).

<details><summary>🚀 New features to boost your workflow: </summary>

- ❄ [Test Analytics](https://docs.codecov.com/docs/test-analytics): Detect flaky tests, report on failures, and find test suite problems.
</details>

Patch:
diff --git a/cli/options/options.go b/cli/options/options.go
index b4e3d5267..12c5db701 100644
--- a/cli/options/options.go
+++ b/cli/options/options.go
@@ -25,11 +25,8 @@ import (
 	"github.com/nspcc-dev/neo-go/pkg/rpcclient"
 	"github.com/nspcc-dev/neo-go/pkg/rpcclient/actor"
 	"github.com/nspcc-dev/neo-go/pkg/rpcclient/invoker"
-	"github.com/nspcc-dev/neo-go/pkg/services/helpers/neofs"
 	"github.com/nspcc-dev/neo-go/pkg/util"
 	"github.com/nspcc-dev/neo-go/pkg/wallet"
-	"github.com/nspcc-dev/neofs-sdk-go/pool"
-	"github.com/nspcc-dev/neofs-sdk-go/user"
 	"github.com/urfave/cli/v2"
 	"go.uber.org/zap"
 	"go.uber.org/zap/zapcore"
@@ -46,13 +43,9 @@ const (
 	DefaultAwaitableTimeout = 3 * 15 * time.Second
 )
 
-const (
-	// RPCEndpointFlag is a long flag name for an RPC endpoint. It can be used to
-	// check for flag presence in the context.
-	RPCEndpointFlag = "rpc-endpoint"
-	// NeoFSRPCEndpointFlag is a long flag name for a NeoFS RPC endpoint.
-	NeoFSRPCEndpointFlag = "fs-rpc-endpoint"
-)
+// RPCEndpointFlag is a long flag name for an RPC endpoint. It can be used to
+// check for flag presence in the context.
+const RPCEndpointFlag = "rpc-endpoint"
 
 // Wallet is a set of flags used for wallet operations.
 var Wallet = []cli.Flag{
@@ -108,22 +101,6 @@ var RPC = []cli.Flag{
 	},
 }
 
-// NeoFSRPC is a set of flags used for NeoFS RPC connections (endpoint).
-var NeoFSRPC = []cli.Flag{&cli.StringSliceFlag{
-	Name:     NeoFSRPCEndpointFlag,
-	Aliases:  []string{"fsr"},
-	Usage:    "List of NeoFS storage node RPC addresses (comma-separated or multiple --fs-rpc-endpoint flags)",
-	Required: true,
-	Action: func(ctx *cli.Context, fsRpcEndpoints []string) error {
-		for _, endpoint := range fsRpcEndpoints {
-			if endpoint == "" {
-				return cli.Exit("NeoFS RPC endpoint cannot contain empty values", 1)
-			}
-		}
-		return nil
-	},
-}}
-
 // Historic is a flag for commands that can perform historic invocations.
 var Historic = &cli.StringFlag{
 	Name:  "historic",
@@ -203,26 +180,6 @@ func GetRPCClient(gctx context.Context, ctx *cli.Context) (*rpcclient.Client, cl
 	return c, nil
 }
 
-// GetNeoFSClientPool returns a NeoFS pool and a signer for the given Context.
-func GetNeoFSClientPool(ctx *cli.Context, acc *wallet.Account) (user.Signer, neofs.PoolWrapper, error) {
-	rpcNeoFS := ctx.StringSlice(NeoFSRPCEndpointFlag)
-	signer := user.NewAutoIDSignerRFC6979(acc.PrivateKey().PrivateKey)
-
-	params := pool.DefaultOptions()
-	params.SetHealthcheckTimeout(neofs.DefaultHealthcheckTimeout)
-	params.SetNodeDialTimeout(neofs.DefaultDialTimeout)
-	params.SetNodeStreamTimeout(neofs.DefaultStreamTimeout)
-	p, err := pool.New(pool.NewFlatNodeParams(rpcNeoFS), signer, params)
-	if err != nil {
-		return nil, neofs.PoolWrapper{}, fmt.Errorf("failed to create NeoFS pool: %w", err)
-	}
-	pWrapper := neofs.PoolWrapper{Pool: p}
-	if err = pWrapper.Dial(context.Background()); err != nil {
-		return nil, neofs.PoolWrapper{}, fmt.Errorf("failed to dial NeoFS pool: %w", err)
-	}
-	return signer, pWrapper, nil
-}
-
 // GetInvoker returns an invoker using the given RPC client, context and signers.
 // It parses "--historic" parameter to adjust it.
 func GetInvoker(c *rpcclient.Client, ctx *cli.Context, signers []transaction.Signer) (*invoker.Invoker, cli.ExitCoder) {
diff --git a/cli/server/dump_bin.go b/cli/server/dump_bin.go
index dc3e9432a..00aeb932e 100644
--- a/cli/server/dump_bin.go
+++ b/cli/server/dump_bin.go
@@ -30,7 +30,7 @@ func dumpBin(ctx *cli.Context) error {
 	count := uint32(ctx.Uint("count"))
 	start := uint32(ctx.Uint("start"))
 
-	chain, _, prometheus, pprof, err := InitBCWithMetrics(cfg, log)
+	chain, prometheus, pprof, err := initBCWithMetrics(cfg, log)
 	if err != nil {
 		return err
 	}
diff --git a/cli/server/server.go b/cli/server/server.go
index 82e8fdf6e..26a9cd686 100644
--- a/cli/server/server.go
+++ b/cli/server/server.go
@@ -140,11 +140,10 @@ func newGraceContext() context.Context {
 	return ctx
 }
 
-// InitBCWithMetrics initializes the blockchain with metrics with the given configuration.
-func InitBCWithMetrics(cfg config.Config, log *zap.Logger) (*core.Blockchain, storage.Store, *metrics.Service, *metrics.Service, error) {
-	chain, store, err := initBlockChain(cfg, log)
+func initBCWithMetrics(cfg config.Config, log *zap.Logger) (*core.Blockchain, *metrics.Service, *metrics.Service, error) {
+	chain, _, err := initBlockChain(cfg, log)
 	if err != nil {
-		return nil, nil, nil, nil, cli.Exit(err, 1)
+		return nil, nil, nil, cli.Exit(err, 1)
 	}
 	prometheus := metrics.NewPrometheusService(cfg.ApplicationConfiguration.Prometheus, log)
 	pprof := metrics.NewPprofService(cfg.ApplicationConfiguration.Pprof, log)
@@ -152,14 +151,14 @@ func InitBCWithMetrics(cfg config.Config, log *zap.Logger) (*core.Blockchain, st
 	go chain.Run()
 	err = prometheus.Start()
 	if err != nil {
-		return nil, nil, nil, nil, cli.Exit(fmt.Errorf("failed to start Prometheus service: %w", err), 1)
+		return nil, nil, nil, cli.Exit(fmt.Errorf("failed to start Prometheus service: %w", err), 1)
 	}
 	err = pprof.Start()
 	if err != nil {
-		return nil, nil, nil, nil, cli.Exit(fmt.Errorf("failed to start Pprof service: %w", err), 1)
+		return nil, nil, nil, cli.Exit(fmt.Errorf("failed to start Pprof service: %w", err), 1)
 	}
 
-	return chain, store, prometheus, pprof, nil
+	return chain, prometheus, pprof, nil
 }
 
 func dumpDB(ctx *cli.Context) error {
@@ -190,7 +189,7 @@ func dumpDB(ctx *cli.Context) error {
 	defer outStream.Close()
 	writer := io.NewBinWriterFromIO(outStream)
 
-	chain, _, prometheus, pprof, err := InitBCWithMetrics(cfg, log)
+	chain, prometheus, pprof, err := initBCWithMetrics(cfg, log)
 	if err != nil {
 		return err
 	}
@@ -250,7 +249,7 @@ func restoreDB(ctx *cli.Context) error {
 		cfg.ApplicationConfiguration.SaveStorageBatch = true
 	}
 
-	chain, _, prometheus, pprof, err := InitBCWithMetrics(cfg, log)
+	chain, prometheus, pprof, err := initBCWithMetrics(cfg, log)
 	if err != nil {
 		return err
 	}
@@ -471,7 +470,7 @@ func startServer(ctx *cli.Context) error {
 		return cli.Exit(err, 1)
 	}
 
-	chain, _, prometheus, pprof, err := InitBCWithMetrics(cfg, log)
+	chain, prometheus, pprof, err := initBCWithMetrics(cfg, log)
 	if err != nil {
 		return cli.Exit(err, 1)
 	}
diff --git a/cli/util/convert.go b/cli/util/convert.go
index 487ba5df4..d2fe4d63c 100644
--- a/cli/util/convert.go
+++ b/cli/util/convert.go
@@ -16,35 +16,6 @@ import (
 	"github.com/urfave/cli/v2"
 )
 
-var neoFSFlags = append([]cli.Flag{
-	&cli.StringFlag{
-		Name:     "container",
-		Aliases:  []string{"cid"},
-		Usage:    "NeoFS container ID to upload objects to",
-		Required: true,
-		Action:   cmdargs.EnsureNotEmpty("container"),
-	},
-	&flags.AddressFlag{
-		Name:  "address",
-		Usage: "Address to use for signing the uploading and searching transactions in NeoFS",
-	},
-	&cli.UintFlag{
-		Name:  "retries",
-		Usage: "Maximum number of NeoFS node request retries",
-		Value: neofs.MaxRetries,
-		Action: func(context *cli.Context, u uint) error {
-			if u < 1 {
-				return cli.Exit("retries should be greater than 0", 1)
-			}
-			return nil
-		},
-	},
-	&cli.UintFlag{
-		Name:  "searchers",
-		Usage: "Number of concurrent searches for objects",
-		Value: 100,
-	}}, options.NeoFSRPC...)
-
 // NewCommands returns util commands for neo-go CLI.
 func NewCommands() []*cli.Command {
 	// By default, RPC flag is required. sendtx and txdump may be called without provided rpc-endpoint.
@@ -64,6 +35,27 @@ func NewCommands() []*cli.Command {
 	}, options.RPC...)
 	txCancelFlags = append(txCancelFlags, options.Wallet...)
 	uploadBinFlags := append([]cli.Flag{
+		&cli.StringSliceFlag{
+			Name:     "fs-rpc-endpoint",
+			Aliases:  []string{"fsr"},
+			Usage:    "List of NeoFS storage node RPC addresses (comma-separated or multiple --fs-rpc-endpoint flags)",
+			Required: true,
+			Action: func(ctx *cli.Context, fsRpcEndpoints []string) error {
+				for _, endpoint := range fsRpcEndpoints {
+					if endpoint == "" {
+						return cli.Exit("NeoFS RPC endpoint cannot contain empty values", 1)
+					}
+				}
+				return nil
+			},
+		},
+		&cli.StringFlag{
+			Name:     "container",
+			Aliases:  []string{"cid"},
+			Usage:    "NeoFS container ID to upload blocks to",
+			Required: true,
+			Action:   cmdargs.EnsureNotEmpty("container"),
+		},
 		&cli.StringFlag{
 			Name:   "block-attribute",
 			Usage:  "Attribute key of the block object",
@@ -76,6 +68,10 @@ func NewCommands() []*cli.Command {
 			Value:  neofs.DefaultIndexFileAttribute,
 			Action: cmdargs.EnsureNotEmpty("index-attribute"),
 		},
+		&flags.AddressFlag{
+			Name:  "address",
+			Usage: "Address to use for signing the uploading and searching transactions in NeoFS",
+		},
 		&cli.UintFlag{
 			Name:  "index-file-size",
 			Usage: "Size of index file",
@@ -86,22 +82,25 @@ func NewCommands() []*cli.Command {
 			Usage: "Number of workers to fetch and upload blocks concurrently",
 			Value: 20,
 		},
+		&cli.UintFlag{
+			Name:  "searchers",
+			Usage: "Number of concurrent searches for blocks",
+			Value: 100,
+		},
+		&cli.UintFlag{
+			Name:  "retries",
+			Usage: "Maximum number of Neo/NeoFS node request retries",
+			Value: neofs.MaxRetries,
+			Action: func(context *cli.Context, u uint) error {
+				if u < 1 {
+					return cli.Exit("retries should be greater than 0", 1)
+				}
+				return nil
+			},
+		},
 		options.Debug,
 	}, options.RPC...)
 	uploadBinFlags = append(uploadBinFlags, options.Wallet...)
-	uploadBinFlags = append(uploadBinFlags, neoFSFlags...)
-
-	uploadStateFlags := append([]cli.Flag{
-		&cli.StringFlag{
-			Name:   "state-attribute",
-			Usage:  "Attribute key of the state object",
-			Value:  neofs.DefaultStateAttribute,
-			Action: cmdargs.EnsureNotEmpty("state-attribute"),
-		},
-		options.Debug, options.Config, options.ConfigFile, options.RelativePath,
-	}, options.Wallet...)
-	uploadStateFlags = append(uploadStateFlags, options.Network...)
-	uploadStateFlags = append(uploadStateFlags, neoFSFlags...)
 	return []*cli.Command{
 		{
 			Name:  "util",
@@ -186,13 +185,6 @@ func NewCommands() []*cli.Command {
 					Action:    uploadBin,
 					Flags:     uploadBinFlags,
 				},
-				{
-					Name:      "upload-state",
-					Usage:     "Start the node, traverse MPT and upload MPT nodes to the NeoFS container at every StateSyncInterval number of blocks",
-					UsageText: "neo-go util upload-state --fs-rpc-endpoint <address1>[,<address2>[...]] --container <cid> --state-attribute state --wallet <wallet> [--wallet-config <config>] [--address <address>] [--searchers <num>] [--retries <num>] [--debug] [--config-path path] [-p/-m/-t] [--config-file file]",
-					Action:    uploadState,
-					Flags:     uploadStateFlags,
-				},
 			},
 		},
 	}
diff --git a/cli/util/upload_bin.go b/cli/util/upload_bin.go
index 757609832..0e710ac8a 100644
--- a/cli/util/upload_bin.go
+++ b/cli/util/upload_bin.go
@@ -19,14 +19,28 @@ import (
 	cid "github.com/nspcc-dev/neofs-sdk-go/container/id"
 	"github.com/nspcc-dev/neofs-sdk-go/object"
 	oid "github.com/nspcc-dev/neofs-sdk-go/object/id"
+	"github.com/nspcc-dev/neofs-sdk-go/pool"
 	"github.com/nspcc-dev/neofs-sdk-go/user"
 	"github.com/urfave/cli/v2"
 )
 
+// poolWrapper wraps a NeoFS pool to adapt its Close method to return an error.
+type poolWrapper struct {
+	*pool.Pool
+}
+
+// Close closes the pool and returns nil.
+func (p poolWrapper) Close() error {
+	p.Pool.Close()
+	return nil
+}
+
 func uploadBin(ctx *cli.Context) error {
 	if err := cmdargs.EnsureNone(ctx); err != nil {
 		return err
 	}
+	rpcNeoFS := ctx.StringSlice("fs-rpc-endpoint")
+	containerIDStr := ctx.String("container")
 	attr := ctx.String("block-attribute")
 	numWorkers := ctx.Uint("workers")
 	maxParallelSearches := ctx.Uint("searchers")
@@ -38,6 +52,12 @@ func uploadBin(ctx *cli.Context) error {
 	if err != nil {
 		return cli.Exit(fmt.Sprintf("failed to load account: %v", err), 1)
 	}
+
+	var containerID cid.ID
+	if err = containerID.DecodeString(containerIDStr); err != nil {
+		return cli.Exit(fmt.Sprintf("failed to decode container ID: %v", err), 1)
+	}
+
 	gctx, cancel := options.GetTimeoutContext(ctx)
 	defer cancel()
 	rpc, err := options.GetRPCClient(gctx, ctx)
@@ -45,20 +65,39 @@ func uploadBin(ctx *cli.Context) error {
 		return cli.Exit(fmt.Sprintf("failed to create RPC client: %v", err), 1)
 	}
 
-	signer, pWrapper, err := options.GetNeoFSClientPool(ctx, acc)
+	signer := user.NewAutoIDSignerRFC6979(acc.PrivateKey().PrivateKey)
+
+	params := pool.DefaultOptions()
+	params.SetHealthcheckTimeout(neofs.DefaultHealthcheckTimeout)
+	params.SetNodeDialTimeout(neofs.DefaultDialTimeout)
+	params.SetNodeStreamTimeout(neofs.DefaultStreamTimeout)
+	p, err := pool.New(pool.NewFlatNodeParams(rpcNeoFS), signer, params)
+	if err != nil {
+		return cli.Exit(fmt.Sprintf("failed to create NeoFS pool: %v", err), 1)
+	}
+	pWrapper := poolWrapper{p}
+	if err = pWrapper.Dial(context.Background()); err != nil {
+		return cli.Exit(fmt.Sprintf("failed to dial NeoFS pool: %v", err), 1)
+	}
+	defer p.Close()
+
+	var containerObj container.Container
+	err = retry(func() error {
+		containerObj, err = p.ContainerGet(ctx.Context, containerID, client.PrmContainerGet{})
+		return err
+	}, maxRetries, debug)
 	if err != nil {
-		return cli.Exit(err, 1)
+		return cli.Exit(fmt.Errorf("failed to get container with ID %s: %w", containerID, err), 1)
 	}
-	defer pWrapper.Close()
+	containerMagic := containerObj.Attribute("Magic")
 
 	v, err := rpc.GetVersion()
 	if err != nil {
 		return cli.Exit(fmt.Sprintf("failed to get version from RPC: %v", err), 1)
 	}
 	magic := strconv.Itoa(int(v.Protocol.Network))
-	containerID, err := getContainer(ctx, pWrapper, magic, maxRetries, debug)
-	if err != nil {
-		return cli.Exit(err, 1)
+	if containerMagic != magic {
+		return cli.Exit(fmt.Sprintf("container magic %s does not match the network magic %s", containerMagic, magic), 1)
 	}
 
 	currentBlockHeight, err := rpc.GetBlockCount()
@@ -99,7 +138,7 @@ func retry(action func() error, maxRetries uint, debug bool) error {
 }
 
 // uploadBlocksAndIndexFiles uploads the blocks and index files to the container using the pool.
-func uploadBlocksAndIndexFiles(ctx *cli.Context, p neofs.PoolWrapper, rpc *rpcclient.Client, signer user.Signer, containerID cid.ID, attr, indexAttributeKey string, buf []byte, currentIndexFileID, indexFileSize, currentBlockHeight uint, numWorkers, maxRetries uint, debug bool) error {
+func uploadBlocksAndIndexFiles(ctx *cli.Context, p poolWrapper, rpc *rpcclient.Client, signer user.Signer, containerID cid.ID, attr, indexAttributeKey string, buf []byte, currentIndexFileID, indexFileSize, currentBlockHeight uint, numWorkers, maxRetries uint, debug bool) error {
 	if currentIndexFileID*indexFileSize >= currentBlockHeight {
 		fmt.Fprintf(ctx.App.Writer, "No new blocks to upload. Need to upload starting from %d, current height %d\n", currentIndexFileID*indexFileSize, currentBlockHeight)
 		return nil
@@ -226,7 +265,7 @@ func uploadBlocksAndIndexFiles(ctx *cli.Context, p neofs.PoolWrapper, rpc *rpccl
 }
 
 // searchIndexFile returns the ID and buffer for the next index file to be uploaded.
-func searchIndexFile(ctx *cli.Context, p neofs.PoolWrapper, containerID cid.ID, privKeys *keys.PrivateKey, signer user.Signer, indexFileSize uint, blockAttributeKey, attributeKey string, maxParallelSearches, maxRetries uint, debug bool) (uint, []byte, error) {
+func searchIndexFile(ctx *cli.Context, p poolWrapper, containerID cid.ID, privKeys *keys.PrivateKey, signer user.Signer, indexFileSize uint, blockAttributeKey, attributeKey string, maxParallelSearches, maxRetries uint, debug bool) (uint, []byte, error) {
 	var (
 		// buf is used to store OIDs of the uploaded blocks.
 		buf    = make([]byte, indexFileSize*oid.Size)
@@ -318,7 +357,7 @@ func searchIndexFile(ctx *cli.Context, p neofs.PoolWrapper, containerID cid.ID,
 // searchObjects searches in parallel for objects with attribute GE startIndex and LT
 // endIndex. It returns a buffered channel of resulting object IDs and closes it once
 // OID search is finished. Errors are sent to errCh in a non-blocking way.
-func searchObjects(ctx context.Context, p neofs.PoolWrapper, containerID cid.ID, privKeys *keys.PrivateKey, blockAttributeKey string, startIndex, endIndex, maxParallelSearches, maxRetries uint, debug bool, errCh chan error, additionalFilters ...object.SearchFilters) chan oid.ID {
+func searchObjects(ctx context.Context, p poolWrapper, containerID cid.ID, privKeys *keys.PrivateKey, blockAttributeKey string, startIndex, endIndex, maxParallelSearches, maxRetries uint, debug bool, errCh chan error, additionalFilters ...object.SearchFilters) chan oid.ID {
 	var res = make(chan oid.ID, 2*neofs.DefaultSearchBatchSize)
 	go func() {
 		var wg sync.WaitGroup
@@ -380,7 +419,7 @@ func searchObjects(ctx context.Context, p neofs.PoolWrapper, containerID cid.ID,
 }
 
 // uploadObj uploads object to the container using provided settings.
-func uploadObj(ctx context.Context, p neofs.PoolWrapper, signer user.Signer, containerID cid.ID, objData []byte, attrs []object.Attribute) (oid.ID, error) {
+func uploadObj(ctx context.Context, p poolWrapper, signer user.Signer, containerID cid.ID, objData []byte, attrs []object.Attribute) (oid.ID, error) {
 	var (
 		hdr              object.Object
 		prmObjectPutInit client.PrmObjectPutInit
@@ -422,28 +461,3 @@ func getBlockIndex(header object.Object, attribute string) (int, error) {
 	}
 	return -1, fmt.Errorf("attribute %s not found", attribute)
 }
-
-// getContainer gets container by ID and checks its magic.
-func getContainer(ctx *cli.Context, p neofs.PoolWrapper, expectedMagic string, maxRetries uint, debug bool) (cid.ID, error) {
-	var (
-		containerObj   container.Container
-		err            error
-		containerIDStr = ctx.String("container")
-	)
-	var containerID cid.ID
-	if err = containerID.DecodeString(containerIDStr); err != nil {
-		return containerID, fmt.Errorf("failed to decode container ID: %w", err)
-	}
-	err = retry(func() error {
-		containerObj, err = p.ContainerGet(ctx.Context, containerID, client.PrmContainerGet{})
-		return err
-	}, maxRetries, debug)
-	if err != nil {
-		return containerID, fmt.Errorf("failed to get container: %w", err)
-	}
-	containerMagic := containerObj.Attribute("Magic")
-	if containerMagic != expectedMagic {
-		return containerID, fmt.Errorf("container magic mismatch: expected %s, got %s", expectedMagic, containerMagic)
-	}
-	return containerID, nil
-}
diff --git a/cli/util/upload_state.go b/cli/util/upload_state.go
deleted file mode 100644
index 8d14c9629..000000000
--- a/cli/util/upload_state.go
+++ /dev/null
@@ -1,206 +0,0 @@
-package util
-
-import (
-	"fmt"
-	"strconv"
-	"time"
-
-	"github.com/nspcc-dev/neo-go/cli/cmdargs"
-	"github.com/nspcc-dev/neo-go/cli/options"
-	"github.com/nspcc-dev/neo-go/cli/server"
-	"github.com/nspcc-dev/neo-go/pkg/core"
-	"github.com/nspcc-dev/neo-go/pkg/core/mpt"
-	"github.com/nspcc-dev/neo-go/pkg/core/storage"
-	"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
-	gio "github.com/nspcc-dev/neo-go/pkg/io"
-	"github.com/nspcc-dev/neo-go/pkg/services/helpers/neofs"
-	"github.com/nspcc-dev/neo-go/pkg/util"
-	"github.com/nspcc-dev/neofs-sdk-go/client"
-	cid "github.com/nspcc-dev/neofs-sdk-go/container/id"
-	"github.com/nspcc-dev/neofs-sdk-go/object"
-	oid "github.com/nspcc-dev/neofs-sdk-go/object/id"
-	"github.com/urfave/cli/v2"
-	"go.uber.org/zap"
-)
-
-func uploadState(ctx *cli.Context) error {
-	if err := cmdargs.EnsureNone(ctx); err != nil {
-		return err
-	}
-	cfg, err := options.GetConfigFromContext(ctx)
-	if err != nil {
-		return cli.Exit(err, 1)
-	}
-	attr := ctx.String("state-attribute")
-	maxRetries := ctx.Uint("retries")
-	debug := ctx.Bool("debug")
-
-	acc, _, err := options.GetAccFromContext(ctx)
-	if err != nil {
-		return cli.Exit(fmt.Sprintf("failed to load account: %v", err), 1)
-	}
-
-	signer, p, err := options.GetNeoFSClientPool(ctx, acc)
-	if err != nil {
-		return cli.Exit(err, 1)
-	}
-	defer p.Close()
-	log, _, logCloser, err := options.HandleLoggingParams(debug, cfg.ApplicationConfiguration)
-	if err != nil {
-		return cli.Exit(err, 1)
-	}
-	if logCloser != nil {
-		defer func() { _ = logCloser() }()
-	}
-
-	chain, store, prometheus, pprof, err := server.InitBCWithMetrics(cfg, log)
-	if err != nil {
-		return err
-	}
-	defer func() {
-		pprof.ShutDown()
-		prometheus.ShutDown()
-		chain.Close()
-	}()
-
-	if chain.GetConfig().Ledger.KeepOnlyLatestState || chain.GetConfig().Ledger.RemoveUntraceableBlocks {
-		return cli.Exit("only full-state node is supported: disable KeepOnlyLatestState and RemoveUntraceableBlocks", 1)
-	}
-	syncInterval := cfg.ProtocolConfiguration.StateSyncInterval
-	if syncInterval == 0 {
-		syncInterval = core.DefaultStateSyncInterval
-	}
-
-	containerID, err := getContainer(ctx, p, strconv.Itoa(int(chain.GetConfig().Magic)), maxRetries, debug)
-	if err != nil {
-		return cli.Exit(err, 1)
-	}
-
-	stateObjCount, err := searchStateIndex(ctx, p, containerID, acc.PrivateKey(), attr, syncInterval, maxRetries, debug)
-	if err != nil {
-		return cli.Exit(fmt.Sprintf("failed searching existing states: %v", err), 1)
-	}
-	stateModule := chain.GetStateModule()
-	currentHeight := int(stateModule.CurrentLocalHeight())
-	currentStateIndex := currentHeight / syncInterval
-	if currentStateIndex <= stateObjCount {
-		log.Info("no new states to upload",
-			zap.Int("number of uploaded state objects", stateObjCount),
-			zap.Int("latest state is uploaded for block", stateObjCount*syncInterval),
-			zap.Int("current height", currentHeight),
-			zap.Int("StateSyncInterval", syncInterval))
-		return nil
-	}
-	log.Info("starting uploading",
-		zap.Int("number of uploaded state objects", stateObjCount),
-		zap.Int("next state to upload for block", stateObjCount*syncInterval),
-		zap.Int("current height", currentHeight),
-		zap.Int("StateSyncInterval", syncInterval),
-		zap.Int("number of states to upload", currentStateIndex-stateObjCount))
-	for state := stateObjCount; state < currentStateIndex; state++ {
-		height := uint32(state * syncInterval)
-		stateRoot, err := stateModule.GetStateRoot(height)
-		if err != nil {
-			return cli.Exit(fmt.Sprintf("failed to get state root for height %d: %v", height, err), 1)
-		}
-		h, err := chain.GetHeader(chain.GetHeaderHash(height))
-		if err != nil {
-			return cli.Exit(fmt.Sprintf("failed to get header %d: %v", height, err), 1)
-		}
-
-		var (
-			hdr              object.Object
-			prmObjectPutInit client.PrmObjectPutInit
-			attrs            = []object.Attribute{
-				*object.NewAttribute(attr, strconv.Itoa(int(height))),
-				*object.NewAttribute("Timestamp", strconv.FormatInt(time.Now().Unix(), 10)),
-				*object.NewAttribute("StateRoot", stateRoot.Root.StringLE()),
-				*object.NewAttribute("StateSyncInterval", strconv.Itoa(syncInterval)),
-				*object.NewAttribute("BlockTime", strconv.FormatUint(h.Timestamp, 10)),
-			}
-		)
-		hdr.SetContainerID(containerID)
-		hdr.SetOwner(signer.UserID())
-		hdr.SetAttributes(attrs...)
-		err = retry(func() error {
-			writer, err := p.ObjectPutInit(ctx.Context, hdr, signer, prmObjectPutInit)
-			if err != nil {
-				return err
-			}
-			start := time.Now()
-			wrt := gio.NewBinWriterFromIO(writer)
-			wrt.WriteB(byte(0))
-			wrt.WriteU32LE(uint32(chain.GetConfig().Magic))
-			wrt.WriteU32LE(height)
-			wrt.WriteBytes(stateRoot.Root[:])
-			err = traverseMPT(stateRoot.Root, store, wrt)
-			if err != nil {
-				_ = writer.Close()
-				return err
-			}
-			err = writer.Close()
-			if err != nil {
-				return err
-			}
-			duration := time.Since(start)
-			res := writer.GetResult()
-			log.Info("uploaded state object",
-				zap.String("object ID", res.StoredObjectID().String()),
-				zap.Uint32("height", height),
-				zap.Duration("time spent", duration))
-			return nil
-		}, maxRetries, debug)
-		if err != nil {
-			return cli.Exit(fmt.Sprintf("failed to upload object at height %d: %v", height, err), 1)
-		}
-	}
-	return nil
-}
-
-func searchStateIndex(ctx *cli.Context, p neofs.PoolWrapper, containerID cid.ID, privKeys *keys.PrivateKey,
-	attributeKey string, syncInterval int, maxRetries uint, debug bool,
-) (int, error) {
-	var (
-		doneCh   = make(chan struct{})
-		errCh    = make(chan error)
-		objCount = 0
-	)
-
-	go func() {
-		defer close(doneCh)
-		for i := 0; ; i++ {
-			indexIDs := searchObjects(ctx.Context, p, containerID, privKeys,
-				attributeKey, uint(i*syncInterval), uint(i*syncInterval)+1, 1, maxRetries, debug, errCh)
-			resOIDs := make([]oid.ID, 0, 1)
-			for id := range indexIDs {
-				resOIDs = append(resOIDs, id)
-			}
-			if len(resOIDs) == 0 {
-				break
-			}
-			if len(resOIDs) > 1 {
-				fmt.Fprintf(ctx.App.Writer, "WARN: %d duplicated state objects with %s: %d found: %s\n", len(resOIDs), attributeKey, i, resOIDs)
-			}
-			objCount++
-		}
-	}()
-	select {
-	case err := <-errCh:
-		return objCount, err
-	case <-doneCh:
-		return objCount, nil
-	}
-}
-
-func traverseMPT(root util.Uint256, store storage.Store, writer *gio.BinWriter) error {
-	cache := storage.NewMemCachedStore(store)
-	billet := mpt.NewBillet(root, mpt.ModeAll, mpt.DummySTTempStoragePrefix, cache)
-	err := billet.Traverse(func(pathToNode []byte, node mpt.Node, nodeBytes []byte) bool {
-		writer.WriteVarBytes(nodeBytes)
-		return writer.Err != nil
-	}, false)
-	if err != nil {
-		return fmt.Errorf("billet traversal error: %w", err)
-	}
-	return nil
-}
diff --git a/config/protocol.mainnet.neofs.yml b/config/protocol.mainnet.neofs.yml
index adec524c1..1cf650f1d 100644
--- a/config/protocol.mainnet.neofs.yml
+++ b/config/protocol.mainnet.neofs.yml
@@ -1,8 +1,9 @@
 ProtocolConfiguration:
   Magic: 91414437
   MaxTraceableBlocks: 17280
+  MaxValidUntilBlockIncrement: 8640
   InitialGASSupply: 52000000
-  TimePerBlock: 15s
+  TimePerBlock: 1s
   MemPoolSize: 50000
   StandbyCommittee:
   - 026fa34ec057d74c2fdf1a18e336d0bd597ea401a0b2ad57340d5c220d09f44086
diff --git a/docs/neofs-blockstorage.md b/docs/neofs-blockstorage.md
index 6c048a120..aa84db0dc 100644
--- a/docs/neofs-blockstorage.md
+++ b/docs/neofs-blockstorage.md
@@ -73,8 +73,8 @@ parameter.
 Once all blocks available in the NeoFS container are processed, the service
 shuts down automatically.
 
-### NeoFS block uploading command
-The `util upload-bin` command is designed to fetch blocks from the RPC node and upload 
+### NeoFS Upload Command
+The `upload-bin` command is designed to fetch blocks from the RPC node and upload 
 them to the NeoFS container. It also creates and uploads index files. Below is an
 example usage of the command:
 
@@ -101,26 +101,4 @@ files are needed (different `index-file-size` or `index-attribute`), `upload-bin
 will upload the entire block sequence starting from genesis since no migration is
 supported yet by this command. Please, add a comment to the
 [#3744](https://github.com/nspcc-dev/neo-go/issues/3744) issue if you need this
-functionality.
-
-### NeoFS state uploading command
-The `util upload-state` command is used to start a node, traverse the MPT over the 
-smart contract storage, and upload MPT nodes to a NeoFS container at every 
-`StateSyncInterval` number of blocks. Below is an example usage of the command:
-
-```shell
-./bin/neo-go util upload-state --cid 9iVfUg8aDHKjPC4LhQXEkVUM4HDkR7UCXYLs8NQwYfSG --wallet-config ./wallet-config.yml --state-attribute State -m -fsr st1.t5.fs.neo.org:8080 -fsr st2.t5.fs.neo.org:8080 -fsr st3.t5.fs.neo.org:8080
-```
-
-Run `./bin/neo-go util upload-state --help` to see the full list of supported options.
-
-This command works as follows:
-1. Searches for the state objects stored in NeoFS to find the latest uploaded object.
-2. Checks if new state objects could be uploaded given the current local state height. 
-3. Traverses the MPT nodes (pre-order) starting from the stateroot at the height of the 
-   latest uploaded state object down to its children.
-4. Uploads the MPT nodes to the NeoFS container.
-5. Repeats steps 3-4 with a step equal to the `StateSyncInterval` number of blocks.
-
-If the command is interrupted, it can be resumed. It starts the uploading process
-from the last uploaded state object.
\ No newline at end of file
+functionality.
\ No newline at end of file
diff --git a/docs/node-configuration.md b/docs/node-configuration.md
index 08af261fc..6f437fa30 100644
--- a/docs/node-configuration.md
+++ b/docs/node-configuration.md
@@ -251,9 +251,7 @@ RPC:
   MaxRequestBodyBytes: 5242880
   MaxRequestHeaderBytes: 1048576
   MaxWebSocketClients: 64
-  MaxWebSocketFeeds: 16
   SessionEnabled: false
-  SessionExpansionEnabled: false
   SessionExpirationTime: 15
   SessionBackedByMPT: false
   SessionPoolSize: 20
@@ -298,9 +296,6 @@ where:
   number (64 by default). Attempts to establish additional connections will
   lead to websocket handshake failures. Use "-1" to disable websocket
   connections (0 will lead to using the default value).
-- `MaxWebSocketFeeds` -- the maximum simultaneous event subscriptions number
-  for a single client (16 by default). Attemps to create additional subscriptions
-  will lead to error.
 - `SessionEnabled` denotes whether session-based iterator JSON-RPC API is enabled.
   If true, then all iterators got from `invoke*` calls will be stored as sessions
   on the server side available for further traverse. `traverseiterator` and
@@ -315,14 +310,6 @@ where:
   requirement, then we recommend either to decrease `SessionExpirationTime` or to
   enable `SessionBackedByMPT`, see `SessionBackedByMPT` documentation for more
   details.
-- `SessionExpansionEnabled` enables partial expansion of iterators returned by 
-  `invoke*` calls. When enabled, the server returns up to `MaxIteratorResultItems`
-  values from the iterator in the initial response. If more items are available, 
-  the result is marked as truncated. When `SessionEnabled` is also true, the 
-  remaining iterator state is stored in a session, allowing further traversal 
-  via `traverseiterator`. If disabled, iterators are either expanded up to 
-  `MaxIteratorResultItems` or require explicit traversal via session-based calls.
-  By default, `SessionExpansionEnabled` is set to `false`.
 - `SessionExpirationTime` is a lifetime of iterator session in seconds. It is set
   to `TimePerBlock` seconds (but not less than 5s) by default and is relevant
   only if `SessionEnabled` is set to `true`.
diff --git a/docs/notifications.md b/docs/notifications.md
index f71e42fcc..4f3187965 100644
--- a/docs/notifications.md
+++ b/docs/notifications.md
@@ -58,9 +58,6 @@ method. Upon successful subscription, clients receive subscription ID for
 subsequent management of this subscription. Subscription is only valid for
 connection lifetime, no long-term client identification is being made.
 
-The maximum number of simultaneous subscriptions can be set server-side
-via `MaxWebSocketFeeds` setting.
-
 Errors are not described down below, but they can be returned as standard
 JSON-RPC errors (most often caused by invalid parameters).
 
diff --git a/docs/rpc.md b/docs/rpc.md
index 633e765d3..3f4d37377 100644
--- a/docs/rpc.md
+++ b/docs/rpc.md
@@ -421,17 +421,6 @@ The invocation records are presented in a flat structure in the order as how the
 Note that invocation records for faulted transactions are kept and are present in the 
 applicationlog. This behaviour differs from notifications which are omitted for faulted transactions.
 
-#### Iterator sessions expansion
-
-The `SessionExpansionEnabled` configuration setting enforces RPC server to expand any iterator
-returned by `invoke*` calls. Specifically, up to `MaxIteratorResultItems` elements are 
-returned in the initial response, and the returned iterator is marked as `Truncated` if the 
-iterator contains more elements beyond the returned batch. In this case, if `SessionEnabled`
-is also `true`, an iterator `ID` and session `ID` is provided allowing clients to retrieve additional elements via
-`traverseiterator`. If SessionExpansionEnabled is false, the iterator behavior depends on 
-`SessionEnabled`: if `SessionEnabled` is `true`, a full iterator object with an ID is returned.
-If `SessionEnabled` is `false`, the iterator is expanded up to `MaxIteratorResultItems`.
-
 ## Reference
 
 * [JSON-RPC 2.0 Specification](http://www.jsonrpc.org/specification)
diff --git a/pkg/config/application_config.go b/pkg/config/application_config.go
index 2b0e05dd5..efbdc5d94 100644
--- a/pkg/config/application_config.go
+++ b/pkg/config/application_config.go
@@ -150,8 +150,5 @@ func (a *ApplicationConfiguration) Validate() error {
 	if err := a.NeoFSBlockFetcher.Validate(); err != nil {
 		return fmt.Errorf("invalid NeoFSBlockFetcher config: %w", err)
 	}
-	if err := a.RPC.Validate(); err != nil {
-		return fmt.Errorf("invalid RPC config: %w", err)
-	}
 	return nil
 }
diff --git a/pkg/config/rpc_config.go b/pkg/config/rpc_config.go
index 307d6e5dc..c9b0a1630 100644
--- a/pkg/config/rpc_config.go
+++ b/pkg/config/rpc_config.go
@@ -1,8 +1,6 @@
 package config
 
 import (
-	"fmt"
-
 	"github.com/nspcc-dev/neo-go/pkg/encoding/fixedn"
 )
 
@@ -21,9 +19,7 @@ type (
 		MaxRequestBodyBytes       int           `yaml:"MaxRequestBodyBytes"`
 		MaxRequestHeaderBytes     int           `yaml:"MaxRequestHeaderBytes"`
 		MaxWebSocketClients       int           `yaml:"MaxWebSocketClients"`
-		MaxWebSocketFeeds         int           `yaml:"MaxWebSocketFeeds"`
 		SessionEnabled            bool          `yaml:"SessionEnabled"`
-		SessionExpansionEnabled   bool          `yaml:"SessionExpansionEnabled"`
 		SessionExpirationTime     int           `yaml:"SessionExpirationTime"`
 		SessionBackedByMPT        bool          `yaml:"SessionBackedByMPT"`
 		SessionPoolSize           int           `yaml:"SessionPoolSize"`
@@ -38,12 +34,3 @@ type (
 		KeyFile      string `yaml:"KeyFile"`
 	}
 )
-
-// Validate checks RPC for internal consistency. It returns an error if the
-// configuration is invalid.
-func (cfg *RPC) Validate() error {
-	if cfg.SessionExpansionEnabled && !cfg.SessionEnabled {
-		return fmt.Errorf("SessionExpansionEnabled requires SessionEnabled")
-	}
-	return nil
-}
diff --git a/pkg/core/blockchain.go b/pkg/core/blockchain.go
index c9d1a26d5..e722d420c 100644
--- a/pkg/core/blockchain.go
+++ b/pkg/core/blockchain.go
@@ -61,8 +61,7 @@ const (
 	defaultTimePerBlock                    = 15 * time.Second
 	// HeaderVerificationGasLimit is the maximum amount of GAS for block header verification.
 	HeaderVerificationGasLimit = 3_00000000 // 3 GAS
-	// DefaultStateSyncInterval is the default interval for state sync.
-	DefaultStateSyncInterval = 40000
+	defaultStateSyncInterval   = 40000
 
 	// defaultBlockTimesCache should be sufficient for tryRunGC() to get in
 	// sync with storeBlock(). Most of the time they differ by some thousands of
@@ -311,7 +310,7 @@ func NewBlockchain(s storage.Store, cfg config.Blockchain, log *zap.Logger) (*Bl
 			return nil, errors.New("P2PStateExchangeExtensions can be enabled either on MPT-complete node (KeepOnlyLatestState=false) or on light GC-enabled node (RemoveUntraceableBlocks=true)")
 		}
 		if cfg.StateSyncInterval <= 0 {
-			cfg.StateSyncInterval = DefaultStateSyncInterval
+			cfg.StateSyncInterval = defaultStateSyncInterval
 			log.Info("StateSyncInterval is not set or wrong, using default value",
 				zap.Int("StateSyncInterval", cfg.StateSyncInterval))
 		}
@@ -321,7 +320,7 @@ func NewBlockchain(s storage.Store, cfg config.Blockchain, log *zap.Logger) (*Bl
 			return nil, errors.New("NeoFSStateSyncExtensions are enabled, but NeoFSBlockFetcher is off")
 		}
 		if cfg.StateSyncInterval <= 0 {
-			cfg.StateSyncInterval = DefaultStateSyncInterval
+			cfg.StateSyncInterval = defaultStateSyncInterval
 			log.Info("StateSyncInterval is not set or wrong, using default value",
 				zap.Int("StateSyncInterval", cfg.StateSyncInterval))
 		}
@@ -1123,7 +1122,7 @@ func (bc *Blockchain) isHardforkEnabled(hf *config.Hardfork, blockHeight uint32)
 	hfs := bc.config.Hardforks
 	if hf != nil {
 		start, ok := hfs[hf.String()]
-		if !ok || start > blockHeight {
+		if !ok || start < blockHeight {
 			return false
 		}
 	}
diff --git a/pkg/core/interop/context.go b/pkg/core/interop/context.go
index c72adbeff..ddfeb11e9 100644
--- a/pkg/core/interop/context.go
+++ b/pkg/core/interop/context.go
@@ -256,10 +256,10 @@ func (c *ContractMD) HFSpecificContractMD(hf *config.Hardfork) *HFSpecificContra
 	}
 	md, ok := c.mdCache[key]
 	if !ok {
-		panic(fmt.Errorf("native contract descriptor cache is not initialized: contract %s, hardfork %s", c.Name, key))
+		panic(fmt.Errorf("native contract descriptor cache is not initialized: contract %s, hardfork %s", c.Hash.StringLE(), key))
 	}
 	if md == nil {
-		panic(fmt.Errorf("native contract descriptor cache is nil: contract %s, hardfork %s", c.Name, key))
+		panic(fmt.Errorf("native contract descriptor cache is nil: contract %s, hardfork %s", c.Hash.StringLE(), key))
 	}
 	return md
 }
diff --git a/pkg/core/interop/iterator/interop.go b/pkg/core/interop/iterator/interop.go
index 44d770e52..40607a9b9 100644
--- a/pkg/core/interop/iterator/interop.go
+++ b/pkg/core/interop/iterator/interop.go
@@ -38,21 +38,12 @@ func IsIterator(item stackitem.Item) bool {
 
 // ValuesTruncated returns an array of up to `maxNum` iterator values. The second
 // return parameter denotes whether iterator is truncated, i.e. has more values.
-// The third return parameter is the next value in the iterator. If the iterator
-// doesn't have more values, then third return parameter is nil. The iterator can
-// be reused for subsequent traversal, but `curr` will not be automatically
-// included in subsequent calls to Values or ValuesTruncated.
-func ValuesTruncated(item stackitem.Item, maxNum int) ([]stackitem.Item, bool, stackitem.Item) {
-	arr, ok := item.Value().(iterator)
-	if !ok {
-		return nil, false, nil
-	}
+// The provided iterator CAN NOT be reused in the subsequent calls to Values and
+// to ValuesTruncated.
+func ValuesTruncated(item stackitem.Item, maxNum int) ([]stackitem.Item, bool) {
 	result := Values(item, maxNum)
-	if arr.Next() {
-		curr := arr.Value()
-		return result, true, curr
-	}
-	return result, false, nil
+	arr := item.Value().(iterator)
+	return result, arr.Next()
 }
 
 // Values returns an array of up to `maxNum` iterator values. The provided
diff --git a/pkg/core/native/management.go b/pkg/core/native/management.go
index 0bd36c138..54640a866 100644
--- a/pkg/core/native/management.go
+++ b/pkg/core/native/management.go
@@ -619,6 +619,8 @@ func (m *Management) OnPersist(ic *interop.Context) error {
 			for _, hf := range config.Hardforks {
 				if _, ok := activeHFs[hf]; ok && ic.IsHardforkActivation(hf) {
 					isUpdate = true
+					activation := hf       // avoid loop variable pointer exporting.
+					activeIn = &activation // reuse ActiveIn variable for the initialization hardfork.
 					// Break immediately since native Initialize should be called starting from the first hardfork in a raw
 					// (if there are multiple hardforks with the same enabling height).
 					break
@@ -633,10 +635,6 @@ func (m *Management) OnPersist(ic *interop.Context) error {
 				currentActiveHFs = append(currentActiveHFs, hf)
 			}
 		}
-		// activeIn is not included into the activeHFs list.
-		if activeIn != nil && activeIn.Cmp(latestHF) > 0 {
-			latestHF = *activeIn
-		}
 		if !(isDeploy || isUpdate) {
 			continue
 		}
@@ -679,7 +677,7 @@ func (m *Management) OnPersist(ic *interop.Context) error {
 		// The rest of activating hardforks also require initialization.
 		for _, hf := range currentActiveHFs {
 			if err := native.Initialize(ic, &hf, hfSpecificMD); err != nil {
-				return fmt.Errorf("initializing %s native contract at HF %s: %w", md.Name, hf, err)
+				return fmt.Errorf("initializing %s native contract at HF %d: %w", md.Name, activeIn, err)
 			}
 		}
 
diff --git a/pkg/neorpc/result/invoke.go b/pkg/neorpc/result/invoke.go
index 4536a1079..2fa32f4e4 100644
--- a/pkg/neorpc/result/invoke.go
+++ b/pkg/neorpc/result/invoke.go
@@ -55,21 +55,17 @@ type iteratorAux struct {
 	Truncated bool              `json:"truncated,omitempty"`
 }
 
-// Iterator represents a VM iterator identifier. It can be in one of three
-// states: 1. If the JSON-RPC server supports session-based iterators, the
-// ID field is set, and the Values field may contain a partial expansion of
-// the iterator if session expansion is enabled. 2. If the JSON-RPC server
-// does not support sessions but allows in-place iteration, the Values field
-// will be populated, and Truncated will indicate whether more items exist.
-// 3. If the JSON-RPC server neither supports sessions nor in-place iteration,
-// all fields will be unset.
+// Iterator represents VM iterator identifier. It either has ID set (for those JSON-RPC servers
+// that support sessions) or non-nil Values and Truncated set (for those JSON-RPC servers that
+// doesn't support sessions but perform in-place iterator traversing) or doesn't have ID, Values
+// and Truncated set at all (for those JSON-RPC servers that doesn't support iterator sessions
+// and doesn't perform in-place iterator traversing).
 type Iterator struct {
 	// ID represents iterator ID. It is non-nil iff JSON-RPC server support session mechanism.
 	ID *uuid.UUID
 
-	// Values contains deserialized VM iterator values with a truncated flag. It may be non-nil
-	// if session expansion is enabled (even when ID is set), or if the JSON-RPC server does not
-	// support sessions but allows in-place iterator traversal.
+	// Values contains deserialized VM iterator values with a truncated flag. It is non-nil
+	// iff JSON-RPC server does not support sessions mechanism and able to traverse iterator.
 	Values    []stackitem.Item
 	Truncated bool
 }
@@ -81,8 +77,7 @@ func (r Iterator) MarshalJSON() ([]byte, error) {
 	if r.ID != nil {
 		iaux.Interface = iteratorInterfaceName
 		iaux.ID = r.ID.String()
-	}
-	if r.Values != nil {
+	} else {
 		value := make([]json.RawMessage, len(r.Values))
 		for i := range r.Values {
 			var err error
@@ -92,8 +87,8 @@ func (r Iterator) MarshalJSON() ([]byte, error) {
 			}
 		}
 		iaux.Value = value
+		iaux.Truncated = r.Truncated
 	}
-	iaux.Truncated = r.Truncated
 	return json.Marshal(iaux)
 }
 
@@ -114,8 +109,7 @@ func (r *Iterator) UnmarshalJSON(data []byte) error {
 			return fmt.Errorf("failed to unmarshal iterator ID: %w", err)
 		}
 		r.ID = &iID
-	}
-	if iteratorAux.Value != nil {
+	} else {
 		r.Values = make([]stackitem.Item, len(iteratorAux.Value))
 		for j := range r.Values {
 			r.Values[j], err = stackitem.FromJSONWithTypes(iteratorAux.Value[j])
@@ -123,8 +117,8 @@ func (r *Iterator) UnmarshalJSON(data []byte) error {
 				return fmt.Errorf("failed to unmarshal iterator values: %w", err)
 			}
 		}
+		r.Truncated = iteratorAux.Truncated
 	}
-	r.Truncated = iteratorAux.Truncated
 	return nil
 }
 
diff --git a/pkg/neorpc/result/version.go b/pkg/neorpc/result/version.go
index 7b28f9b1a..cb33f1c40 100644
--- a/pkg/neorpc/result/version.go
+++ b/pkg/neorpc/result/version.go
@@ -27,12 +27,6 @@ type (
 	RPC struct {
 		MaxIteratorResultItems int  `json:"maxiteratorresultitems"`
 		SessionEnabled         bool `json:"sessionenabled"`
-
-		// Below are NeoGo-specific extensions to the JSON-RPC protocol that are
-		// returned by the server in case they're enabled.
-
-		// SessionExpansionEnabled is true if the server supports iterator expansion.
-		SessionExpansionEnabled bool `json:"sessionexpansionenabled,omitempty"`
 	}
 
 	// Protocol represents network-dependent parameters.
diff --git a/pkg/network/server.go b/pkg/network/server.go
index fbd5145f9..bc7decb0c 100644
--- a/pkg/network/server.go
+++ b/pkg/network/server.go
@@ -913,7 +913,7 @@ func (s *Server) requestHeaders(p Peer) error {
 	return p.EnqueueP2PMessage(NewMessage(CMDGetHeaders, pl))
 }
 
-// handlePong processes a pong request.
+// handlePing processes a pong request.
 func (s *Server) handlePong(p Peer, pong *payload.Ping) error {
 	err := p.HandlePong(pong)
 	if err != nil {
diff --git a/pkg/network/tcp_peer.go b/pkg/network/tcp_peer.go
index c2471517f..496935d94 100644
--- a/pkg/network/tcp_peer.go
+++ b/pkg/network/tcp_peer.go
@@ -110,7 +110,7 @@ func (p *TCPPeer) BroadcastHPPacket(ctx context.Context, msg []byte) error {
 	return p.putPacketIntoQueue(ctx, p.hpSendQ, msg)
 }
 
-// putMsgIntoQueue serializes the given Message and puts it into given queue if
+// putMessageIntoQueue serializes the given Message and puts it into given queue if
 // the peer has done handshaking.
 func (p *TCPPeer) putMsgIntoQueue(queue chan<- []byte, msg *Message) error {
 	b, err := msg.Bytes()
diff --git a/pkg/rpcclient/invoker/invoker.go b/pkg/rpcclient/invoker/invoker.go
index c8872795e..ae7b6aa56 100644
--- a/pkg/rpcclient/invoker/invoker.go
+++ b/pkg/rpcclient/invoker/invoker.go
@@ -217,8 +217,6 @@ func termSession(rpc RPCSessions, sessionID uuid.UUID) error {
 // process. If num <= 0 then DefaultIteratorResultItems number of elements is
 // requested. If result contains no elements, then either Iterator has no
 // elements or session was expired and terminated by the server.
-// If server has SessionExpansionEnabled, then it's possible to have both
-// session-backed and expanded ones.
 func (v *Invoker) TraverseIterator(sessionID uuid.UUID, iterator *result.Iterator, num int) ([]stackitem.Item, error) {
 	return iterateNext(v.client, sessionID, iterator, num)
 }
@@ -228,22 +226,12 @@ func iterateNext(rpc RPCSessions, sessionID uuid.UUID, iterator *result.Iterator
 		num = DefaultIteratorResultItems
 	}
 
-	var result []stackitem.Item
-
-	if iterator.Values != nil {
-		count := min(num, len(iterator.Values))
-		result = append(result, iterator.Values[:count]...)
-		iterator.Values = iterator.Values[count:]
-		num -= count
-	}
-
-	if num > 0 && iterator.ID != nil {
-		batch, err := rpc.TraverseIterator(sessionID, *iterator.ID, num)
-		if err != nil {
-			return nil, err
-		}
-		result = append(result, batch...)
+	if iterator.ID != nil {
+		return rpc.TraverseIterator(sessionID, *iterator.ID, num)
 	}
+	num = min(num, len(iterator.Values))
+	items := iterator.Values[:num]
+	iterator.Values = iterator.Values[num:]
 
-	return result, nil
+	return items, nil
 }
diff --git a/pkg/rpcclient/unwrap/unwrap.go b/pkg/rpcclient/unwrap/unwrap.go
index 9ac15442f..770961da6 100644
--- a/pkg/rpcclient/unwrap/unwrap.go
+++ b/pkg/rpcclient/unwrap/unwrap.go
@@ -322,7 +322,7 @@ func ArrayOfBytes(r *result.Invoke, err error) ([][]byte, error) {
 	return res, nil
 }
 
-// ArrayOfUTF8Strings checks the result for correct state (HALT) and then extracts a
+// ArrayOfUTB8Strings checks the result for correct state (HALT) and then extracts a
 // slice of UTF-8 strings from the returned stack item.
 func ArrayOfUTF8Strings(r *result.Invoke, err error) ([]string, error) {
 	a, err := Array(r, err)
diff --git a/pkg/services/blockfetcher/blockfetcher.go b/pkg/services/blockfetcher/blockfetcher.go
index 4eed1c6ba..5b7bb7cd2 100644
--- a/pkg/services/blockfetcher/blockfetcher.go
+++ b/pkg/services/blockfetcher/blockfetcher.go
@@ -51,6 +51,17 @@ type Ledger interface {
 	HeaderHeight() uint32
 }
 
+// poolWrapper wraps a NeoFS pool to adapt its Close method to return an error.
+type poolWrapper struct {
+	*pool.Pool
+}
+
+// Close closes the pool and returns nil.
+func (p poolWrapper) Close() error {
+	p.Pool.Close()
+	return nil
+}
+
 type indexedOID struct {
 	Index int
 	OID   oid.ID
@@ -70,7 +81,7 @@ type Service struct {
 	headerSizeMap map[int]int
 
 	chain   Ledger
-	pool    neofs.PoolWrapper
+	pool    poolWrapper
 	enqueue func(obj bqueue.Indexable) error
 	account *wallet.Account
 
@@ -155,7 +166,7 @@ func New(chain Ledger, cfg config.NeoFSBlockFetcher, logger *zap.Logger, put fun
 	}
 	return &Service{
 		chain:         chain,
-		pool:          neofs.PoolWrapper{Pool: p},
+		pool:          poolWrapper{Pool: p},
 		log:           logger,
 		cfg:           cfg,
 		operationMode: opt,
diff --git a/pkg/services/helpers/neofs/blockstorage.go b/pkg/services/helpers/neofs/blockstorage.go
index 6485d352f..f88a6daa9 100644
--- a/pkg/services/helpers/neofs/blockstorage.go
+++ b/pkg/services/helpers/neofs/blockstorage.go
@@ -2,8 +2,6 @@ package neofs
 
 import (
 	"time"
-
-	"github.com/nspcc-dev/neofs-sdk-go/pool"
 )
 
 // Constants related to NeoFS block storage.
@@ -18,8 +16,6 @@ const (
 	DefaultBlockAttribute = "Block"
 	// DefaultIndexFileAttribute is the default attribute name for index file objects.
 	DefaultIndexFileAttribute = "Index"
-	// DefaultStateAttribute is the default attribute name for state objects.
-	DefaultStateAttribute = "State"
 
 	// DefaultSearchBatchSize is a number of objects to search in a batch. We need to
 	// search with EQ filter to avoid partially-completed SEARCH responses. If EQ search
@@ -54,14 +50,3 @@ const (
 	// MaxBackoff is the maximum backoff duration.
 	MaxBackoff = 20 * time.Second
 )
-
-// PoolWrapper wraps a NeoFS pool to adapt its Close method to return an error.
-type PoolWrapper struct {
-	*pool.Pool
-}
-
-// Close closes the pool and returns nil.
-func (p PoolWrapper) Close() error {
-	p.Pool.Close()
-	return nil
-}
diff --git a/pkg/services/rpcsrv/server.go b/pkg/services/rpcsrv/server.go
index 5c2f0d4c8..b27502e0a 100644
--- a/pkg/services/rpcsrv/server.go
+++ b/pkg/services/rpcsrv/server.go
@@ -185,10 +185,6 @@ type (
 		ID string
 		// Item represents Iterator stackitem.
 		Item stackitem.Item
-		// Curr represents current Iterator value got after initial iterator traversal if in-place iterator
-		// traversal is performed. This value must be used to prepend to the resulting items list during
-		// the subsequent iterator traversal.
-		Curr stackitem.Item
 	}
 )
 
@@ -315,10 +311,6 @@ func New(chain Ledger, conf config.RPC, coreServer *network.Server,
 		conf.MaxWebSocketClients = defaultMaxWebSocketClients
 		log.Info("MaxWebSocketClients is not set or wrong, setting default value", zap.Int("MaxWebSocketClients", defaultMaxWebSocketClients))
 	}
-	if conf.MaxWebSocketFeeds == 0 {
-		conf.MaxWebSocketFeeds = defaultMaxFeeds
-		log.Info("MaxWebSocketFeeds is not set or wrong, setting default value", zap.Int("MaxWebSocketFeeds", defaultMaxFeeds))
-	}
 	var oracleWrapped = new(atomic.Value)
 	if orc != nil {
 		oracleWrapped.Store(orc)
@@ -530,7 +522,7 @@ func (s *Server) handleHTTPRequest(w http.ResponseWriter, httpRequest *http.Requ
 		}
 		resChan := make(chan abstractResult) // response.abstract or response.abstractBatch
 		subChan := make(chan intEvent, notificationBufSize)
-		subscr := &subscriber{writer: subChan, feeds: make([]feed, s.config.MaxWebSocketFeeds)}
+		subscr := &subscriber{writer: subChan}
 		s.subsLock.Lock()
 		s.subscribers[subscr] = true
 		s.subsLock.Unlock()
@@ -568,7 +560,7 @@ func (s *Server) handleHTTPRequest(w http.ResponseWriter, httpRequest *http.Requ
 // RegisterLocal performs local client registration.
 func (s *Server) RegisterLocal(ctx context.Context, events chan<- neorpc.Notification) func(*neorpc.Request) (*neorpc.Response, error) {
 	subChan := make(chan intEvent, notificationBufSize)
-	subscr := &subscriber{writer: subChan, feeds: make([]feed, s.config.MaxWebSocketFeeds)}
+	subscr := &subscriber{writer: subChan}
 	s.subsLock.Lock()
 	s.subscribers[subscr] = true
 	s.subsLock.Unlock()
@@ -886,9 +878,8 @@ func (s *Server) getVersion(_ params.Params) (any, *neorpc.Error) {
 		Nonce:     s.coreServer.ID(),
 		UserAgent: s.coreServer.UserAgent,
 		RPC: result.RPC{
-			MaxIteratorResultItems:  s.config.MaxIteratorResultItems,
-			SessionEnabled:          s.config.SessionEnabled,
-			SessionExpansionEnabled: s.config.SessionExpansionEnabled,
+			MaxIteratorResultItems: s.config.MaxIteratorResultItems,
+			SessionEnabled:         s.config.SessionEnabled,
 		},
 		Protocol: result.Protocol{
 			AddressVersion:              address.NEO3Prefix,
@@ -2492,17 +2483,13 @@ func (s *Server) postProcessExecStack(stack []stackitem.Item) *session {
 	var sess session
 
 	for i, v := range stack {
-		var (
-			id   uuid.UUID
-			curr stackitem.Item
-		)
+		var id uuid.UUID
 
-		stack[i], id, curr = s.registerOrDumpIterator(v)
+		stack[i], id = s.registerOrDumpIterator(v)
 		if id != (uuid.UUID{}) {
 			sess.iteratorIdentifiers = append(sess.iteratorIdentifiers, &iteratorIdentifier{
 				ID:   id.String(),
 				Item: v,
-				Curr: curr,
 			})
 		}
 	}
@@ -2514,29 +2501,22 @@ func (s *Server) postProcessExecStack(stack []stackitem.Item) *session {
 
 // registerOrDumpIterator changes iterator interop stack items into result.Iterator
 // interop stack items and returns a uuid for it if sessions are enabled. All the other stack
-// items are not changed. The third return value is the current iterator value if it's not nil.
-func (s *Server) registerOrDumpIterator(item stackitem.Item) (stackitem.Item, uuid.UUID, stackitem.Item) {
+// items are not changed.
+func (s *Server) registerOrDumpIterator(item stackitem.Item) (stackitem.Item, uuid.UUID) {
 	var iterID uuid.UUID
 
 	if (item.Type() != stackitem.InteropT) || !iterator.IsIterator(item) {
-		return item, iterID, nil
+		return item, iterID
 	}
-
-	var (
-		resIterator result.Iterator
-		curr        stackitem.Item
-	)
+	var resIterator result.Iterator
 
 	if s.config.SessionEnabled {
 		iterID = uuid.New()
 		resIterator.ID = &iterID
-		if s.config.SessionExpansionEnabled {
-			resIterator.Values, resIterator.Truncated, curr = iterator.ValuesTruncated(item, s.config.MaxIteratorResultItems)
-		}
 	} else {
-		resIterator.Values, resIterator.Truncated, curr = iterator.ValuesTruncated(item, s.config.MaxIteratorResultItems)
+		resIterator.Values, resIterator.Truncated = iterator.ValuesTruncated(item, s.config.MaxIteratorResultItems)
 	}
-	return stackitem.NewInterop(resIterator), iterID, curr
+	return stackitem.NewInterop(resIterator), iterID
 }
 
 func (s *Server) traverseIterator(reqParams params.Params) (any, *neorpc.Error) {
@@ -2581,16 +2561,8 @@ func (s *Server) traverseIterator(reqParams params.Params) (any, *neorpc.Error)
 	)
 	for _, it := range session.iteratorIdentifiers {
 		if iIDStr == it.ID {
+			iVals = iterator.Values(it.Item, count)
 			found = true
-			if it.Curr != nil {
-				if count <= 0 {
-					break
-				}
-				iVals = append(iVals, it.Curr)
-				it.Curr = nil
-				count--
-			}
-			iVals = append(iVals, iterator.Values(it.Item, count)...)
 			break
 		}
 	}
diff --git a/pkg/services/rpcsrv/subscription.go b/pkg/services/rpcsrv/subscription.go
index 96d4969c2..9e3b05893 100644
--- a/pkg/services/rpcsrv/subscription.go
+++ b/pkg/services/rpcsrv/subscription.go
@@ -23,7 +23,7 @@ type (
 		// cheaper doing it this way rather than creating a map),
 		// pointing to an EventID is an obvious overkill at the moment, but
 		// that's not for long.
-		feeds []feed
+		feeds [maxFeeds]feed
 	}
 	// feed stores subscriber's desired event ID with filter.
 	feed struct {
@@ -43,8 +43,8 @@ func (f feed) Filter() neorpc.SubscriptionFilter {
 }
 
 const (
-	// The default maximum number of subscriptions per one client.
-	defaultMaxFeeds = 16
+	// Maximum number of subscriptions per one client.
+	maxFeeds = 16
 
 	// This sets notification messages buffer depth. It may seem to be quite
 	// big, but there is a big gap in speed between internal event processing


Test patch:
diff --git a/cli/server/server_test.go b/cli/server/server_test.go
index b988d46f7..8eb1de819 100644
--- a/cli/server/server_test.go
+++ b/cli/server/server_test.go
@@ -185,11 +185,11 @@ func TestInitBCWithMetrics(t *testing.T) {
 	})
 
 	t.Run("bad store", func(t *testing.T) {
-		_, _, _, _, err = InitBCWithMetrics(config.Config{}, logger)
+		_, _, _, err = initBCWithMetrics(config.Config{}, logger)
 		require.Error(t, err)
 	})
 
-	chain, _, prometheus, pprof, err := InitBCWithMetrics(cfg, logger)
+	chain, prometheus, pprof, err := initBCWithMetrics(cfg, logger)
 	require.NoError(t, err)
 	t.Cleanup(func() {
 		chain.Close()
diff --git a/cli/util/util_test.go b/cli/util/util_test.go
index 8771ec2a4..6e9b0f4bb 100644
--- a/cli/util/util_test.go
+++ b/cli/util/util_test.go
@@ -204,6 +204,8 @@ func TestUploadBin(t *testing.T) {
 	e.In.WriteString("one\r")
 	e.RunWithErrorCheckExit(t, "failed to load account", append(args, "--cid", "test", "--wallet", "./not-exist.json", "--rpc-endpoint", "https://test")...)
 	e.In.WriteString("one\r")
+	e.RunWithErrorCheckExit(t, "failed to decode container ID", append(args, "--cid", "test", "--wallet", testcli.ValidatorWallet, "--rpc-endpoint", "https://test")...)
+	e.In.WriteString("one\r")
 	e.RunWithErrorCheckExit(t, "failed to create RPC client", append(args, "--cid", "9iVfUg8aDHKjPC4LhQXEkVUM4HDkR7UCXYLs8NQwYfSG", "--wallet", testcli.ValidatorWallet, "--rpc-endpoint", "https://test")...)
 	e.In.WriteString("one\r")
 	e.RunWithErrorCheckExit(t, "failed to dial NeoFS pool", append(args, "--cid", "9iVfUg8aDHKjPC4LhQXEkVUM4HDkR7UCXYLs8NQwYfSG", "--wallet", testcli.ValidatorWallet, "--rpc-endpoint", "http://"+e.RPC.Addresses()[0])...)
diff --git a/config/protocol.testnet.neofs.yml b/config/protocol.testnet.neofs.yml
index 041227bf2..417ed32c3 100644
--- a/config/protocol.testnet.neofs.yml
+++ b/config/protocol.testnet.neofs.yml
@@ -1,8 +1,9 @@
 ProtocolConfiguration:
   Magic: 735783775
   MaxTraceableBlocks: 17280
+  MaxValidUntilBlockIncrement: 8640
   InitialGASSupply: 52000000
-  TimePerBlock: 15s
+  TimePerBlock: 1s
   MemPoolSize: 50000
   StandbyCommittee:
   - 0337f5f45e5be5aeae4a919d0787fcb743656560949061d5b8b05509b85ffbfd53
diff --git a/pkg/neorpc/result/version_test.go b/pkg/neorpc/result/version_test.go
index 7537ad912..162f92919 100644
--- a/pkg/neorpc/result/version_test.go
+++ b/pkg/neorpc/result/version_test.go
@@ -88,9 +88,8 @@ func TestVersion_MarshalUnmarshalJSON(t *testing.T) {
 		Nonce:     1677922561,
 		UserAgent: "/NEO-GO:0.98.6/",
 		RPC: RPC{
-			MaxIteratorResultItems:  100,
-			SessionEnabled:          true,
-			SessionExpansionEnabled: false,
+			MaxIteratorResultItems: 100,
+			SessionEnabled:         true,
 		},
 		Protocol: Protocol{
 			AddressVersion:              53,
diff --git a/pkg/rpcclient/invoker/invoker_test.go b/pkg/rpcclient/invoker/invoker_test.go
index 4cc2656d1..699a99fab 100644
--- a/pkg/rpcclient/invoker/invoker_test.go
+++ b/pkg/rpcclient/invoker/invoker_test.go
@@ -51,13 +51,7 @@ func (r *rpcInv) TerminateSession(sessionID uuid.UUID) (bool, error) {
 	return r.resTrm, r.err
 }
 func (r *rpcInv) TraverseIterator(sessionID, iteratorID uuid.UUID, maxItemsCount int) ([]stackitem.Item, error) {
-	if r.err != nil {
-		return nil, r.err
-	}
-	maxItemsCount = min(maxItemsCount, len(r.resItm))
-	items := r.resItm[:maxItemsCount]
-	r.resItm = r.resItm[maxItemsCount:]
-	return items, nil
+	return r.resItm, r.err
 }
 
 func TestInvoker(t *testing.T) {
@@ -163,40 +157,6 @@ func TestInvoker(t *testing.T) {
 			require.Equal(t, []stackitem.Item{stackitem.Make(42)}, res)
 		}
 	})
-	t.Run("traverse iterator with session expansion", func(t *testing.T) {
-		mockClient := &rpcInv{
-			resItm: []stackitem.Item{
-				stackitem.Make(1),
-				stackitem.Make(2),
-				stackitem.Make(3),
-			},
-			err: nil,
-		}
-		inv := New(mockClient, nil)
-
-		sessionID := uuid.New()
-		iteratorID := uuid.New()
-		iter := &result.Iterator{
-			ID:     &iteratorID,
-			Values: []stackitem.Item{stackitem.Make(10), stackitem.Make(20)},
-		}
-		res, err := inv.TraverseIterator(sessionID, iter, 2)
-		require.NoError(t, err)
-		require.Equal(t, []stackitem.Item{stackitem.Make(10), stackitem.Make(20)}, res)
-
-		res, err = inv.TraverseIterator(sessionID, iter, 2)
-		require.NoError(t, err)
-		require.Equal(t, []stackitem.Item{stackitem.Make(1), stackitem.Make(2)}, res)
-
-		res, err = inv.TraverseIterator(sessionID, iter, 2)
-		require.NoError(t, err)
-		require.Equal(t, []stackitem.Item{stackitem.Make(3)}, res)
-
-		mockClient.resItm = nil
-		res, err = inv.TraverseIterator(sessionID, iter, 2)
-		require.NoError(t, err)
-		require.Empty(t, res)
-	})
 }
 
 func TestInvokerSigners(t *testing.T) {
diff --git a/pkg/rpcclient/unwrap/unwrap_test.go b/pkg/rpcclient/unwrap/unwrap_test.go
index 03553e329..7962240e5 100644
--- a/pkg/rpcclient/unwrap/unwrap_test.go
+++ b/pkg/rpcclient/unwrap/unwrap_test.go
@@ -309,12 +309,6 @@ func TestSessionIterator(t *testing.T) {
 	require.NoError(t, err)
 	require.Equal(t, sid, rs)
 	require.Equal(t, iter, ri)
-
-	iterExpanded := result.Iterator{Values: []stackitem.Item{stackitem.Make(1), stackitem.Make(2)}, Truncated: true}
-	rs, ri, err = SessionIterator(&result.Invoke{State: "HALT", Stack: []stackitem.Item{stackitem.NewInterop(iterExpanded)}}, nil)
-	require.NoError(t, err)
-	require.Empty(t, rs)
-	require.Equal(t, iterExpanded, ri)
 }
 
 func TestArraySessionIterator(t *testing.T) {
diff --git a/pkg/services/rpcsrv/client_test.go b/pkg/services/rpcsrv/client_test.go
index 16a8732c6..30d4bc7dc 100644
--- a/pkg/services/rpcsrv/client_test.go
+++ b/pkg/services/rpcsrv/client_test.go
@@ -1450,6 +1450,9 @@ func TestClient_IteratorSessions(t *testing.T) {
 	t.Cleanup(c.Close)
 	require.NoError(t, c.Init())
 
+	storageHash, err := util.Uint160DecodeStringLE(storageContractHash)
+	require.NoError(t, err)
+
 	// storageItemsCount is the amount of storage items stored in Storage contract, it's hard-coded in the contract code.
 	const storageItemsCount = 255
 	expected := make([][]byte, storageItemsCount)
@@ -1655,6 +1658,8 @@ func TestClientOracle(t *testing.T) {
 
 func TestClient_Iterator_SessionConfigVariations(t *testing.T) {
 	var expected [][]byte
+	storageHash, err := util.Uint160DecodeStringLE(storageContractHash)
+	require.NoError(t, err)
 	// storageItemsCount is the amount of storage items stored in Storage contract, it's hard-coded in the contract code.
 	const storageItemsCount = 255
 
@@ -2518,186 +2523,3 @@ func TestGetBlockNotifications(t *testing.T) {
 		require.NotEmpty(t, bn)
 	})
 }
-
-func TestClient_SessionExpasionExtension(t *testing.T) {
-	const (
-		// storageItemsCount is the amount of storage items stored in Storage contract,
-		// it's hard-coded in the contract code.
-		storageItemsCount              = 255
-		expectedMaxIteratorResultItems = 50
-		batchSize                      = 33
-	)
-
-	expected := make([][]byte, storageItemsCount)
-	for i := range storageItemsCount {
-		expected[i] = stackitem.NewBigInteger(big.NewInt(int64(i))).Bytes()
-	}
-	slices.SortFunc(expected, func(a, b []byte) int {
-		if len(a) != len(b) {
-			return len(a) - len(b)
-		}
-		return bytes.Compare(a, b)
-	})
-	setup := func(t *testing.T, cfgMod func(*config.Config)) *rpcclient.Client {
-		chain, rpcSrv, httpSrv := initClearServerWithCustomConfig(t, cfgMod)
-		t.Cleanup(rpcSrv.Shutdown)
-		t.Cleanup(httpSrv.Close)
-
-		for _, b := range getTestBlocks(t) {
-			require.NoError(t, chain.AddBlock(b))
-		}
-
-		c, err := rpcclient.New(context.Background(), httpSrv.URL, rpcclient.Options{})
-		require.NoError(t, err)
-		require.NoError(t, c.Init())
-		t.Cleanup(c.Close)
-
-		return c
-	}
-	prepareSession := func(t *testing.T, c *rpcclient.Client) (uuid.UUID, uuid.UUID, []stackitem.Item) {
-		res, err := c.InvokeFunction(storageHash, "iterateOverValues", []smartcontract.Parameter{}, nil)
-		require.NoError(t, err)
-		require.NotEmpty(t, res.Session)
-		require.Equal(t, 1, len(res.Stack))
-		require.Equal(t, stackitem.InteropT, res.Stack[0].Type())
-		iterator, ok := res.Stack[0].Value().(result.Iterator)
-		require.True(t, ok)
-		require.NotEmpty(t, iterator.ID)
-		return res.Session, *iterator.ID, iterator.Values
-	}
-	t.Run("session disabled", func(t *testing.T) {
-		c := setup(t, func(cfg *config.Config) {
-			cfg.ApplicationConfiguration.RPC.Enabled = true
-			cfg.ApplicationConfiguration.RPC.SessionEnabled = false
-			cfg.ApplicationConfiguration.RPC.SessionExpansionEnabled = true
-			cfg.ApplicationConfiguration.RPC.MaxIteratorResultItems = expectedMaxIteratorResultItems
-		})
-		invokeRes, err := c.InvokeFunction(storageHash, "iterateOverValues", []smartcontract.Parameter{}, nil)
-		require.NoError(t, err)
-		require.Equal(t, vmstate.Halt.String(), invokeRes.State)
-		require.NotEmpty(t, invokeRes.Script)
-		require.Empty(t, invokeRes.Session)
-
-		require.Len(t, invokeRes.Stack, 1)
-		stackItem := invokeRes.Stack[0]
-		require.Equal(t, stackitem.InteropT, stackItem.Type())
-
-		iterVal, ok := stackItem.Value().(result.Iterator)
-		require.True(t, ok)
-
-		require.Equal(t, expectedMaxIteratorResultItems, len(iterVal.Values))
-		for i := range len(iterVal.Values) {
-			require.Equal(t, expected[i], iterVal.Values[i].Value().([]byte), i)
-		}
-		require.True(t, iterVal.Truncated)
-		require.Empty(t, iterVal.ID)
-	})
-	t.Run("session disabled and session expansion disabled", func(t *testing.T) {
-		c := setup(t, func(cfg *config.Config) {
-			cfg.ApplicationConfiguration.RPC.Enabled = true
-			cfg.ApplicationConfiguration.RPC.SessionEnabled = false
-			cfg.ApplicationConfiguration.RPC.SessionExpansionEnabled = false
-			cfg.ApplicationConfiguration.RPC.MaxIteratorResultItems = expectedMaxIteratorResultItems
-		})
-
-		invokeRes, err := c.InvokeFunction(storageHash, "iterateOverValues", []smartcontract.Parameter{}, nil)
-		require.NoError(t, err)
-		require.Equal(t, vmstate.Halt.String(), invokeRes.State)
-		require.NotEmpty(t, invokeRes.Script)
-		require.Empty(t, invokeRes.Session)
-
-		require.Len(t, invokeRes.Stack, 1)
-		stackItem := invokeRes.Stack[0]
-		require.Equal(t, stackitem.InteropT, stackItem.Type())
-
-		iterVal, ok := stackItem.Value().(result.Iterator)
-		require.True(t, ok)
-
-		require.Equal(t, expectedMaxIteratorResultItems, len(iterVal.Values))
-		for i := range len(iterVal.Values) {
-			require.Equal(t, expected[i], iterVal.Values[i].Value().([]byte), i)
-		}
-		require.True(t, iterVal.Truncated)
-		require.Empty(t, iterVal.ID)
-	})
-	t.Run("session enabled and SessionBackedByMPT", func(t *testing.T) {
-		c := setup(t, func(cfg *config.Config) {
-			cfg.ApplicationConfiguration.RPC.Enabled = true
-			cfg.ApplicationConfiguration.RPC.SessionEnabled = true
-			cfg.ApplicationConfiguration.RPC.SessionBackedByMPT = true
-			cfg.ApplicationConfiguration.RPC.SessionExpansionEnabled = true
-			cfg.ApplicationConfiguration.RPC.MaxIteratorResultItems = expectedMaxIteratorResultItems
-		})
-
-		sID, iID, curr := prepareSession(t, c)
-		require.Equal(t, len(curr), expectedMaxIteratorResultItems)
-		for i := range curr {
-			require.Equal(t, expected[i], curr[i].Value().([]byte), i)
-		}
-
-		var batchCount int
-		for {
-			batch, err := c.TraverseIterator(sID, iID, batchSize)
-			require.NoError(t, err)
-
-			if len(batch) == 0 {
-				break
-			}
-			for i := range batch {
-				require.Equal(t, expected[expectedMaxIteratorResultItems+batchCount*batchSize+i], batch[i].Value().([]byte), i)
-			}
-			batchCount++
-		}
-		require.Equal(t, (storageItemsCount-expectedMaxIteratorResultItems)/batchSize+1, batchCount)
-	})
-
-	t.Run("session enabled", func(t *testing.T) {
-		c := setup(t, func(cfg *config.Config) {
-			cfg.ApplicationConfiguration.RPC.Enabled = true
-			cfg.ApplicationConfiguration.RPC.SessionEnabled = true
-			cfg.ApplicationConfiguration.RPC.SessionExpansionEnabled = true
-			cfg.ApplicationConfiguration.RPC.MaxIteratorResultItems = expectedMaxIteratorResultItems
-		})
-
-		sID, iID, localItems := prepareSession(t, c)
-		require.Equal(t, expectedMaxIteratorResultItems, len(localItems))
-		for i := range localItems {
-			require.Equal(t, expected[i], localItems[i].Value().([]byte))
-		}
-
-		firstBatch, err := c.TraverseIterator(sID, iID, 20)
-		require.NoError(t, err)
-		require.Len(t, firstBatch, 20)
-		for i := range firstBatch {
-			require.Equal(t, expected[expectedMaxIteratorResultItems+i], firstBatch[i].Value().([]byte))
-		}
-
-		secondBatch, err := c.TraverseIterator(sID, iID, 20)
-		require.NoError(t, err)
-		require.Len(t, secondBatch, 20)
-		for i := range secondBatch {
-			require.Equal(t, expected[expectedMaxIteratorResultItems+20+i], secondBatch[i].Value().([]byte))
-		}
-
-		thirdBatch, err := c.TraverseIterator(sID, iID, 25)
-		require.NoError(t, err)
-		require.Len(t, thirdBatch, 25)
-		for i := range thirdBatch {
-			require.Equal(t, expected[expectedMaxIteratorResultItems+20+20+i], thirdBatch[i].Value().([]byte))
-		}
-
-		idx := expectedMaxIteratorResultItems + 20 + 20 + 25
-		for {
-			batch, err := c.TraverseIterator(sID, iID, batchSize)
-			require.NoError(t, err)
-			if len(batch) == 0 {
-				break
-			}
-			for i := range batch {
-				require.Equal(t, expected[idx+i], batch[i].Value().([]byte))
-			}
-			idx += len(batch)
-		}
-		require.Equal(t, storageItemsCount, idx)
-	})
-}
diff --git a/pkg/services/rpcsrv/server_test.go b/pkg/services/rpcsrv/server_test.go
index c93878d23..dcb026087 100644
--- a/pkg/services/rpcsrv/server_test.go
+++ b/pkg/services/rpcsrv/server_test.go
@@ -133,10 +133,6 @@ var (
 	// nnsHash is a hash of NEP-11 non-divisible "examples/nft-nd-nns" contract
 	// deployed at block #11 of basic testing chain.
 	nnsHash, _ = util.Uint160DecodeStringLE(nnsContractHash)
-	// storageHash is a hash of "Storage" contract
-	// placed in "internal/basicchain/testdata/storage/storage_contract.go"
-	// deployed at block #22 of basic testing chain.
-	storageHash, _ = util.Uint160DecodeStringLE(storageContractHash)
 	// nfsoHash is a hash of NEP-11 divisible "examples/nft-d" ("NeoFS
 	// Object") contract deployed at block #17 of basic testing chain.
 	nfsoHash, _ = util.Uint160DecodeStringLE(nfsoContractHash)
@@ -3217,35 +3213,6 @@ func testRPCProtocol(t *testing.T, doRPCCall func(string, string, *testing.T) []
 			body := doRPCCall(rpc, httpSrv.URL, t)
 			checkErrGetResult(t, body, true, neorpc.ErrUnknownIteratorCode)
 		})
-		t.Run("sessions expansion enabled", func(t *testing.T) {
-			chain2, _, httpSrv2 := initClearServerWithCustomConfig(t, func(c *config.Config) {
-				c.ApplicationConfiguration.RPC.SessionEnabled = true
-				c.ApplicationConfiguration.RPC.SessionExpansionEnabled = true
-			})
-			for _, b := range getTestBlocks(t) {
-				require.NoError(t, chain2.AddBlock(b))
-			}
-			rpc := fmt.Sprintf(`{"jsonrpc": "2.0", "id": 1, "method": "invokefunction", "params": ["%s", "iterateOverValues"]}"`, storageContractHash)
-			body := doRPCCall(rpc, httpSrv2.URL, t)
-			resp := checkErrGetResult(t, body, false, 0)
-			res := new(result.Invoke)
-			err := json.Unmarshal(resp, &res)
-			require.NoErrorf(t, err, "could not parse response: %s", resp)
-			require.NotEmpty(t, res.Session)
-			require.Equal(t, 1, len(res.Stack))
-			require.Equal(t, stackitem.InteropT, res.Stack[0].Type())
-			iterator, ok := res.Stack[0].Value().(result.Iterator)
-			require.True(t, ok)
-			require.NotEmpty(t, iterator.ID)
-
-			expectedCount := 0
-			rpc = fmt.Sprintf(`{"jsonrpc": "2.0", "id": 1, "method": "traverseiterator", "params": ["%s", "%s", %d]}"`, res.Session.String(), iterator.ID.String(), expectedCount)
-			body = doRPCCall(rpc, httpSrv2.URL, t)
-			resp = checkErrGetResult(t, body, false, 0)
-			resResp := new([]json.RawMessage)
-			require.NoError(t, json.Unmarshal(resp, resResp))
-			require.Equal(t, expectedCount, len(*resResp))
-		})
 	})
 	t.Run("terminatesession", func(t *testing.T) {
 		rpc := `{"jsonrpc": "2.0", "id": 1, "method": "terminatesession", "params": ["%s"]}"`
diff --git a/pkg/services/rpcsrv/subscription_test.go b/pkg/services/rpcsrv/subscription_test.go
index 613727e2a..57cd79942 100644
--- a/pkg/services/rpcsrv/subscription_test.go
+++ b/pkg/services/rpcsrv/subscription_test.go
@@ -4,7 +4,6 @@ import (
 	"encoding/base64"
 	"encoding/json"
 	"fmt"
-	"net/http/httptest"
 	"strings"
 	"sync"
 	"testing"
@@ -77,11 +76,7 @@ func getNotification(t *testing.T, respCh <-chan []byte) *neorpc.Notification {
 
 func initCleanServerAndWSClient(t *testing.T, startNetworkServer ...bool) (*core.Blockchain, *Server, *websocket.Conn, chan []byte) {
 	chain, rpcSrv, httpSrv := initClearServerWithInMemoryChain(t)
-	ws, respMsgs := initWSClient(t, httpSrv, rpcSrv, startNetworkServer...)
-	return chain, rpcSrv, ws, respMsgs
-}
 
-func initWSClient(t *testing.T, httpSrv *httptest.Server, rpcSrv *Server, startNetworkServer ...bool) (*websocket.Conn, chan []byte) {
 	dialer := websocket.Dialer{HandshakeTimeout: 5 * time.Second}
 	url := "ws" + strings.TrimPrefix(httpSrv.URL, "http") + "/ws"
 	ws, r, err := dialer.Dial(url, nil)
@@ -113,7 +108,7 @@ func initWSClient(t *testing.T, httpSrv *httptest.Server, rpcSrv *Server, startN
 			rpcSrv.coreServer.Shutdown()
 		}
 	})
-	return ws, respMsgs
+	return chain, rpcSrv, ws, respMsgs
 }
 
 func callSubscribe(t *testing.T, ws *websocket.Conn, msgs <-chan []byte, params string) string {
@@ -582,10 +577,9 @@ func TestHeaderOfAddedBlockSubscriptions(t *testing.T) {
 	callUnsubscribe(t, c, respMsgs, headerSubID)
 }
 
-func testMaxSubscriptions(t *testing.T, f func(*config.Config), maxFeeds int) {
+func TestMaxSubscriptions(t *testing.T) {
 	var subIDs = make([]string, 0)
-	_, rpcSrv, httpSrv := initClearServerWithCustomConfig(t, f)
-	c, respMsgs := initWSClient(t, httpSrv, rpcSrv)
+	_, _, c, respMsgs := initCleanServerAndWSClient(t)
 
 	for i := range maxFeeds + 1 {
 		var s string
@@ -606,17 +600,6 @@ func testMaxSubscriptions(t *testing.T, f func(*config.Config), maxFeeds int) {
 	}
 }
 
-func TestMaxSubscriptions(t *testing.T) {
-	t.Run("default", func(t *testing.T) {
-		testMaxSubscriptions(t, nil, defaultMaxFeeds)
-	})
-	t.Run("maxfeeds=x2", func(t *testing.T) {
-		testMaxSubscriptions(t, func(c *config.Config) {
-			c.ApplicationConfiguration.RPC.MaxWebSocketFeeds = defaultMaxFeeds * 2
-		}, defaultMaxFeeds*2)
-	})
-}
-
 func TestBadSubUnsub(t *testing.T) {
 	var subCases = map[string]string{
 		"no params":              `{"jsonrpc": "2.0", "method": "subscribe", "params": [], "id": 1}`,