nspcc-dev / neo-go

Go Node and SDK for the Neo blockchain
MIT License
124 stars 79 forks source link

neotest: Provide the ability to write any values ​​in the tested contract storage #2926

Open cthulhu-rider opened 1 year ago

cthulhu-rider commented 1 year ago

Context

I try to test migration of contracts' storage items using framework provided by neotest package. Sometimes contract stores items within its methods, so technically I could just call exact method. The inconvenience is that such elements are stored in the contract as a result of API methods that are not always easy to call within the test due to the need to follow business logic. At the same time, I did not find other ways.

Proposal

Add functionality to neotest package which allows to directly write key-value item into the contract storage.

Possible solution

The least affecting approach I thought about is to add analogue of https://pkg.go.dev/github.com/nspcc-dev/neo-go@v0.101.0/pkg/neotest#CompileFile

func CompileContractWithDirectStorageAccess(/*same as CompileFile*/) (writeMethod string)

which, after reading the source code of the contract (but before compiling), adds direct-write method for the life of the test contract. You can call this method later like any other one.

func AnyFreeName(key, value []byte) {
  storage.Put(storage.GetContext(), key, value)
}
roman-khimov commented 1 year ago

Changing contract on the fly is not a good idea, we're supposed to compile/deploy them exactly. Currently it's possible to use custom stores for chain.NewSingleWithCustomConfigAndStore() and chain.NewMultiWithCustomConfigAndStore where you have a complete storage API (seems like you're already doing it). But as you know underlying store may be a bit outdated compared to the internal Blockchain memory cache. Blockchain is specifically designed to never expose its memcache/DAO, but we may think about some specific hooks for tests, that I think will be safer and more appropriate way to handle this problem.

cthulhu-rider commented 1 year ago

think about some specific hooks for tests

Ofc test hooks in core lib would be much better than contract surgery.

cthulhu-rider commented 1 year ago

Here's a workaround suggested by @roman-khimov for the case here when you just want to add contract with some data in advance.

    lowLevelStore := storage.NewMemoryStore()
    _dao := dao.NewSimple(lowLevelStore, false, true)

    nativeContracts := native.NewContracts(config.ProtocolConfiguration{})

    err := nativeContracts.Management.InitializeCache(_dao)
    require.NoError(tb, err)

    err = native.PutContractState(_dao, &_state)
    require.NoError(tb, err)

    // store values in lowLevelStore

    _, err = _dao.PersistSync()
    _, err = cachedStore.PersistSync()

    // init blockchain
    useDefaultConfig := func(*config.Blockchain) {}
    blockChain, alphabetSigner := chain.NewSingleWithCustomConfigAndStore(tb, useDefaultConfig, lowLevelStore, true)

However, it is worth noting that after that contract invocations fail with contract not found exceptions. https://pkg.go.dev/github.com/nspcc-dev/neo-go/pkg/core#Blockchain.GetContractState returns nil, but https://pkg.go.dev/github.com/nspcc-dev/neo-go/pkg/core#Blockchain.GetContractScriptHash works.

roman-khimov commented 1 year ago

Please try

blockChain, _ := chain.NewSingleWithCustomConfigAndStore(tb, useDefaultConfig, lowLevelStore, true)
blockChain.Close()
blockChain, alphabetSigner := chain.NewSingleWithCustomConfigAndStore(tb, useDefaultConfig, lowLevelStore, true)

Yeah, it's weird. But otherwise management contract's cache is not really initialized, because we're starting with an (almost) clean DB, it's inited for the genesis and that's it.

cthulhu-rider commented 1 year ago

Please try

It really works. The only thing I also needed to do is to make no-op Close method of the storage.Store passed on the first init. Without this, blockChain.Close reset underlying maps.