FindStates length != FindStorageByHashHistoric length for specific contract #1539

Open
opened 2025-12-28 17:16:47 +00:00 by sami · 4 comments
Owner

Originally created by @ixje on GitHub (Jul 22, 2025).

Current Behavior

Using the same stateroot I get a different number of results while dumping all states related to a specific contract (0x341cbe084b3b15fccf15064c7b7790f5f0f659d2). For other contracts it works as expected.

Expected Behavior

I expect the length of the results of FindStates to equal that of FindStorageByHashHistoric

Steps to Reproduce

package main

import (
	"context"
	"fmt"

	"github.com/nspcc-dev/neo-go/pkg/neorpc/result"
	"github.com/nspcc-dev/neo-go/pkg/rpcclient"
	"github.com/nspcc-dev/neo-go/pkg/util"
)

func main() {
	hash, _ := util.Uint160DecodeStringLE("341cbe084b3b15fccf15064c7b7790f5f0f659d2") // different length
	//hash, _ := util.Uint160DecodeStringLE("8eb3bdf5ed4ac1516d316c6b1b207a3cf5eb7567") // same length

	host := "https://rpc10.n3.nspcc.ru:10331"
	client, _ := rpcclient.New(context.TODO(), host, rpcclient.Options{})
	stateResponse, _ := client.GetStateRootByHeight(7683665)
	stateRoot := stateResponse.Root

	var start []byte
	var states []result.KeyValue
	for {
		response, err := client.FindStates(stateRoot, hash, nil, start, nil)
		if err != nil {
			fmt.Printf("failed to find states: %v\n", err)
		}
		if len(response.Results) == 0 {
			break
		}

		states = append(states, response.Results...)
		if !response.Truncated {
			break
		}
		start = response.Results[len(response.Results)-1].Key
	}

	var startInt *int
	var storages []result.KeyValue
	for {
		response, _ := client.FindStorageByHashHistoric(stateRoot, hash, nil, startInt)
		if len(response.Results) == 0 {
			break
		}
		storages = append(storages, response.Results...)
		if !response.Truncated {
			break
		}
		startInt = &response.Next
	}

	fmt.Printf("state len: %d storage len: %d\n", len(states), len(storages))
}

currently outputs

state len: 1728 storage len: 1723
Originally created by @ixje on GitHub (Jul 22, 2025). ## Current Behavior Using the same stateroot I get a different number of results while dumping all states related to a specific contract (`0x341cbe084b3b15fccf15064c7b7790f5f0f659d2`). For other contracts it works as expected. ## Expected Behavior I expect the length of the results of `FindStates` to equal that of `FindStorageByHashHistoric` ## Steps to Reproduce ```go package main import ( "context" "fmt" "github.com/nspcc-dev/neo-go/pkg/neorpc/result" "github.com/nspcc-dev/neo-go/pkg/rpcclient" "github.com/nspcc-dev/neo-go/pkg/util" ) func main() { hash, _ := util.Uint160DecodeStringLE("341cbe084b3b15fccf15064c7b7790f5f0f659d2") // different length //hash, _ := util.Uint160DecodeStringLE("8eb3bdf5ed4ac1516d316c6b1b207a3cf5eb7567") // same length host := "https://rpc10.n3.nspcc.ru:10331" client, _ := rpcclient.New(context.TODO(), host, rpcclient.Options{}) stateResponse, _ := client.GetStateRootByHeight(7683665) stateRoot := stateResponse.Root var start []byte var states []result.KeyValue for { response, err := client.FindStates(stateRoot, hash, nil, start, nil) if err != nil { fmt.Printf("failed to find states: %v\n", err) } if len(response.Results) == 0 { break } states = append(states, response.Results...) if !response.Truncated { break } start = response.Results[len(response.Results)-1].Key } var startInt *int var storages []result.KeyValue for { response, _ := client.FindStorageByHashHistoric(stateRoot, hash, nil, startInt) if len(response.Results) == 0 { break } storages = append(storages, response.Results...) if !response.Truncated { break } startInt = &response.Next } fmt.Printf("state len: %d storage len: %d\n", len(states), len(storages)) } ``` currently outputs ```shell state len: 1728 storage len: 1723 ```
Author
Owner

@ixje commented on GitHub (Nov 18, 2025):

