Skip to content

Conversation

ixje
Copy link
Contributor

@ixje ixje commented Jul 21, 2025

Problem

#2406

We (coz) have used the feature (in neo-express) for a couple of years now and have learned that

  • multi-node support really isn't worth supporting
  • not knowing contract child dependencies so far hasn't been an issue (point 3. of original issue)
  • similarly point 4. of the original issue is theoretically a "problem" but in practice not yet encountered. So again can be ignored for the time being.
  • in our experience the ability to only overwrite the contract storage or only the contract state (options in neo-express) is hardly used (more on this below)

Solution

The still relevant parts are addressed with this PR, being the basic functionality of fetching a contract state from a remote chain and storing it locally. From practice this was always done when the node was not running, so I implemented it as a subcommand of neo-go db.

The most relevant discussion imo is how to handle the persisting to storage, specifically around contract IDs. In neo-express I implemented it as follows

  1. if the remote contract hash does not exist, persist 1:1
  2. if it does exist, overwrite the state if --force is specified (but keep the local contract ID)

This PR is slightly different where for 1. we do an additional check to see if the the remote contract ID is not already in use locally.

open things to address

  1. after running the db download-contract command, the first time the node is started the new contract cannot be found (via the getcontractstate RPC method). Only after a shutdown and starting again it can be found. It's unclear to me why
  2. the 2 TODOs in the code. I'm sure there is an opinion from your side how to expose the required data. I didn't want to guess, so took the easiest approach for me to start the discussion.

Copy link

codecov bot commented Jul 21, 2025

Codecov Report

Attention: Patch coverage is 14.89362% with 160 lines in your changes missing coverage. Please review.

Project coverage is 81.75%. Comparing base (2971b8a) to head (e051836).
Report is 20 commits behind head on master.

Files with missing lines Patch % Lines
cli/server/server.go 15.90% 148 Missing ⚠️
pkg/core/native/management.go 0.00% 12 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master    #3960      +/-   ##
==========================================
- Coverage   82.03%   81.75%   -0.29%     
==========================================
  Files         348      348              
  Lines       49222    49419     +197     
==========================================
+ Hits        40380    40402      +22     
- Misses       7147     7329     +182     
+ Partials     1695     1688       -7     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@ixje
Copy link
Contributor Author

ixje commented Jul 21, 2025

