Skip to content

Commit 3d603a2

Browse files
services: add statefetcher service for NeoFS-based state sync
Close #3793 Signed-off-by: Ekaterina Pavlova <[email protected]>
1 parent 2bdc778 commit 3d603a2

18 files changed

+1121
-203
lines changed

cli/util/upload_state.go

Lines changed: 18 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -64,39 +64,39 @@ func uploadState(ctx *cli.Context) error {
6464
if chain.GetConfig().Ledger.KeepOnlyLatestState || chain.GetConfig().Ledger.RemoveUntraceableBlocks {
6565
return cli.Exit("only full-state node is supported: disable KeepOnlyLatestState and RemoveUntraceableBlocks", 1)
6666
}
67-
syncInterval := cfg.ProtocolConfiguration.StateSyncInterval
68-
if syncInterval == 0 {
69-
syncInterval = core.DefaultStateSyncInterval
67+
syncInterval := cfg.ApplicationConfiguration.NeoFSStateFetcher.StateInterval
68+
if syncInterval <= 0 {
69+
syncInterval = uint32(neofs.DefaultStateInterval)
7070
}
7171

7272
containerID, err := getContainer(ctx, p, strconv.Itoa(int(chain.GetConfig().Magic)), maxRetries, debug)
7373
if err != nil {
7474
return cli.Exit(err, 1)
7575
}
7676

77-
stateObjCount, err := searchStateIndex(ctx, p, containerID, acc.PrivateKey(), attr, syncInterval, maxRetries, debug)
77+
stateObjCount, err := searchStateIndex(ctx, p, containerID, acc.PrivateKey(), attr, int(syncInterval), maxRetries, debug)
7878
if err != nil {
7979
return cli.Exit(fmt.Sprintf("failed searching existing states: %v", err), 1)
8080
}
8181
stateModule := chain.GetStateModule()
82-
currentHeight := int(stateModule.CurrentLocalHeight())
82+
currentHeight := stateModule.CurrentLocalHeight()
8383
currentStateIndex := currentHeight / syncInterval
8484
if currentStateIndex < stateObjCount {
8585
log.Info("no new states to upload",
86-
zap.Int("number of uploaded state objects", stateObjCount),
87-
zap.Int("latest state is uploaded for block", (stateObjCount-1)*syncInterval),
88-
zap.Int("current height", currentHeight),
89-
zap.Int("StateSyncInterval", syncInterval))
86+
zap.Uint32("number of uploaded state objects", stateObjCount),
87+
zap.Uint32("latest state is uploaded for block", (stateObjCount-1)*syncInterval),
88+
zap.Uint32("current height", currentHeight),
89+
zap.Uint32("StateSyncInterval", syncInterval))
9090
return nil
9191
}
9292
log.Info("starting uploading",
93-
zap.Int("number of uploaded state objects", stateObjCount),
94-
zap.Int("next state to upload for block", stateObjCount*syncInterval),
95-
zap.Int("current height", currentHeight),
96-
zap.Int("StateSyncInterval", syncInterval),
97-
zap.Int("number of states to upload", currentStateIndex-stateObjCount))
93+
zap.Uint32("number of uploaded state objects", stateObjCount),
94+
zap.Uint32("next state to upload for block", stateObjCount*syncInterval),
95+
zap.Uint32("current height", currentHeight),
96+
zap.Uint32("StateSyncInterval", syncInterval),
97+
zap.Uint32("number of states to upload", currentStateIndex-stateObjCount))
9898
for state := stateObjCount; state <= currentStateIndex; state++ {
99-
height := uint32(state * syncInterval)
99+
height := state * syncInterval
100100
stateRoot, err := stateModule.GetStateRoot(height)
101101
if err != nil {
102102
return cli.Exit(fmt.Sprintf("failed to get state root for height %d: %v", height, err), 1)
@@ -113,7 +113,7 @@ func uploadState(ctx *cli.Context) error {
113113
*object.NewAttribute(attr, strconv.Itoa(int(height))),
114114
*object.NewAttribute("Timestamp", strconv.FormatInt(time.Now().Unix(), 10)),
115115
*object.NewAttribute("StateRoot", stateRoot.Root.StringLE()),
116-
*object.NewAttribute("StateSyncInterval", strconv.Itoa(syncInterval)),
116+
*object.NewAttribute("StateSyncInterval", strconv.Itoa(int(syncInterval))),
117117
*object.NewAttribute("BlockTime", strconv.FormatUint(h.Timestamp, 10)),
118118
}
119119
)
@@ -157,11 +157,11 @@ func uploadState(ctx *cli.Context) error {
157157

158158
func searchStateIndex(ctx *cli.Context, p neofs.PoolWrapper, containerID cid.ID, privKeys *keys.PrivateKey,
159159
attributeKey string, syncInterval int, maxRetries uint, debug bool,
160-
) (int, error) {
160+
) (uint32, error) {
161161
var (
162162
doneCh = make(chan struct{})
163163
errCh = make(chan error)
164-
objCount = 0
164+
objCount = uint32(0)
165165
)
166166

167167
go func() {

docs/neofs-blockstorage.md

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,29 @@ following attributes:
2626
- the number of OIDs included into index file (`IndexSize:128000`)
2727
- second-precision index file uploading timestamp (`Timestamp:1627894840`)
2828

29+
### State storage schema
30+
31+
A single NeoFS container is used to store state objects. Each container
32+
has network magic attribute (`Magic:56753`). Each state is stored in a binary
33+
form as a separate object with a unique OID and a set of attributes:
34+
- state object identifier with state index value (`State:1`)
35+
- block hash in the LE form (`StateSyncInterval:5412a781caf278c0736556c0e544c7cfdbb6e3c62ae221ef53646be89364566b`)
36+
- previous block hash in the LE form (`StateRoot:3654a054d82a8178c7dfacecc2c57282e23468a42ee407f14506368afe22d929`)
37+
- millisecond-precision block creation timestamp (`BlockTime:1627894840919`)
38+
- second-precision block uploading timestamp (`Timestamp:1627894840`)
39+
40+
The binary form of the state object contains:
41+
42+
```
43+
1 byte: Version (0)
44+
4 bytes: Network magic (uint32)
45+
4 bytes: Block height (uint32)
46+
32 bytes: State root (Uint256)
47+
Variable-length sequence of contract storage key-value pairs, each encoded as:
48+
Variable-length key (varbytes)
49+
Variable-length value (varbytes)
50+
```
51+
2952
### NeoFS BlockFetcher
3053

3154
NeoFS BlockFetcher service is designed as an alternative to P2P synchronisation
@@ -103,6 +126,35 @@ supported yet by this command. Please, add a comment to the
103126
[#3744](https://github.com/nspcc-dev/neo-go/issues/3744) issue if you need this
104127
functionality.
105128

129+
### NeoFS StateFetcher
130+
131+
NeoFS StateFetcher service enables state synchronization for non-archival nodes
132+
by fetching contract storage key-value pairs from a trusted NeoFS container.
133+
It serves as an alternative to P2P-based MPT node synchronization, integrating
134+
with the `statesync` module. If storage-based synchronization fails to complete
135+
(e.g., due to an incorrect state root or insufficient data), the `statesync`
136+
module falls back to requesting MPT nodes via P2P to ensure synchronization.
137+
138+
#### Operation flow
139+
140+
1. **State Object Search**:
141+
Searches the NeoFS container for objects with the configured `StateAttribute`
142+
(e.g., `State`), filtering by block height aligned with `StateInterval`
143+
(e.g., heights 0, 40000, 80000).
144+
2. **Storage Data Fetching**:
145+
Fetches contract storage key-value pairs from the identified state object.
146+
3. **State Synchronization**:
147+
Passes key-value pairs to the `statesync` module, along with the synchronization
148+
height and expected state root. The `statesync` module builds a local MPT trie,
149+
flushing batches of up to 1000 pairs to storage. If the trie’s state root matches
150+
the expected root at height P, synchronization completes, and an atomic state
151+
jump occurs. If synchronization fails (e.g., wrong state root or incomplete data),
152+
`statesync` requests MPT nodes via P2P to complete the process.
153+
154+
Once state object available in the NeoFS container are processed, the service
155+
shuts down automatically.
156+
157+
106158
### NeoFS state uploading command
107159
The `util upload-state` command is used to start a node, traverse the MPT over the
108160
smart contract storage, and upload MPT nodes to a NeoFS container at every

docs/node-configuration.md

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ node-related settings described in the table below.
2424
| LogPath | `string` | "", so only console logging | File path where to store node logs. |
2525
| LogTimestamp | `bool` | Defined by TTY probe on stdout channel. | Defines whether to enable timestamp logging. If not set, then timestamp logging enabled iff the program is running in TTY (but this behaviour may be overriden by `--force-timestamp-logs` CLI flag if specified). Note that this option, if combined with `LogEncoding: "json"`, can't completely disable timestamp logging. |
2626
| NeoFSBlockFetcher | [NeoFS BlockFetcher Configuration](#NeoFS-BlockFetcher-Configuration) | | NeoFS BlockFetcher module configuration. See the [NeoFS BlockFetcher Configuration](#NeoFS-BlockFetcher-Configuration) section for details. |
27+
| NeoFSStateFetcher | [NeoFS StateFetcher Configuration](#NeoFS-StateFetcher-Configuration) | | NeoFS StateFetcher module configuration. See the [NeoFS StateFetcher Configuration](#NeoFS-StateFetcher-Configuration) section for details. |
2728
| Oracle | [Oracle Configuration](#Oracle-Configuration) | | Oracle module configuration. See the [Oracle Configuration](#Oracle-Configuration) section for details. |
2829
| P2P | [P2P Configuration](#P2P-Configuration) | | Configuration values for P2P network interaction. See the [P2P Configuration](#P2P-Configuration) section for details. |
2930
| P2PNotary | [P2P Notary Configuration](#P2P-Notary-Configuration) | | P2P Notary module configuration. See the [P2P Notary Configuration](#P2P-Notary-Configuration) section for details. |
@@ -216,6 +217,44 @@ where:
216217
setting depends on the NeoFS block storage configuration and is applicable only if
217218
`SkipIndexFilesSearch` is set to `false`. It's set to 128000 by default.
218219

220+
### NeoFS StateFetcher Configuration
221+
222+
`NeoFSStateFetcher` configuration section contains settings for NeoFS
223+
StateFetcher module and has the following structure:
224+
```
225+
NeoFSStateFetcher:
226+
Enabled: true
227+
UnlockWallet:
228+
Path: "./wallet.json"
229+
Password: "pass"
230+
Addresses:
231+
- st1.storage.fs.neo.org:8080
232+
- st2.storage.fs.neo.org:8080
233+
- st3.storage.fs.neo.org:8080
234+
- st4.storage.fs.neo.org:8080
235+
Timeout: 10m
236+
ContainerID: "7a1cn9LNmAcHjESKWxRGG7RSZ55YHJF6z2xDLTCuTZ6c"
237+
StateAttribute: "Block"
238+
StateInterval: 40000
239+
```
240+
where:
241+
- `Enabled` enables NeoFS StateFetcher module.
242+
- `UnlockWallet` contains wallet settings to retrieve account to sign requests to
243+
NeoFS. Without this setting, the module will use randomly generated private key.
244+
For configuration details see [Unlock Wallet Configuration](#Unlock-Wallet-Configuration)
245+
- `Addresses` is a list of NeoFS storage nodes addresses. This parameter is required.
246+
- `Timeout` is a timeout for a single request to NeoFS storage node (10 minutes by
247+
default).
248+
- `ContainerID` is a container ID to fetch blocks from. This parameter is required.
249+
- `StateAttribute` is an attribute name of NeoFS object that contains state
250+
data. It's set to `State` by default.
251+
- `StateInterval` specifies the block height interval between consecutive state
252+
objects in the NeoFS container. By default, it's set to 40000.
253+
254+
If `NeoFSStateFetcher` section is not specified and `NeoFSStateSyncExtensions` is
255+
`true`, then [NeoFS StateFetcher Configuration](#NeoFS-StateFetcher-Configuration)
256+
module configuration will be used.
257+
219258
### Metrics Services Configuration
220259

221260
Metrics services configuration describes options for metrics services (pprof,
@@ -419,6 +458,7 @@ protocol-related settings described in the table below.
419458
| P2PNotaryRequestPayloadPoolSize | `int` | `1000` | Size of the node's P2P Notary request payloads memory pool where P2P Notary requests are stored before main or fallback transaction is completed and added to the chain.<br>This option is valid only if `P2PSigExtensions` are enabled. | Not supported by the C# node, thus may affect heterogeneous networks functionality. |
420459
| P2PSigExtensions | `bool` | `false` | Enables following additional Notary service related logic:<br>• Transaction attribute `NotaryAssisted`<br>• Network payload of the `P2PNotaryRequest` type<br>• Native `Notary` contract<br>• Notary node module | Not supported by the C# node, thus may affect heterogeneous networks functionality. |
421460
| P2PStateExchangeExtensions | `bool` | `false` | Enables the following P2P MPT state data exchange logic: <br>• `StateSyncInterval` protocol setting <br>• P2P commands `GetMPTDataCMD` and `MPTDataCMD` | Not supported by the C# node, thus may affect heterogeneous networks functionality. Can be supported either on MPT-complete node (`KeepOnlyLatestState`=`false`) or on light GC-enabled node (`RemoveUntraceableBlocks=true`) in which case `KeepOnlyLatestState` setting doesn't change the behavior, an appropriate set of MPTs is always stored (see `RemoveUntraceableBlocks`). |
461+
| NeoFSStateSyncExtensions | `bool` | `false` | Enables the NeoFS state data exchange | Not supported by the C# node, thus may affect heterogeneous networks functionality. Can be supported either on MPT-complete node (`KeepOnlyLatestState`=`false`) or on light GC-enabled node (`RemoveUntraceableBlocks=true`) in which case `KeepOnlyLatestState` setting doesn't change the behavior, an appropriate set of MPTs is always stored (see `RemoveUntraceableBlocks`). |
422462
| ReservedAttributes | `bool` | `false` | Allows to have reserved attributes range for experimental or private purposes. |
423463
| SeedList | `[]string` | [] | List of initial nodes addresses used to establish connectivity. |
424464
| StandbyCommittee | `[]string` | [] | List of public keys of standby committee validators are chosen from. | The list of keys is not required to be sorted, but it must be exactly the same within the configuration files of all the nodes in the network. |

internal/fakechain/fakechain.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -428,6 +428,11 @@ func (s *FakeStateSync) AddMPTNodes(nodes [][]byte) error {
428428
panic("TODO")
429429
}
430430

431+
// AddContractStorageData implements the StateSync interface.
432+
func (s *FakeStateSync) AddContractStorageData(key string, value []byte, syncHeight uint32, expectedRoot util.Uint256) error {
433+
panic("TODO")
434+
}
435+
431436
// BlockHeight implements the StateSync interface.
432437
func (s *FakeStateSync) BlockHeight() uint32 {
433438
return 0
@@ -460,6 +465,11 @@ func (s *FakeStateSync) NeedMPTNodes() bool {
460465
panic("TODO")
461466
}
462467

468+
// NeedContractStorageData implements the StateSync interface.
469+
func (s *FakeStateSync) NeedContractStorageData() bool {
470+
panic("TODO")
471+
}
472+
463473
// Traverse implements the StateSync interface.
464474
func (s *FakeStateSync) Traverse(root util.Uint256, process func(node mpt.Node, nodeBytes []byte) bool) error {
465475
if s.TraverseFunc != nil {

pkg/config/application_config.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ type ApplicationConfiguration struct {
2929
P2PNotary P2PNotary `yaml:"P2PNotary"`
3030
StateRoot StateRoot `yaml:"StateRoot"`
3131
NeoFSBlockFetcher NeoFSBlockFetcher `yaml:"NeoFSBlockFetcher"`
32+
NeoFSStateFetcher NeoFSStateFetcher `yaml:"NeoFSStateFetcher"`
3233
}
3334

3435
// EqualsButServices returns true when the o is the same as a except for services
@@ -149,6 +150,13 @@ func (a *ApplicationConfiguration) Validate() error {
149150
if err := a.NeoFSBlockFetcher.Validate(); err != nil {
150151
return fmt.Errorf("invalid NeoFSBlockFetcher config: %w", err)
151152
}
153+
// if NeoFSService configuration is omitted the NeoFSBlockFetcher.NeoFSService will be used.
154+
if a.NeoFSStateFetcher.NeoFSService.Enabled && a.NeoFSStateFetcher.NeoFSService.ContainerID == "" {
155+
a.NeoFSStateFetcher.NeoFSService = a.NeoFSBlockFetcher.NeoFSService
156+
}
157+
if err := a.NeoFSStateFetcher.Validate(); err != nil {
158+
return fmt.Errorf("invalid NeoFSStateFetcher config: %w", err)
159+
}
152160
if err := a.RPC.Validate(); err != nil {
153161
return fmt.Errorf("invalid RPC config: %w", err)
154162
}

pkg/config/application_config_test.go

Lines changed: 33 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -148,10 +148,11 @@ func TestApplicationConfiguration_Validate(t *testing.T) {
148148
{
149149
cfg: ApplicationConfiguration{
150150
NeoFSBlockFetcher: NeoFSBlockFetcher{
151-
InternalService: InternalService{Enabled: true},
152-
Timeout: time.Second,
153-
ContainerID: validContainerID,
154-
Addresses: []string{"127.0.0.1"},
151+
NeoFSService: NeoFSService{
152+
InternalService: InternalService{Enabled: true},
153+
Timeout: time.Second,
154+
ContainerID: validContainerID,
155+
Addresses: []string{"127.0.0.1"}},
155156
OIDBatchSize: 10,
156157
BQueueSize: 20,
157158
SkipIndexFilesSearch: true,
@@ -163,12 +164,13 @@ func TestApplicationConfiguration_Validate(t *testing.T) {
163164
{
164165
cfg: ApplicationConfiguration{
165166
NeoFSBlockFetcher: NeoFSBlockFetcher{
166-
InternalService: InternalService{Enabled: true},
167-
Timeout: time.Second,
168-
ContainerID: "",
169-
Addresses: []string{"127.0.0.1"},
170-
OIDBatchSize: 10,
171-
BQueueSize: 20,
167+
NeoFSService: NeoFSService{
168+
InternalService: InternalService{Enabled: true},
169+
Timeout: time.Second,
170+
ContainerID: "",
171+
Addresses: []string{"127.0.0.1"}},
172+
OIDBatchSize: 10,
173+
BQueueSize: 20,
172174
},
173175
},
174176
shouldFail: true,
@@ -177,12 +179,13 @@ func TestApplicationConfiguration_Validate(t *testing.T) {
177179
{
178180
cfg: ApplicationConfiguration{
179181
NeoFSBlockFetcher: NeoFSBlockFetcher{
180-
InternalService: InternalService{Enabled: true},
181-
Timeout: time.Second,
182-
ContainerID: invalidContainerID,
183-
Addresses: []string{"127.0.0.1"},
184-
OIDBatchSize: 10,
185-
BQueueSize: 20,
182+
NeoFSService: NeoFSService{
183+
InternalService: InternalService{Enabled: true},
184+
Timeout: time.Second,
185+
ContainerID: invalidContainerID,
186+
Addresses: []string{"127.0.0.1"}},
187+
OIDBatchSize: 10,
188+
BQueueSize: 20,
186189
},
187190
},
188191
shouldFail: true,
@@ -191,12 +194,13 @@ func TestApplicationConfiguration_Validate(t *testing.T) {
191194
{
192195
cfg: ApplicationConfiguration{
193196
NeoFSBlockFetcher: NeoFSBlockFetcher{
194-
InternalService: InternalService{Enabled: true},
195-
Timeout: time.Second,
196-
ContainerID: validContainerID,
197-
Addresses: []string{},
198-
OIDBatchSize: 10,
199-
BQueueSize: 20,
197+
NeoFSService: NeoFSService{
198+
InternalService: InternalService{Enabled: true},
199+
Timeout: time.Second,
200+
ContainerID: validContainerID,
201+
Addresses: []string{}},
202+
OIDBatchSize: 10,
203+
BQueueSize: 20,
200204
},
201205
},
202206
shouldFail: true,
@@ -205,12 +209,13 @@ func TestApplicationConfiguration_Validate(t *testing.T) {
205209
{
206210
cfg: ApplicationConfiguration{
207211
NeoFSBlockFetcher: NeoFSBlockFetcher{
208-
InternalService: InternalService{Enabled: true},
209-
Timeout: time.Second,
210-
ContainerID: validContainerID,
211-
Addresses: []string{"127.0.0.1"},
212-
OIDBatchSize: 10,
213-
BQueueSize: 5,
212+
NeoFSService: NeoFSService{
213+
InternalService: InternalService{Enabled: true},
214+
Timeout: time.Second,
215+
ContainerID: validContainerID,
216+
Addresses: []string{"127.0.0.1"}},
217+
OIDBatchSize: 10,
218+
BQueueSize: 5,
214219
},
215220
},
216221
shouldFail: true,

pkg/config/blockfetcher_config.go

Lines changed: 0 additions & 48 deletions
This file was deleted.

0 commit comments

Comments
 (0)