So I'm running into this issue again, but now also to the point that it's returning duplicate keys where Csharp does not and as a result neo-express can't download the contract state using a neo-go node.

This

dotnet neoxp.dll contract download -h 8361435 -i default.neo-express 0x3491b358a9ddce38cb567e2bb8bd1bf783cd556d https://rpc10.n3.nspcc.ru:10331

fails when using a neo-go node with the error

System.ArgumentException: An item with the same key has already been added. Key: BRgB

but this works with a neo-cli node

dotnet neoxp.dll contract download -h 8361435 -i default.neo-express 0x3491b358a9ddce38cb567e2bb8bd1bf783cd556d http://seed1.neo.org:10332

I did a little bit of high level investigation using this code and found that neo-go returns duplicate keys

package main

import (
	"context"
	"encoding/hex"
	"fmt"

	"github.com/nspcc-dev/neo-go/pkg/neorpc/result"
	"github.com/nspcc-dev/neo-go/pkg/rpcclient"
	"github.com/nspcc-dev/neo-go/pkg/util"
)

func main() {
	rpcCsharp := initRPC("http://seed1.neo.org:10332")
	rpcNeoGo := initRPC("https://rpc10.n3.nspcc.ru:10331")
	ItemContract, _ := util.Uint160DecodeStringLE("3491b358a9ddce38cb567e2bb8bd1bf783cd556d")

	for _, client := range []*rpcclient.Client{rpcCsharp, rpcNeoGo} {
		fmt.Printf("collecting state for %s", client.Endpoint())
		state, err := collectState(client, ItemContract)
		if err != nil {
			panic(fmt.Sprintf("failed to collect state for %s: %v", client.Endpoint(), err))
		}
		fmt.Printf("are state keys unique? %t", AreKeysUnique(state))
	}
}

func initRPC(host string) *rpcclient.Client {
	opts := rpcclient.Options{}
	c, err := rpcclient.New(context.TODO(), host, opts)
	if err != nil {
		panic(err)
	}
	err = c.Init()
	if err != nil {
		panic(fmt.Errorf("failed to initialise RPC client: %w", err))
	}
	return c
}

func AreKeysUnique(items []result.KeyValue) bool {
	seen := make(map[string]struct{}, len(items))
	unique := true
	for _, kv := range items {
		k := hex.EncodeToString(kv.Key) // safe, no copy
		if _, exists := seen[k]; exists {
			fmt.Printf("duplicate key found: %s\n", k)
			unique = false
		}
		seen[k] = struct{}{}
	}
	return unique
}

func collectState(c *rpcclient.Client, hash util.Uint160) ([]result.KeyValue, error) {
	height := uint32(8361435)
	stateResponse, err := c.GetStateRootByHeight(height)
	if err != nil {
		panic(fmt.Errorf("failed to get stateroot for height %d: %w", height, err))
	}
	stateRoot := stateResponse.Root

	var start []byte
	var states []result.KeyValue
	for {
		response, err := c.FindStates(stateRoot, hash, nil, start, nil)
		if err != nil {
			return nil, err
		}
		if len(response.Results) == 0 {
			break
		}
		states = append(states, response.Results...)
		if !response.Truncated {
			break
		}
		start = response.Results[len(response.Results)-1].Key
	}
	return states, nil
}

For Csharp it indicates that the keys are unique. But for neo-go it indicates they are not unique and prints 58 duplicates.

Further more if I call findstates with these params (note: this was a print from neo-express)

["0x3f08a2c192454b27502051ed273a119a88abc390c232898dd062ceee93b14466","0x3491b358a9ddce38cb567e2bb8bd1bf783cd556d","",""]

then sort the keys and perform a diff, there is exactly 1 key different between neo-cli and neo-go in the first 100 results. The last 2 keys of the sorted results for neo-cli are

05050f
0506

but for neo-go are

05050f
050601

This might be a starting point for investigation.