I overlooked downloading the contract storage 🙈 . Still open to input on the TODOs how to best expose the data. Consider this a draft (I don't have the option to change it to one)

Copy link
Member

@AnnaShaleva AnnaShaleva left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good feature, but some critical decisions need to be polished.

Comment on lines +95 to +100
&cli.UintFlag{
Name: "height",
Usage: "Height at which to get the contract state for",
Required: false,
DefaultText: "latest",
},
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have options.Historic designed specifically for such kind of things.

Comment on lines 125 to 131
{
Name: "download-contract",
Usage: "Download a contract including storage from a remote chain into the local database",
UsageText: "neo-go db download-contract -c contract-hash -r endpoint [--height height] [--config-file] [--force]",
Action: downloadContract,
Flags: cfgDownloadFlags,
},
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd say that this command is closer to contract subset of commands (e.g. ./neo-go contract download). Also, it reminds me of ./neo-go wallet import-deployed, so for the sake of CLI interface unification we can use it as ./neo-go contract import-deployed. But it's a matter of discussion because, as @ixje said, this commend performs a direct DB modification.

@roman-khimov?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

performs a direct DB modification

Which makes it a db command for me. But I'm thinking of separating download and import stages (consider contract state as well). In which case contract download + db import-contract would be a nice pair. This separation can be useful for unit tests similar to ones we have in neofs-contract repository where some state is stored in the repository and imported as needed for tests.

This needs to be discussed, it is possible to unify these tools and we better do this now, before we have 25 variations on the same thing.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Vote up for separation.

Comment on lines 101 to 106
&cli.StringFlag{
Name: "contract-hash",
Usage: "Script hash of the contract to download",
Required: true,
Aliases: []string{"c"},
},
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It should be of flags.Address type. We already have a designated flag for this, it can be moved to options package and reused from both wallet and contract:

neo-go/cli/wallet/wallet.go

Lines 300 to 305 in 4340c76

&flags.AddressFlag{
Name: "contract",
Aliases: []string{"c"},
Required: true,
Usage: "Contract hash or address",
},

Comment on lines 750 to 753
ch, err := util.Uint160DecodeStringLE(ctx.String("contract-hash")[2:])
if err != nil {
return cli.Exit(fmt.Errorf("failed to decode contract hash: %v", err), 1)
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This code will be simplified after flag type replacement.

Comment on lines 791 to 793
_, err := native.GetContractByID(d, contractState.ID)
if !errors.Is(err, storage.ErrKeyNotFound) {
// ID is in use, get a new one
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need to check two cases: if err == nil means that ID is occupied, we can take the next free one. if err != nil && !errors.Is(err, storage.ErrKeyNotFound) means that it's likely some internal DB error, we should exit in this case.

Comment on lines 841 to 849
prefix := append([]byte{0x08}, hash.BytesBE()...)
states, err := client.FindStates(
stateResponse.Root,
managementContract,
prefix,
nil,
nil,
)
if err != nil {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

... we can get rid of this code. Just invoke native Management's getContract via historic Invoker, that's it.

Using internal knowledge of native Management storage like []byte{0x08} at upper-level modules is not the best idea, it should be avoided if we can. The drawback of the suggested approach is: it won't work with C# node, they don't support historic invocations. @ixje is it critical?

Copy link
Contributor Author

@ixje ixje Jul 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Critical no, but might be inconvenient as now we'll need to add some graceful handling if the RPC node doesn't support historic fetching.

How about adding a comment explaining the prefix used while maintaining support for both types of nodes? It is not as if this is likely to ever change

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, let's stick to the implementation-agnostic solution.

How about adding a comment

Agree, it's needed.

Also, let's replace magic 0x08 byte with native.PrefixContract constant, so that it will be more clear to other developers.

Comment on lines 850 to 854
return nil, fmt.Errorf(
"failed to fetch contract state for contract %s: %w",
hash.StringLE(),
err,
)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use single line.

Comment on lines 827 to 831
func GetContractStateAtHeight(
client *rpcclient.Client,
height uint32,
hash util.Uint160,
) (*state.Contract, error) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use single line for this.

Comment on lines 805 to 810
// TODO: check with nspcc if we can update PutContractState with an extra param
// or else how to have the cache for ManagementContract initialized such that it doesn't panic
// trying to run markUpdated()
func PutContractStateNoCache(d *dao.Simple, cs *state.Contract) error {
return putContractState(d, cs, false)
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See the comment above at downloadContract.


// TODO: decide how to best expose. There are too many options that for the time I picked a duplicate
// function to have something working and let nspcc decide their preferred exposing option
func GetNextContractID(d *dao.Simple) (int32, error) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's firstly decide where to place DB modification code.

@ixje
Copy link
Contributor Author

ixje commented Jul 22, 2025

Just now seeing the feedback. Will update after lunch

@ixje
Copy link
Contributor Author

ixje commented Jul 22, 2025

#3960 (comment)
This needs to be discussed, it is possible to unify these tools and we better do this now, before we have 25 variations on the same thing.

I'm going to refrain from further updates to the PR until you've internally decided what the next move is before I get to throw away significant parts.

@AnnaShaleva
Copy link
Member

Discussed, let's restructure the code in the following way:

  1. contract download command that will iterate through the historic contract storage via findstates RPC (this code is already present in the current PR, it just should be moved to a separate command) and dump the full contract state and contract storage to a file.
  2. The dump format used by https://github.com/nspcc-dev/neofs-contract/blob/63a8deaafd21ce741d026e3e3973093596e41932/tests/dump/creator.go#L20 is JSON for contract states + CSV for contract storage. However, given the fact that in NeoGo we'll use contract download to download a single contract per command run, to me it looks more native to flush state+storage pair to a single JSON file in the following form:
{
    "contract": {/*JSON-ized state.Contract bytes*/},
    "storage": {/*the list of serialized KV-pairs in base64*/}
}

This representation differs from the one used by NeoFS contracts. @roman-khimov, ACK?
3. db import-contract command that will read contract/storage from file and insert it into the blockchain. This code is also written in this PR and just needs to be transferred to a proper place. Regarding direct DB modifications (#3960 (comment)): let's follow the approach implemented in https://github.com/nspcc-dev/neofs-contract/blob/63a8deaafd21ce741d026e3e3973093596e41932/tests/migration/storage.go#L78, it solves the problem without direct Blockchain modifications. Once #2926 is done, we'll be able to migrate onto less invasive approach.

@ixje, if there are some additional questions, we may setup a meeting call and discuss them in person.

@roman-khimov
Copy link
Member

This representation differs from the one used by NeoFS contracts. @roman-khimov, ACK?

Seems to be easy to adopt. The only concern is contracts with huge state, CSV is easy to iterate over and process entry by entry, JSON is different.

@AnnaShaleva
Copy link
Member

AnnaShaleva commented Aug 5, 2025

CSV is easy to iterate over and process entry by entry, JSON is different.

Agreed to use split format: JSON for contract states + CSV for storage (a raw set of key-value pairs without contract identifier).

@ixje
Copy link
Contributor Author

ixje commented Aug 5, 2025

fyi; I'm about to go on a holiday, so won't be able to pick this up until late August.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants