Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
186 changes: 186 additions & 0 deletions cli/server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,23 @@ import (
"github.com/nspcc-dev/neo-go/pkg/core"
"github.com/nspcc-dev/neo-go/pkg/core/block"
"github.com/nspcc-dev/neo-go/pkg/core/chaindump"
"github.com/nspcc-dev/neo-go/pkg/core/dao"
"github.com/nspcc-dev/neo-go/pkg/core/native"
"github.com/nspcc-dev/neo-go/pkg/core/state"
corestate "github.com/nspcc-dev/neo-go/pkg/core/stateroot"
"github.com/nspcc-dev/neo-go/pkg/core/storage"
"github.com/nspcc-dev/neo-go/pkg/core/transaction"
"github.com/nspcc-dev/neo-go/pkg/interop/native/management"
"github.com/nspcc-dev/neo-go/pkg/io"
"github.com/nspcc-dev/neo-go/pkg/network"
"github.com/nspcc-dev/neo-go/pkg/rpcclient"
"github.com/nspcc-dev/neo-go/pkg/services/metrics"
"github.com/nspcc-dev/neo-go/pkg/services/notary"
"github.com/nspcc-dev/neo-go/pkg/services/oracle"
"github.com/nspcc-dev/neo-go/pkg/services/rpcsrv"
"github.com/nspcc-dev/neo-go/pkg/services/stateroot"
"github.com/nspcc-dev/neo-go/pkg/util"
"github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
"github.com/urfave/cli/v2"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
Expand Down Expand Up @@ -82,6 +89,27 @@ func NewCommands() []*cli.Command {
Usage: "Height of the state to reset DB to",
Required: true,
})
var cfgDownloadFlags = slices.Clone(cfgFlags)
cfgDownloadFlags = append(cfgDownloadFlags, options.RPC...)
cfgDownloadFlags = append(cfgDownloadFlags,
&cli.UintFlag{
Name: "height",
Usage: "Height at which to get the contract state for",
Required: false,
DefaultText: "latest",
},
Comment on lines +98 to +103
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.

&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",
},

&cli.BoolFlag{
Name: "force",
Usage: "Overwrite local contract state",
Required: false,
},
)
return []*cli.Command{
{
Name: "node",
Expand All @@ -94,6 +122,13 @@ func NewCommands() []*cli.Command {
Name: "db",
Usage: "Database manipulations",
Subcommands: []*cli.Command{
{
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.

{
Name: "dump",
Usage: "Dump blocks (starting with the genesis or specified block) to the file",
Expand Down Expand Up @@ -686,3 +721,154 @@ func Logo() string {
/_/ |_/_____/\____/ \____/\____/
`
}

func downloadContract(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)
}
gctx, cancel := options.GetTimeoutContext(ctx)
defer cancel()

c, err := options.GetRPCClient(gctx, ctx)
if err != nil {
return cli.Exit(err, 1)
}

h := uint32(ctx.Uint("height"))
if h == 0 {
count, err := c.GetBlockCount()
if err != nil {
return cli.Exit(err, 1)
}
h = uint32(count) - 1
}

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.


contractState, err := GetContractStateAtHeight(c, h, ch)
if err != nil {
return cli.Exit(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.

Useless blank line.

if contractState.ID < 0 {
return cli.Exit(fmt.Errorf("native contract download is not supported: %v", contractState.ID), 1)
}

log, _, logCloser, err := options.HandleLoggingParams(ctx, cfg.ApplicationConfiguration)
if err != nil {
return cli.Exit(err, 1)
}
if logCloser != nil {
defer func() { _ = logCloser() }()
}

chain, store, err := initBlockChain(cfg, log)
if err != nil {
return cli.Exit(fmt.Errorf("failed to create Blockchain instance: %w", err), 1)
}

force := ctx.Bool("force")
existingState := chain.GetContractState(ch)
if existingState != nil {
if !force {
return cli.Exit(fmt.Errorf("contract already exists in the chain. Use --force to overwrite"), 1)
}
contractState.ID = existingState.ID
}

d := dao.NewSimple(store, cfg.ProtocolConfiguration.StateRootInHeader)

if !force {
// remote contract does not exist in chain by contract hash, but its ID can already be
// in use.
Copy link
Member

Choose a reason for hiding this comment

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

Comment should start with a capital letter and end with a period.

_, err := native.GetContractByID(d, contractState.ID)
Copy link
Member

Choose a reason for hiding this comment

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

Not critical, but chain.GetContractScriptHash is sufficient here.

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.

newID, err := native.GetNextContractID(d)
if err != nil {
return err
}
contractState.ID = newID
}
}

err = native.PutContractStateNoCache(d, contractState)
Copy link
Member

Choose a reason for hiding this comment

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

Native cache is tighten to DAO, native.PutContractState will panic here because native Management is trying to update its cache whereas cache wasn't initialized for newly-created DAO d.

I don't quite like the fact that we use non-initialized newly-created DAO d for this operation. I'd say that the ideal way is to use wrapped/private DAO inherited from chain's DAO:

func (dao *Simple) GetPrivate() *Simple {

With this approach native cache will work properly and we won't need any workarounds to disable it. The problem is that this approach requires additional getter defined on Blockchain to expose chain's DAO which is also not the best solution. Requires @roman-khimov ACK.

An alternative I thought of - move contract insertion code to the Blockchain itself, like any other methods that perform direct chain's state modification (jumpToState, resetState, etc.)

Copy link
Member

Choose a reason for hiding this comment

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

@AnnaShaleva, check neofs-contract code, it solved a similar problem IIRC.

if err != nil {
return cli.Exit(fmt.Errorf("failed to put contract state: %w", err), 1)
}

_, err = d.Persist()
if err != nil {
return cli.Exit(fmt.Errorf("failed to persist storage: %w", err), 1)
}

nowstate := chain.GetContractState(ch)
if nowstate != nil {
fmt.Println("yeah")
}
Copy link
Member

Choose a reason for hiding this comment

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

Won't work with the current implementation, because Management uses cache to retrieve state, and cache is not updated.

So I'd suggest to remove this call, it doesn't carry any functional meaning.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

that was a leftover


err = store.Close()
if err != nil {
return cli.Exit(fmt.Errorf("failed to close the DB: %w", err), 1)
}

fmt.Printf("downloaded \"%s\" (0x%s)\n", contractState.Manifest.Name, contractState.Hash.StringLE())
Copy link
Member

Choose a reason for hiding this comment

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

Don't escape double quotes, wrap the string into backticks (`) instead.

Copy link
Member

Choose a reason for hiding this comment

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

Use fmt.Fprintf(ctx.App.Writer, ...).

return nil
}

// GetContractStateAtHeight gets the contract state for the given smart contract hash at the specific height.
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.

stateResponse, err := client.GetStateRootByHeight(height)
if err != nil {
return nil, fmt.Errorf("failed to get stateroot for height %d: %w", height, err)
}
Copy link
Member

Choose a reason for hiding this comment

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

Once options.Historic flag is used, use GetRPCWithInvoker, it will return historic Invoker and then...


managementContract, err := util.Uint160DecodeBytesBE([]byte(management.Hash))
if err != nil {
return nil, err
}
Copy link
Member

Choose a reason for hiding this comment

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

nativehashes.ContractManagement.

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.

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.

}
if stateLen := len(states.Results); stateLen != 1 {
return nil, fmt.Errorf(
"unexpected response length for fetch contract contract. Expected 1 got %d", stateLen,
)
}

siArr, err := stackitem.DeserializeLimited(states.Results[0].Value, stackitem.MaxDeserialized*2)
if err != nil {
return nil, err
}

var contractState state.Contract
err = contractState.FromStackItem(siArr)
if err != nil {
return nil, err
}

return &contractState, nil
}
21 changes: 21 additions & 0 deletions pkg/core/native/management.go
Original file line number Diff line number Diff line change
Expand Up @@ -802,6 +802,13 @@ func PutContractState(d *dao.Simple, cs *state.Contract) error {
return putContractState(d, cs, true)
}

// 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.


// putContractState is an internal PutContractState representation.
func putContractState(d *dao.Simple, cs *state.Contract, updateCache bool) error {
key := MakeContractKey(cs.Hash)
Expand Down Expand Up @@ -860,3 +867,17 @@ func checkScriptAndMethods(ic *interop.Context, script []byte, methods []manifes

return nil
}

// 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.

si := d.GetStorageItem(ManagementContractID, keyNextAvailableID)
if si == nil {
return 0, errors.New("nextAvailableID is not initialized")
}
id := bigint.FromBytes(si)
ret := int32(id.Int64())
id.Add(id, intOne)
d.PutBigInt(ManagementContractID, keyNextAvailableID, id)
return ret, nil
}
Loading