@ixje commented on GitHub (Nov 18, 2025): So I'm running into this issue again, but now also to the point that it's returning duplicate keys where Csharp does not and as a result neo-express can't download the contract state using a neo-go node. This ```shell dotnet neoxp.dll contract download -h 8361435 -i default.neo-express 0x3491b358a9ddce38cb567e2bb8bd1bf783cd556d https://rpc10.n3.nspcc.ru:10331 ``` fails when using a neo-go node with the error ``` System.ArgumentException: An item with the same key has already been added. Key: BRgB ``` but this works with a neo-cli node ``` dotnet neoxp.dll contract download -h 8361435 -i default.neo-express 0x3491b358a9ddce38cb567e2bb8bd1bf783cd556d http://seed1.neo.org:10332 ``` ------- I did a little bit of high level investigation using this code and found that neo-go returns duplicate keys ```golang package main import ( "context" "encoding/hex" "fmt" "github.com/nspcc-dev/neo-go/pkg/neorpc/result" "github.com/nspcc-dev/neo-go/pkg/rpcclient" "github.com/nspcc-dev/neo-go/pkg/util" ) func main() { rpcCsharp := initRPC("http://seed1.neo.org:10332") rpcNeoGo := initRPC("https://rpc10.n3.nspcc.ru:10331") ItemContract, _ := util.Uint160DecodeStringLE("3491b358a9ddce38cb567e2bb8bd1bf783cd556d") for _, client := range []*rpcclient.Client{rpcCsharp, rpcNeoGo} { fmt.Printf("collecting state for %s", client.Endpoint()) state, err := collectState(client, ItemContract) if err != nil { panic(fmt.Sprintf("failed to collect state for %s: %v", client.Endpoint(), err)) } fmt.Printf("are state keys unique? %t", AreKeysUnique(state)) } } func initRPC(host string) *rpcclient.Client { opts := rpcclient.Options{} c, err := rpcclient.New(context.TODO(), host, opts) if err != nil { panic(err) } err = c.Init() if err != nil { panic(fmt.Errorf("failed to initialise RPC client: %w", err)) } return c } func AreKeysUnique(items []result.KeyValue) bool { seen := make(map[string]struct{}, len(items)) unique := true for _, kv := range items { k := hex.EncodeToString(kv.Key) // safe, no copy if _, exists := seen[k]; exists { fmt.Printf("duplicate key found: %s\n", k) unique = false } seen[k] = struct{}{} } return unique } func collectState(c *rpcclient.Client, hash util.Uint160) ([]result.KeyValue, error) { height := uint32(8361435) stateResponse, err := c.GetStateRootByHeight(height) if err != nil { panic(fmt.Errorf("failed to get stateroot for height %d: %w", height, err)) } stateRoot := stateResponse.Root var start []byte var states []result.KeyValue for { response, err := c.FindStates(stateRoot, hash, nil, start, nil) if err != nil { return nil, err } if len(response.Results) == 0 { break } states = append(states, response.Results...) if !response.Truncated { break } start = response.Results[len(response.Results)-1].Key } return states, nil } ``` For Csharp it indicates that the keys are unique. But for neo-go it indicates they are not unique and prints 58 duplicates. Further more if I call findstates with these params (note: this was a print from neo-express) ``` ["0x3f08a2c192454b27502051ed273a119a88abc390c232898dd062ceee93b14466","0x3491b358a9ddce38cb567e2bb8bd1bf783cd556d","",""] ``` then sort the keys and perform a diff, there is exactly 1 key different between neo-cli and neo-go in the first 100 results. The last 2 keys of the sorted results for neo-cli are ``` 05050f 0506 ``` but for neo-go are ``` 05050f 050601 ``` This might be a starting point for investigation.
Author
Owner

@roman-khimov commented on GitHub (Nov 18, 2025):

Thanks a lot for info, it seems like some off-by-one error, we'll try to get to it soon.

@roman-khimov commented on GitHub (Nov 18, 2025): Thanks a lot for info, it seems like some off-by-one error, we'll try to get to it soon.
Author
Owner

@roman-khimov commented on GitHub (Nov 18, 2025):

Can also be related to #3103.

@roman-khimov commented on GitHub (Nov 18, 2025): Can also be related to #3103.
Author
Owner

@ixje commented on GitHub (Nov 18, 2025):

Can also be related to #3103.

That does look pretty related

current findstates behaviour matches the C# one

Except this statement of anna doesn't seem to match anymore. If I don't manually sort the keys then the response is very different. Fwiw; the order didn't really affect me

@ixje commented on GitHub (Nov 18, 2025): > Can also be related to #3103. That does look pretty related > current findstates behaviour matches the C# one Except this statement of anna doesn't seem to match anymore. If I don't manually sort the keys then the response is very different. Fwiw; the order didn't really affect me
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
nspcc-dev/neo-go#1539
No description provided.