From c2b3ee3d8e1b9e7c33062da7811ace6086372064 Mon Sep 17 00:00:00 2001 From: Roman Khimov Date: Wed, 8 Jun 2022 15:17:40 +0300 Subject: [PATCH 01/21] core: move basic chain creation into a package of its own This allows to reuse it across different packages. testchain can't be used because of circular dependencies. Init() is not changed except for filepath.Join() use instead of direct string appends which is a better approach anyway. rootpath is required because current directory will change from package to package. --- .gitignore | 1 + internal/basicchain/basic.go | 244 ++++++++++++++++++ .../basicchain}/testdata/invoke/invoke.yml | 0 .../testdata/invoke/invokescript_contract.go | 0 .../basicchain}/testdata/test_contract.go | 0 .../basicchain}/testdata/test_contract.yml | 0 .../testdata/verify/verification_contract.go | 0 .../testdata/verify/verification_contract.yml | 0 .../verification_with_args_contract.go | 0 .../verification_with_args_contract.yml | 0 pkg/core/basic_chain_test.go | 228 +--------------- pkg/core/blockchain_neotest_test.go | 5 +- pkg/core/interop_system_neotest_test.go | 2 +- pkg/core/native/native_test/neo_test.go | 2 +- pkg/core/native_management_test.go | 5 +- pkg/core/stateroot_test.go | 3 +- pkg/core/statesync_test.go | 5 +- 17 files changed, 260 insertions(+), 235 deletions(-) create mode 100644 internal/basicchain/basic.go rename {pkg/rpc/server => internal/basicchain}/testdata/invoke/invoke.yml (100%) rename {pkg/rpc/server => internal/basicchain}/testdata/invoke/invokescript_contract.go (100%) rename {pkg/rpc/server => internal/basicchain}/testdata/test_contract.go (100%) rename {pkg/rpc/server => internal/basicchain}/testdata/test_contract.yml (100%) rename {pkg/rpc/server => internal/basicchain}/testdata/verify/verification_contract.go (100%) rename {pkg/rpc/server => internal/basicchain}/testdata/verify/verification_contract.yml (100%) rename {pkg/rpc/server => internal/basicchain}/testdata/verify_args/verification_with_args_contract.go (100%) rename {pkg/rpc/server => internal/basicchain}/testdata/verify_args/verification_with_args_contract.yml (100%) diff --git a/.gitignore b/.gitignore index 2a797be7f..88843343e 100644 --- a/.gitignore +++ b/.gitignore @@ -46,6 +46,7 @@ examples/*/*.json # Fuzzing testdata. testdata/ !cli/testdata +!internal/basicchain/testdata !pkg/compiler/testdata !pkg/config/testdata !pkg/consensus/testdata diff --git a/internal/basicchain/basic.go b/internal/basicchain/basic.go new file mode 100644 index 000000000..caf56bf36 --- /dev/null +++ b/internal/basicchain/basic.go @@ -0,0 +1,244 @@ +package basicchain + +import ( + "encoding/base64" + "encoding/hex" + "math/big" + "path" + "path/filepath" + "testing" + + "github.com/nspcc-dev/neo-go/pkg/core/native" + "github.com/nspcc-dev/neo-go/pkg/core/native/nativenames" + "github.com/nspcc-dev/neo-go/pkg/core/native/noderoles" + "github.com/nspcc-dev/neo-go/pkg/encoding/fixedn" + "github.com/nspcc-dev/neo-go/pkg/io" + "github.com/nspcc-dev/neo-go/pkg/neotest" + "github.com/nspcc-dev/neo-go/pkg/rpc/client/nns" + "github.com/nspcc-dev/neo-go/pkg/util" + "github.com/nspcc-dev/neo-go/pkg/vm/stackitem" + "github.com/nspcc-dev/neo-go/pkg/wallet" + "github.com/stretchr/testify/require" +) + +const neoAmount = 99999000 + +// Init pushes some predefined set of transactions into the given chain, it needs a path to +// the root project directory. +func Init(t *testing.T, rootpath string, e *neotest.Executor) { + if !e.Chain.GetConfig().P2PSigExtensions { + t.Fatal("P2PSigExtensions should be enabled to init basic chain") + } + + var ( + // examplesPrefix is a prefix of the example smart-contracts. + examplesPrefix = filepath.Join(rootpath, "examples") + // testDataPrefix is used to retrieve smart contracts that should be deployed to + // Basic chain. + testDataPrefix = filepath.Join(rootpath, "internal", "basicchain", "testdata") + notaryModulePath = filepath.Join(rootpath, "pkg", "services", "notary") + ) + + gasHash := e.NativeHash(t, nativenames.Gas) + neoHash := e.NativeHash(t, nativenames.Neo) + policyHash := e.NativeHash(t, nativenames.Policy) + notaryHash := e.NativeHash(t, nativenames.Notary) + designationHash := e.NativeHash(t, nativenames.Designation) + t.Logf("native GAS hash: %v", gasHash) + t.Logf("native NEO hash: %v", neoHash) + t.Logf("native Policy hash: %v", policyHash) + t.Logf("native Notary hash: %v", notaryHash) + t.Logf("Block0 hash: %s", e.Chain.GetHeaderHash(0).StringLE()) + + acc0 := e.Validator.(neotest.MultiSigner).Single(2) // priv0 index->order and order->index conversion + priv0ScriptHash := acc0.ScriptHash() + acc1 := e.Validator.(neotest.MultiSigner).Single(0) // priv1 index->order and order->index conversion + priv1ScriptHash := acc1.ScriptHash() + neoValidatorInvoker := e.ValidatorInvoker(neoHash) + gasValidatorInvoker := e.ValidatorInvoker(gasHash) + neoPriv0Invoker := e.NewInvoker(neoHash, acc0) + gasPriv0Invoker := e.NewInvoker(gasHash, acc0) + designateSuperInvoker := e.NewInvoker(designationHash, e.Validator, e.Committee) + + deployContractFromPriv0 := func(t *testing.T, path, contractName string, configPath string, expectedID int32) (util.Uint256, util.Uint256, util.Uint160) { + txDeployHash, cH := newDeployTx(t, e, acc0, path, configPath, true) + b := e.TopBlock(t) + return b.Hash(), txDeployHash, cH + } + + e.CheckGASBalance(t, priv0ScriptHash, big.NewInt(5000_0000)) // gas bounty + + // Block #1: move 1000 GAS and neoAmount NEO to priv0. + txMoveNeo := neoValidatorInvoker.PrepareInvoke(t, "transfer", e.Validator.ScriptHash(), priv0ScriptHash, neoAmount, nil) + txMoveGas := gasValidatorInvoker.PrepareInvoke(t, "transfer", e.Validator.ScriptHash(), priv0ScriptHash, int64(fixedn.Fixed8FromInt64(1000)), nil) + b := e.AddNewBlock(t, txMoveNeo, txMoveGas) + e.CheckHalt(t, txMoveNeo.Hash(), stackitem.Make(true)) + e.CheckHalt(t, txMoveGas.Hash(), stackitem.Make(true)) + t.Logf("Block1 hash: %s", b.Hash().StringLE()) + bw := io.NewBufBinWriter() + b.EncodeBinary(bw.BinWriter) + require.NoError(t, bw.Err) + jsonB, err := b.MarshalJSON() + require.NoError(t, err) + t.Logf("Block1 base64: %s", base64.StdEncoding.EncodeToString(bw.Bytes())) + t.Logf("Block1 JSON: %s", string(jsonB)) + bw.Reset() + b.Header.EncodeBinary(bw.BinWriter) + require.NoError(t, bw.Err) + jsonH, err := b.Header.MarshalJSON() + require.NoError(t, err) + t.Logf("Header1 base64: %s", base64.StdEncoding.EncodeToString(bw.Bytes())) + t.Logf("Header1 JSON: %s", string(jsonH)) + jsonTxMoveNeo, err := txMoveNeo.MarshalJSON() + require.NoError(t, err) + t.Logf("txMoveNeo hash: %s", txMoveNeo.Hash().StringLE()) + t.Logf("txMoveNeo JSON: %s", string(jsonTxMoveNeo)) + t.Logf("txMoveNeo base64: %s", base64.StdEncoding.EncodeToString(txMoveNeo.Bytes())) + t.Logf("txMoveGas hash: %s", txMoveGas.Hash().StringLE()) + + e.EnsureGASBalance(t, priv0ScriptHash, func(balance *big.Int) bool { return balance.Cmp(big.NewInt(1000*native.GASFactor)) >= 0 }) + // info for getblockheader rpc tests + t.Logf("header hash: %s", b.Hash().StringLE()) + buf := io.NewBufBinWriter() + b.Header.EncodeBinary(buf.BinWriter) + t.Logf("header: %s", hex.EncodeToString(buf.Bytes())) + + // Block #2: deploy test_contract (Rubles contract). + cfgPath := filepath.Join(testDataPrefix, "test_contract.yml") + block2H, txDeployH, cHash := deployContractFromPriv0(t, filepath.Join(testDataPrefix, "test_contract.go"), "Rubl", cfgPath, 1) + t.Logf("txDeploy: %s", txDeployH.StringLE()) + t.Logf("Block2 hash: %s", block2H.StringLE()) + + // Block #3: invoke `putValue` method on the test_contract. + rublPriv0Invoker := e.NewInvoker(cHash, acc0) + txInvH := rublPriv0Invoker.Invoke(t, true, "putValue", "testkey", "testvalue") + t.Logf("txInv: %s", txInvH.StringLE()) + + // Block #4: transfer 1000 NEO from priv0 to priv1. + neoPriv0Invoker.Invoke(t, true, "transfer", priv0ScriptHash, priv1ScriptHash, 1000, nil) + + // Block #5: initialize rubles contract and transfer 1000 rubles from the contract to priv0. + initTx := rublPriv0Invoker.PrepareInvoke(t, "init") + transferTx := e.NewUnsignedTx(t, rublPriv0Invoker.Hash, "transfer", cHash, priv0ScriptHash, 1000, nil) + e.SignTx(t, transferTx, 1500_0000, acc0) // Set system fee manually to avoid verification failure. + e.AddNewBlock(t, initTx, transferTx) + e.CheckHalt(t, initTx.Hash(), stackitem.NewBool(true)) + e.CheckHalt(t, transferTx.Hash(), stackitem.Make(true)) + t.Logf("receiveRublesTx: %v", transferTx.Hash().StringLE()) + + // Block #6: transfer 123 rubles from priv0 to priv1 + transferTxH := rublPriv0Invoker.Invoke(t, true, "transfer", priv0ScriptHash, priv1ScriptHash, 123, nil) + t.Logf("sendRublesTx: %v", transferTxH.StringLE()) + + // Block #7: push verification contract into the chain. + verifyPath := filepath.Join(testDataPrefix, "verify", "verification_contract.go") + verifyCfg := filepath.Join(testDataPrefix, "verify", "verification_contract.yml") + _, _, _ = deployContractFromPriv0(t, verifyPath, "Verify", verifyCfg, 2) + + // Block #8: deposit some GAS to notary contract for priv0. + transferTxH = gasPriv0Invoker.Invoke(t, true, "transfer", priv0ScriptHash, notaryHash, 10_0000_0000, []interface{}{priv0ScriptHash, int64(e.Chain.BlockHeight() + 1000)}) + t.Logf("notaryDepositTxPriv0: %v", transferTxH.StringLE()) + + // Block #9: designate new Notary node. + ntr, err := wallet.NewWalletFromFile(path.Join(notaryModulePath, "./testdata/notary1.json")) + require.NoError(t, err) + require.NoError(t, ntr.Accounts[0].Decrypt("one", ntr.Scrypt)) + designateSuperInvoker.Invoke(t, stackitem.Null{}, "designateAsRole", + int64(noderoles.P2PNotary), []interface{}{ntr.Accounts[0].PrivateKey().PublicKey().Bytes()}) + t.Logf("Designated Notary node: %s", hex.EncodeToString(ntr.Accounts[0].PrivateKey().PublicKey().Bytes())) + + // Block #10: push verification contract with arguments into the chain. + verifyPath = filepath.Join(testDataPrefix, "verify_args", "verification_with_args_contract.go") + verifyCfg = filepath.Join(testDataPrefix, "verify_args", "verification_with_args_contract.yml") + _, _, _ = deployContractFromPriv0(t, verifyPath, "VerifyWithArgs", verifyCfg, 3) // block #10 + + // Block #11: push NameService contract into the chain. + nsPath := filepath.Join(examplesPrefix, "nft-nd-nns") + nsConfigPath := filepath.Join(nsPath, "nns.yml") + _, _, nsHash := deployContractFromPriv0(t, nsPath, nsPath, nsConfigPath, 4) // block #11 + nsCommitteeInvoker := e.CommitteeInvoker(nsHash) + nsPriv0Invoker := e.NewInvoker(nsHash, acc0) + + // Block #12: transfer funds to committee for further NS record registration. + gasValidatorInvoker.Invoke(t, true, "transfer", + e.Validator.ScriptHash(), e.Committee.ScriptHash(), 1000_00000000, nil) // block #12 + + // Block #13: add `.com` root to NNS. + nsCommitteeInvoker.Invoke(t, stackitem.Null{}, "addRoot", "com") // block #13 + + // Block #14: register `neo.com` via NNS. + registerTxH := nsPriv0Invoker.Invoke(t, true, "register", + "neo.com", priv0ScriptHash) // block #14 + res := e.GetTxExecResult(t, registerTxH) + require.Equal(t, 1, len(res.Events)) // transfer + tokenID, err := res.Events[0].Item.Value().([]stackitem.Item)[3].TryBytes() + require.NoError(t, err) + t.Logf("NNS token #1 ID (hex): %s", hex.EncodeToString(tokenID)) + + // Block #15: set A record type with priv0 owner via NNS. + nsPriv0Invoker.Invoke(t, stackitem.Null{}, "setRecord", "neo.com", int64(nns.A), "1.2.3.4") // block #15 + + // Block #16: invoke `test_contract.go`: put new value with the same key to check `getstate` RPC call + txPutNewValue := rublPriv0Invoker.PrepareInvoke(t, "putValue", "testkey", "newtestvalue") // tx1 + // Invoke `test_contract.go`: put values to check `findstates` RPC call. + txPut1 := rublPriv0Invoker.PrepareInvoke(t, "putValue", "aa", "v1") // tx2 + txPut2 := rublPriv0Invoker.PrepareInvoke(t, "putValue", "aa10", "v2") // tx3 + txPut3 := rublPriv0Invoker.PrepareInvoke(t, "putValue", "aa50", "v3") // tx4 + e.AddNewBlock(t, txPutNewValue, txPut1, txPut2, txPut3) // block #16 + e.CheckHalt(t, txPutNewValue.Hash(), stackitem.NewBool(true)) + e.CheckHalt(t, txPut1.Hash(), stackitem.NewBool(true)) + e.CheckHalt(t, txPut2.Hash(), stackitem.NewBool(true)) + e.CheckHalt(t, txPut3.Hash(), stackitem.NewBool(true)) + + // Block #17: deploy NeoFS Object contract (NEP11-Divisible). + nfsPath := filepath.Join(examplesPrefix, "nft-d") + nfsConfigPath := filepath.Join(nfsPath, "nft.yml") + _, _, nfsHash := deployContractFromPriv0(t, nfsPath, nfsPath, nfsConfigPath, 5) // block #17 + nfsPriv0Invoker := e.NewInvoker(nfsHash, acc0) + nfsPriv1Invoker := e.NewInvoker(nfsHash, acc1) + + // Block #18: mint 1.00 NFSO token by transferring 10 GAS to NFSO contract. + containerID := util.Uint256{1, 2, 3} + objectID := util.Uint256{4, 5, 6} + txGas0toNFSH := gasPriv0Invoker.Invoke(t, true, "transfer", + priv0ScriptHash, nfsHash, 10_0000_0000, []interface{}{containerID.BytesBE(), objectID.BytesBE()}) // block #18 + res = e.GetTxExecResult(t, txGas0toNFSH) + require.Equal(t, 2, len(res.Events)) // GAS transfer + NFSO transfer + tokenID, err = res.Events[1].Item.Value().([]stackitem.Item)[3].TryBytes() + require.NoError(t, err) + t.Logf("NFSO token #1 ID (hex): %s", hex.EncodeToString(tokenID)) + + // Block #19: transfer 0.25 NFSO from priv0 to priv1. + nfsPriv0Invoker.Invoke(t, true, "transfer", priv0ScriptHash, priv1ScriptHash, 25, tokenID, nil) // block #19 + + // Block #20: transfer 1000 GAS to priv1. + gasValidatorInvoker.Invoke(t, true, "transfer", e.Validator.ScriptHash(), + priv1ScriptHash, int64(fixedn.Fixed8FromInt64(1000)), nil) // block #20 + + // Block #21: transfer 0.05 NFSO from priv1 back to priv0. + nfsPriv1Invoker.Invoke(t, true, "transfer", priv1ScriptHash, priv0ScriptHash, 5, tokenID, nil) // block #21 + + // Compile contract to test `invokescript` RPC call + invokePath := filepath.Join(testDataPrefix, "invoke", "invokescript_contract.go") + invokeCfg := filepath.Join(testDataPrefix, "invoke", "invoke.yml") + _, _ = newDeployTx(t, e, acc0, invokePath, invokeCfg, false) + + // Prepare some transaction for future submission. + txSendRaw := neoPriv0Invoker.PrepareInvoke(t, "transfer", priv0ScriptHash, priv1ScriptHash, int64(fixedn.Fixed8FromInt64(1000)), nil) + bw.Reset() + txSendRaw.EncodeBinary(bw.BinWriter) + t.Logf("sendrawtransaction: \n\tbase64: %s\n\tHash LE: %s", base64.StdEncoding.EncodeToString(bw.Bytes()), txSendRaw.Hash().StringLE()) + + sr20, err := e.Chain.GetStateModule().GetStateRoot(20) + require.NoError(t, err) + t.Logf("Block #20 stateroot LE: %s", sr20.Root.StringLE()) +} + +func newDeployTx(t *testing.T, e *neotest.Executor, sender neotest.Signer, sourcePath, configPath string, deploy bool) (util.Uint256, util.Uint160) { + c := neotest.CompileFile(t, sender.ScriptHash(), sourcePath, configPath) + t.Logf("contract (%s): \n\tHash: %s\n\tAVM: %s", sourcePath, c.Hash.StringLE(), base64.StdEncoding.EncodeToString(c.NEF.Script)) + if deploy { + return e.DeployContractBy(t, sender, c, nil), c.Hash + } + return util.Uint256{}, c.Hash +} diff --git a/pkg/rpc/server/testdata/invoke/invoke.yml b/internal/basicchain/testdata/invoke/invoke.yml similarity index 100% rename from pkg/rpc/server/testdata/invoke/invoke.yml rename to internal/basicchain/testdata/invoke/invoke.yml diff --git a/pkg/rpc/server/testdata/invoke/invokescript_contract.go b/internal/basicchain/testdata/invoke/invokescript_contract.go similarity index 100% rename from pkg/rpc/server/testdata/invoke/invokescript_contract.go rename to internal/basicchain/testdata/invoke/invokescript_contract.go diff --git a/pkg/rpc/server/testdata/test_contract.go b/internal/basicchain/testdata/test_contract.go similarity index 100% rename from pkg/rpc/server/testdata/test_contract.go rename to internal/basicchain/testdata/test_contract.go diff --git a/pkg/rpc/server/testdata/test_contract.yml b/internal/basicchain/testdata/test_contract.yml similarity index 100% rename from pkg/rpc/server/testdata/test_contract.yml rename to internal/basicchain/testdata/test_contract.yml diff --git a/pkg/rpc/server/testdata/verify/verification_contract.go b/internal/basicchain/testdata/verify/verification_contract.go similarity index 100% rename from pkg/rpc/server/testdata/verify/verification_contract.go rename to internal/basicchain/testdata/verify/verification_contract.go diff --git a/pkg/rpc/server/testdata/verify/verification_contract.yml b/internal/basicchain/testdata/verify/verification_contract.yml similarity index 100% rename from pkg/rpc/server/testdata/verify/verification_contract.yml rename to internal/basicchain/testdata/verify/verification_contract.yml diff --git a/pkg/rpc/server/testdata/verify_args/verification_with_args_contract.go b/internal/basicchain/testdata/verify_args/verification_with_args_contract.go similarity index 100% rename from pkg/rpc/server/testdata/verify_args/verification_with_args_contract.go rename to internal/basicchain/testdata/verify_args/verification_with_args_contract.go diff --git a/pkg/rpc/server/testdata/verify_args/verification_with_args_contract.yml b/internal/basicchain/testdata/verify_args/verification_with_args_contract.yml similarity index 100% rename from pkg/rpc/server/testdata/verify_args/verification_with_args_contract.yml rename to internal/basicchain/testdata/verify_args/verification_with_args_contract.yml diff --git a/pkg/core/basic_chain_test.go b/pkg/core/basic_chain_test.go index 5984ef504..ad87db912 100644 --- a/pkg/core/basic_chain_test.go +++ b/pkg/core/basic_chain_test.go @@ -1,34 +1,21 @@ package core_test import ( - "encoding/base64" - "encoding/hex" - "math/big" "os" - "path" "path/filepath" "testing" "time" + "github.com/nspcc-dev/neo-go/internal/basicchain" "github.com/nspcc-dev/neo-go/pkg/config" "github.com/nspcc-dev/neo-go/pkg/core/chaindump" - "github.com/nspcc-dev/neo-go/pkg/core/native" - "github.com/nspcc-dev/neo-go/pkg/core/native/nativenames" - "github.com/nspcc-dev/neo-go/pkg/core/native/noderoles" - "github.com/nspcc-dev/neo-go/pkg/encoding/fixedn" "github.com/nspcc-dev/neo-go/pkg/io" "github.com/nspcc-dev/neo-go/pkg/neotest" "github.com/nspcc-dev/neo-go/pkg/neotest/chain" - "github.com/nspcc-dev/neo-go/pkg/rpc/client/nns" - "github.com/nspcc-dev/neo-go/pkg/util" - "github.com/nspcc-dev/neo-go/pkg/vm/stackitem" - "github.com/nspcc-dev/neo-go/pkg/wallet" "github.com/stretchr/testify/require" ) const ( - // examplesPrefix is a prefix of the example smart-contracts. - examplesPrefix = "../../examples/" // basicChainPrefix is a prefix used to store Basic chain .acc file for tests. // It is also used to retrieve smart contracts that should be deployed to // Basic chain. @@ -56,7 +43,7 @@ func TestCreateBasicChain(t *testing.T) { }) e := neotest.NewExecutor(t, bc, validators, committee) - initBasicChain(t, e) + basicchain.Init(t, "../../", e) if saveChain { outStream, err := os.Create(basicChainPrefix + "testblocks.acc") @@ -73,214 +60,3 @@ func TestCreateBasicChain(t *testing.T) { require.False(t, saveChain) } - -func initBasicChain(t *testing.T, e *neotest.Executor) { - if !e.Chain.GetConfig().P2PSigExtensions { - t.Fatal("P2PSigExtensions should be enabled to init basic chain") - } - - const neoAmount = 99999000 - - gasHash := e.NativeHash(t, nativenames.Gas) - neoHash := e.NativeHash(t, nativenames.Neo) - policyHash := e.NativeHash(t, nativenames.Policy) - notaryHash := e.NativeHash(t, nativenames.Notary) - designationHash := e.NativeHash(t, nativenames.Designation) - t.Logf("native GAS hash: %v", gasHash) - t.Logf("native NEO hash: %v", neoHash) - t.Logf("native Policy hash: %v", policyHash) - t.Logf("native Notary hash: %v", notaryHash) - t.Logf("Block0 hash: %s", e.Chain.GetHeaderHash(0).StringLE()) - - acc0 := e.Validator.(neotest.MultiSigner).Single(2) // priv0 index->order and order->index conversion - priv0ScriptHash := acc0.ScriptHash() - acc1 := e.Validator.(neotest.MultiSigner).Single(0) // priv1 index->order and order->index conversion - priv1ScriptHash := acc1.ScriptHash() - neoValidatorInvoker := e.ValidatorInvoker(neoHash) - gasValidatorInvoker := e.ValidatorInvoker(gasHash) - neoPriv0Invoker := e.NewInvoker(neoHash, acc0) - gasPriv0Invoker := e.NewInvoker(gasHash, acc0) - designateSuperInvoker := e.NewInvoker(designationHash, e.Validator, e.Committee) - - deployContractFromPriv0 := func(t *testing.T, path, contractName string, configPath string, expectedID int32) (util.Uint256, util.Uint256, util.Uint160) { - txDeployHash, cH := newDeployTx(t, e, acc0, path, configPath, true) - b := e.TopBlock(t) - return b.Hash(), txDeployHash, cH - } - - e.CheckGASBalance(t, priv0ScriptHash, big.NewInt(5000_0000)) // gas bounty - - // Block #1: move 1000 GAS and neoAmount NEO to priv0. - txMoveNeo := neoValidatorInvoker.PrepareInvoke(t, "transfer", e.Validator.ScriptHash(), priv0ScriptHash, neoAmount, nil) - txMoveGas := gasValidatorInvoker.PrepareInvoke(t, "transfer", e.Validator.ScriptHash(), priv0ScriptHash, int64(fixedn.Fixed8FromInt64(1000)), nil) - b := e.AddNewBlock(t, txMoveNeo, txMoveGas) - e.CheckHalt(t, txMoveNeo.Hash(), stackitem.Make(true)) - e.CheckHalt(t, txMoveGas.Hash(), stackitem.Make(true)) - t.Logf("Block1 hash: %s", b.Hash().StringLE()) - bw := io.NewBufBinWriter() - b.EncodeBinary(bw.BinWriter) - require.NoError(t, bw.Err) - jsonB, err := b.MarshalJSON() - require.NoError(t, err) - t.Logf("Block1 base64: %s", base64.StdEncoding.EncodeToString(bw.Bytes())) - t.Logf("Block1 JSON: %s", string(jsonB)) - bw.Reset() - b.Header.EncodeBinary(bw.BinWriter) - require.NoError(t, bw.Err) - jsonH, err := b.Header.MarshalJSON() - require.NoError(t, err) - t.Logf("Header1 base64: %s", base64.StdEncoding.EncodeToString(bw.Bytes())) - t.Logf("Header1 JSON: %s", string(jsonH)) - jsonTxMoveNeo, err := txMoveNeo.MarshalJSON() - require.NoError(t, err) - t.Logf("txMoveNeo hash: %s", txMoveNeo.Hash().StringLE()) - t.Logf("txMoveNeo JSON: %s", string(jsonTxMoveNeo)) - t.Logf("txMoveNeo base64: %s", base64.StdEncoding.EncodeToString(txMoveNeo.Bytes())) - t.Logf("txMoveGas hash: %s", txMoveGas.Hash().StringLE()) - - e.EnsureGASBalance(t, priv0ScriptHash, func(balance *big.Int) bool { return balance.Cmp(big.NewInt(1000*native.GASFactor)) >= 0 }) - // info for getblockheader rpc tests - t.Logf("header hash: %s", b.Hash().StringLE()) - buf := io.NewBufBinWriter() - b.Header.EncodeBinary(buf.BinWriter) - t.Logf("header: %s", hex.EncodeToString(buf.Bytes())) - - // Block #2: deploy test_contract (Rubles contract). - cfgPath := basicChainPrefix + "test_contract.yml" - block2H, txDeployH, cHash := deployContractFromPriv0(t, basicChainPrefix+"test_contract.go", "Rubl", cfgPath, 1) - t.Logf("txDeploy: %s", txDeployH.StringLE()) - t.Logf("Block2 hash: %s", block2H.StringLE()) - - // Block #3: invoke `putValue` method on the test_contract. - rublPriv0Invoker := e.NewInvoker(cHash, acc0) - txInvH := rublPriv0Invoker.Invoke(t, true, "putValue", "testkey", "testvalue") - t.Logf("txInv: %s", txInvH.StringLE()) - - // Block #4: transfer 1000 NEO from priv0 to priv1. - neoPriv0Invoker.Invoke(t, true, "transfer", priv0ScriptHash, priv1ScriptHash, 1000, nil) - - // Block #5: initialize rubles contract and transfer 1000 rubles from the contract to priv0. - initTx := rublPriv0Invoker.PrepareInvoke(t, "init") - transferTx := e.NewUnsignedTx(t, rublPriv0Invoker.Hash, "transfer", cHash, priv0ScriptHash, 1000, nil) - e.SignTx(t, transferTx, 1500_0000, acc0) // Set system fee manually to avoid verification failure. - e.AddNewBlock(t, initTx, transferTx) - e.CheckHalt(t, initTx.Hash(), stackitem.NewBool(true)) - e.CheckHalt(t, transferTx.Hash(), stackitem.Make(true)) - t.Logf("receiveRublesTx: %v", transferTx.Hash().StringLE()) - - // Block #6: transfer 123 rubles from priv0 to priv1 - transferTxH := rublPriv0Invoker.Invoke(t, true, "transfer", priv0ScriptHash, priv1ScriptHash, 123, nil) - t.Logf("sendRublesTx: %v", transferTxH.StringLE()) - - // Block #7: push verification contract into the chain. - verifyPath := filepath.Join(basicChainPrefix, "verify", "verification_contract.go") - verifyCfg := filepath.Join(basicChainPrefix, "verify", "verification_contract.yml") - _, _, _ = deployContractFromPriv0(t, verifyPath, "Verify", verifyCfg, 2) - - // Block #8: deposit some GAS to notary contract for priv0. - transferTxH = gasPriv0Invoker.Invoke(t, true, "transfer", priv0ScriptHash, notaryHash, 10_0000_0000, []interface{}{priv0ScriptHash, int64(e.Chain.BlockHeight() + 1000)}) - t.Logf("notaryDepositTxPriv0: %v", transferTxH.StringLE()) - - // Block #9: designate new Notary node. - ntr, err := wallet.NewWalletFromFile(path.Join(notaryModulePath, "./testdata/notary1.json")) - require.NoError(t, err) - require.NoError(t, ntr.Accounts[0].Decrypt("one", ntr.Scrypt)) - designateSuperInvoker.Invoke(t, stackitem.Null{}, "designateAsRole", - int64(noderoles.P2PNotary), []interface{}{ntr.Accounts[0].PrivateKey().PublicKey().Bytes()}) - t.Logf("Designated Notary node: %s", hex.EncodeToString(ntr.Accounts[0].PrivateKey().PublicKey().Bytes())) - - // Block #10: push verification contract with arguments into the chain. - verifyPath = filepath.Join(basicChainPrefix, "verify_args", "verification_with_args_contract.go") - verifyCfg = filepath.Join(basicChainPrefix, "verify_args", "verification_with_args_contract.yml") - _, _, _ = deployContractFromPriv0(t, verifyPath, "VerifyWithArgs", verifyCfg, 3) // block #10 - - // Block #11: push NameService contract into the chain. - nsPath := examplesPrefix + "nft-nd-nns/" - nsConfigPath := nsPath + "nns.yml" - _, _, nsHash := deployContractFromPriv0(t, nsPath, nsPath, nsConfigPath, 4) // block #11 - nsCommitteeInvoker := e.CommitteeInvoker(nsHash) - nsPriv0Invoker := e.NewInvoker(nsHash, acc0) - - // Block #12: transfer funds to committee for further NS record registration. - gasValidatorInvoker.Invoke(t, true, "transfer", - e.Validator.ScriptHash(), e.Committee.ScriptHash(), 1000_00000000, nil) // block #12 - - // Block #13: add `.com` root to NNS. - nsCommitteeInvoker.Invoke(t, stackitem.Null{}, "addRoot", "com") // block #13 - - // Block #14: register `neo.com` via NNS. - registerTxH := nsPriv0Invoker.Invoke(t, true, "register", - "neo.com", priv0ScriptHash) // block #14 - res := e.GetTxExecResult(t, registerTxH) - require.Equal(t, 1, len(res.Events)) // transfer - tokenID, err := res.Events[0].Item.Value().([]stackitem.Item)[3].TryBytes() - require.NoError(t, err) - t.Logf("NNS token #1 ID (hex): %s", hex.EncodeToString(tokenID)) - - // Block #15: set A record type with priv0 owner via NNS. - nsPriv0Invoker.Invoke(t, stackitem.Null{}, "setRecord", "neo.com", int64(nns.A), "1.2.3.4") // block #15 - - // Block #16: invoke `test_contract.go`: put new value with the same key to check `getstate` RPC call - txPutNewValue := rublPriv0Invoker.PrepareInvoke(t, "putValue", "testkey", "newtestvalue") // tx1 - // Invoke `test_contract.go`: put values to check `findstates` RPC call. - txPut1 := rublPriv0Invoker.PrepareInvoke(t, "putValue", "aa", "v1") // tx2 - txPut2 := rublPriv0Invoker.PrepareInvoke(t, "putValue", "aa10", "v2") // tx3 - txPut3 := rublPriv0Invoker.PrepareInvoke(t, "putValue", "aa50", "v3") // tx4 - e.AddNewBlock(t, txPutNewValue, txPut1, txPut2, txPut3) // block #16 - e.CheckHalt(t, txPutNewValue.Hash(), stackitem.NewBool(true)) - e.CheckHalt(t, txPut1.Hash(), stackitem.NewBool(true)) - e.CheckHalt(t, txPut2.Hash(), stackitem.NewBool(true)) - e.CheckHalt(t, txPut3.Hash(), stackitem.NewBool(true)) - - // Block #17: deploy NeoFS Object contract (NEP11-Divisible). - nfsPath := examplesPrefix + "nft-d/" - nfsConfigPath := nfsPath + "nft.yml" - _, _, nfsHash := deployContractFromPriv0(t, nfsPath, nfsPath, nfsConfigPath, 5) // block #17 - nfsPriv0Invoker := e.NewInvoker(nfsHash, acc0) - nfsPriv1Invoker := e.NewInvoker(nfsHash, acc1) - - // Block #18: mint 1.00 NFSO token by transferring 10 GAS to NFSO contract. - containerID := util.Uint256{1, 2, 3} - objectID := util.Uint256{4, 5, 6} - txGas0toNFSH := gasPriv0Invoker.Invoke(t, true, "transfer", - priv0ScriptHash, nfsHash, 10_0000_0000, []interface{}{containerID.BytesBE(), objectID.BytesBE()}) // block #18 - res = e.GetTxExecResult(t, txGas0toNFSH) - require.Equal(t, 2, len(res.Events)) // GAS transfer + NFSO transfer - tokenID, err = res.Events[1].Item.Value().([]stackitem.Item)[3].TryBytes() - require.NoError(t, err) - t.Logf("NFSO token #1 ID (hex): %s", hex.EncodeToString(tokenID)) - - // Block #19: transfer 0.25 NFSO from priv0 to priv1. - nfsPriv0Invoker.Invoke(t, true, "transfer", priv0ScriptHash, priv1ScriptHash, 25, tokenID, nil) // block #19 - - // Block #20: transfer 1000 GAS to priv1. - gasValidatorInvoker.Invoke(t, true, "transfer", e.Validator.ScriptHash(), - priv1ScriptHash, int64(fixedn.Fixed8FromInt64(1000)), nil) // block #20 - - // Block #21: transfer 0.05 NFSO from priv1 back to priv0. - nfsPriv1Invoker.Invoke(t, true, "transfer", priv1ScriptHash, priv0ScriptHash, 5, tokenID, nil) // block #21 - - // Compile contract to test `invokescript` RPC call - invokePath := filepath.Join(basicChainPrefix, "invoke", "invokescript_contract.go") - invokeCfg := filepath.Join(basicChainPrefix, "invoke", "invoke.yml") - _, _ = newDeployTx(t, e, acc0, invokePath, invokeCfg, false) - - // Prepare some transaction for future submission. - txSendRaw := neoPriv0Invoker.PrepareInvoke(t, "transfer", priv0ScriptHash, priv1ScriptHash, int64(fixedn.Fixed8FromInt64(1000)), nil) - bw.Reset() - txSendRaw.EncodeBinary(bw.BinWriter) - t.Logf("sendrawtransaction: \n\tbase64: %s\n\tHash LE: %s", base64.StdEncoding.EncodeToString(bw.Bytes()), txSendRaw.Hash().StringLE()) - - sr20, err := e.Chain.GetStateModule().GetStateRoot(20) - require.NoError(t, err) - t.Logf("Block #20 stateroot LE: %s", sr20.Root.StringLE()) -} - -func newDeployTx(t *testing.T, e *neotest.Executor, sender neotest.Signer, sourcePath, configPath string, deploy bool) (util.Uint256, util.Uint160) { - c := neotest.CompileFile(t, sender.ScriptHash(), sourcePath, configPath) - t.Logf("contract (%s): \n\tHash: %s\n\tAVM: %s", sourcePath, c.Hash.StringLE(), base64.StdEncoding.EncodeToString(c.NEF.Script)) - if deploy { - return e.DeployContractBy(t, sender, c, nil), c.Hash - } - return util.Uint256{}, c.Hash -} diff --git a/pkg/core/blockchain_neotest_test.go b/pkg/core/blockchain_neotest_test.go index 91a25d12f..4049e3c51 100644 --- a/pkg/core/blockchain_neotest_test.go +++ b/pkg/core/blockchain_neotest_test.go @@ -10,6 +10,7 @@ import ( "testing" "time" + "github.com/nspcc-dev/neo-go/internal/basicchain" "github.com/nspcc-dev/neo-go/internal/contracts" "github.com/nspcc-dev/neo-go/internal/random" "github.com/nspcc-dev/neo-go/pkg/compiler" @@ -81,7 +82,7 @@ func testDumpAndRestore(t *testing.T, dumpF, restoreF func(c *config.ProtocolCon bc, validators, committee := chain.NewMultiWithCustomConfig(t, dumpF) e := neotest.NewExecutor(t, bc, validators, committee) - initBasicChain(t, e) + basicchain.Init(t, "../../", e) require.True(t, bc.BlockHeight() > 5) // ensure that test is valid w := io.NewBufBinWriter() @@ -147,7 +148,7 @@ func TestBlockchain_StartFromExistingDB(t *testing.T) { require.NoError(t, err) go bc.Run() e := neotest.NewExecutor(t, bc, validators, committee) - initBasicChain(t, e) + basicchain.Init(t, "../../", e) require.True(t, bc.BlockHeight() > 5, "ensure that basic chain is correctly initialised") // Information for further tests. diff --git a/pkg/core/interop_system_neotest_test.go b/pkg/core/interop_system_neotest_test.go index 7fa7ba060..99fcee560 100644 --- a/pkg/core/interop_system_neotest_test.go +++ b/pkg/core/interop_system_neotest_test.go @@ -263,7 +263,7 @@ func TestSystemRuntimeBurnGas(t *testing.T) { func TestSystemContractCreateAccount_Hardfork(t *testing.T) { bc, acc := chain.NewSingleWithCustomConfig(t, func(c *config.ProtocolConfiguration) { - c.P2PSigExtensions = true // `initBasicChain` requires Notary enabled + c.P2PSigExtensions = true // `basicchain.Init` requires Notary enabled c.Hardforks = map[string]uint32{ config.HFAspidochelone.String(): 2, } diff --git a/pkg/core/native/native_test/neo_test.go b/pkg/core/native/native_test/neo_test.go index 26eb69085..6d8ee3275 100644 --- a/pkg/core/native/native_test/neo_test.go +++ b/pkg/core/native/native_test/neo_test.go @@ -279,7 +279,7 @@ func TestNEO_RecursiveGASMint(t *testing.T) { e := neoCommitteeInvoker.Executor gasValidatorInvoker := e.ValidatorInvoker(e.NativeHash(t, nativenames.Gas)) - c := neotest.CompileFile(t, e.Validator.ScriptHash(), "../../../rpc/server/testdata/test_contract.go", "../../../rpc/server/testdata/test_contract.yml") + c := neotest.CompileFile(t, e.Validator.ScriptHash(), "../../../../internal/basicchain/testdata/test_contract.go", "../../../../internal/basicchain/testdata/test_contract.yml") e.DeployContract(t, c, nil) gasValidatorInvoker.Invoke(t, true, "transfer", e.Validator.ScriptHash(), c.Hash, int64(2_0000_0000), nil) diff --git a/pkg/core/native_management_test.go b/pkg/core/native_management_test.go index e38a8cf96..e02e022dd 100644 --- a/pkg/core/native_management_test.go +++ b/pkg/core/native_management_test.go @@ -3,6 +3,7 @@ package core_test import ( "testing" + "github.com/nspcc-dev/neo-go/internal/basicchain" "github.com/nspcc-dev/neo-go/pkg/config" "github.com/nspcc-dev/neo-go/pkg/core/native/nativenames" "github.com/nspcc-dev/neo-go/pkg/neotest" @@ -22,10 +23,10 @@ func TestManagement_GetNEP17Contracts(t *testing.T) { t.Run("basic chain", func(t *testing.T) { bc, validators, committee := chain.NewMultiWithCustomConfig(t, func(c *config.ProtocolConfiguration) { - c.P2PSigExtensions = true // `initBasicChain` requires Notary enabled + c.P2PSigExtensions = true // `basicchain.Init` requires Notary enabled }) e := neotest.NewExecutor(t, bc, validators, committee) - initBasicChain(t, e) + basicchain.Init(t, "../../", e) require.ElementsMatch(t, []util.Uint160{e.NativeHash(t, nativenames.Neo), e.NativeHash(t, nativenames.Gas), e.ContractHash(t, 1)}, bc.GetNEP17Contracts()) diff --git a/pkg/core/stateroot_test.go b/pkg/core/stateroot_test.go index 86df6c8b5..f0ed8d1d5 100644 --- a/pkg/core/stateroot_test.go +++ b/pkg/core/stateroot_test.go @@ -8,6 +8,7 @@ import ( "testing" "time" + "github.com/nspcc-dev/neo-go/internal/basicchain" "github.com/nspcc-dev/neo-go/internal/testserdes" "github.com/nspcc-dev/neo-go/pkg/config" "github.com/nspcc-dev/neo-go/pkg/config/netmode" @@ -302,7 +303,7 @@ func TestStateroot_GetLatestStateHeight(t *testing.T) { c.P2PSigExtensions = true }) e := neotest.NewExecutor(t, bc, validators, committee) - initBasicChain(t, e) + basicchain.Init(t, "../../", e) m := bc.GetStateModule() for i := uint32(0); i < bc.BlockHeight(); i++ { diff --git a/pkg/core/statesync_test.go b/pkg/core/statesync_test.go index 4162c921b..326c40a58 100644 --- a/pkg/core/statesync_test.go +++ b/pkg/core/statesync_test.go @@ -3,6 +3,7 @@ package core_test import ( "testing" + "github.com/nspcc-dev/neo-go/internal/basicchain" "github.com/nspcc-dev/neo-go/pkg/config" "github.com/nspcc-dev/neo-go/pkg/core/block" "github.com/nspcc-dev/neo-go/pkg/core/mpt" @@ -291,13 +292,13 @@ func TestStateSyncModule_RestoreBasicChain(t *testing.T) { c.P2PStateExchangeExtensions = true c.StateSyncInterval = stateSyncInterval c.MaxTraceableBlocks = maxTraceable - c.P2PSigExtensions = true // `initBasicChain` assumes Notary is enabled. + c.P2PSigExtensions = true // `basicchain.Init` assumes Notary is enabled. } bcSpoutStore := storage.NewMemoryStore() bcSpout, validators, committee := chain.NewMultiWithCustomConfigAndStore(t, spoutCfg, bcSpoutStore, false) go bcSpout.Run() // Will close it manually at the end. e := neotest.NewExecutor(t, bcSpout, validators, committee) - initBasicChain(t, e) + basicchain.Init(t, "../../", e) // make spout chain higher that latest state sync point (add several blocks up to stateSyncPoint+2) e.AddNewBlock(t) From 10f94e6119ba3a984012ea15a2d7c3afc3447c3f Mon Sep 17 00:00:00 2001 From: Roman Khimov Date: Wed, 8 Jun 2022 15:28:08 +0300 Subject: [PATCH 02/21] core: move chain dump test into its own package --- pkg/core/blockchain_neotest_test.go | 78 ------------------------ pkg/core/chaindump/dump_test.go | 92 +++++++++++++++++++++++++++++ 2 files changed, 92 insertions(+), 78 deletions(-) create mode 100644 pkg/core/chaindump/dump_test.go diff --git a/pkg/core/blockchain_neotest_test.go b/pkg/core/blockchain_neotest_test.go index 4049e3c51..ef765774f 100644 --- a/pkg/core/blockchain_neotest_test.go +++ b/pkg/core/blockchain_neotest_test.go @@ -18,7 +18,6 @@ import ( "github.com/nspcc-dev/neo-go/pkg/config/netmode" "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/fee" "github.com/nspcc-dev/neo-go/pkg/core/interop/interopnames" @@ -49,83 +48,6 @@ import ( "github.com/stretchr/testify/require" ) -func TestBlockchain_DumpAndRestore(t *testing.T) { - t.Run("no state root", func(t *testing.T) { - testDumpAndRestore(t, func(c *config.ProtocolConfiguration) { - c.StateRootInHeader = false - c.P2PSigExtensions = true - }, nil) - }) - t.Run("with state root", func(t *testing.T) { - testDumpAndRestore(t, func(c *config.ProtocolConfiguration) { - c.StateRootInHeader = true - c.P2PSigExtensions = true - }, nil) - }) - t.Run("remove untraceable", func(t *testing.T) { - // Dump can only be created if all blocks and transactions are present. - testDumpAndRestore(t, func(c *config.ProtocolConfiguration) { - c.P2PSigExtensions = true - }, func(c *config.ProtocolConfiguration) { - c.MaxTraceableBlocks = 2 - c.RemoveUntraceableBlocks = true - c.P2PSigExtensions = true - }) - }) -} - -func testDumpAndRestore(t *testing.T, dumpF, restoreF func(c *config.ProtocolConfiguration)) { - if restoreF == nil { - restoreF = dumpF - } - - bc, validators, committee := chain.NewMultiWithCustomConfig(t, dumpF) - e := neotest.NewExecutor(t, bc, validators, committee) - - basicchain.Init(t, "../../", e) - require.True(t, bc.BlockHeight() > 5) // ensure that test is valid - - w := io.NewBufBinWriter() - require.NoError(t, chaindump.Dump(bc, w.BinWriter, 0, bc.BlockHeight()+1)) - require.NoError(t, w.Err) - - buf := w.Bytes() - t.Run("invalid start", func(t *testing.T) { - bc2, _, _ := chain.NewMultiWithCustomConfig(t, restoreF) - - r := io.NewBinReaderFromBuf(buf) - require.Error(t, chaindump.Restore(bc2, r, 2, 1, nil)) - }) - t.Run("good", func(t *testing.T) { - bc2, _, _ := chain.NewMultiWithCustomConfig(t, dumpF) - - r := io.NewBinReaderFromBuf(buf) - require.NoError(t, chaindump.Restore(bc2, r, 0, 2, nil)) - require.Equal(t, uint32(1), bc2.BlockHeight()) - - r = io.NewBinReaderFromBuf(buf) // new reader because start is relative to dump - require.NoError(t, chaindump.Restore(bc2, r, 2, 1, nil)) - t.Run("check handler", func(t *testing.T) { - lastIndex := uint32(0) - errStopped := errors.New("stopped") - f := func(b *block.Block) error { - lastIndex = b.Index - if b.Index >= bc.BlockHeight()-1 { - return errStopped - } - return nil - } - require.NoError(t, chaindump.Restore(bc2, r, 0, 1, f)) - require.Equal(t, bc2.BlockHeight(), lastIndex) - - r = io.NewBinReaderFromBuf(buf) - err := chaindump.Restore(bc2, r, 4, bc.BlockHeight()-bc2.BlockHeight(), f) - require.True(t, errors.Is(err, errStopped)) - require.Equal(t, bc.BlockHeight()-1, lastIndex) - }) - }) -} - func newLevelDBForTestingWithPath(t testing.TB, dbPath string) (storage.Store, string) { if dbPath == "" { dbPath = t.TempDir() diff --git a/pkg/core/chaindump/dump_test.go b/pkg/core/chaindump/dump_test.go new file mode 100644 index 000000000..5f5c8c49b --- /dev/null +++ b/pkg/core/chaindump/dump_test.go @@ -0,0 +1,92 @@ +package chaindump_test + +import ( + "errors" + "testing" + + "github.com/nspcc-dev/neo-go/internal/basicchain" + "github.com/nspcc-dev/neo-go/pkg/config" + "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/io" + "github.com/nspcc-dev/neo-go/pkg/neotest" + "github.com/nspcc-dev/neo-go/pkg/neotest/chain" + "github.com/stretchr/testify/require" +) + +func TestBlockchain_DumpAndRestore(t *testing.T) { + t.Run("no state root", func(t *testing.T) { + testDumpAndRestore(t, func(c *config.ProtocolConfiguration) { + c.StateRootInHeader = false + c.P2PSigExtensions = true + }, nil) + }) + t.Run("with state root", func(t *testing.T) { + testDumpAndRestore(t, func(c *config.ProtocolConfiguration) { + c.StateRootInHeader = true + c.P2PSigExtensions = true + }, nil) + }) + t.Run("remove untraceable", func(t *testing.T) { + // Dump can only be created if all blocks and transactions are present. + testDumpAndRestore(t, func(c *config.ProtocolConfiguration) { + c.P2PSigExtensions = true + }, func(c *config.ProtocolConfiguration) { + c.MaxTraceableBlocks = 2 + c.RemoveUntraceableBlocks = true + c.P2PSigExtensions = true + }) + }) +} + +func testDumpAndRestore(t *testing.T, dumpF, restoreF func(c *config.ProtocolConfiguration)) { + if restoreF == nil { + restoreF = dumpF + } + + bc, validators, committee := chain.NewMultiWithCustomConfig(t, dumpF) + e := neotest.NewExecutor(t, bc, validators, committee) + + basicchain.Init(t, "../../../", e) + require.True(t, bc.BlockHeight() > 5) // ensure that test is valid + + w := io.NewBufBinWriter() + require.NoError(t, chaindump.Dump(bc, w.BinWriter, 0, bc.BlockHeight()+1)) + require.NoError(t, w.Err) + + buf := w.Bytes() + t.Run("invalid start", func(t *testing.T) { + bc2, _, _ := chain.NewMultiWithCustomConfig(t, restoreF) + + r := io.NewBinReaderFromBuf(buf) + require.Error(t, chaindump.Restore(bc2, r, 2, 1, nil)) + }) + t.Run("good", func(t *testing.T) { + bc2, _, _ := chain.NewMultiWithCustomConfig(t, dumpF) + + r := io.NewBinReaderFromBuf(buf) + require.NoError(t, chaindump.Restore(bc2, r, 0, 2, nil)) + require.Equal(t, uint32(1), bc2.BlockHeight()) + + r = io.NewBinReaderFromBuf(buf) // new reader because start is relative to dump + require.NoError(t, chaindump.Restore(bc2, r, 2, 1, nil)) + t.Run("check handler", func(t *testing.T) { + lastIndex := uint32(0) + errStopped := errors.New("stopped") + f := func(b *block.Block) error { + lastIndex = b.Index + if b.Index >= bc.BlockHeight()-1 { + return errStopped + } + return nil + } + require.NoError(t, chaindump.Restore(bc2, r, 0, 1, f)) + require.Equal(t, bc2.BlockHeight(), lastIndex) + + r = io.NewBinReaderFromBuf(buf) + err := chaindump.Restore(bc2, r, 4, bc.BlockHeight()-bc2.BlockHeight(), f) + require.True(t, errors.Is(err, errStopped)) + require.Equal(t, bc.BlockHeight()-1, lastIndex) + }) + }) +} From 209b977e9abdc790f0ce3927e00c3b7dac3317c6 Mon Sep 17 00:00:00 2001 From: Roman Khimov Date: Wed, 8 Jun 2022 16:02:07 +0300 Subject: [PATCH 03/21] core: move contract-related interop code into appropriate package And move one of the tests with it. --- pkg/core/interop/contract/account.go | 71 ++++++++++++++++++++++++++ pkg/core/interop/contract/call.go | 7 +++ pkg/core/interop/contract/call_test.go | 25 +++++++++ pkg/core/interop_system.go | 70 ------------------------- pkg/core/interop_system_core_test.go | 8 --- pkg/core/interops.go | 6 +-- 6 files changed, 106 insertions(+), 81 deletions(-) create mode 100644 pkg/core/interop/contract/account.go create mode 100644 pkg/core/interop/contract/call_test.go diff --git a/pkg/core/interop/contract/account.go b/pkg/core/interop/contract/account.go new file mode 100644 index 000000000..b87b294ec --- /dev/null +++ b/pkg/core/interop/contract/account.go @@ -0,0 +1,71 @@ +package contract + +import ( + "crypto/elliptic" + "errors" + "math" + + "github.com/nspcc-dev/neo-go/pkg/config" + "github.com/nspcc-dev/neo-go/pkg/core/fee" + "github.com/nspcc-dev/neo-go/pkg/core/interop" + "github.com/nspcc-dev/neo-go/pkg/crypto/hash" + "github.com/nspcc-dev/neo-go/pkg/crypto/keys" + "github.com/nspcc-dev/neo-go/pkg/smartcontract" + "github.com/nspcc-dev/neo-go/pkg/vm/stackitem" +) + +// CreateMultisigAccount calculates multisig contract scripthash for a +// given m and a set of public keys. +func CreateMultisigAccount(ic *interop.Context) error { + m := ic.VM.Estack().Pop().BigInt() + mu64 := m.Uint64() + if !m.IsUint64() || mu64 > math.MaxInt32 { + return errors.New("m must be positive and fit int32") + } + arr := ic.VM.Estack().Pop().Array() + pubs := make(keys.PublicKeys, len(arr)) + for i, pk := range arr { + p, err := keys.NewPublicKeyFromBytes(pk.Value().([]byte), elliptic.P256()) + if err != nil { + return err + } + pubs[i] = p + } + var invokeFee int64 + if ic.IsHardforkEnabled(config.HFAspidochelone) { + invokeFee = fee.ECDSAVerifyPrice * int64(len(pubs)) + } else { + invokeFee = 1 << 8 + } + invokeFee *= ic.BaseExecFee() + if !ic.VM.AddGas(invokeFee) { + return errors.New("gas limit exceeded") + } + script, err := smartcontract.CreateMultiSigRedeemScript(int(mu64), pubs) + if err != nil { + return err + } + ic.VM.Estack().PushItem(stackitem.NewByteArray(hash.Hash160(script).BytesBE())) + return nil +} + +// CreateStandardAccount calculates contract scripthash for a given public key. +func CreateStandardAccount(ic *interop.Context) error { + h := ic.VM.Estack().Pop().Bytes() + p, err := keys.NewPublicKeyFromBytes(h, elliptic.P256()) + if err != nil { + return err + } + var invokeFee int64 + if ic.IsHardforkEnabled(config.HFAspidochelone) { + invokeFee = fee.ECDSAVerifyPrice + } else { + invokeFee = 1 << 8 + } + invokeFee *= ic.BaseExecFee() + if !ic.VM.AddGas(invokeFee) { + return errors.New("gas limit exceeded") + } + ic.VM.Estack().PushItem(stackitem.NewByteArray(p.GetScriptHash().BytesBE())) + return nil +} diff --git a/pkg/core/interop/contract/call.go b/pkg/core/interop/contract/call.go index 26a23aaa1..7bed15e45 100644 --- a/pkg/core/interop/contract/call.go +++ b/pkg/core/interop/contract/call.go @@ -3,6 +3,7 @@ package contract import ( "errors" "fmt" + "math/big" "strings" "github.com/nspcc-dev/neo-go/pkg/core/dao" @@ -172,3 +173,9 @@ func CallFromNative(ic *interop.Context, caller util.Uint160, cs *state.Contract } return nil } + +// GetCallFlags returns current context calling flags. +func GetCallFlags(ic *interop.Context) error { + ic.VM.Estack().PushItem(stackitem.NewBigInteger(big.NewInt(int64(ic.VM.Context().GetCallFlags())))) + return nil +} diff --git a/pkg/core/interop/contract/call_test.go b/pkg/core/interop/contract/call_test.go new file mode 100644 index 000000000..155a9cb90 --- /dev/null +++ b/pkg/core/interop/contract/call_test.go @@ -0,0 +1,25 @@ +package contract_test + +import ( + "math/big" + "testing" + + "github.com/nspcc-dev/neo-go/pkg/core/block" + "github.com/nspcc-dev/neo-go/pkg/core/interop/contract" + "github.com/nspcc-dev/neo-go/pkg/core/transaction" + "github.com/nspcc-dev/neo-go/pkg/neotest/chain" + "github.com/nspcc-dev/neo-go/pkg/smartcontract/callflag" + "github.com/nspcc-dev/neo-go/pkg/smartcontract/trigger" + "github.com/nspcc-dev/neo-go/pkg/util" + "github.com/nspcc-dev/neo-go/pkg/vm/opcode" + "github.com/stretchr/testify/require" +) + +func TestGetCallFlags(t *testing.T) { + bc, _ := chain.NewSingle(t) + ic := bc.GetTestVM(trigger.Application, &transaction.Transaction{}, &block.Block{}) + + ic.VM.LoadScriptWithHash([]byte{byte(opcode.RET)}, util.Uint160{1, 2, 3}, callflag.All) + require.NoError(t, contract.GetCallFlags(ic)) + require.Equal(t, int64(callflag.All), ic.VM.Estack().Pop().Value().(*big.Int).Int64()) +} diff --git a/pkg/core/interop_system.go b/pkg/core/interop_system.go index 96d9b0a09..c7c9731a2 100644 --- a/pkg/core/interop_system.go +++ b/pkg/core/interop_system.go @@ -2,23 +2,15 @@ package core import ( "context" - "crypto/elliptic" "errors" "fmt" - "math" - "math/big" - "github.com/nspcc-dev/neo-go/pkg/config" "github.com/nspcc-dev/neo-go/pkg/core/block" - "github.com/nspcc-dev/neo-go/pkg/core/fee" "github.com/nspcc-dev/neo-go/pkg/core/interop" istorage "github.com/nspcc-dev/neo-go/pkg/core/interop/storage" "github.com/nspcc-dev/neo-go/pkg/core/native" "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/crypto/hash" - "github.com/nspcc-dev/neo-go/pkg/crypto/keys" - "github.com/nspcc-dev/neo-go/pkg/smartcontract" "github.com/nspcc-dev/neo-go/pkg/vm/stackitem" ) @@ -206,65 +198,3 @@ func storageFind(ic *interop.Context) error { return nil } - -// contractCreateMultisigAccount calculates multisig contract scripthash for a -// given m and a set of public keys. -func contractCreateMultisigAccount(ic *interop.Context) error { - m := ic.VM.Estack().Pop().BigInt() - mu64 := m.Uint64() - if !m.IsUint64() || mu64 > math.MaxInt32 { - return errors.New("m must be positive and fit int32") - } - arr := ic.VM.Estack().Pop().Array() - pubs := make(keys.PublicKeys, len(arr)) - for i, pk := range arr { - p, err := keys.NewPublicKeyFromBytes(pk.Value().([]byte), elliptic.P256()) - if err != nil { - return err - } - pubs[i] = p - } - var invokeFee int64 - if ic.IsHardforkEnabled(config.HFAspidochelone) { - invokeFee = fee.ECDSAVerifyPrice * int64(len(pubs)) - } else { - invokeFee = 1 << 8 - } - invokeFee *= ic.BaseExecFee() - if !ic.VM.AddGas(invokeFee) { - return errors.New("gas limit exceeded") - } - script, err := smartcontract.CreateMultiSigRedeemScript(int(mu64), pubs) - if err != nil { - return err - } - ic.VM.Estack().PushItem(stackitem.NewByteArray(hash.Hash160(script).BytesBE())) - return nil -} - -// contractCreateStandardAccount calculates contract scripthash for a given public key. -func contractCreateStandardAccount(ic *interop.Context) error { - h := ic.VM.Estack().Pop().Bytes() - p, err := keys.NewPublicKeyFromBytes(h, elliptic.P256()) - if err != nil { - return err - } - var invokeFee int64 - if ic.IsHardforkEnabled(config.HFAspidochelone) { - invokeFee = fee.ECDSAVerifyPrice - } else { - invokeFee = 1 << 8 - } - invokeFee *= ic.BaseExecFee() - if !ic.VM.AddGas(invokeFee) { - return errors.New("gas limit exceeded") - } - ic.VM.Estack().PushItem(stackitem.NewByteArray(p.GetScriptHash().BytesBE())) - return nil -} - -// contractGetCallFlags returns current context calling flags. -func contractGetCallFlags(ic *interop.Context) error { - ic.VM.Estack().PushItem(stackitem.NewBigInteger(big.NewInt(int64(ic.VM.Context().GetCallFlags())))) - return nil -} diff --git a/pkg/core/interop_system_core_test.go b/pkg/core/interop_system_core_test.go index e0211922f..09e38c788 100644 --- a/pkg/core/interop_system_core_test.go +++ b/pkg/core/interop_system_core_test.go @@ -706,14 +706,6 @@ func TestContractCall(t *testing.T) { }) } -func TestContractGetCallFlags(t *testing.T) { - v, ic, _ := createVM(t) - - v.LoadScriptWithHash([]byte{byte(opcode.RET)}, util.Uint160{1, 2, 3}, callflag.All) - require.NoError(t, contractGetCallFlags(ic)) - require.Equal(t, int64(callflag.All), v.Estack().Pop().Value().(*big.Int).Int64()) -} - func TestRuntimeCheckWitness(t *testing.T) { _, ic, bc := createVM(t) diff --git a/pkg/core/interops.go b/pkg/core/interops.go index cf2e3982b..7de730dff 100644 --- a/pkg/core/interops.go +++ b/pkg/core/interops.go @@ -33,9 +33,9 @@ var systemInterops = []interop.Function{ {Name: interopnames.SystemContractCall, Func: contract.Call, Price: 1 << 15, RequiredFlags: callflag.ReadStates | callflag.AllowCall, ParamCount: 4}, {Name: interopnames.SystemContractCallNative, Func: native.Call, Price: 0, ParamCount: 1}, - {Name: interopnames.SystemContractCreateMultisigAccount, Func: contractCreateMultisigAccount, Price: 0, ParamCount: 2}, - {Name: interopnames.SystemContractCreateStandardAccount, Func: contractCreateStandardAccount, Price: 0, ParamCount: 1}, - {Name: interopnames.SystemContractGetCallFlags, Func: contractGetCallFlags, Price: 1 << 10}, + {Name: interopnames.SystemContractCreateMultisigAccount, Func: contract.CreateMultisigAccount, Price: 0, ParamCount: 2}, + {Name: interopnames.SystemContractCreateStandardAccount, Func: contract.CreateStandardAccount, Price: 0, ParamCount: 1}, + {Name: interopnames.SystemContractGetCallFlags, Func: contract.GetCallFlags, Price: 1 << 10}, {Name: interopnames.SystemContractNativeOnPersist, Func: native.OnPersist, Price: 0, RequiredFlags: callflag.States}, {Name: interopnames.SystemContractNativePostPersist, Func: native.PostPersist, Price: 0, RequiredFlags: callflag.States}, {Name: interopnames.SystemCryptoCheckMultisig, Func: crypto.ECDSASecp256r1CheckMultisig, Price: 0, ParamCount: 2}, From bb021d077840cf81abd60172c1fd71941ac6b7ee Mon Sep 17 00:00:00 2001 From: Roman Khimov Date: Wed, 8 Jun 2022 16:41:28 +0300 Subject: [PATCH 04/21] native: unbind PutContractState from Management It doesn't need Management's state, ID can't really change. --- pkg/core/interop_system_core_test.go | 28 ++++++++++++++-------------- pkg/core/native/management.go | 22 +++++++++++----------- 2 files changed, 25 insertions(+), 25 deletions(-) diff --git a/pkg/core/interop_system_core_test.go b/pkg/core/interop_system_core_test.go index 09e38c788..4c9f3ae73 100644 --- a/pkg/core/interop_system_core_test.go +++ b/pkg/core/interop_system_core_test.go @@ -133,10 +133,10 @@ func TestRuntimeGetNotifications(t *testing.T) { } func TestRuntimeGetInvocationCounter(t *testing.T) { - v, ic, bc := createVM(t) + v, ic, _ := createVM(t) cs, _ := contracts.GetTestContractState(t, pathToInternalContracts, 4, 5, random.Uint160()) // sender and IDs are not important for the test - require.NoError(t, bc.contracts.Management.PutContractState(ic.DAO, cs)) + require.NoError(t, native.PutContractState(ic.DAO, cs)) ic.Invocations[hash.Hash160([]byte{2})] = 42 @@ -161,9 +161,9 @@ func TestRuntimeGetInvocationCounter(t *testing.T) { } func TestStoragePut(t *testing.T) { - _, cs, ic, bc := createVMAndContractState(t) + _, cs, ic, _ := createVMAndContractState(t) - require.NoError(t, bc.contracts.Management.PutContractState(ic.DAO, cs)) + require.NoError(t, native.PutContractState(ic.DAO, cs)) initVM := func(t *testing.T, key, value []byte, gas int64) { v := ic.SpawnVM() @@ -218,9 +218,9 @@ func TestStoragePut(t *testing.T) { } func TestStorageDelete(t *testing.T) { - v, cs, ic, bc := createVMAndContractState(t) + v, cs, ic, _ := createVMAndContractState(t) - require.NoError(t, bc.contracts.Management.PutContractState(ic.DAO, cs)) + require.NoError(t, native.PutContractState(ic.DAO, cs)) v.LoadScriptWithHash(cs.NEF.Script, cs.Hash, callflag.All) put := func(key, value string, flag int) { v.Estack().PushVal(value) @@ -254,7 +254,7 @@ func BenchmarkStorageFind(b *testing.B) { for count := 10; count <= 10000; count *= 10 { b.Run(fmt.Sprintf("%dElements", count), func(b *testing.B) { v, contractState, context, chain := createVMAndContractState(b) - require.NoError(b, chain.contracts.Management.PutContractState(chain.dao, contractState)) + require.NoError(b, native.PutContractState(chain.dao, contractState)) items := make(map[string]state.StorageItem) for i := 0; i < count; i++ { @@ -298,7 +298,7 @@ func BenchmarkStorageFindIteratorNext(b *testing.B) { for name, last := range cases { b.Run(name, func(b *testing.B) { v, contractState, context, chain := createVMAndContractState(b) - require.NoError(b, chain.contracts.Management.PutContractState(chain.dao, contractState)) + require.NoError(b, native.PutContractState(chain.dao, contractState)) items := make(map[string]state.StorageItem) for i := 0; i < count; i++ { @@ -381,7 +381,7 @@ func TestStorageFind(t *testing.T) { []byte{222}, } - require.NoError(t, chain.contracts.Management.PutContractState(chain.dao, contractState)) + require.NoError(t, native.PutContractState(chain.dao, contractState)) id := contractState.ID @@ -577,11 +577,11 @@ func loadScriptWithHashAndFlags(ic *interop.Context, script []byte, hash util.Ui } func TestContractCall(t *testing.T) { - _, ic, bc := createVM(t) + _, ic, _ := createVM(t) cs, currCs := contracts.GetTestContractState(t, pathToInternalContracts, 4, 5, random.Uint160()) // sender and IDs are not important for the test - require.NoError(t, bc.contracts.Management.PutContractState(ic.DAO, cs)) - require.NoError(t, bc.contracts.Management.PutContractState(ic.DAO, currCs)) + require.NoError(t, native.PutContractState(ic.DAO, cs)) + require.NoError(t, native.PutContractState(ic.DAO, currCs)) currScript := currCs.NEF.Script h := cs.Hash @@ -707,7 +707,7 @@ func TestContractCall(t *testing.T) { } func TestRuntimeCheckWitness(t *testing.T) { - _, ic, bc := createVM(t) + _, ic, _ := createVM(t) script := []byte{byte(opcode.RET)} scriptHash := hash.Hash160(script) @@ -878,7 +878,7 @@ func TestRuntimeCheckWitness(t *testing.T) { }, }, } - require.NoError(t, bc.contracts.Management.PutContractState(ic.DAO, contractState)) + require.NoError(t, native.PutContractState(ic.DAO, contractState)) loadScriptWithHashAndFlags(ic, contractScript, contractScriptHash, callflag.All) ic.Tx = tx check(t, ic, targetHash.BytesBE(), false, true) diff --git a/pkg/core/native/management.go b/pkg/core/native/management.go index eee25bef6..549289b8d 100644 --- a/pkg/core/native/management.go +++ b/pkg/core/native/management.go @@ -272,8 +272,8 @@ func (m *Management) deployWithData(ic *interop.Context, args []stackitem.Item) return contractToStack(newcontract) } -func (m *Management) markUpdated(d *dao.Simple, hash util.Uint160, cs *state.Contract) { - cache := d.GetRWCache(m.ID).(*ManagementCache) +func markUpdated(d *dao.Simple, hash util.Uint160, cs *state.Contract) { + cache := d.GetRWCache(ManagementContractID).(*ManagementCache) delete(cache.nep11, hash) delete(cache.nep17, hash) if cs == nil { @@ -314,7 +314,7 @@ func (m *Management) Deploy(d *dao.Simple, sender util.Uint160, neff *nef.File, Manifest: *manif, }, } - err = m.PutContractState(d, newcontract) + err = PutContractState(d, newcontract) if err != nil { return nil, err } @@ -378,7 +378,7 @@ func (m *Management) Update(d *dao.Simple, hash util.Uint160, neff *nef.File, ma return nil, err } contract.UpdateCounter++ - err = m.PutContractState(d, &contract) + err = PutContractState(d, &contract) if err != nil { return nil, err } @@ -412,7 +412,7 @@ func (m *Management) Destroy(d *dao.Simple, hash util.Uint160) error { return true }) m.Policy.blockAccountInternal(d, hash) - m.markUpdated(d, hash, nil) + markUpdated(d, hash, nil) return nil } @@ -489,7 +489,7 @@ func (m *Management) OnPersist(ic *interop.Context) error { if err := native.Initialize(ic); err != nil { return fmt.Errorf("initializing %s native contract: %w", md.Name, err) } - err := m.putContractState(ic.DAO, cs, false) // Perform cache update manually. + err := putContractState(ic.DAO, cs, false) // Perform cache update manually. if err != nil { return err } @@ -573,18 +573,18 @@ func (m *Management) Initialize(ic *interop.Context) error { } // PutContractState saves given contract state into given DAO. -func (m *Management) PutContractState(d *dao.Simple, cs *state.Contract) error { - return m.putContractState(d, cs, true) +func PutContractState(d *dao.Simple, cs *state.Contract) error { + return putContractState(d, cs, true) } // putContractState is an internal PutContractState representation. -func (m *Management) putContractState(d *dao.Simple, cs *state.Contract, updateCache bool) error { +func putContractState(d *dao.Simple, cs *state.Contract, updateCache bool) error { key := MakeContractKey(cs.Hash) - if err := putConvertibleToDAO(m.ID, d, key, cs); err != nil { + if err := putConvertibleToDAO(ManagementContractID, d, key, cs); err != nil { return err } if updateCache { - m.markUpdated(d, cs.Hash, cs) + markUpdated(d, cs.Hash, cs) } if cs.UpdateCounter != 0 { // Update. return nil From ff1545b7eb3992bc653b4c7b2b51c81db7d7cd86 Mon Sep 17 00:00:00 2001 From: Roman Khimov Date: Wed, 8 Jun 2022 16:52:30 +0300 Subject: [PATCH 05/21] core: move TestContractCall into the contract package It belongs there and now it can be moved. No functional changes. --- pkg/core/interop/contract/call_test.go | 157 +++++++++++++++++++++++++ pkg/core/interop_system_core_test.go | 140 ---------------------- 2 files changed, 157 insertions(+), 140 deletions(-) diff --git a/pkg/core/interop/contract/call_test.go b/pkg/core/interop/contract/call_test.go index 155a9cb90..474586b83 100644 --- a/pkg/core/interop/contract/call_test.go +++ b/pkg/core/interop/contract/call_test.go @@ -2,19 +2,27 @@ package contract_test import ( "math/big" + "path/filepath" "testing" + "github.com/nspcc-dev/neo-go/internal/contracts" + "github.com/nspcc-dev/neo-go/internal/random" "github.com/nspcc-dev/neo-go/pkg/core/block" + "github.com/nspcc-dev/neo-go/pkg/core/interop" "github.com/nspcc-dev/neo-go/pkg/core/interop/contract" + "github.com/nspcc-dev/neo-go/pkg/core/native" "github.com/nspcc-dev/neo-go/pkg/core/transaction" "github.com/nspcc-dev/neo-go/pkg/neotest/chain" "github.com/nspcc-dev/neo-go/pkg/smartcontract/callflag" "github.com/nspcc-dev/neo-go/pkg/smartcontract/trigger" "github.com/nspcc-dev/neo-go/pkg/util" "github.com/nspcc-dev/neo-go/pkg/vm/opcode" + "github.com/nspcc-dev/neo-go/pkg/vm/stackitem" "github.com/stretchr/testify/require" ) +var pathToInternalContracts = filepath.Join("..", "..", "..", "..", "internal", "contracts") + func TestGetCallFlags(t *testing.T) { bc, _ := chain.NewSingle(t) ic := bc.GetTestVM(trigger.Application, &transaction.Transaction{}, &block.Block{}) @@ -23,3 +31,152 @@ func TestGetCallFlags(t *testing.T) { require.NoError(t, contract.GetCallFlags(ic)) require.Equal(t, int64(callflag.All), ic.VM.Estack().Pop().Value().(*big.Int).Int64()) } + +func TestCall(t *testing.T) { + bc, _ := chain.NewSingle(t) + ic := bc.GetTestVM(trigger.Application, &transaction.Transaction{}, &block.Block{}) + + cs, currCs := contracts.GetTestContractState(t, pathToInternalContracts, 4, 5, random.Uint160()) // sender and IDs are not important for the test + require.NoError(t, native.PutContractState(ic.DAO, cs)) + require.NoError(t, native.PutContractState(ic.DAO, currCs)) + + currScript := currCs.NEF.Script + h := cs.Hash + + addArgs := stackitem.NewArray([]stackitem.Item{stackitem.Make(1), stackitem.Make(2)}) + t.Run("Good", func(t *testing.T) { + t.Run("2 arguments", func(t *testing.T) { + loadScript(ic, currScript, 42) + ic.VM.Estack().PushVal(addArgs) + ic.VM.Estack().PushVal(callflag.All) + ic.VM.Estack().PushVal("add") + ic.VM.Estack().PushVal(h.BytesBE()) + require.NoError(t, contract.Call(ic)) + require.NoError(t, ic.VM.Run()) + require.Equal(t, 2, ic.VM.Estack().Len()) + require.Equal(t, big.NewInt(3), ic.VM.Estack().Pop().Value()) + require.Equal(t, big.NewInt(42), ic.VM.Estack().Pop().Value()) + }) + t.Run("3 arguments", func(t *testing.T) { + loadScript(ic, currScript, 42) + ic.VM.Estack().PushVal(stackitem.NewArray( + append(addArgs.Value().([]stackitem.Item), stackitem.Make(3)))) + ic.VM.Estack().PushVal(callflag.All) + ic.VM.Estack().PushVal("add") + ic.VM.Estack().PushVal(h.BytesBE()) + require.NoError(t, contract.Call(ic)) + require.NoError(t, ic.VM.Run()) + require.Equal(t, 2, ic.VM.Estack().Len()) + require.Equal(t, big.NewInt(6), ic.VM.Estack().Pop().Value()) + require.Equal(t, big.NewInt(42), ic.VM.Estack().Pop().Value()) + }) + }) + + t.Run("CallExInvalidFlag", func(t *testing.T) { + loadScript(ic, currScript, 42) + ic.VM.Estack().PushVal(addArgs) + ic.VM.Estack().PushVal(byte(0xFF)) + ic.VM.Estack().PushVal("add") + ic.VM.Estack().PushVal(h.BytesBE()) + require.Error(t, contract.Call(ic)) + }) + + runInvalid := func(args ...interface{}) func(t *testing.T) { + return func(t *testing.T) { + loadScriptWithHashAndFlags(ic, currScript, h, callflag.All, 42) + for i := range args { + ic.VM.Estack().PushVal(args[i]) + } + // interops can both return error and panic, + // we don't care which kind of error has occurred + require.Panics(t, func() { + err := contract.Call(ic) + if err != nil { + panic(err) + } + }) + } + } + + t.Run("Invalid", func(t *testing.T) { + t.Run("Hash", runInvalid(addArgs, "add", h.BytesBE()[1:])) + t.Run("MissingHash", runInvalid(addArgs, "add", util.Uint160{}.BytesBE())) + t.Run("Method", runInvalid(addArgs, stackitem.NewInterop("add"), h.BytesBE())) + t.Run("MissingMethod", runInvalid(addArgs, "sub", h.BytesBE())) + t.Run("DisallowedMethod", runInvalid(stackitem.NewArray(nil), "ret7", h.BytesBE())) + t.Run("Arguments", runInvalid(1, "add", h.BytesBE())) + t.Run("NotEnoughArguments", runInvalid( + stackitem.NewArray([]stackitem.Item{stackitem.Make(1)}), "add", h.BytesBE())) + t.Run("TooMuchArguments", runInvalid( + stackitem.NewArray([]stackitem.Item{ + stackitem.Make(1), stackitem.Make(2), stackitem.Make(3), stackitem.Make(4)}), + "add", h.BytesBE())) + }) + + t.Run("ReturnValues", func(t *testing.T) { + t.Run("Many", func(t *testing.T) { + loadScript(ic, currScript, 42) + ic.VM.Estack().PushVal(stackitem.NewArray(nil)) + ic.VM.Estack().PushVal(callflag.All) + ic.VM.Estack().PushVal("invalidReturn") + ic.VM.Estack().PushVal(h.BytesBE()) + require.NoError(t, contract.Call(ic)) + require.Error(t, ic.VM.Run()) + }) + t.Run("Void", func(t *testing.T) { + loadScript(ic, currScript, 42) + ic.VM.Estack().PushVal(stackitem.NewArray(nil)) + ic.VM.Estack().PushVal(callflag.All) + ic.VM.Estack().PushVal("justReturn") + ic.VM.Estack().PushVal(h.BytesBE()) + require.NoError(t, contract.Call(ic)) + require.NoError(t, ic.VM.Run()) + require.Equal(t, 2, ic.VM.Estack().Len()) + require.Equal(t, stackitem.Null{}, ic.VM.Estack().Pop().Item()) + require.Equal(t, big.NewInt(42), ic.VM.Estack().Pop().Value()) + }) + }) + + t.Run("IsolatedStack", func(t *testing.T) { + loadScript(ic, currScript, 42) + ic.VM.Estack().PushVal(stackitem.NewArray(nil)) + ic.VM.Estack().PushVal(callflag.All) + ic.VM.Estack().PushVal("drop") + ic.VM.Estack().PushVal(h.BytesBE()) + require.NoError(t, contract.Call(ic)) + require.Error(t, ic.VM.Run()) + }) + + t.Run("CallInitialize", func(t *testing.T) { + t.Run("Directly", runInvalid(stackitem.NewArray([]stackitem.Item{}), "_initialize", h.BytesBE())) + + loadScript(ic, currScript, 42) + ic.VM.Estack().PushVal(stackitem.NewArray([]stackitem.Item{stackitem.Make(5)})) + ic.VM.Estack().PushVal(callflag.All) + ic.VM.Estack().PushVal("add3") + ic.VM.Estack().PushVal(h.BytesBE()) + require.NoError(t, contract.Call(ic)) + require.NoError(t, ic.VM.Run()) + require.Equal(t, 2, ic.VM.Estack().Len()) + require.Equal(t, big.NewInt(8), ic.VM.Estack().Pop().Value()) + require.Equal(t, big.NewInt(42), ic.VM.Estack().Pop().Value()) + }) +} + +func loadScript(ic *interop.Context, script []byte, args ...interface{}) { + ic.SpawnVM() + ic.VM.LoadScriptWithFlags(script, callflag.AllowCall) + for i := range args { + ic.VM.Estack().PushVal(args[i]) + } + ic.VM.GasLimit = -1 +} + +func loadScriptWithHashAndFlags(ic *interop.Context, script []byte, hash util.Uint160, f callflag.CallFlag, args ...interface{}) { + ic.SpawnVM() + ic.VM.LoadScriptWithHash(script, hash, f) + for i := range args { + ic.VM.Estack().PushVal(args[i]) + } + ic.VM.GasLimit = -1 +} diff --git a/pkg/core/interop_system_core_test.go b/pkg/core/interop_system_core_test.go index 4c9f3ae73..d2a08cf76 100644 --- a/pkg/core/interop_system_core_test.go +++ b/pkg/core/interop_system_core_test.go @@ -13,7 +13,6 @@ import ( "github.com/nspcc-dev/neo-go/pkg/config/netmode" "github.com/nspcc-dev/neo-go/pkg/core/block" "github.com/nspcc-dev/neo-go/pkg/core/interop" - "github.com/nspcc-dev/neo-go/pkg/core/interop/contract" "github.com/nspcc-dev/neo-go/pkg/core/interop/iterator" "github.com/nspcc-dev/neo-go/pkg/core/interop/runtime" istorage "github.com/nspcc-dev/neo-go/pkg/core/interop/storage" @@ -558,15 +557,6 @@ func createVMAndContractState(t testing.TB) (*vm.VM, *state.Contract, *interop.C return v, contractState, context, chain } -func loadScript(ic *interop.Context, script []byte, args ...interface{}) { - ic.SpawnVM() - ic.VM.LoadScriptWithFlags(script, callflag.AllowCall) - for i := range args { - ic.VM.Estack().PushVal(args[i]) - } - ic.VM.GasLimit = -1 -} - func loadScriptWithHashAndFlags(ic *interop.Context, script []byte, hash util.Uint160, f callflag.CallFlag, args ...interface{}) { ic.SpawnVM() ic.VM.LoadScriptWithHash(script, hash, f) @@ -576,136 +566,6 @@ func loadScriptWithHashAndFlags(ic *interop.Context, script []byte, hash util.Ui ic.VM.GasLimit = -1 } -func TestContractCall(t *testing.T) { - _, ic, _ := createVM(t) - - cs, currCs := contracts.GetTestContractState(t, pathToInternalContracts, 4, 5, random.Uint160()) // sender and IDs are not important for the test - require.NoError(t, native.PutContractState(ic.DAO, cs)) - require.NoError(t, native.PutContractState(ic.DAO, currCs)) - - currScript := currCs.NEF.Script - h := cs.Hash - - addArgs := stackitem.NewArray([]stackitem.Item{stackitem.Make(1), stackitem.Make(2)}) - t.Run("Good", func(t *testing.T) { - t.Run("2 arguments", func(t *testing.T) { - loadScript(ic, currScript, 42) - ic.VM.Estack().PushVal(addArgs) - ic.VM.Estack().PushVal(callflag.All) - ic.VM.Estack().PushVal("add") - ic.VM.Estack().PushVal(h.BytesBE()) - require.NoError(t, contract.Call(ic)) - require.NoError(t, ic.VM.Run()) - require.Equal(t, 2, ic.VM.Estack().Len()) - require.Equal(t, big.NewInt(3), ic.VM.Estack().Pop().Value()) - require.Equal(t, big.NewInt(42), ic.VM.Estack().Pop().Value()) - }) - t.Run("3 arguments", func(t *testing.T) { - loadScript(ic, currScript, 42) - ic.VM.Estack().PushVal(stackitem.NewArray( - append(addArgs.Value().([]stackitem.Item), stackitem.Make(3)))) - ic.VM.Estack().PushVal(callflag.All) - ic.VM.Estack().PushVal("add") - ic.VM.Estack().PushVal(h.BytesBE()) - require.NoError(t, contract.Call(ic)) - require.NoError(t, ic.VM.Run()) - require.Equal(t, 2, ic.VM.Estack().Len()) - require.Equal(t, big.NewInt(6), ic.VM.Estack().Pop().Value()) - require.Equal(t, big.NewInt(42), ic.VM.Estack().Pop().Value()) - }) - }) - - t.Run("CallExInvalidFlag", func(t *testing.T) { - loadScript(ic, currScript, 42) - ic.VM.Estack().PushVal(addArgs) - ic.VM.Estack().PushVal(byte(0xFF)) - ic.VM.Estack().PushVal("add") - ic.VM.Estack().PushVal(h.BytesBE()) - require.Error(t, contract.Call(ic)) - }) - - runInvalid := func(args ...interface{}) func(t *testing.T) { - return func(t *testing.T) { - loadScriptWithHashAndFlags(ic, currScript, h, callflag.All, 42) - for i := range args { - ic.VM.Estack().PushVal(args[i]) - } - // interops can both return error and panic, - // we don't care which kind of error has occurred - require.Panics(t, func() { - err := contract.Call(ic) - if err != nil { - panic(err) - } - }) - } - } - - t.Run("Invalid", func(t *testing.T) { - t.Run("Hash", runInvalid(addArgs, "add", h.BytesBE()[1:])) - t.Run("MissingHash", runInvalid(addArgs, "add", util.Uint160{}.BytesBE())) - t.Run("Method", runInvalid(addArgs, stackitem.NewInterop("add"), h.BytesBE())) - t.Run("MissingMethod", runInvalid(addArgs, "sub", h.BytesBE())) - t.Run("DisallowedMethod", runInvalid(stackitem.NewArray(nil), "ret7", h.BytesBE())) - t.Run("Arguments", runInvalid(1, "add", h.BytesBE())) - t.Run("NotEnoughArguments", runInvalid( - stackitem.NewArray([]stackitem.Item{stackitem.Make(1)}), "add", h.BytesBE())) - t.Run("TooMuchArguments", runInvalid( - stackitem.NewArray([]stackitem.Item{ - stackitem.Make(1), stackitem.Make(2), stackitem.Make(3), stackitem.Make(4)}), - "add", h.BytesBE())) - }) - - t.Run("ReturnValues", func(t *testing.T) { - t.Run("Many", func(t *testing.T) { - loadScript(ic, currScript, 42) - ic.VM.Estack().PushVal(stackitem.NewArray(nil)) - ic.VM.Estack().PushVal(callflag.All) - ic.VM.Estack().PushVal("invalidReturn") - ic.VM.Estack().PushVal(h.BytesBE()) - require.NoError(t, contract.Call(ic)) - require.Error(t, ic.VM.Run()) - }) - t.Run("Void", func(t *testing.T) { - loadScript(ic, currScript, 42) - ic.VM.Estack().PushVal(stackitem.NewArray(nil)) - ic.VM.Estack().PushVal(callflag.All) - ic.VM.Estack().PushVal("justReturn") - ic.VM.Estack().PushVal(h.BytesBE()) - require.NoError(t, contract.Call(ic)) - require.NoError(t, ic.VM.Run()) - require.Equal(t, 2, ic.VM.Estack().Len()) - require.Equal(t, stackitem.Null{}, ic.VM.Estack().Pop().Item()) - require.Equal(t, big.NewInt(42), ic.VM.Estack().Pop().Value()) - }) - }) - - t.Run("IsolatedStack", func(t *testing.T) { - loadScript(ic, currScript, 42) - ic.VM.Estack().PushVal(stackitem.NewArray(nil)) - ic.VM.Estack().PushVal(callflag.All) - ic.VM.Estack().PushVal("drop") - ic.VM.Estack().PushVal(h.BytesBE()) - require.NoError(t, contract.Call(ic)) - require.Error(t, ic.VM.Run()) - }) - - t.Run("CallInitialize", func(t *testing.T) { - t.Run("Directly", runInvalid(stackitem.NewArray([]stackitem.Item{}), "_initialize", h.BytesBE())) - - loadScript(ic, currScript, 42) - ic.VM.Estack().PushVal(stackitem.NewArray([]stackitem.Item{stackitem.Make(5)})) - ic.VM.Estack().PushVal(callflag.All) - ic.VM.Estack().PushVal("add3") - ic.VM.Estack().PushVal(h.BytesBE()) - require.NoError(t, contract.Call(ic)) - require.NoError(t, ic.VM.Run()) - require.Equal(t, 2, ic.VM.Estack().Len()) - require.Equal(t, big.NewInt(8), ic.VM.Estack().Pop().Value()) - require.Equal(t, big.NewInt(42), ic.VM.Estack().Pop().Value()) - }) -} - func TestRuntimeCheckWitness(t *testing.T) { _, ic, _ := createVM(t) From cdb55740ea116f89168e2f2d50d57426e4a21b6f Mon Sep 17 00:00:00 2001 From: Roman Khimov Date: Wed, 8 Jun 2022 16:56:06 +0300 Subject: [PATCH 06/21] core: move ContractCreate*Account tests into the contract pkg No functional changes. --- pkg/core/interop/contract/account_test.go | 115 ++++++++++++++++++++++ pkg/core/interop_system_neotest_test.go | 99 ------------------- 2 files changed, 115 insertions(+), 99 deletions(-) create mode 100644 pkg/core/interop/contract/account_test.go diff --git a/pkg/core/interop/contract/account_test.go b/pkg/core/interop/contract/account_test.go new file mode 100644 index 000000000..a4ad6a60a --- /dev/null +++ b/pkg/core/interop/contract/account_test.go @@ -0,0 +1,115 @@ +package contract_test + +import ( + "math" + "math/big" + "testing" + + "github.com/nspcc-dev/neo-go/pkg/core/interop/interopnames" + "github.com/nspcc-dev/neo-go/pkg/crypto/hash" + "github.com/nspcc-dev/neo-go/pkg/crypto/keys" + "github.com/nspcc-dev/neo-go/pkg/io" + "github.com/nspcc-dev/neo-go/pkg/neotest" + "github.com/nspcc-dev/neo-go/pkg/neotest/chain" + "github.com/nspcc-dev/neo-go/pkg/smartcontract" + "github.com/nspcc-dev/neo-go/pkg/util" + "github.com/nspcc-dev/neo-go/pkg/vm/emit" + "github.com/stretchr/testify/require" +) + +func TestCreateStandardAccount(t *testing.T) { + bc, acc := chain.NewSingle(t) + e := neotest.NewExecutor(t, bc, acc, acc) + w := io.NewBufBinWriter() + + t.Run("Good", func(t *testing.T) { + priv, err := keys.NewPrivateKey() + require.NoError(t, err) + pub := priv.PublicKey() + + emit.Bytes(w.BinWriter, pub.Bytes()) + emit.Syscall(w.BinWriter, interopnames.SystemContractCreateStandardAccount) + require.NoError(t, w.Err) + script := w.Bytes() + + tx := e.PrepareInvocation(t, script, []neotest.Signer{e.Validator}, bc.BlockHeight()+1) + e.AddNewBlock(t, tx) + e.CheckHalt(t, tx.Hash()) + + res := e.GetTxExecResult(t, tx.Hash()) + value := res.Stack[0].Value().([]byte) + u, err := util.Uint160DecodeBytesBE(value) + require.NoError(t, err) + require.Equal(t, pub.GetScriptHash(), u) + }) + t.Run("InvalidKey", func(t *testing.T) { + w.Reset() + emit.Bytes(w.BinWriter, []byte{1, 2, 3}) + emit.Syscall(w.BinWriter, interopnames.SystemContractCreateStandardAccount) + require.NoError(t, w.Err) + script := w.Bytes() + + tx := e.PrepareInvocation(t, script, []neotest.Signer{e.Validator}, bc.BlockHeight()+1) + e.AddNewBlock(t, tx) + e.CheckFault(t, tx.Hash(), "invalid prefix 1") + }) +} + +func TestCreateMultisigAccount(t *testing.T) { + bc, acc := chain.NewSingle(t) + e := neotest.NewExecutor(t, bc, acc, acc) + w := io.NewBufBinWriter() + + createScript := func(t *testing.T, pubs []interface{}, m int) []byte { + w.Reset() + emit.Array(w.BinWriter, pubs...) + emit.Int(w.BinWriter, int64(m)) + emit.Syscall(w.BinWriter, interopnames.SystemContractCreateMultisigAccount) + require.NoError(t, w.Err) + return w.Bytes() + } + t.Run("Good", func(t *testing.T) { + m, n := 3, 5 + pubs := make(keys.PublicKeys, n) + arr := make([]interface{}, n) + for i := range pubs { + pk, err := keys.NewPrivateKey() + require.NoError(t, err) + pubs[i] = pk.PublicKey() + arr[i] = pubs[i].Bytes() + } + script := createScript(t, arr, m) + + txH := e.InvokeScript(t, script, []neotest.Signer{acc}) + e.CheckHalt(t, txH) + res := e.GetTxExecResult(t, txH) + value := res.Stack[0].Value().([]byte) + u, err := util.Uint160DecodeBytesBE(value) + require.NoError(t, err) + expected, err := smartcontract.CreateMultiSigRedeemScript(m, pubs) + require.NoError(t, err) + require.Equal(t, hash.Hash160(expected), u) + }) + t.Run("InvalidKey", func(t *testing.T) { + script := createScript(t, []interface{}{[]byte{1, 2, 3}}, 1) + e.InvokeScriptCheckFAULT(t, script, []neotest.Signer{acc}, "invalid prefix 1") + }) + t.Run("Invalid m", func(t *testing.T) { + pk, err := keys.NewPrivateKey() + require.NoError(t, err) + script := createScript(t, []interface{}{pk.PublicKey().Bytes()}, 2) + e.InvokeScriptCheckFAULT(t, script, []neotest.Signer{acc}, "length of the signatures (2) is higher then the number of public keys") + }) + t.Run("m overflows int32", func(t *testing.T) { + pk, err := keys.NewPrivateKey() + require.NoError(t, err) + m := big.NewInt(math.MaxInt32) + m.Add(m, big.NewInt(1)) + w.Reset() + emit.Array(w.BinWriter, pk.Bytes()) + emit.BigInt(w.BinWriter, m) + emit.Syscall(w.BinWriter, interopnames.SystemContractCreateMultisigAccount) + require.NoError(t, w.Err) + e.InvokeScriptCheckFAULT(t, w.Bytes(), []neotest.Signer{acc}, "m must be positive and fit int32") + }) +} diff --git a/pkg/core/interop_system_neotest_test.go b/pkg/core/interop_system_neotest_test.go index 99fcee560..9047aa1d2 100644 --- a/pkg/core/interop_system_neotest_test.go +++ b/pkg/core/interop_system_neotest_test.go @@ -15,13 +15,11 @@ import ( "github.com/nspcc-dev/neo-go/pkg/core/interop/interopnames" "github.com/nspcc-dev/neo-go/pkg/core/native/nativenames" "github.com/nspcc-dev/neo-go/pkg/core/transaction" - "github.com/nspcc-dev/neo-go/pkg/crypto/hash" "github.com/nspcc-dev/neo-go/pkg/crypto/keys" "github.com/nspcc-dev/neo-go/pkg/encoding/address" "github.com/nspcc-dev/neo-go/pkg/io" "github.com/nspcc-dev/neo-go/pkg/neotest" "github.com/nspcc-dev/neo-go/pkg/neotest/chain" - "github.com/nspcc-dev/neo-go/pkg/smartcontract" "github.com/nspcc-dev/neo-go/pkg/smartcontract/manifest" "github.com/nspcc-dev/neo-go/pkg/util" "github.com/nspcc-dev/neo-go/pkg/util/slice" @@ -55,103 +53,6 @@ func TestSystemRuntimeGetRandom_DifferentTransactions(t *testing.T) { require.NotEqual(t, r1, r2) } -func TestSystemContractCreateStandardAccount(t *testing.T) { - bc, acc := chain.NewSingle(t) - e := neotest.NewExecutor(t, bc, acc, acc) - w := io.NewBufBinWriter() - - t.Run("Good", func(t *testing.T) { - priv, err := keys.NewPrivateKey() - require.NoError(t, err) - pub := priv.PublicKey() - - emit.Bytes(w.BinWriter, pub.Bytes()) - emit.Syscall(w.BinWriter, interopnames.SystemContractCreateStandardAccount) - require.NoError(t, w.Err) - script := w.Bytes() - - tx := e.PrepareInvocation(t, script, []neotest.Signer{e.Validator}, bc.BlockHeight()+1) - e.AddNewBlock(t, tx) - e.CheckHalt(t, tx.Hash()) - - res := e.GetTxExecResult(t, tx.Hash()) - value := res.Stack[0].Value().([]byte) - u, err := util.Uint160DecodeBytesBE(value) - require.NoError(t, err) - require.Equal(t, pub.GetScriptHash(), u) - }) - t.Run("InvalidKey", func(t *testing.T) { - w.Reset() - emit.Bytes(w.BinWriter, []byte{1, 2, 3}) - emit.Syscall(w.BinWriter, interopnames.SystemContractCreateStandardAccount) - require.NoError(t, w.Err) - script := w.Bytes() - - tx := e.PrepareInvocation(t, script, []neotest.Signer{e.Validator}, bc.BlockHeight()+1) - e.AddNewBlock(t, tx) - e.CheckFault(t, tx.Hash(), "invalid prefix 1") - }) -} - -func TestSystemContractCreateMultisigAccount(t *testing.T) { - bc, acc := chain.NewSingle(t) - e := neotest.NewExecutor(t, bc, acc, acc) - w := io.NewBufBinWriter() - - createScript := func(t *testing.T, pubs []interface{}, m int) []byte { - w.Reset() - emit.Array(w.BinWriter, pubs...) - emit.Int(w.BinWriter, int64(m)) - emit.Syscall(w.BinWriter, interopnames.SystemContractCreateMultisigAccount) - require.NoError(t, w.Err) - return w.Bytes() - } - t.Run("Good", func(t *testing.T) { - m, n := 3, 5 - pubs := make(keys.PublicKeys, n) - arr := make([]interface{}, n) - for i := range pubs { - pk, err := keys.NewPrivateKey() - require.NoError(t, err) - pubs[i] = pk.PublicKey() - arr[i] = pubs[i].Bytes() - } - script := createScript(t, arr, m) - - txH := e.InvokeScript(t, script, []neotest.Signer{acc}) - e.CheckHalt(t, txH) - res := e.GetTxExecResult(t, txH) - value := res.Stack[0].Value().([]byte) - u, err := util.Uint160DecodeBytesBE(value) - require.NoError(t, err) - expected, err := smartcontract.CreateMultiSigRedeemScript(m, pubs) - require.NoError(t, err) - require.Equal(t, hash.Hash160(expected), u) - }) - t.Run("InvalidKey", func(t *testing.T) { - script := createScript(t, []interface{}{[]byte{1, 2, 3}}, 1) - e.InvokeScriptCheckFAULT(t, script, []neotest.Signer{acc}, "invalid prefix 1") - }) - t.Run("Invalid m", func(t *testing.T) { - pk, err := keys.NewPrivateKey() - require.NoError(t, err) - script := createScript(t, []interface{}{pk.PublicKey().Bytes()}, 2) - e.InvokeScriptCheckFAULT(t, script, []neotest.Signer{acc}, "length of the signatures (2) is higher then the number of public keys") - }) - t.Run("m overflows int32", func(t *testing.T) { - pk, err := keys.NewPrivateKey() - require.NoError(t, err) - m := big.NewInt(math.MaxInt32) - m.Add(m, big.NewInt(1)) - w.Reset() - emit.Array(w.BinWriter, pk.Bytes()) - emit.BigInt(w.BinWriter, m) - emit.Syscall(w.BinWriter, interopnames.SystemContractCreateMultisigAccount) - require.NoError(t, w.Err) - e.InvokeScriptCheckFAULT(t, w.Bytes(), []neotest.Signer{acc}, "m must be positive and fit int32") - }) -} - func TestSystemRuntimeGasLeft(t *testing.T) { const runtimeGasLeftPrice = 1 << 4 From d70caf1da12f35dad549b239bec4bb650a16f609 Mon Sep 17 00:00:00 2001 From: Roman Khimov Date: Wed, 8 Jun 2022 18:12:41 +0300 Subject: [PATCH 07/21] core: move GetScriptContainer to runtime It also brings ToStackItem to Block and Transaction, previously this was avoided to separate block and transaction packages from VM. But turns out `transaction` depends on `stackitem` already, so this makes little sense (but can be shuffled in another way if needed). Context.Container is still a hash.Hashable because we have a number of occasions (header or MPT root verification) where there is no ToStackItem implementation possible. Maybe they can go with `nil` Container, but I don't want to have this risk for now. --- pkg/core/block/block.go | 17 ++++++++++++++ pkg/core/interop/runtime/engine.go | 15 ++++++++++++ pkg/core/interop_system.go | 19 --------------- pkg/core/interops.go | 2 +- pkg/core/native/ledger.go | 36 +++-------------------------- pkg/core/transaction/transaction.go | 16 +++++++++++++ 6 files changed, 52 insertions(+), 53 deletions(-) diff --git a/pkg/core/block/block.go b/pkg/core/block/block.go index c9c50c70f..bdf3e0e57 100644 --- a/pkg/core/block/block.go +++ b/pkg/core/block/block.go @@ -4,11 +4,13 @@ import ( "encoding/json" "errors" "math" + "math/big" "github.com/nspcc-dev/neo-go/pkg/core/transaction" "github.com/nspcc-dev/neo-go/pkg/crypto/hash" "github.com/nspcc-dev/neo-go/pkg/io" "github.com/nspcc-dev/neo-go/pkg/util" + "github.com/nspcc-dev/neo-go/pkg/vm/stackitem" ) const ( @@ -211,3 +213,18 @@ func (b *Block) GetExpectedBlockSizeWithoutTransactions(txCount int) int { } return size } + +// ToStackItem converts Block to stackitem.Item. +func (b *Block) ToStackItem() stackitem.Item { + return stackitem.NewArray([]stackitem.Item{ + stackitem.NewByteArray(b.Hash().BytesBE()), + stackitem.NewBigInteger(big.NewInt(int64(b.Version))), + stackitem.NewByteArray(b.PrevHash.BytesBE()), + stackitem.NewByteArray(b.MerkleRoot.BytesBE()), + stackitem.NewBigInteger(big.NewInt(int64(b.Timestamp))), + stackitem.NewBigInteger(new(big.Int).SetUint64(b.Nonce)), + stackitem.NewBigInteger(big.NewInt(int64(b.Index))), + stackitem.NewByteArray(b.NextConsensus.BytesBE()), + stackitem.NewBigInteger(big.NewInt(int64(len(b.Transactions)))), + }) +} diff --git a/pkg/core/interop/runtime/engine.go b/pkg/core/interop/runtime/engine.go index ee7de946b..1223d878a 100644 --- a/pkg/core/interop/runtime/engine.go +++ b/pkg/core/interop/runtime/engine.go @@ -10,6 +10,10 @@ import ( "go.uber.org/zap" ) +type itemable interface { + ToStackItem() stackitem.Item +} + const ( // MaxEventNameLen is the maximum length of a name for event. MaxEventNameLen = 32 @@ -37,6 +41,17 @@ func GetEntryScriptHash(ic *interop.Context) error { return ic.VM.PushContextScriptHash(ic.VM.Istack().Len() - 1) } +// GetScriptContainer returns transaction or block that contains the script +// being run. +func GetScriptContainer(ic *interop.Context) error { + c, ok := ic.Container.(itemable) + if !ok { + return errors.New("unknown script container") + } + ic.VM.Estack().PushItem(c.ToStackItem()) + return nil +} + // Platform returns the name of the platform. func Platform(ic *interop.Context) error { ic.VM.Estack().PushItem(stackitem.NewByteArray([]byte("NEO"))) diff --git a/pkg/core/interop_system.go b/pkg/core/interop_system.go index c7c9731a2..b483fb16b 100644 --- a/pkg/core/interop_system.go +++ b/pkg/core/interop_system.go @@ -5,12 +5,9 @@ import ( "errors" "fmt" - "github.com/nspcc-dev/neo-go/pkg/core/block" "github.com/nspcc-dev/neo-go/pkg/core/interop" istorage "github.com/nspcc-dev/neo-go/pkg/core/interop/storage" - "github.com/nspcc-dev/neo-go/pkg/core/native" "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/vm/stackitem" ) @@ -26,22 +23,6 @@ type StorageContext struct { ReadOnly bool } -// engineGetScriptContainer returns transaction or block that contains the script -// being run. -func engineGetScriptContainer(ic *interop.Context) error { - var item stackitem.Item - switch t := ic.Container.(type) { - case *transaction.Transaction: - item = native.TransactionToStackItem(t) - case *block.Block: - item = native.BlockToStackItem(t) - default: - return errors.New("unknown script container") - } - ic.VM.Estack().PushItem(item) - return nil -} - // storageDelete deletes stored key-value pair. func storageDelete(ic *interop.Context) error { stcInterface := ic.VM.Estack().Pop().Value() diff --git a/pkg/core/interops.go b/pkg/core/interops.go index 7de730dff..50c8ee723 100644 --- a/pkg/core/interops.go +++ b/pkg/core/interops.go @@ -54,7 +54,7 @@ var systemInterops = []interop.Function{ {Name: interopnames.SystemRuntimeGetNetwork, Func: runtime.GetNetwork, Price: 1 << 3}, {Name: interopnames.SystemRuntimeGetNotifications, Func: runtime.GetNotifications, Price: 1 << 12, ParamCount: 1}, {Name: interopnames.SystemRuntimeGetRandom, Func: runtime.GetRandom, Price: 0}, - {Name: interopnames.SystemRuntimeGetScriptContainer, Func: engineGetScriptContainer, Price: 1 << 3}, + {Name: interopnames.SystemRuntimeGetScriptContainer, Func: runtime.GetScriptContainer, Price: 1 << 3}, {Name: interopnames.SystemRuntimeGetTime, Func: runtime.GetTime, Price: 1 << 3, RequiredFlags: callflag.ReadStates}, {Name: interopnames.SystemRuntimeGetTrigger, Func: runtime.GetTrigger, Price: 1 << 3}, {Name: interopnames.SystemRuntimeLog, Func: runtime.Log, Price: 1 << 15, RequiredFlags: callflag.AllowNotify, diff --git a/pkg/core/native/ledger.go b/pkg/core/native/ledger.go index c3962c311..e75f476eb 100644 --- a/pkg/core/native/ledger.go +++ b/pkg/core/native/ledger.go @@ -5,7 +5,6 @@ import ( "math" "math/big" - "github.com/nspcc-dev/neo-go/pkg/core/block" "github.com/nspcc-dev/neo-go/pkg/core/dao" "github.com/nspcc-dev/neo-go/pkg/core/interop" "github.com/nspcc-dev/neo-go/pkg/core/native/nativenames" @@ -117,7 +116,7 @@ func (l *Ledger) getBlock(ic *interop.Context, params []stackitem.Item) stackite if err != nil || !isTraceableBlock(ic, block.Index) { return stackitem.Null{} } - return BlockToStackItem(block) + return block.ToStackItem() } // getTransaction returns transaction to the SC. @@ -126,7 +125,7 @@ func (l *Ledger) getTransaction(ic *interop.Context, params []stackitem.Item) st if err != nil || !isTraceableBlock(ic, h) { return stackitem.Null{} } - return TransactionToStackItem(tx) + return tx.ToStackItem() } // getTransactionHeight returns transaction height to the SC. @@ -150,7 +149,7 @@ func (l *Ledger) getTransactionFromBlock(ic *interop.Context, params []stackitem if index >= uint32(len(block.Transactions)) { panic("wrong transaction index") } - return TransactionToStackItem(block.Transactions[index]) + return block.Transactions[index].ToStackItem() } // getTransactionSigners returns transaction signers to the SC. @@ -228,35 +227,6 @@ func getTransactionAndHeight(d *dao.Simple, item stackitem.Item) (*transaction.T return d.GetTransaction(hash) } -// BlockToStackItem converts block.Block to stackitem.Item. -func BlockToStackItem(b *block.Block) stackitem.Item { - return stackitem.NewArray([]stackitem.Item{ - stackitem.NewByteArray(b.Hash().BytesBE()), - stackitem.NewBigInteger(big.NewInt(int64(b.Version))), - stackitem.NewByteArray(b.PrevHash.BytesBE()), - stackitem.NewByteArray(b.MerkleRoot.BytesBE()), - stackitem.NewBigInteger(big.NewInt(int64(b.Timestamp))), - stackitem.NewBigInteger(new(big.Int).SetUint64(b.Nonce)), - stackitem.NewBigInteger(big.NewInt(int64(b.Index))), - stackitem.NewByteArray(b.NextConsensus.BytesBE()), - stackitem.NewBigInteger(big.NewInt(int64(len(b.Transactions)))), - }) -} - -// TransactionToStackItem converts transaction.Transaction to stackitem.Item. -func TransactionToStackItem(t *transaction.Transaction) stackitem.Item { - return stackitem.NewArray([]stackitem.Item{ - stackitem.NewByteArray(t.Hash().BytesBE()), - stackitem.NewBigInteger(big.NewInt(int64(t.Version))), - stackitem.NewBigInteger(big.NewInt(int64(t.Nonce))), - stackitem.NewByteArray(t.Sender().BytesBE()), - stackitem.NewBigInteger(big.NewInt(int64(t.SystemFee))), - stackitem.NewBigInteger(big.NewInt(int64(t.NetworkFee))), - stackitem.NewBigInteger(big.NewInt(int64(t.ValidUntilBlock))), - stackitem.NewByteArray(t.Script), - }) -} - // SignersToStackItem converts transaction.Signers to stackitem.Item. func SignersToStackItem(signers []transaction.Signer) stackitem.Item { res := make([]stackitem.Item, len(signers)) diff --git a/pkg/core/transaction/transaction.go b/pkg/core/transaction/transaction.go index eea2807ea..57047f8fe 100644 --- a/pkg/core/transaction/transaction.go +++ b/pkg/core/transaction/transaction.go @@ -6,12 +6,14 @@ import ( "errors" "fmt" "math" + "math/big" "math/rand" "github.com/nspcc-dev/neo-go/pkg/crypto/hash" "github.com/nspcc-dev/neo-go/pkg/encoding/address" "github.com/nspcc-dev/neo-go/pkg/io" "github.com/nspcc-dev/neo-go/pkg/util" + "github.com/nspcc-dev/neo-go/pkg/vm/stackitem" ) const ( @@ -451,3 +453,17 @@ func (t *Transaction) HasSigner(hash util.Uint160) bool { } return false } + +// ToStackItem converts Transaction to stackitem.Item. +func (t *Transaction) ToStackItem() stackitem.Item { + return stackitem.NewArray([]stackitem.Item{ + stackitem.NewByteArray(t.Hash().BytesBE()), + stackitem.NewBigInteger(big.NewInt(int64(t.Version))), + stackitem.NewBigInteger(big.NewInt(int64(t.Nonce))), + stackitem.NewByteArray(t.Sender().BytesBE()), + stackitem.NewBigInteger(big.NewInt(int64(t.SystemFee))), + stackitem.NewBigInteger(big.NewInt(int64(t.NetworkFee))), + stackitem.NewBigInteger(big.NewInt(int64(t.ValidUntilBlock))), + stackitem.NewByteArray(t.Script), + }) +} From 0055b18a8af90911f14a1afa4f21aee102d43876 Mon Sep 17 00:00:00 2001 From: Roman Khimov Date: Wed, 8 Jun 2022 18:20:34 +0300 Subject: [PATCH 08/21] core: export CreateGenesisBlock Nothing bad with it being public. --- pkg/core/blockchain.go | 4 ++-- pkg/core/interop_system_core_test.go | 2 +- pkg/core/util.go | 4 ++-- pkg/core/util_test.go | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/pkg/core/blockchain.go b/pkg/core/blockchain.go index 694d99b33..bd2a7d318 100644 --- a/pkg/core/blockchain.go +++ b/pkg/core/blockchain.go @@ -324,7 +324,7 @@ func (bc *Blockchain) init() error { bc.dao.PutVersion(ver) bc.dao.Version = ver bc.persistent.Version = ver - genesisBlock, err := createGenesisBlock(bc.config) + genesisBlock, err := CreateGenesisBlock(bc.config) if err != nil { return err } @@ -386,7 +386,7 @@ func (bc *Blockchain) init() error { if len(bc.headerHashes) > 0 { targetHash = bc.headerHashes[len(bc.headerHashes)-1] } else { - genesisBlock, err := createGenesisBlock(bc.config) + genesisBlock, err := CreateGenesisBlock(bc.config) if err != nil { return err } diff --git a/pkg/core/interop_system_core_test.go b/pkg/core/interop_system_core_test.go index d2a08cf76..d9a4b98b6 100644 --- a/pkg/core/interop_system_core_test.go +++ b/pkg/core/interop_system_core_test.go @@ -84,7 +84,7 @@ func getSharpTestGenesis(t *testing.T) *block.Block { cfg, err := config.Load(configPath, netmode.MainNet) require.NoError(t, err) - b, err := createGenesisBlock(cfg.ProtocolConfiguration) + b, err := CreateGenesisBlock(cfg.ProtocolConfiguration) require.NoError(t, err) return b } diff --git a/pkg/core/util.go b/pkg/core/util.go index 308a75151..8ba894c52 100644 --- a/pkg/core/util.go +++ b/pkg/core/util.go @@ -13,8 +13,8 @@ import ( "github.com/nspcc-dev/neo-go/pkg/vm/opcode" ) -// createGenesisBlock creates a genesis block based on the given configuration. -func createGenesisBlock(cfg config.ProtocolConfiguration) (*block.Block, error) { +// CreateGenesisBlock creates a genesis block based on the given configuration. +func CreateGenesisBlock(cfg config.ProtocolConfiguration) (*block.Block, error) { validators, err := validatorsFromConfig(cfg) if err != nil { return nil, err diff --git a/pkg/core/util_test.go b/pkg/core/util_test.go index 0d6215604..7925db1a8 100644 --- a/pkg/core/util_test.go +++ b/pkg/core/util_test.go @@ -14,7 +14,7 @@ func TestGenesisBlockMainNet(t *testing.T) { cfg, err := config.Load("../../config", netmode.MainNet) require.NoError(t, err) - block, err := createGenesisBlock(cfg.ProtocolConfiguration) + block, err := CreateGenesisBlock(cfg.ProtocolConfiguration) require.NoError(t, err) expect := "1f4d1defa46faa5e7b9b8d3f79a06bec777d7c26c4aa5f6f5899a291daa87c15" From 2127cc414690b82e54d292af318ffcdd9316cefc Mon Sep 17 00:00:00 2001 From: Roman Khimov Date: Wed, 8 Jun 2022 18:46:49 +0300 Subject: [PATCH 09/21] core: move Runtime tests to runtime package --- pkg/core/interop/runtime/ext_test.go | 540 ++++++++++++++++++++++++ pkg/core/interop_system_core_test.go | 395 ----------------- pkg/core/interop_system_neotest_test.go | 110 ----- 3 files changed, 540 insertions(+), 505 deletions(-) create mode 100644 pkg/core/interop/runtime/ext_test.go diff --git a/pkg/core/interop/runtime/ext_test.go b/pkg/core/interop/runtime/ext_test.go new file mode 100644 index 000000000..bc262fe4a --- /dev/null +++ b/pkg/core/interop/runtime/ext_test.go @@ -0,0 +1,540 @@ +package runtime_test + +import ( + "encoding/json" + "math" + "math/big" + "path/filepath" + "testing" + + "github.com/nspcc-dev/neo-go/internal/contracts" + "github.com/nspcc-dev/neo-go/internal/random" + "github.com/nspcc-dev/neo-go/pkg/config" + "github.com/nspcc-dev/neo-go/pkg/config/netmode" + "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/interop" + "github.com/nspcc-dev/neo-go/pkg/core/interop/interopnames" + "github.com/nspcc-dev/neo-go/pkg/core/interop/runtime" + "github.com/nspcc-dev/neo-go/pkg/core/native" + "github.com/nspcc-dev/neo-go/pkg/core/native/nativenames" + "github.com/nspcc-dev/neo-go/pkg/core/state" + "github.com/nspcc-dev/neo-go/pkg/core/transaction" + "github.com/nspcc-dev/neo-go/pkg/crypto/hash" + "github.com/nspcc-dev/neo-go/pkg/crypto/keys" + "github.com/nspcc-dev/neo-go/pkg/encoding/address" + "github.com/nspcc-dev/neo-go/pkg/io" + "github.com/nspcc-dev/neo-go/pkg/neotest" + "github.com/nspcc-dev/neo-go/pkg/neotest/chain" + "github.com/nspcc-dev/neo-go/pkg/smartcontract/callflag" + "github.com/nspcc-dev/neo-go/pkg/smartcontract/manifest" + "github.com/nspcc-dev/neo-go/pkg/smartcontract/nef" + "github.com/nspcc-dev/neo-go/pkg/smartcontract/trigger" + "github.com/nspcc-dev/neo-go/pkg/util" + "github.com/nspcc-dev/neo-go/pkg/vm" + "github.com/nspcc-dev/neo-go/pkg/vm/emit" + "github.com/nspcc-dev/neo-go/pkg/vm/opcode" + "github.com/nspcc-dev/neo-go/pkg/vm/stackitem" + "github.com/stretchr/testify/require" +) + +var pathToInternalContracts = filepath.Join("..", "..", "..", "..", "internal", "contracts") + +func getSharpTestTx(sender util.Uint160) *transaction.Transaction { + tx := transaction.New([]byte{byte(opcode.PUSH2)}, 0) + tx.Nonce = 0 + tx.Signers = append(tx.Signers, transaction.Signer{ + Account: sender, + Scopes: transaction.CalledByEntry, + }) + tx.Attributes = []transaction.Attribute{} + tx.Scripts = append(tx.Scripts, transaction.Witness{InvocationScript: []byte{}, VerificationScript: []byte{}}) + return tx +} + +func getSharpTestGenesis(t *testing.T) *block.Block { + const configPath = "../../../../config" + + cfg, err := config.Load(configPath, netmode.MainNet) + require.NoError(t, err) + b, err := core.CreateGenesisBlock(cfg.ProtocolConfiguration) + require.NoError(t, err) + return b +} + +func createVM(t testing.TB) (*vm.VM, *interop.Context, *core.Blockchain) { + chain, _ := chain.NewSingle(t) + ic := chain.GetTestVM(trigger.Application, &transaction.Transaction{}, &block.Block{}) + v := ic.SpawnVM() + return v, ic, chain +} + +func loadScriptWithHashAndFlags(ic *interop.Context, script []byte, hash util.Uint160, f callflag.CallFlag, args ...interface{}) { + ic.SpawnVM() + ic.VM.LoadScriptWithHash(script, hash, f) + for i := range args { + ic.VM.Estack().PushVal(args[i]) + } + ic.VM.GasLimit = -1 +} + +func TestBurnGas(t *testing.T) { + bc, acc := chain.NewSingle(t) + e := neotest.NewExecutor(t, bc, acc, acc) + managementInvoker := e.ValidatorInvoker(e.NativeHash(t, nativenames.Management)) + + cs, _ := contracts.GetTestContractState(t, pathToInternalContracts, 0, 1, acc.ScriptHash()) + rawManifest, err := json.Marshal(cs.Manifest) + require.NoError(t, err) + rawNef, err := cs.NEF.Bytes() + require.NoError(t, err) + tx := managementInvoker.PrepareInvoke(t, "deploy", rawNef, rawManifest) + e.AddNewBlock(t, tx) + e.CheckHalt(t, tx.Hash()) + cInvoker := e.ValidatorInvoker(cs.Hash) + + t.Run("good", func(t *testing.T) { + h := cInvoker.Invoke(t, stackitem.Null{}, "burnGas", int64(1)) + res := e.GetTxExecResult(t, h) + + t.Run("gas limit exceeded", func(t *testing.T) { + tx := e.NewUnsignedTx(t, cs.Hash, "burnGas", int64(2)) + e.SignTx(t, tx, res.GasConsumed, acc) + e.AddNewBlock(t, tx) + e.CheckFault(t, tx.Hash(), "GAS limit exceeded") + }) + }) + t.Run("too big integer", func(t *testing.T) { + gas := big.NewInt(math.MaxInt64) + gas.Add(gas, big.NewInt(1)) + + cInvoker.InvokeFail(t, "invalid GAS value", "burnGas", gas) + }) + t.Run("zero GAS", func(t *testing.T) { + cInvoker.InvokeFail(t, "GAS must be positive", "burnGas", int64(0)) + }) +} + +func TestCheckWitness(t *testing.T) { + _, ic, _ := createVM(t) + + script := []byte{byte(opcode.RET)} + scriptHash := hash.Hash160(script) + check := func(t *testing.T, ic *interop.Context, arg interface{}, shouldFail bool, expected ...bool) { + ic.VM.Estack().PushVal(arg) + err := runtime.CheckWitness(ic) + if shouldFail { + require.Error(t, err) + } else { + require.NoError(t, err) + require.NotNil(t, expected) + actual, ok := ic.VM.Estack().Pop().Value().(bool) + require.True(t, ok) + require.Equal(t, expected[0], actual) + } + } + t.Run("error", func(t *testing.T) { + t.Run("not a hash or key", func(t *testing.T) { + check(t, ic, []byte{1, 2, 3}, true) + }) + t.Run("script container is not a transaction", func(t *testing.T) { + loadScriptWithHashAndFlags(ic, script, scriptHash, callflag.ReadStates) + check(t, ic, random.Uint160().BytesBE(), true) + }) + t.Run("check scope", func(t *testing.T) { + t.Run("CustomGroups, missing ReadStates flag", func(t *testing.T) { + hash := random.Uint160() + tx := &transaction.Transaction{ + Signers: []transaction.Signer{ + { + Account: hash, + Scopes: transaction.CustomGroups, + AllowedGroups: []*keys.PublicKey{}, + }, + }, + } + ic.Tx = tx + callingScriptHash := scriptHash + loadScriptWithHashAndFlags(ic, script, callingScriptHash, callflag.All) + ic.VM.LoadScriptWithHash([]byte{0x1}, random.Uint160(), callflag.AllowCall) + check(t, ic, hash.BytesBE(), true) + }) + t.Run("Rules, missing ReadStates flag", func(t *testing.T) { + hash := random.Uint160() + pk, err := keys.NewPrivateKey() + require.NoError(t, err) + tx := &transaction.Transaction{ + Signers: []transaction.Signer{ + { + Account: hash, + Scopes: transaction.Rules, + Rules: []transaction.WitnessRule{{ + Action: transaction.WitnessAllow, + Condition: (*transaction.ConditionGroup)(pk.PublicKey()), + }}, + }, + }, + } + ic.Tx = tx + callingScriptHash := scriptHash + loadScriptWithHashAndFlags(ic, script, callingScriptHash, callflag.All) + ic.VM.LoadScriptWithHash([]byte{0x1}, random.Uint160(), callflag.AllowCall) + check(t, ic, hash.BytesBE(), true) + }) + }) + }) + t.Run("positive", func(t *testing.T) { + t.Run("calling scripthash", func(t *testing.T) { + t.Run("hashed witness", func(t *testing.T) { + callingScriptHash := scriptHash + loadScriptWithHashAndFlags(ic, script, callingScriptHash, callflag.All) + ic.VM.LoadScriptWithHash([]byte{0x1}, random.Uint160(), callflag.All) + check(t, ic, callingScriptHash.BytesBE(), false, true) + }) + t.Run("keyed witness", func(t *testing.T) { + pk, err := keys.NewPrivateKey() + require.NoError(t, err) + callingScriptHash := pk.PublicKey().GetScriptHash() + loadScriptWithHashAndFlags(ic, script, callingScriptHash, callflag.All) + ic.VM.LoadScriptWithHash([]byte{0x1}, random.Uint160(), callflag.All) + check(t, ic, pk.PublicKey().Bytes(), false, true) + }) + }) + t.Run("check scope", func(t *testing.T) { + t.Run("Global", func(t *testing.T) { + hash := random.Uint160() + tx := &transaction.Transaction{ + Signers: []transaction.Signer{ + { + Account: hash, + Scopes: transaction.Global, + }, + }, + } + loadScriptWithHashAndFlags(ic, script, scriptHash, callflag.ReadStates) + ic.Tx = tx + check(t, ic, hash.BytesBE(), false, true) + }) + t.Run("CalledByEntry", func(t *testing.T) { + hash := random.Uint160() + tx := &transaction.Transaction{ + Signers: []transaction.Signer{ + { + Account: hash, + Scopes: transaction.CalledByEntry, + }, + }, + } + loadScriptWithHashAndFlags(ic, script, scriptHash, callflag.ReadStates) + ic.Tx = tx + check(t, ic, hash.BytesBE(), false, true) + }) + t.Run("CustomContracts", func(t *testing.T) { + hash := random.Uint160() + tx := &transaction.Transaction{ + Signers: []transaction.Signer{ + { + Account: hash, + Scopes: transaction.CustomContracts, + AllowedContracts: []util.Uint160{scriptHash}, + }, + }, + } + loadScriptWithHashAndFlags(ic, script, scriptHash, callflag.ReadStates) + ic.Tx = tx + check(t, ic, hash.BytesBE(), false, true) + }) + t.Run("CustomGroups", func(t *testing.T) { + t.Run("unknown scripthash", func(t *testing.T) { + hash := random.Uint160() + tx := &transaction.Transaction{ + Signers: []transaction.Signer{ + { + Account: hash, + Scopes: transaction.CustomGroups, + AllowedGroups: []*keys.PublicKey{}, + }, + }, + } + loadScriptWithHashAndFlags(ic, script, scriptHash, callflag.ReadStates) + ic.Tx = tx + check(t, ic, hash.BytesBE(), false, false) + }) + t.Run("positive", func(t *testing.T) { + targetHash := random.Uint160() + pk, err := keys.NewPrivateKey() + require.NoError(t, err) + tx := &transaction.Transaction{ + Signers: []transaction.Signer{ + { + Account: targetHash, + Scopes: transaction.CustomGroups, + AllowedGroups: []*keys.PublicKey{pk.PublicKey()}, + }, + }, + } + contractScript := []byte{byte(opcode.PUSH1), byte(opcode.RET)} + contractScriptHash := hash.Hash160(contractScript) + ne, err := nef.NewFile(contractScript) + require.NoError(t, err) + contractState := &state.Contract{ + ContractBase: state.ContractBase{ + ID: 15, + Hash: contractScriptHash, + NEF: *ne, + Manifest: manifest.Manifest{ + Groups: []manifest.Group{{PublicKey: pk.PublicKey(), Signature: make([]byte, keys.SignatureLen)}}, + }, + }, + } + require.NoError(t, native.PutContractState(ic.DAO, contractState)) + loadScriptWithHashAndFlags(ic, contractScript, contractScriptHash, callflag.All) + ic.Tx = tx + check(t, ic, targetHash.BytesBE(), false, true) + }) + }) + t.Run("Rules", func(t *testing.T) { + t.Run("no match", func(t *testing.T) { + hash := random.Uint160() + tx := &transaction.Transaction{ + Signers: []transaction.Signer{ + { + Account: hash, + Scopes: transaction.Rules, + Rules: []transaction.WitnessRule{{ + Action: transaction.WitnessAllow, + Condition: (*transaction.ConditionScriptHash)(&hash), + }}, + }, + }, + } + loadScriptWithHashAndFlags(ic, script, scriptHash, callflag.ReadStates) + ic.Tx = tx + check(t, ic, hash.BytesBE(), false, false) + }) + t.Run("allow", func(t *testing.T) { + hash := random.Uint160() + var cond = true + tx := &transaction.Transaction{ + Signers: []transaction.Signer{ + { + Account: hash, + Scopes: transaction.Rules, + Rules: []transaction.WitnessRule{{ + Action: transaction.WitnessAllow, + Condition: (*transaction.ConditionBoolean)(&cond), + }}, + }, + }, + } + loadScriptWithHashAndFlags(ic, script, scriptHash, callflag.ReadStates) + ic.Tx = tx + check(t, ic, hash.BytesBE(), false, true) + }) + t.Run("deny", func(t *testing.T) { + hash := random.Uint160() + var cond = true + tx := &transaction.Transaction{ + Signers: []transaction.Signer{ + { + Account: hash, + Scopes: transaction.Rules, + Rules: []transaction.WitnessRule{{ + Action: transaction.WitnessDeny, + Condition: (*transaction.ConditionBoolean)(&cond), + }}, + }, + }, + } + loadScriptWithHashAndFlags(ic, script, scriptHash, callflag.ReadStates) + ic.Tx = tx + check(t, ic, hash.BytesBE(), false, false) + }) + }) + t.Run("bad scope", func(t *testing.T) { + hash := random.Uint160() + tx := &transaction.Transaction{ + Signers: []transaction.Signer{ + { + Account: hash, + Scopes: transaction.None, + }, + }, + } + loadScriptWithHashAndFlags(ic, script, scriptHash, callflag.ReadStates) + ic.Tx = tx + check(t, ic, hash.BytesBE(), false, false) + }) + }) + }) +} + +func TestGasLeft(t *testing.T) { + const runtimeGasLeftPrice = 1 << 4 + + bc, acc := chain.NewSingle(t) + e := neotest.NewExecutor(t, bc, acc, acc) + w := io.NewBufBinWriter() + + gasLimit := 1100 + emit.Syscall(w.BinWriter, interopnames.SystemRuntimeGasLeft) + emit.Syscall(w.BinWriter, interopnames.SystemRuntimeGasLeft) + require.NoError(t, w.Err) + tx := transaction.New(w.Bytes(), int64(gasLimit)) + tx.Nonce = neotest.Nonce() + tx.ValidUntilBlock = e.Chain.BlockHeight() + 1 + e.SignTx(t, tx, int64(gasLimit), acc) + e.AddNewBlock(t, tx) + e.CheckHalt(t, tx.Hash()) + res := e.GetTxExecResult(t, tx.Hash()) + l1 := res.Stack[0].Value().(*big.Int) + l2 := res.Stack[1].Value().(*big.Int) + + require.Equal(t, int64(gasLimit-runtimeGasLeftPrice*interop.DefaultBaseExecFee), l1.Int64()) + require.Equal(t, int64(gasLimit-2*runtimeGasLeftPrice*interop.DefaultBaseExecFee), l2.Int64()) +} + +func TestGetAddressVersion(t *testing.T) { + bc, acc := chain.NewSingle(t) + e := neotest.NewExecutor(t, bc, acc, acc) + w := io.NewBufBinWriter() + + emit.Syscall(w.BinWriter, interopnames.SystemRuntimeGetAddressVersion) + require.NoError(t, w.Err) + e.InvokeScriptCheckHALT(t, w.Bytes(), []neotest.Signer{acc}, stackitem.NewBigInteger(big.NewInt(int64(address.NEO3Prefix)))) +} + +func TestGetInvocationCounter(t *testing.T) { + v, ic, _ := createVM(t) + + cs, _ := contracts.GetTestContractState(t, pathToInternalContracts, 4, 5, random.Uint160()) // sender and IDs are not important for the test + require.NoError(t, native.PutContractState(ic.DAO, cs)) + + ic.Invocations[hash.Hash160([]byte{2})] = 42 + + t.Run("No invocations", func(t *testing.T) { + v.Load([]byte{1}) + // do not return an error in this case. + require.NoError(t, runtime.GetInvocationCounter(ic)) + require.EqualValues(t, 1, v.Estack().Pop().BigInt().Int64()) + }) + t.Run("NonZero", func(t *testing.T) { + v.Load([]byte{2}) + require.NoError(t, runtime.GetInvocationCounter(ic)) + require.EqualValues(t, 42, v.Estack().Pop().BigInt().Int64()) + }) + t.Run("Contract", func(t *testing.T) { + w := io.NewBufBinWriter() + emit.AppCall(w.BinWriter, cs.Hash, "invocCounter", callflag.All) + v.LoadWithFlags(w.Bytes(), callflag.All) + require.NoError(t, v.Run()) + require.EqualValues(t, 1, v.Estack().Pop().BigInt().Int64()) + }) +} + +func TestGetNetwork(t *testing.T) { + bc, acc := chain.NewSingle(t) + e := neotest.NewExecutor(t, bc, acc, acc) + w := io.NewBufBinWriter() + + emit.Syscall(w.BinWriter, interopnames.SystemRuntimeGetNetwork) + require.NoError(t, w.Err) + e.InvokeScriptCheckHALT(t, w.Bytes(), []neotest.Signer{acc}, stackitem.NewBigInteger(big.NewInt(int64(bc.GetConfig().Magic)))) +} + +func TestGetNotifications(t *testing.T) { + v, ic, _ := createVM(t) + + ic.Notifications = []state.NotificationEvent{ + {ScriptHash: util.Uint160{1}, Name: "Event1", Item: stackitem.NewArray([]stackitem.Item{stackitem.NewByteArray([]byte{11})})}, + {ScriptHash: util.Uint160{2}, Name: "Event2", Item: stackitem.NewArray([]stackitem.Item{stackitem.NewByteArray([]byte{22})})}, + {ScriptHash: util.Uint160{1}, Name: "Event1", Item: stackitem.NewArray([]stackitem.Item{stackitem.NewByteArray([]byte{33})})}, + } + + t.Run("NoFilter", func(t *testing.T) { + v.Estack().PushVal(stackitem.Null{}) + require.NoError(t, runtime.GetNotifications(ic)) + + arr := v.Estack().Pop().Array() + require.Equal(t, len(ic.Notifications), len(arr)) + for i := range arr { + elem := arr[i].Value().([]stackitem.Item) + require.Equal(t, ic.Notifications[i].ScriptHash.BytesBE(), elem[0].Value()) + name, err := stackitem.ToString(elem[1]) + require.NoError(t, err) + require.Equal(t, ic.Notifications[i].Name, name) + ic.Notifications[i].Item.MarkAsReadOnly() // tiny hack for test to be able to compare object references. + require.Equal(t, ic.Notifications[i].Item, elem[2]) + } + }) + + t.Run("WithFilter", func(t *testing.T) { + h := util.Uint160{2}.BytesBE() + v.Estack().PushVal(h) + require.NoError(t, runtime.GetNotifications(ic)) + + arr := v.Estack().Pop().Array() + require.Equal(t, 1, len(arr)) + elem := arr[0].Value().([]stackitem.Item) + require.Equal(t, h, elem[0].Value()) + name, err := stackitem.ToString(elem[1]) + require.NoError(t, err) + require.Equal(t, ic.Notifications[1].Name, name) + require.Equal(t, ic.Notifications[1].Item, elem[2]) + }) +} + +func TestGetRandom_DifferentTransactions(t *testing.T) { + bc, acc := chain.NewSingle(t) + e := neotest.NewExecutor(t, bc, acc, acc) + + w := io.NewBufBinWriter() + emit.Syscall(w.BinWriter, interopnames.SystemRuntimeGetRandom) + require.NoError(t, w.Err) + script := w.Bytes() + + tx1 := e.PrepareInvocation(t, script, []neotest.Signer{e.Validator}, bc.BlockHeight()+1) + tx2 := e.PrepareInvocation(t, script, []neotest.Signer{e.Validator}, bc.BlockHeight()+1) + e.AddNewBlock(t, tx1, tx2) + e.CheckHalt(t, tx1.Hash()) + e.CheckHalt(t, tx2.Hash()) + + res1 := e.GetTxExecResult(t, tx1.Hash()) + res2 := e.GetTxExecResult(t, tx2.Hash()) + + r1, err := res1.Stack[0].TryInteger() + require.NoError(t, err) + r2, err := res2.Stack[0].TryInteger() + require.NoError(t, err) + require.NotEqual(t, r1, r2) +} + +// Tests are taken from +// https://github.com/neo-project/neo/blob/master/tests/neo.UnitTests/SmartContract/UT_ApplicationEngine.Runtime.cs +func TestGetRandomCompatibility(t *testing.T) { + bc, _ := chain.NewSingle(t) + + b := getSharpTestGenesis(t) + tx := getSharpTestTx(util.Uint160{}) + ic := bc.GetTestVM(trigger.Application, tx, b) + ic.Network = 860833102 // Old mainnet magic used by C# tests. + + ic.VM = vm.New() + ic.VM.LoadScript([]byte{0x01}) + ic.VM.GasLimit = 1100_00000000 + + require.NoError(t, runtime.GetRandom(ic)) + require.Equal(t, "271339657438512451304577787170704246350", ic.VM.Estack().Pop().BigInt().String()) + + require.NoError(t, runtime.GetRandom(ic)) + require.Equal(t, "98548189559099075644778613728143131367", ic.VM.Estack().Pop().BigInt().String()) + + require.NoError(t, runtime.GetRandom(ic)) + require.Equal(t, "247654688993873392544380234598471205121", ic.VM.Estack().Pop().BigInt().String()) + + require.NoError(t, runtime.GetRandom(ic)) + require.Equal(t, "291082758879475329976578097236212073607", ic.VM.Estack().Pop().BigInt().String()) + + require.NoError(t, runtime.GetRandom(ic)) + require.Equal(t, "247152297361212656635216876565962360375", ic.VM.Estack().Pop().BigInt().String()) +} diff --git a/pkg/core/interop_system_core_test.go b/pkg/core/interop_system_core_test.go index d9a4b98b6..d58c9fd52 100644 --- a/pkg/core/interop_system_core_test.go +++ b/pkg/core/interop_system_core_test.go @@ -7,158 +7,26 @@ import ( "path/filepath" "testing" - "github.com/nspcc-dev/neo-go/internal/contracts" "github.com/nspcc-dev/neo-go/internal/random" "github.com/nspcc-dev/neo-go/pkg/config" - "github.com/nspcc-dev/neo-go/pkg/config/netmode" - "github.com/nspcc-dev/neo-go/pkg/core/block" "github.com/nspcc-dev/neo-go/pkg/core/interop" "github.com/nspcc-dev/neo-go/pkg/core/interop/iterator" - "github.com/nspcc-dev/neo-go/pkg/core/interop/runtime" istorage "github.com/nspcc-dev/neo-go/pkg/core/interop/storage" "github.com/nspcc-dev/neo-go/pkg/core/native" "github.com/nspcc-dev/neo-go/pkg/core/state" "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/crypto/hash" - "github.com/nspcc-dev/neo-go/pkg/crypto/keys" - "github.com/nspcc-dev/neo-go/pkg/io" "github.com/nspcc-dev/neo-go/pkg/smartcontract/callflag" "github.com/nspcc-dev/neo-go/pkg/smartcontract/manifest" "github.com/nspcc-dev/neo-go/pkg/smartcontract/nef" "github.com/nspcc-dev/neo-go/pkg/smartcontract/trigger" - "github.com/nspcc-dev/neo-go/pkg/util" "github.com/nspcc-dev/neo-go/pkg/vm" - "github.com/nspcc-dev/neo-go/pkg/vm/emit" - "github.com/nspcc-dev/neo-go/pkg/vm/opcode" "github.com/nspcc-dev/neo-go/pkg/vm/stackitem" "github.com/stretchr/testify/require" ) var pathToInternalContracts = filepath.Join("..", "..", "internal", "contracts") -// Tests are taken from -// https://github.com/neo-project/neo/blob/master/tests/neo.UnitTests/SmartContract/UT_ApplicationEngine.Runtime.cs -func TestRuntimeGetRandomCompatibility(t *testing.T) { - bc := newTestChain(t) - - b := getSharpTestGenesis(t) - tx := getSharpTestTx(util.Uint160{}) - ic := bc.newInteropContext(trigger.Application, bc.dao.GetWrapped(), b, tx) - ic.Network = 860833102 // Old mainnet magic used by C# tests. - - ic.VM = vm.New() - ic.VM.LoadScript([]byte{0x01}) - ic.VM.GasLimit = 1100_00000000 - - require.NoError(t, runtime.GetRandom(ic)) - require.Equal(t, "271339657438512451304577787170704246350", ic.VM.Estack().Pop().BigInt().String()) - - require.NoError(t, runtime.GetRandom(ic)) - require.Equal(t, "98548189559099075644778613728143131367", ic.VM.Estack().Pop().BigInt().String()) - - require.NoError(t, runtime.GetRandom(ic)) - require.Equal(t, "247654688993873392544380234598471205121", ic.VM.Estack().Pop().BigInt().String()) - - require.NoError(t, runtime.GetRandom(ic)) - require.Equal(t, "291082758879475329976578097236212073607", ic.VM.Estack().Pop().BigInt().String()) - - require.NoError(t, runtime.GetRandom(ic)) - require.Equal(t, "247152297361212656635216876565962360375", ic.VM.Estack().Pop().BigInt().String()) -} - -func getSharpTestTx(sender util.Uint160) *transaction.Transaction { - tx := transaction.New([]byte{byte(opcode.PUSH2)}, 0) - tx.Nonce = 0 - tx.Signers = append(tx.Signers, transaction.Signer{ - Account: sender, - Scopes: transaction.CalledByEntry, - }) - tx.Attributes = []transaction.Attribute{} - tx.Scripts = append(tx.Scripts, transaction.Witness{InvocationScript: []byte{}, VerificationScript: []byte{}}) - return tx -} - -func getSharpTestGenesis(t *testing.T) *block.Block { - const configPath = "../../config" - - cfg, err := config.Load(configPath, netmode.MainNet) - require.NoError(t, err) - b, err := CreateGenesisBlock(cfg.ProtocolConfiguration) - require.NoError(t, err) - return b -} - -func TestRuntimeGetNotifications(t *testing.T) { - v, ic, _ := createVM(t) - - ic.Notifications = []state.NotificationEvent{ - {ScriptHash: util.Uint160{1}, Name: "Event1", Item: stackitem.NewArray([]stackitem.Item{stackitem.NewByteArray([]byte{11})})}, - {ScriptHash: util.Uint160{2}, Name: "Event2", Item: stackitem.NewArray([]stackitem.Item{stackitem.NewByteArray([]byte{22})})}, - {ScriptHash: util.Uint160{1}, Name: "Event1", Item: stackitem.NewArray([]stackitem.Item{stackitem.NewByteArray([]byte{33})})}, - } - - t.Run("NoFilter", func(t *testing.T) { - v.Estack().PushVal(stackitem.Null{}) - require.NoError(t, runtime.GetNotifications(ic)) - - arr := v.Estack().Pop().Array() - require.Equal(t, len(ic.Notifications), len(arr)) - for i := range arr { - elem := arr[i].Value().([]stackitem.Item) - require.Equal(t, ic.Notifications[i].ScriptHash.BytesBE(), elem[0].Value()) - name, err := stackitem.ToString(elem[1]) - require.NoError(t, err) - require.Equal(t, ic.Notifications[i].Name, name) - ic.Notifications[i].Item.MarkAsReadOnly() // tiny hack for test to be able to compare object references. - require.Equal(t, ic.Notifications[i].Item, elem[2]) - } - }) - - t.Run("WithFilter", func(t *testing.T) { - h := util.Uint160{2}.BytesBE() - v.Estack().PushVal(h) - require.NoError(t, runtime.GetNotifications(ic)) - - arr := v.Estack().Pop().Array() - require.Equal(t, 1, len(arr)) - elem := arr[0].Value().([]stackitem.Item) - require.Equal(t, h, elem[0].Value()) - name, err := stackitem.ToString(elem[1]) - require.NoError(t, err) - require.Equal(t, ic.Notifications[1].Name, name) - require.Equal(t, ic.Notifications[1].Item, elem[2]) - }) -} - -func TestRuntimeGetInvocationCounter(t *testing.T) { - v, ic, _ := createVM(t) - - cs, _ := contracts.GetTestContractState(t, pathToInternalContracts, 4, 5, random.Uint160()) // sender and IDs are not important for the test - require.NoError(t, native.PutContractState(ic.DAO, cs)) - - ic.Invocations[hash.Hash160([]byte{2})] = 42 - - t.Run("No invocations", func(t *testing.T) { - v.Load([]byte{1}) - // do not return an error in this case. - require.NoError(t, runtime.GetInvocationCounter(ic)) - require.EqualValues(t, 1, v.Estack().Pop().BigInt().Int64()) - }) - t.Run("NonZero", func(t *testing.T) { - v.Load([]byte{2}) - require.NoError(t, runtime.GetInvocationCounter(ic)) - require.EqualValues(t, 42, v.Estack().Pop().BigInt().Int64()) - }) - t.Run("Contract", func(t *testing.T) { - w := io.NewBufBinWriter() - emit.AppCall(w.BinWriter, cs.Hash, "invocCounter", callflag.All) - v.LoadWithFlags(w.Bytes(), callflag.All) - require.NoError(t, v.Run()) - require.EqualValues(t, 1, v.Estack().Pop().BigInt().Int64()) - }) -} - func TestStoragePut(t *testing.T) { _, cs, ic, _ := createVMAndContractState(t) @@ -557,269 +425,6 @@ func createVMAndContractState(t testing.TB) (*vm.VM, *state.Contract, *interop.C return v, contractState, context, chain } -func loadScriptWithHashAndFlags(ic *interop.Context, script []byte, hash util.Uint160, f callflag.CallFlag, args ...interface{}) { - ic.SpawnVM() - ic.VM.LoadScriptWithHash(script, hash, f) - for i := range args { - ic.VM.Estack().PushVal(args[i]) - } - ic.VM.GasLimit = -1 -} - -func TestRuntimeCheckWitness(t *testing.T) { - _, ic, _ := createVM(t) - - script := []byte{byte(opcode.RET)} - scriptHash := hash.Hash160(script) - check := func(t *testing.T, ic *interop.Context, arg interface{}, shouldFail bool, expected ...bool) { - ic.VM.Estack().PushVal(arg) - err := runtime.CheckWitness(ic) - if shouldFail { - require.Error(t, err) - } else { - require.NoError(t, err) - require.NotNil(t, expected) - actual, ok := ic.VM.Estack().Pop().Value().(bool) - require.True(t, ok) - require.Equal(t, expected[0], actual) - } - } - t.Run("error", func(t *testing.T) { - t.Run("not a hash or key", func(t *testing.T) { - check(t, ic, []byte{1, 2, 3}, true) - }) - t.Run("script container is not a transaction", func(t *testing.T) { - loadScriptWithHashAndFlags(ic, script, scriptHash, callflag.ReadStates) - check(t, ic, random.Uint160().BytesBE(), true) - }) - t.Run("check scope", func(t *testing.T) { - t.Run("CustomGroups, missing ReadStates flag", func(t *testing.T) { - hash := random.Uint160() - tx := &transaction.Transaction{ - Signers: []transaction.Signer{ - { - Account: hash, - Scopes: transaction.CustomGroups, - AllowedGroups: []*keys.PublicKey{}, - }, - }, - } - ic.Tx = tx - callingScriptHash := scriptHash - loadScriptWithHashAndFlags(ic, script, callingScriptHash, callflag.All) - ic.VM.LoadScriptWithHash([]byte{0x1}, random.Uint160(), callflag.AllowCall) - check(t, ic, hash.BytesBE(), true) - }) - t.Run("Rules, missing ReadStates flag", func(t *testing.T) { - hash := random.Uint160() - pk, err := keys.NewPrivateKey() - require.NoError(t, err) - tx := &transaction.Transaction{ - Signers: []transaction.Signer{ - { - Account: hash, - Scopes: transaction.Rules, - Rules: []transaction.WitnessRule{{ - Action: transaction.WitnessAllow, - Condition: (*transaction.ConditionGroup)(pk.PublicKey()), - }}, - }, - }, - } - ic.Tx = tx - callingScriptHash := scriptHash - loadScriptWithHashAndFlags(ic, script, callingScriptHash, callflag.All) - ic.VM.LoadScriptWithHash([]byte{0x1}, random.Uint160(), callflag.AllowCall) - check(t, ic, hash.BytesBE(), true) - }) - }) - }) - t.Run("positive", func(t *testing.T) { - t.Run("calling scripthash", func(t *testing.T) { - t.Run("hashed witness", func(t *testing.T) { - callingScriptHash := scriptHash - loadScriptWithHashAndFlags(ic, script, callingScriptHash, callflag.All) - ic.VM.LoadScriptWithHash([]byte{0x1}, random.Uint160(), callflag.All) - check(t, ic, callingScriptHash.BytesBE(), false, true) - }) - t.Run("keyed witness", func(t *testing.T) { - pk, err := keys.NewPrivateKey() - require.NoError(t, err) - callingScriptHash := pk.PublicKey().GetScriptHash() - loadScriptWithHashAndFlags(ic, script, callingScriptHash, callflag.All) - ic.VM.LoadScriptWithHash([]byte{0x1}, random.Uint160(), callflag.All) - check(t, ic, pk.PublicKey().Bytes(), false, true) - }) - }) - t.Run("check scope", func(t *testing.T) { - t.Run("Global", func(t *testing.T) { - hash := random.Uint160() - tx := &transaction.Transaction{ - Signers: []transaction.Signer{ - { - Account: hash, - Scopes: transaction.Global, - }, - }, - } - loadScriptWithHashAndFlags(ic, script, scriptHash, callflag.ReadStates) - ic.Tx = tx - check(t, ic, hash.BytesBE(), false, true) - }) - t.Run("CalledByEntry", func(t *testing.T) { - hash := random.Uint160() - tx := &transaction.Transaction{ - Signers: []transaction.Signer{ - { - Account: hash, - Scopes: transaction.CalledByEntry, - }, - }, - } - loadScriptWithHashAndFlags(ic, script, scriptHash, callflag.ReadStates) - ic.Tx = tx - check(t, ic, hash.BytesBE(), false, true) - }) - t.Run("CustomContracts", func(t *testing.T) { - hash := random.Uint160() - tx := &transaction.Transaction{ - Signers: []transaction.Signer{ - { - Account: hash, - Scopes: transaction.CustomContracts, - AllowedContracts: []util.Uint160{scriptHash}, - }, - }, - } - loadScriptWithHashAndFlags(ic, script, scriptHash, callflag.ReadStates) - ic.Tx = tx - check(t, ic, hash.BytesBE(), false, true) - }) - t.Run("CustomGroups", func(t *testing.T) { - t.Run("unknown scripthash", func(t *testing.T) { - hash := random.Uint160() - tx := &transaction.Transaction{ - Signers: []transaction.Signer{ - { - Account: hash, - Scopes: transaction.CustomGroups, - AllowedGroups: []*keys.PublicKey{}, - }, - }, - } - loadScriptWithHashAndFlags(ic, script, scriptHash, callflag.ReadStates) - ic.Tx = tx - check(t, ic, hash.BytesBE(), false, false) - }) - t.Run("positive", func(t *testing.T) { - targetHash := random.Uint160() - pk, err := keys.NewPrivateKey() - require.NoError(t, err) - tx := &transaction.Transaction{ - Signers: []transaction.Signer{ - { - Account: targetHash, - Scopes: transaction.CustomGroups, - AllowedGroups: []*keys.PublicKey{pk.PublicKey()}, - }, - }, - } - contractScript := []byte{byte(opcode.PUSH1), byte(opcode.RET)} - contractScriptHash := hash.Hash160(contractScript) - ne, err := nef.NewFile(contractScript) - require.NoError(t, err) - contractState := &state.Contract{ - ContractBase: state.ContractBase{ - ID: 15, - Hash: contractScriptHash, - NEF: *ne, - Manifest: manifest.Manifest{ - Groups: []manifest.Group{{PublicKey: pk.PublicKey(), Signature: make([]byte, keys.SignatureLen)}}, - }, - }, - } - require.NoError(t, native.PutContractState(ic.DAO, contractState)) - loadScriptWithHashAndFlags(ic, contractScript, contractScriptHash, callflag.All) - ic.Tx = tx - check(t, ic, targetHash.BytesBE(), false, true) - }) - }) - t.Run("Rules", func(t *testing.T) { - t.Run("no match", func(t *testing.T) { - hash := random.Uint160() - tx := &transaction.Transaction{ - Signers: []transaction.Signer{ - { - Account: hash, - Scopes: transaction.Rules, - Rules: []transaction.WitnessRule{{ - Action: transaction.WitnessAllow, - Condition: (*transaction.ConditionScriptHash)(&hash), - }}, - }, - }, - } - loadScriptWithHashAndFlags(ic, script, scriptHash, callflag.ReadStates) - ic.Tx = tx - check(t, ic, hash.BytesBE(), false, false) - }) - t.Run("allow", func(t *testing.T) { - hash := random.Uint160() - var cond = true - tx := &transaction.Transaction{ - Signers: []transaction.Signer{ - { - Account: hash, - Scopes: transaction.Rules, - Rules: []transaction.WitnessRule{{ - Action: transaction.WitnessAllow, - Condition: (*transaction.ConditionBoolean)(&cond), - }}, - }, - }, - } - loadScriptWithHashAndFlags(ic, script, scriptHash, callflag.ReadStates) - ic.Tx = tx - check(t, ic, hash.BytesBE(), false, true) - }) - t.Run("deny", func(t *testing.T) { - hash := random.Uint160() - var cond = true - tx := &transaction.Transaction{ - Signers: []transaction.Signer{ - { - Account: hash, - Scopes: transaction.Rules, - Rules: []transaction.WitnessRule{{ - Action: transaction.WitnessDeny, - Condition: (*transaction.ConditionBoolean)(&cond), - }}, - }, - }, - } - loadScriptWithHashAndFlags(ic, script, scriptHash, callflag.ReadStates) - ic.Tx = tx - check(t, ic, hash.BytesBE(), false, false) - }) - }) - t.Run("bad scope", func(t *testing.T) { - hash := random.Uint160() - tx := &transaction.Transaction{ - Signers: []transaction.Signer{ - { - Account: hash, - Scopes: transaction.None, - }, - }, - } - loadScriptWithHashAndFlags(ic, script, scriptHash, callflag.ReadStates) - ic.Tx = tx - check(t, ic, hash.BytesBE(), false, false) - }) - }) - }) -} - // TestNativeGetMethod is needed to ensure that methods list has the same sorting // rule as we expect inside the `ContractMD.GetMethod`. func TestNativeGetMethod(t *testing.T) { diff --git a/pkg/core/interop_system_neotest_test.go b/pkg/core/interop_system_neotest_test.go index 9047aa1d2..25f125ebc 100644 --- a/pkg/core/interop_system_neotest_test.go +++ b/pkg/core/interop_system_neotest_test.go @@ -3,7 +3,6 @@ package core_test import ( "encoding/json" "fmt" - "math" "math/big" "strings" "testing" @@ -11,12 +10,10 @@ import ( "github.com/nspcc-dev/neo-go/internal/contracts" "github.com/nspcc-dev/neo-go/pkg/compiler" "github.com/nspcc-dev/neo-go/pkg/config" - "github.com/nspcc-dev/neo-go/pkg/core/interop" "github.com/nspcc-dev/neo-go/pkg/core/interop/interopnames" "github.com/nspcc-dev/neo-go/pkg/core/native/nativenames" "github.com/nspcc-dev/neo-go/pkg/core/transaction" "github.com/nspcc-dev/neo-go/pkg/crypto/keys" - "github.com/nspcc-dev/neo-go/pkg/encoding/address" "github.com/nspcc-dev/neo-go/pkg/io" "github.com/nspcc-dev/neo-go/pkg/neotest" "github.com/nspcc-dev/neo-go/pkg/neotest/chain" @@ -28,56 +25,6 @@ import ( "github.com/stretchr/testify/require" ) -func TestSystemRuntimeGetRandom_DifferentTransactions(t *testing.T) { - bc, acc := chain.NewSingle(t) - e := neotest.NewExecutor(t, bc, acc, acc) - - w := io.NewBufBinWriter() - emit.Syscall(w.BinWriter, interopnames.SystemRuntimeGetRandom) - require.NoError(t, w.Err) - script := w.Bytes() - - tx1 := e.PrepareInvocation(t, script, []neotest.Signer{e.Validator}, bc.BlockHeight()+1) - tx2 := e.PrepareInvocation(t, script, []neotest.Signer{e.Validator}, bc.BlockHeight()+1) - e.AddNewBlock(t, tx1, tx2) - e.CheckHalt(t, tx1.Hash()) - e.CheckHalt(t, tx2.Hash()) - - res1 := e.GetTxExecResult(t, tx1.Hash()) - res2 := e.GetTxExecResult(t, tx2.Hash()) - - r1, err := res1.Stack[0].TryInteger() - require.NoError(t, err) - r2, err := res2.Stack[0].TryInteger() - require.NoError(t, err) - require.NotEqual(t, r1, r2) -} - -func TestSystemRuntimeGasLeft(t *testing.T) { - const runtimeGasLeftPrice = 1 << 4 - - bc, acc := chain.NewSingle(t) - e := neotest.NewExecutor(t, bc, acc, acc) - w := io.NewBufBinWriter() - - gasLimit := 1100 - emit.Syscall(w.BinWriter, interopnames.SystemRuntimeGasLeft) - emit.Syscall(w.BinWriter, interopnames.SystemRuntimeGasLeft) - require.NoError(t, w.Err) - tx := transaction.New(w.Bytes(), int64(gasLimit)) - tx.Nonce = neotest.Nonce() - tx.ValidUntilBlock = e.Chain.BlockHeight() + 1 - e.SignTx(t, tx, int64(gasLimit), acc) - e.AddNewBlock(t, tx) - e.CheckHalt(t, tx.Hash()) - res := e.GetTxExecResult(t, tx.Hash()) - l1 := res.Stack[0].Value().(*big.Int) - l2 := res.Stack[1].Value().(*big.Int) - - require.Equal(t, int64(gasLimit-runtimeGasLeftPrice*interop.DefaultBaseExecFee), l1.Int64()) - require.Equal(t, int64(gasLimit-2*runtimeGasLeftPrice*interop.DefaultBaseExecFee), l2.Int64()) -} - func TestLoadToken(t *testing.T) { bc, acc := chain.NewSingle(t) e := neotest.NewExecutor(t, bc, acc, acc) @@ -105,63 +52,6 @@ func TestLoadToken(t *testing.T) { }) } -func TestSystemRuntimeGetNetwork(t *testing.T) { - bc, acc := chain.NewSingle(t) - e := neotest.NewExecutor(t, bc, acc, acc) - w := io.NewBufBinWriter() - - emit.Syscall(w.BinWriter, interopnames.SystemRuntimeGetNetwork) - require.NoError(t, w.Err) - e.InvokeScriptCheckHALT(t, w.Bytes(), []neotest.Signer{acc}, stackitem.NewBigInteger(big.NewInt(int64(bc.GetConfig().Magic)))) -} - -func TestSystemRuntimeGetAddressVersion(t *testing.T) { - bc, acc := chain.NewSingle(t) - e := neotest.NewExecutor(t, bc, acc, acc) - w := io.NewBufBinWriter() - - emit.Syscall(w.BinWriter, interopnames.SystemRuntimeGetAddressVersion) - require.NoError(t, w.Err) - e.InvokeScriptCheckHALT(t, w.Bytes(), []neotest.Signer{acc}, stackitem.NewBigInteger(big.NewInt(int64(address.NEO3Prefix)))) -} - -func TestSystemRuntimeBurnGas(t *testing.T) { - bc, acc := chain.NewSingle(t) - e := neotest.NewExecutor(t, bc, acc, acc) - managementInvoker := e.ValidatorInvoker(e.NativeHash(t, nativenames.Management)) - - cs, _ := contracts.GetTestContractState(t, pathToInternalContracts, 0, 1, acc.ScriptHash()) - rawManifest, err := json.Marshal(cs.Manifest) - require.NoError(t, err) - rawNef, err := cs.NEF.Bytes() - require.NoError(t, err) - tx := managementInvoker.PrepareInvoke(t, "deploy", rawNef, rawManifest) - e.AddNewBlock(t, tx) - e.CheckHalt(t, tx.Hash()) - cInvoker := e.ValidatorInvoker(cs.Hash) - - t.Run("good", func(t *testing.T) { - h := cInvoker.Invoke(t, stackitem.Null{}, "burnGas", int64(1)) - res := e.GetTxExecResult(t, h) - - t.Run("gas limit exceeded", func(t *testing.T) { - tx := e.NewUnsignedTx(t, cs.Hash, "burnGas", int64(2)) - e.SignTx(t, tx, res.GasConsumed, acc) - e.AddNewBlock(t, tx) - e.CheckFault(t, tx.Hash(), "GAS limit exceeded") - }) - }) - t.Run("too big integer", func(t *testing.T) { - gas := big.NewInt(math.MaxInt64) - gas.Add(gas, big.NewInt(1)) - - cInvoker.InvokeFail(t, "invalid GAS value", "burnGas", gas) - }) - t.Run("zero GAS", func(t *testing.T) { - cInvoker.InvokeFail(t, "GAS must be positive", "burnGas", int64(0)) - }) -} - func TestSystemContractCreateAccount_Hardfork(t *testing.T) { bc, acc := chain.NewSingleWithCustomConfig(t, func(c *config.ProtocolConfiguration) { c.P2PSigExtensions = true // `basicchain.Init` requires Notary enabled From e7e80fda648ecde060db3d70f105deef6adb4338 Mon Sep 17 00:00:00 2001 From: Roman Khimov Date: Wed, 8 Jun 2022 18:51:27 +0300 Subject: [PATCH 10/21] core: move TestNativeGetMethod to the native package --- pkg/core/interop_system_core_test.go | 16 ---------------- pkg/core/native/contract_test.go | 23 +++++++++++++++++++++++ 2 files changed, 23 insertions(+), 16 deletions(-) create mode 100644 pkg/core/native/contract_test.go diff --git a/pkg/core/interop_system_core_test.go b/pkg/core/interop_system_core_test.go index d58c9fd52..2c7496c10 100644 --- a/pkg/core/interop_system_core_test.go +++ b/pkg/core/interop_system_core_test.go @@ -8,7 +8,6 @@ import ( "testing" "github.com/nspcc-dev/neo-go/internal/random" - "github.com/nspcc-dev/neo-go/pkg/config" "github.com/nspcc-dev/neo-go/pkg/core/interop" "github.com/nspcc-dev/neo-go/pkg/core/interop/iterator" istorage "github.com/nspcc-dev/neo-go/pkg/core/interop/storage" @@ -424,18 +423,3 @@ func createVMAndContractState(t testing.TB) (*vm.VM, *state.Contract, *interop.C v, context, chain := createVM(t) return v, contractState, context, chain } - -// TestNativeGetMethod is needed to ensure that methods list has the same sorting -// rule as we expect inside the `ContractMD.GetMethod`. -func TestNativeGetMethod(t *testing.T) { - cfg := config.ProtocolConfiguration{P2PSigExtensions: true} - cs := native.NewContracts(cfg) - for _, c := range cs.Contracts { - t.Run(c.Metadata().Name, func(t *testing.T) { - for _, m := range c.Metadata().Methods { - _, ok := c.Metadata().GetMethod(m.MD.Name, len(m.MD.Parameters)) - require.True(t, ok) - } - }) - } -} diff --git a/pkg/core/native/contract_test.go b/pkg/core/native/contract_test.go new file mode 100644 index 000000000..3e61320f1 --- /dev/null +++ b/pkg/core/native/contract_test.go @@ -0,0 +1,23 @@ +package native + +import ( + "testing" + + "github.com/nspcc-dev/neo-go/pkg/config" + "github.com/stretchr/testify/require" +) + +// TestNativeGetMethod is needed to ensure that methods list has the same sorting +// rule as we expect inside the `ContractMD.GetMethod`. +func TestNativeGetMethod(t *testing.T) { + cfg := config.ProtocolConfiguration{P2PSigExtensions: true} + cs := NewContracts(cfg) + for _, c := range cs.Contracts { + t.Run(c.Metadata().Name, func(t *testing.T) { + for _, m := range c.Metadata().Methods { + _, ok := c.Metadata().GetMethod(m.MD.Name, len(m.MD.Parameters)) + require.True(t, ok) + } + }) + } +} From f0d7a1da2a7006be40f2c36bc32c31d5d730c390 Mon Sep 17 00:00:00 2001 From: Roman Khimov Date: Wed, 8 Jun 2022 19:01:34 +0300 Subject: [PATCH 11/21] core: move contract-related tests to the contract package --- pkg/core/interop/contract/account_test.go | 58 +++ pkg/core/interop/contract/call_test.go | 426 ++++++++++++++++++ pkg/core/interop_system_neotest_test.go | 500 ---------------------- 3 files changed, 484 insertions(+), 500 deletions(-) delete mode 100644 pkg/core/interop_system_neotest_test.go diff --git a/pkg/core/interop/contract/account_test.go b/pkg/core/interop/contract/account_test.go index a4ad6a60a..84a655571 100644 --- a/pkg/core/interop/contract/account_test.go +++ b/pkg/core/interop/contract/account_test.go @@ -5,7 +5,9 @@ import ( "math/big" "testing" + "github.com/nspcc-dev/neo-go/pkg/config" "github.com/nspcc-dev/neo-go/pkg/core/interop/interopnames" + "github.com/nspcc-dev/neo-go/pkg/core/transaction" "github.com/nspcc-dev/neo-go/pkg/crypto/hash" "github.com/nspcc-dev/neo-go/pkg/crypto/keys" "github.com/nspcc-dev/neo-go/pkg/io" @@ -13,6 +15,7 @@ import ( "github.com/nspcc-dev/neo-go/pkg/neotest/chain" "github.com/nspcc-dev/neo-go/pkg/smartcontract" "github.com/nspcc-dev/neo-go/pkg/util" + "github.com/nspcc-dev/neo-go/pkg/util/slice" "github.com/nspcc-dev/neo-go/pkg/vm/emit" "github.com/stretchr/testify/require" ) @@ -113,3 +116,58 @@ func TestCreateMultisigAccount(t *testing.T) { e.InvokeScriptCheckFAULT(t, w.Bytes(), []neotest.Signer{acc}, "m must be positive and fit int32") }) } + +func TestCreateAccount_Hardfork(t *testing.T) { + bc, acc := chain.NewSingleWithCustomConfig(t, func(c *config.ProtocolConfiguration) { + c.P2PSigExtensions = true // `basicchain.Init` requires Notary enabled + c.Hardforks = map[string]uint32{ + config.HFAspidochelone.String(): 2, + } + }) + e := neotest.NewExecutor(t, bc, acc, acc) + + priv, err := keys.NewPrivateKey() + require.NoError(t, err) + pub := priv.PublicKey() + + w := io.NewBufBinWriter() + emit.Array(w.BinWriter, []interface{}{pub.Bytes(), pub.Bytes(), pub.Bytes()}...) + emit.Int(w.BinWriter, int64(2)) + emit.Syscall(w.BinWriter, interopnames.SystemContractCreateMultisigAccount) + require.NoError(t, w.Err) + multisigScript := slice.Copy(w.Bytes()) + + w.Reset() + emit.Bytes(w.BinWriter, pub.Bytes()) + emit.Syscall(w.BinWriter, interopnames.SystemContractCreateStandardAccount) + require.NoError(t, w.Err) + standardScript := slice.Copy(w.Bytes()) + + createAccTx := func(t *testing.T, script []byte) *transaction.Transaction { + tx := e.PrepareInvocation(t, script, []neotest.Signer{e.Committee}, bc.BlockHeight()+1) + return tx + } + + // blocks #1, #2: old prices + tx1Standard := createAccTx(t, standardScript) + tx1Multisig := createAccTx(t, multisigScript) + e.AddNewBlock(t, tx1Standard, tx1Multisig) + e.CheckHalt(t, tx1Standard.Hash()) + e.CheckHalt(t, tx1Multisig.Hash()) + tx2Standard := createAccTx(t, standardScript) + tx2Multisig := createAccTx(t, multisigScript) + e.AddNewBlock(t, tx2Standard, tx2Multisig) + e.CheckHalt(t, tx2Standard.Hash()) + e.CheckHalt(t, tx2Multisig.Hash()) + + // block #3: updated prices (larger than the previous ones) + tx3Standard := createAccTx(t, standardScript) + tx3Multisig := createAccTx(t, multisigScript) + e.AddNewBlock(t, tx3Standard, tx3Multisig) + e.CheckHalt(t, tx3Standard.Hash()) + e.CheckHalt(t, tx3Multisig.Hash()) + require.True(t, tx1Standard.SystemFee == tx2Standard.SystemFee) + require.True(t, tx1Multisig.SystemFee == tx2Multisig.SystemFee) + require.True(t, tx2Standard.SystemFee < tx3Standard.SystemFee) + require.True(t, tx2Multisig.SystemFee < tx3Multisig.SystemFee) +} diff --git a/pkg/core/interop/contract/call_test.go b/pkg/core/interop/contract/call_test.go index 474586b83..f4159acd3 100644 --- a/pkg/core/interop/contract/call_test.go +++ b/pkg/core/interop/contract/call_test.go @@ -1,19 +1,26 @@ package contract_test import ( + "encoding/json" + "fmt" "math/big" "path/filepath" + "strings" "testing" "github.com/nspcc-dev/neo-go/internal/contracts" "github.com/nspcc-dev/neo-go/internal/random" + "github.com/nspcc-dev/neo-go/pkg/compiler" "github.com/nspcc-dev/neo-go/pkg/core/block" "github.com/nspcc-dev/neo-go/pkg/core/interop" "github.com/nspcc-dev/neo-go/pkg/core/interop/contract" "github.com/nspcc-dev/neo-go/pkg/core/native" + "github.com/nspcc-dev/neo-go/pkg/core/native/nativenames" "github.com/nspcc-dev/neo-go/pkg/core/transaction" + "github.com/nspcc-dev/neo-go/pkg/neotest" "github.com/nspcc-dev/neo-go/pkg/neotest/chain" "github.com/nspcc-dev/neo-go/pkg/smartcontract/callflag" + "github.com/nspcc-dev/neo-go/pkg/smartcontract/manifest" "github.com/nspcc-dev/neo-go/pkg/smartcontract/trigger" "github.com/nspcc-dev/neo-go/pkg/util" "github.com/nspcc-dev/neo-go/pkg/vm/opcode" @@ -163,6 +170,425 @@ func TestCall(t *testing.T) { }) } +func TestLoadToken(t *testing.T) { + bc, acc := chain.NewSingle(t) + e := neotest.NewExecutor(t, bc, acc, acc) + managementInvoker := e.ValidatorInvoker(e.NativeHash(t, nativenames.Management)) + + cs, _ := contracts.GetTestContractState(t, pathToInternalContracts, 0, 1, acc.ScriptHash()) + rawManifest, err := json.Marshal(cs.Manifest) + require.NoError(t, err) + rawNef, err := cs.NEF.Bytes() + require.NoError(t, err) + tx := managementInvoker.PrepareInvoke(t, "deploy", rawNef, rawManifest) + e.AddNewBlock(t, tx) + e.CheckHalt(t, tx.Hash()) + cInvoker := e.ValidatorInvoker(cs.Hash) + + t.Run("good", func(t *testing.T) { + realBalance, _ := bc.GetGoverningTokenBalance(acc.ScriptHash()) + cInvoker.Invoke(t, stackitem.NewBigInteger(big.NewInt(realBalance.Int64()+1)), "callT0", acc.ScriptHash()) + }) + t.Run("invalid param count", func(t *testing.T) { + cInvoker.InvokeFail(t, "method not found: callT2/1", "callT2", acc.ScriptHash()) + }) + t.Run("invalid contract", func(t *testing.T) { + cInvoker.InvokeFail(t, "token contract 0000000000000000000000000000000000000000 not found: key not found", "callT1") + }) +} + +func TestSnapshotIsolation_Exceptions(t *testing.T) { + bc, acc := chain.NewSingle(t) + e := neotest.NewExecutor(t, bc, acc, acc) + + // Contract A puts value in the storage, emits notifications and panics. + srcA := `package contractA + import ( + "github.com/nspcc-dev/neo-go/pkg/interop/contract" + "github.com/nspcc-dev/neo-go/pkg/interop/runtime" + "github.com/nspcc-dev/neo-go/pkg/interop/storage" + ) + func DoAndPanic(key, value []byte, nNtf int) int { // avoid https://github.com/nspcc-dev/neo-go/issues/2509 + c := storage.GetContext() + storage.Put(c, key, value) + for i := 0; i < nNtf; i++ { + runtime.Notify("NotificationFromA", i) + } + panic("panic from A") + } + func CheckA(key []byte, nNtf int) bool { + c := storage.GetContext() + value := storage.Get(c, key) + // If called from B, then no storage changes made by A should be visible by this moment (they have been discarded after exception handling). + if value != nil { + return false + } + notifications := runtime.GetNotifications(nil) + if len(notifications) != nNtf { + return false + } + // If called from B, then no notifications made by A should be visible by this moment (they have been discarded after exception handling). + for i := 0; i < len(notifications); i++ { + ntf := notifications[i] + name := string(ntf[1].([]byte)) + if name == "NotificationFromA" { + return false + } + } + return true + } + func CheckB() bool { + return contract.Call(runtime.GetCallingScriptHash(), "checkStorageChanges", contract.All).(bool) + }` + ctrA := neotest.CompileSource(t, acc.ScriptHash(), strings.NewReader(srcA), &compiler.Options{ + NoEventsCheck: true, + NoPermissionsCheck: true, + Name: "contractA", + Permissions: []manifest.Permission{{Methods: manifest.WildStrings{Value: nil}}}, + }) + e.DeployContract(t, ctrA, nil) + + var hashAStr string + for i := 0; i < util.Uint160Size; i++ { + hashAStr += fmt.Sprintf("%#x", ctrA.Hash[i]) + if i != util.Uint160Size-1 { + hashAStr += ", " + } + } + // Contract B puts value in the storage, emits notifications and calls A either + // in try-catch block or without it. After that checks that proper notifications + // and storage changes are available from different contexts. + srcB := `package contractB + import ( + "github.com/nspcc-dev/neo-go/pkg/interop" + "github.com/nspcc-dev/neo-go/pkg/interop/contract" + "github.com/nspcc-dev/neo-go/pkg/interop/runtime" + "github.com/nspcc-dev/neo-go/pkg/interop/storage" + "github.com/nspcc-dev/neo-go/pkg/interop/util" + ) + var caughtKey = []byte("caught") + func DoAndCatch(shouldRecover bool, keyA, valueA, keyB, valueB []byte, nNtfA, nNtfB1, nNtfB2 int) { + if shouldRecover { + defer func() { + if r := recover(); r != nil { + keyA := []byte("keyA") // defer can not capture variables from outside + nNtfB1 := 2 + nNtfB2 := 4 + c := storage.GetContext() + storage.Put(c, caughtKey, []byte{}) + for i := 0; i < nNtfB2; i++ { + runtime.Notify("NotificationFromB after panic", i) + } + // Check that storage changes and notifications made by A are reverted. + ok := contract.Call(interop.Hash160{` + hashAStr + `}, "checkA", contract.All, keyA, nNtfB1+nNtfB2).(bool) + if !ok { + util.Abort() // should never ABORT if snapshot isolation is correctly implemented. + } + // Check that storage changes made by B after catch are still available in current context. + ok = CheckStorageChanges() + if !ok { + util.Abort() // should never ABORT if snapshot isolation is correctly implemented. + } + // Check that storage changes made by B after catch are still available from the outside context. + ok = contract.Call(interop.Hash160{` + hashAStr + `}, "checkB", contract.All).(bool) + if !ok { + util.Abort() // should never ABORT if snapshot isolation is correctly implemented. + } + } + }() + } + c := storage.GetContext() + storage.Put(c, keyB, valueB) + for i := 0; i < nNtfB1; i++ { + runtime.Notify("NotificationFromB before panic", i) + } + contract.Call(interop.Hash160{` + hashAStr + `}, "doAndPanic", contract.All, keyA, valueA, nNtfA) + } + func CheckStorageChanges() bool { + c := storage.GetContext() + itm := storage.Get(c, caughtKey) + return itm != nil + }` + ctrB := neotest.CompileSource(t, acc.ScriptHash(), strings.NewReader(srcB), &compiler.Options{ + Name: "contractB", + NoEventsCheck: true, + NoPermissionsCheck: true, + Permissions: []manifest.Permission{{Methods: manifest.WildStrings{Value: nil}}}, + }) + e.DeployContract(t, ctrB, nil) + + keyA := []byte("keyA") // hard-coded in the contract code due to `defer` inability to capture variables from outside. + valueA := []byte("valueA") // hard-coded in the contract code + keyB := []byte("keyB") + valueB := []byte("valueB") + nNtfA := 3 + nNtfBBeforePanic := 2 // hard-coded in the contract code + nNtfBAfterPanic := 4 // hard-coded in the contract code + ctrInvoker := e.NewInvoker(ctrB.Hash, e.Committee) + + // Firstly, do not catch exception and check that all notifications are presented in the notifications list. + h := ctrInvoker.InvokeFail(t, `unhandled exception: "panic from A"`, "doAndCatch", false, keyA, valueA, keyB, valueB, nNtfA, nNtfBBeforePanic, nNtfBAfterPanic) + aer := e.GetTxExecResult(t, h) + require.Equal(t, nNtfBBeforePanic+nNtfA, len(aer.Events)) + + // Then catch exception thrown by A and check that only notifications/storage changes from B are saved. + h = ctrInvoker.Invoke(t, stackitem.Null{}, "doAndCatch", true, keyA, valueA, keyB, valueB, nNtfA, nNtfBBeforePanic, nNtfBAfterPanic) + aer = e.GetTxExecResult(t, h) + require.Equal(t, nNtfBBeforePanic+nNtfBAfterPanic, len(aer.Events)) +} + +// This test is written to test nested calls with try-catch block and proper notifications handling. +func TestSnapshotIsolation_NestedContextException(t *testing.T) { + bc, acc := chain.NewSingle(t) + e := neotest.NewExecutor(t, bc, acc, acc) + + srcA := `package contractA + import ( + "github.com/nspcc-dev/neo-go/pkg/interop/contract" + "github.com/nspcc-dev/neo-go/pkg/interop/runtime" + ) + func CallA() { + runtime.Notify("Calling A") + contract.Call(runtime.GetExecutingScriptHash(), "a", contract.All) + runtime.Notify("Finish") + } + func A() { + defer func() { + if r := recover(); r != nil { + runtime.Notify("Caught") + } + }() + runtime.Notify("A") + contract.Call(runtime.GetExecutingScriptHash(), "b", contract.All) + runtime.Notify("Unreachable A") + } + func B() int { + runtime.Notify("B") + contract.Call(runtime.GetExecutingScriptHash(), "c", contract.All) + runtime.Notify("Unreachable B") + return 5 + } + func C() { + runtime.Notify("C") + panic("exception from C") + }` + ctrA := neotest.CompileSource(t, acc.ScriptHash(), strings.NewReader(srcA), &compiler.Options{ + NoEventsCheck: true, + NoPermissionsCheck: true, + Name: "contractA", + Permissions: []manifest.Permission{{Methods: manifest.WildStrings{Value: nil}}}, + }) + e.DeployContract(t, ctrA, nil) + + ctrInvoker := e.NewInvoker(ctrA.Hash, e.Committee) + h := ctrInvoker.Invoke(t, stackitem.Null{}, "callA") + aer := e.GetTxExecResult(t, h) + require.Equal(t, 4, len(aer.Events)) + require.Equal(t, "Calling A", aer.Events[0].Name) + require.Equal(t, "A", aer.Events[1].Name) + require.Equal(t, "Caught", aer.Events[2].Name) + require.Equal(t, "Finish", aer.Events[3].Name) +} + +// This test is written to avoid https://github.com/neo-project/neo/issues/2746. +func TestSnapshotIsolation_CallToItself(t *testing.T) { + bc, acc := chain.NewSingle(t) + e := neotest.NewExecutor(t, bc, acc, acc) + + // Contract A calls method of self and throws if storage changes made by Do are unavailable after call to it. + srcA := `package contractA + import ( + "github.com/nspcc-dev/neo-go/pkg/interop/contract" + "github.com/nspcc-dev/neo-go/pkg/interop/runtime" + "github.com/nspcc-dev/neo-go/pkg/interop/storage" + ) + var key = []byte("key") + func Test() { + contract.Call(runtime.GetExecutingScriptHash(), "callMyselfAndCheck", contract.All) + } + func CallMyselfAndCheck() { + contract.Call(runtime.GetExecutingScriptHash(), "do", contract.All) + c := storage.GetContext() + val := storage.Get(c, key) + if val == nil { + panic("changes from previous context were not persisted") + } + } + func Do() { + c := storage.GetContext() + storage.Put(c, key, []byte("value")) + } + func Check() { + c := storage.GetContext() + val := storage.Get(c, key) + if val == nil { + panic("value is nil") + } + } +` + ctrA := neotest.CompileSource(t, acc.ScriptHash(), strings.NewReader(srcA), &compiler.Options{ + NoEventsCheck: true, + NoPermissionsCheck: true, + Name: "contractA", + Permissions: []manifest.Permission{{Methods: manifest.WildStrings{Value: nil}}}, + }) + e.DeployContract(t, ctrA, nil) + + ctrInvoker := e.NewInvoker(ctrA.Hash, e.Committee) + ctrInvoker.Invoke(t, stackitem.Null{}, "test") + + // A separate call is needed to check whether all VM contexts were properly + // unwrapped and persisted during the previous call. + ctrInvoker.Invoke(t, stackitem.Null{}, "check") +} + +// This test is written to check https://github.com/nspcc-dev/neo-go/issues/2509 +// and https://github.com/neo-project/neo/pull/2745#discussion_r879167180. +func TestRET_after_FINALLY_PanicInsideVoidMethod(t *testing.T) { + bc, acc := chain.NewSingle(t) + e := neotest.NewExecutor(t, bc, acc, acc) + + // Contract A throws catchable exception. It also has a non-void method. + srcA := `package contractA + func Panic() { + panic("panic from A") + } + func ReturnSomeValue() int { + return 5 + }` + ctrA := neotest.CompileSource(t, acc.ScriptHash(), strings.NewReader(srcA), &compiler.Options{ + NoEventsCheck: true, + NoPermissionsCheck: true, + Name: "contractA", + }) + e.DeployContract(t, ctrA, nil) + + var hashAStr string + for i := 0; i < util.Uint160Size; i++ { + hashAStr += fmt.Sprintf("%#x", ctrA.Hash[i]) + if i != util.Uint160Size-1 { + hashAStr += ", " + } + } + // Contract B calls A and catches the exception thrown by A. + srcB := `package contractB + import ( + "github.com/nspcc-dev/neo-go/pkg/interop" + "github.com/nspcc-dev/neo-go/pkg/interop/contract" + ) + func Catch() { + defer func() { + if r := recover(); r != nil { + // Call method with return value to check https://github.com/neo-project/neo/pull/2745#discussion_r879167180. + contract.Call(interop.Hash160{` + hashAStr + `}, "returnSomeValue", contract.All) + } + }() + contract.Call(interop.Hash160{` + hashAStr + `}, "panic", contract.All) + }` + ctrB := neotest.CompileSource(t, acc.ScriptHash(), strings.NewReader(srcB), &compiler.Options{ + Name: "contractB", + NoEventsCheck: true, + NoPermissionsCheck: true, + Permissions: []manifest.Permission{ + { + Methods: manifest.WildStrings{Value: nil}, + }, + }, + }) + e.DeployContract(t, ctrB, nil) + + ctrInvoker := e.NewInvoker(ctrB.Hash, e.Committee) + ctrInvoker.Invoke(t, stackitem.Null{}, "catch") +} + +// This test is written to check https://github.com/neo-project/neo/pull/2745#discussion_r879125733. +func TestRET_after_FINALLY_CallNonVoidAfterVoidMethod(t *testing.T) { + bc, acc := chain.NewSingle(t) + e := neotest.NewExecutor(t, bc, acc, acc) + + // Contract A has two methods. One of them has no return value, and the other has it. + srcA := `package contractA + import "github.com/nspcc-dev/neo-go/pkg/interop/runtime" + func NoRet() { + runtime.Notify("no ret") + } + func HasRet() int { + runtime.Notify("ret") + return 5 + }` + ctrA := neotest.CompileSource(t, acc.ScriptHash(), strings.NewReader(srcA), &compiler.Options{ + NoEventsCheck: true, + NoPermissionsCheck: true, + Name: "contractA", + }) + e.DeployContract(t, ctrA, nil) + + var hashAStr string + for i := 0; i < util.Uint160Size; i++ { + hashAStr += fmt.Sprintf("%#x", ctrA.Hash[i]) + if i != util.Uint160Size-1 { + hashAStr += ", " + } + } + // Contract B calls A in try-catch block. + srcB := `package contractB + import ( + "github.com/nspcc-dev/neo-go/pkg/interop" + "github.com/nspcc-dev/neo-go/pkg/interop/contract" + "github.com/nspcc-dev/neo-go/pkg/interop/util" + ) + func CallAInTryCatch() { + defer func() { + if r := recover(); r != nil { + util.Abort() // should never happen + } + }() + contract.Call(interop.Hash160{` + hashAStr + `}, "noRet", contract.All) + contract.Call(interop.Hash160{` + hashAStr + `}, "hasRet", contract.All) + }` + ctrB := neotest.CompileSource(t, acc.ScriptHash(), strings.NewReader(srcB), &compiler.Options{ + Name: "contractB", + NoEventsCheck: true, + NoPermissionsCheck: true, + Permissions: []manifest.Permission{ + { + Methods: manifest.WildStrings{Value: nil}, + }, + }, + }) + e.DeployContract(t, ctrB, nil) + + ctrInvoker := e.NewInvoker(ctrB.Hash, e.Committee) + h := ctrInvoker.Invoke(t, stackitem.Null{}, "callAInTryCatch") + aer := e.GetTxExecResult(t, h) + + require.Equal(t, 1, len(aer.Stack)) +} + +// This test is created to check https://github.com/neo-project/neo/pull/2755#discussion_r880087983. +func TestCALLL_from_VoidContext(t *testing.T) { + bc, acc := chain.NewSingle(t) + e := neotest.NewExecutor(t, bc, acc, acc) + + // Contract A has void method `CallHasRet` which calls non-void method `HasRet`. + srcA := `package contractA + func CallHasRet() { // Creates a context with non-nil onUnload. + HasRet() + } + func HasRet() int { // CALL_L clones parent context, check that onUnload is not cloned. + return 5 + }` + ctrA := neotest.CompileSource(t, acc.ScriptHash(), strings.NewReader(srcA), &compiler.Options{ + NoEventsCheck: true, + NoPermissionsCheck: true, + Name: "contractA", + }) + e.DeployContract(t, ctrA, nil) + + ctrInvoker := e.NewInvoker(ctrA.Hash, e.Committee) + ctrInvoker.Invoke(t, stackitem.Null{}, "callHasRet") +} + func loadScript(ic *interop.Context, script []byte, args ...interface{}) { ic.SpawnVM() ic.VM.LoadScriptWithFlags(script, callflag.AllowCall) diff --git a/pkg/core/interop_system_neotest_test.go b/pkg/core/interop_system_neotest_test.go deleted file mode 100644 index 25f125ebc..000000000 --- a/pkg/core/interop_system_neotest_test.go +++ /dev/null @@ -1,500 +0,0 @@ -package core_test - -import ( - "encoding/json" - "fmt" - "math/big" - "strings" - "testing" - - "github.com/nspcc-dev/neo-go/internal/contracts" - "github.com/nspcc-dev/neo-go/pkg/compiler" - "github.com/nspcc-dev/neo-go/pkg/config" - "github.com/nspcc-dev/neo-go/pkg/core/interop/interopnames" - "github.com/nspcc-dev/neo-go/pkg/core/native/nativenames" - "github.com/nspcc-dev/neo-go/pkg/core/transaction" - "github.com/nspcc-dev/neo-go/pkg/crypto/keys" - "github.com/nspcc-dev/neo-go/pkg/io" - "github.com/nspcc-dev/neo-go/pkg/neotest" - "github.com/nspcc-dev/neo-go/pkg/neotest/chain" - "github.com/nspcc-dev/neo-go/pkg/smartcontract/manifest" - "github.com/nspcc-dev/neo-go/pkg/util" - "github.com/nspcc-dev/neo-go/pkg/util/slice" - "github.com/nspcc-dev/neo-go/pkg/vm/emit" - "github.com/nspcc-dev/neo-go/pkg/vm/stackitem" - "github.com/stretchr/testify/require" -) - -func TestLoadToken(t *testing.T) { - bc, acc := chain.NewSingle(t) - e := neotest.NewExecutor(t, bc, acc, acc) - managementInvoker := e.ValidatorInvoker(e.NativeHash(t, nativenames.Management)) - - cs, _ := contracts.GetTestContractState(t, pathToInternalContracts, 0, 1, acc.ScriptHash()) - rawManifest, err := json.Marshal(cs.Manifest) - require.NoError(t, err) - rawNef, err := cs.NEF.Bytes() - require.NoError(t, err) - tx := managementInvoker.PrepareInvoke(t, "deploy", rawNef, rawManifest) - e.AddNewBlock(t, tx) - e.CheckHalt(t, tx.Hash()) - cInvoker := e.ValidatorInvoker(cs.Hash) - - t.Run("good", func(t *testing.T) { - realBalance, _ := bc.GetGoverningTokenBalance(acc.ScriptHash()) - cInvoker.Invoke(t, stackitem.NewBigInteger(big.NewInt(realBalance.Int64()+1)), "callT0", acc.ScriptHash()) - }) - t.Run("invalid param count", func(t *testing.T) { - cInvoker.InvokeFail(t, "method not found: callT2/1", "callT2", acc.ScriptHash()) - }) - t.Run("invalid contract", func(t *testing.T) { - cInvoker.InvokeFail(t, "token contract 0000000000000000000000000000000000000000 not found: key not found", "callT1") - }) -} - -func TestSystemContractCreateAccount_Hardfork(t *testing.T) { - bc, acc := chain.NewSingleWithCustomConfig(t, func(c *config.ProtocolConfiguration) { - c.P2PSigExtensions = true // `basicchain.Init` requires Notary enabled - c.Hardforks = map[string]uint32{ - config.HFAspidochelone.String(): 2, - } - }) - e := neotest.NewExecutor(t, bc, acc, acc) - - priv, err := keys.NewPrivateKey() - require.NoError(t, err) - pub := priv.PublicKey() - - w := io.NewBufBinWriter() - emit.Array(w.BinWriter, []interface{}{pub.Bytes(), pub.Bytes(), pub.Bytes()}...) - emit.Int(w.BinWriter, int64(2)) - emit.Syscall(w.BinWriter, interopnames.SystemContractCreateMultisigAccount) - require.NoError(t, w.Err) - multisigScript := slice.Copy(w.Bytes()) - - w.Reset() - emit.Bytes(w.BinWriter, pub.Bytes()) - emit.Syscall(w.BinWriter, interopnames.SystemContractCreateStandardAccount) - require.NoError(t, w.Err) - standardScript := slice.Copy(w.Bytes()) - - createAccTx := func(t *testing.T, script []byte) *transaction.Transaction { - tx := e.PrepareInvocation(t, script, []neotest.Signer{e.Committee}, bc.BlockHeight()+1) - return tx - } - - // blocks #1, #2: old prices - tx1Standard := createAccTx(t, standardScript) - tx1Multisig := createAccTx(t, multisigScript) - e.AddNewBlock(t, tx1Standard, tx1Multisig) - e.CheckHalt(t, tx1Standard.Hash()) - e.CheckHalt(t, tx1Multisig.Hash()) - tx2Standard := createAccTx(t, standardScript) - tx2Multisig := createAccTx(t, multisigScript) - e.AddNewBlock(t, tx2Standard, tx2Multisig) - e.CheckHalt(t, tx2Standard.Hash()) - e.CheckHalt(t, tx2Multisig.Hash()) - - // block #3: updated prices (larger than the previous ones) - tx3Standard := createAccTx(t, standardScript) - tx3Multisig := createAccTx(t, multisigScript) - e.AddNewBlock(t, tx3Standard, tx3Multisig) - e.CheckHalt(t, tx3Standard.Hash()) - e.CheckHalt(t, tx3Multisig.Hash()) - require.True(t, tx1Standard.SystemFee == tx2Standard.SystemFee) - require.True(t, tx1Multisig.SystemFee == tx2Multisig.SystemFee) - require.True(t, tx2Standard.SystemFee < tx3Standard.SystemFee) - require.True(t, tx2Multisig.SystemFee < tx3Multisig.SystemFee) -} - -func TestSnapshotIsolation_Exceptions(t *testing.T) { - bc, acc := chain.NewSingle(t) - e := neotest.NewExecutor(t, bc, acc, acc) - - // Contract A puts value in the storage, emits notifications and panics. - srcA := `package contractA - import ( - "github.com/nspcc-dev/neo-go/pkg/interop/contract" - "github.com/nspcc-dev/neo-go/pkg/interop/runtime" - "github.com/nspcc-dev/neo-go/pkg/interop/storage" - ) - func DoAndPanic(key, value []byte, nNtf int) int { // avoid https://github.com/nspcc-dev/neo-go/issues/2509 - c := storage.GetContext() - storage.Put(c, key, value) - for i := 0; i < nNtf; i++ { - runtime.Notify("NotificationFromA", i) - } - panic("panic from A") - } - func CheckA(key []byte, nNtf int) bool { - c := storage.GetContext() - value := storage.Get(c, key) - // If called from B, then no storage changes made by A should be visible by this moment (they have been discarded after exception handling). - if value != nil { - return false - } - notifications := runtime.GetNotifications(nil) - if len(notifications) != nNtf { - return false - } - // If called from B, then no notifications made by A should be visible by this moment (they have been discarded after exception handling). - for i := 0; i < len(notifications); i++ { - ntf := notifications[i] - name := string(ntf[1].([]byte)) - if name == "NotificationFromA" { - return false - } - } - return true - } - func CheckB() bool { - return contract.Call(runtime.GetCallingScriptHash(), "checkStorageChanges", contract.All).(bool) - }` - ctrA := neotest.CompileSource(t, acc.ScriptHash(), strings.NewReader(srcA), &compiler.Options{ - NoEventsCheck: true, - NoPermissionsCheck: true, - Name: "contractA", - Permissions: []manifest.Permission{{Methods: manifest.WildStrings{Value: nil}}}, - }) - e.DeployContract(t, ctrA, nil) - - var hashAStr string - for i := 0; i < util.Uint160Size; i++ { - hashAStr += fmt.Sprintf("%#x", ctrA.Hash[i]) - if i != util.Uint160Size-1 { - hashAStr += ", " - } - } - // Contract B puts value in the storage, emits notifications and calls A either - // in try-catch block or without it. After that checks that proper notifications - // and storage changes are available from different contexts. - srcB := `package contractB - import ( - "github.com/nspcc-dev/neo-go/pkg/interop" - "github.com/nspcc-dev/neo-go/pkg/interop/contract" - "github.com/nspcc-dev/neo-go/pkg/interop/runtime" - "github.com/nspcc-dev/neo-go/pkg/interop/storage" - "github.com/nspcc-dev/neo-go/pkg/interop/util" - ) - var caughtKey = []byte("caught") - func DoAndCatch(shouldRecover bool, keyA, valueA, keyB, valueB []byte, nNtfA, nNtfB1, nNtfB2 int) { - if shouldRecover { - defer func() { - if r := recover(); r != nil { - keyA := []byte("keyA") // defer can not capture variables from outside - nNtfB1 := 2 - nNtfB2 := 4 - c := storage.GetContext() - storage.Put(c, caughtKey, []byte{}) - for i := 0; i < nNtfB2; i++ { - runtime.Notify("NotificationFromB after panic", i) - } - // Check that storage changes and notifications made by A are reverted. - ok := contract.Call(interop.Hash160{` + hashAStr + `}, "checkA", contract.All, keyA, nNtfB1+nNtfB2).(bool) - if !ok { - util.Abort() // should never ABORT if snapshot isolation is correctly implemented. - } - // Check that storage changes made by B after catch are still available in current context. - ok = CheckStorageChanges() - if !ok { - util.Abort() // should never ABORT if snapshot isolation is correctly implemented. - } - // Check that storage changes made by B after catch are still available from the outside context. - ok = contract.Call(interop.Hash160{` + hashAStr + `}, "checkB", contract.All).(bool) - if !ok { - util.Abort() // should never ABORT if snapshot isolation is correctly implemented. - } - } - }() - } - c := storage.GetContext() - storage.Put(c, keyB, valueB) - for i := 0; i < nNtfB1; i++ { - runtime.Notify("NotificationFromB before panic", i) - } - contract.Call(interop.Hash160{` + hashAStr + `}, "doAndPanic", contract.All, keyA, valueA, nNtfA) - } - func CheckStorageChanges() bool { - c := storage.GetContext() - itm := storage.Get(c, caughtKey) - return itm != nil - }` - ctrB := neotest.CompileSource(t, acc.ScriptHash(), strings.NewReader(srcB), &compiler.Options{ - Name: "contractB", - NoEventsCheck: true, - NoPermissionsCheck: true, - Permissions: []manifest.Permission{{Methods: manifest.WildStrings{Value: nil}}}, - }) - e.DeployContract(t, ctrB, nil) - - keyA := []byte("keyA") // hard-coded in the contract code due to `defer` inability to capture variables from outside. - valueA := []byte("valueA") // hard-coded in the contract code - keyB := []byte("keyB") - valueB := []byte("valueB") - nNtfA := 3 - nNtfBBeforePanic := 2 // hard-coded in the contract code - nNtfBAfterPanic := 4 // hard-coded in the contract code - ctrInvoker := e.NewInvoker(ctrB.Hash, e.Committee) - - // Firstly, do not catch exception and check that all notifications are presented in the notifications list. - h := ctrInvoker.InvokeFail(t, `unhandled exception: "panic from A"`, "doAndCatch", false, keyA, valueA, keyB, valueB, nNtfA, nNtfBBeforePanic, nNtfBAfterPanic) - aer := e.GetTxExecResult(t, h) - require.Equal(t, nNtfBBeforePanic+nNtfA, len(aer.Events)) - - // Then catch exception thrown by A and check that only notifications/storage changes from B are saved. - h = ctrInvoker.Invoke(t, stackitem.Null{}, "doAndCatch", true, keyA, valueA, keyB, valueB, nNtfA, nNtfBBeforePanic, nNtfBAfterPanic) - aer = e.GetTxExecResult(t, h) - require.Equal(t, nNtfBBeforePanic+nNtfBAfterPanic, len(aer.Events)) -} - -// This test is written to test nested calls with try-catch block and proper notifications handling. -func TestSnapshotIsolation_NestedContextException(t *testing.T) { - bc, acc := chain.NewSingle(t) - e := neotest.NewExecutor(t, bc, acc, acc) - - srcA := `package contractA - import ( - "github.com/nspcc-dev/neo-go/pkg/interop/contract" - "github.com/nspcc-dev/neo-go/pkg/interop/runtime" - ) - func CallA() { - runtime.Notify("Calling A") - contract.Call(runtime.GetExecutingScriptHash(), "a", contract.All) - runtime.Notify("Finish") - } - func A() { - defer func() { - if r := recover(); r != nil { - runtime.Notify("Caught") - } - }() - runtime.Notify("A") - contract.Call(runtime.GetExecutingScriptHash(), "b", contract.All) - runtime.Notify("Unreachable A") - } - func B() int { - runtime.Notify("B") - contract.Call(runtime.GetExecutingScriptHash(), "c", contract.All) - runtime.Notify("Unreachable B") - return 5 - } - func C() { - runtime.Notify("C") - panic("exception from C") - }` - ctrA := neotest.CompileSource(t, acc.ScriptHash(), strings.NewReader(srcA), &compiler.Options{ - NoEventsCheck: true, - NoPermissionsCheck: true, - Name: "contractA", - Permissions: []manifest.Permission{{Methods: manifest.WildStrings{Value: nil}}}, - }) - e.DeployContract(t, ctrA, nil) - - ctrInvoker := e.NewInvoker(ctrA.Hash, e.Committee) - h := ctrInvoker.Invoke(t, stackitem.Null{}, "callA") - aer := e.GetTxExecResult(t, h) - require.Equal(t, 4, len(aer.Events)) - require.Equal(t, "Calling A", aer.Events[0].Name) - require.Equal(t, "A", aer.Events[1].Name) - require.Equal(t, "Caught", aer.Events[2].Name) - require.Equal(t, "Finish", aer.Events[3].Name) -} - -// This test is written to avoid https://github.com/neo-project/neo/issues/2746. -func TestSnapshotIsolation_CallToItself(t *testing.T) { - bc, acc := chain.NewSingle(t) - e := neotest.NewExecutor(t, bc, acc, acc) - - // Contract A calls method of self and throws if storage changes made by Do are unavailable after call to it. - srcA := `package contractA - import ( - "github.com/nspcc-dev/neo-go/pkg/interop/contract" - "github.com/nspcc-dev/neo-go/pkg/interop/runtime" - "github.com/nspcc-dev/neo-go/pkg/interop/storage" - ) - var key = []byte("key") - func Test() { - contract.Call(runtime.GetExecutingScriptHash(), "callMyselfAndCheck", contract.All) - } - func CallMyselfAndCheck() { - contract.Call(runtime.GetExecutingScriptHash(), "do", contract.All) - c := storage.GetContext() - val := storage.Get(c, key) - if val == nil { - panic("changes from previous context were not persisted") - } - } - func Do() { - c := storage.GetContext() - storage.Put(c, key, []byte("value")) - } - func Check() { - c := storage.GetContext() - val := storage.Get(c, key) - if val == nil { - panic("value is nil") - } - } -` - ctrA := neotest.CompileSource(t, acc.ScriptHash(), strings.NewReader(srcA), &compiler.Options{ - NoEventsCheck: true, - NoPermissionsCheck: true, - Name: "contractA", - Permissions: []manifest.Permission{{Methods: manifest.WildStrings{Value: nil}}}, - }) - e.DeployContract(t, ctrA, nil) - - ctrInvoker := e.NewInvoker(ctrA.Hash, e.Committee) - ctrInvoker.Invoke(t, stackitem.Null{}, "test") - - // A separate call is needed to check whether all VM contexts were properly - // unwrapped and persisted during the previous call. - ctrInvoker.Invoke(t, stackitem.Null{}, "check") -} - -// This test is written to check https://github.com/nspcc-dev/neo-go/issues/2509 -// and https://github.com/neo-project/neo/pull/2745#discussion_r879167180. -func TestRET_after_FINALLY_PanicInsideVoidMethod(t *testing.T) { - bc, acc := chain.NewSingle(t) - e := neotest.NewExecutor(t, bc, acc, acc) - - // Contract A throws catchable exception. It also has a non-void method. - srcA := `package contractA - func Panic() { - panic("panic from A") - } - func ReturnSomeValue() int { - return 5 - }` - ctrA := neotest.CompileSource(t, acc.ScriptHash(), strings.NewReader(srcA), &compiler.Options{ - NoEventsCheck: true, - NoPermissionsCheck: true, - Name: "contractA", - }) - e.DeployContract(t, ctrA, nil) - - var hashAStr string - for i := 0; i < util.Uint160Size; i++ { - hashAStr += fmt.Sprintf("%#x", ctrA.Hash[i]) - if i != util.Uint160Size-1 { - hashAStr += ", " - } - } - // Contract B calls A and catches the exception thrown by A. - srcB := `package contractB - import ( - "github.com/nspcc-dev/neo-go/pkg/interop" - "github.com/nspcc-dev/neo-go/pkg/interop/contract" - ) - func Catch() { - defer func() { - if r := recover(); r != nil { - // Call method with return value to check https://github.com/neo-project/neo/pull/2745#discussion_r879167180. - contract.Call(interop.Hash160{` + hashAStr + `}, "returnSomeValue", contract.All) - } - }() - contract.Call(interop.Hash160{` + hashAStr + `}, "panic", contract.All) - }` - ctrB := neotest.CompileSource(t, acc.ScriptHash(), strings.NewReader(srcB), &compiler.Options{ - Name: "contractB", - NoEventsCheck: true, - NoPermissionsCheck: true, - Permissions: []manifest.Permission{ - { - Methods: manifest.WildStrings{Value: nil}, - }, - }, - }) - e.DeployContract(t, ctrB, nil) - - ctrInvoker := e.NewInvoker(ctrB.Hash, e.Committee) - ctrInvoker.Invoke(t, stackitem.Null{}, "catch") -} - -// This test is written to check https://github.com/neo-project/neo/pull/2745#discussion_r879125733. -func TestRET_after_FINALLY_CallNonVoidAfterVoidMethod(t *testing.T) { - bc, acc := chain.NewSingle(t) - e := neotest.NewExecutor(t, bc, acc, acc) - - // Contract A has two methods. One of them has no return value, and the other has it. - srcA := `package contractA - import "github.com/nspcc-dev/neo-go/pkg/interop/runtime" - func NoRet() { - runtime.Notify("no ret") - } - func HasRet() int { - runtime.Notify("ret") - return 5 - }` - ctrA := neotest.CompileSource(t, acc.ScriptHash(), strings.NewReader(srcA), &compiler.Options{ - NoEventsCheck: true, - NoPermissionsCheck: true, - Name: "contractA", - }) - e.DeployContract(t, ctrA, nil) - - var hashAStr string - for i := 0; i < util.Uint160Size; i++ { - hashAStr += fmt.Sprintf("%#x", ctrA.Hash[i]) - if i != util.Uint160Size-1 { - hashAStr += ", " - } - } - // Contract B calls A in try-catch block. - srcB := `package contractB - import ( - "github.com/nspcc-dev/neo-go/pkg/interop" - "github.com/nspcc-dev/neo-go/pkg/interop/contract" - "github.com/nspcc-dev/neo-go/pkg/interop/util" - ) - func CallAInTryCatch() { - defer func() { - if r := recover(); r != nil { - util.Abort() // should never happen - } - }() - contract.Call(interop.Hash160{` + hashAStr + `}, "noRet", contract.All) - contract.Call(interop.Hash160{` + hashAStr + `}, "hasRet", contract.All) - }` - ctrB := neotest.CompileSource(t, acc.ScriptHash(), strings.NewReader(srcB), &compiler.Options{ - Name: "contractB", - NoEventsCheck: true, - NoPermissionsCheck: true, - Permissions: []manifest.Permission{ - { - Methods: manifest.WildStrings{Value: nil}, - }, - }, - }) - e.DeployContract(t, ctrB, nil) - - ctrInvoker := e.NewInvoker(ctrB.Hash, e.Committee) - h := ctrInvoker.Invoke(t, stackitem.Null{}, "callAInTryCatch") - aer := e.GetTxExecResult(t, h) - - require.Equal(t, 1, len(aer.Stack)) -} - -// This test is created to check https://github.com/neo-project/neo/pull/2755#discussion_r880087983. -func TestCALLL_from_VoidContext(t *testing.T) { - bc, acc := chain.NewSingle(t) - e := neotest.NewExecutor(t, bc, acc, acc) - - // Contract A has void method `CallHasRet` which calls non-void method `HasRet`. - srcA := `package contractA - func CallHasRet() { // Creates a context with non-nil onUnload. - HasRet() - } - func HasRet() int { // CALL_L clones parent context, check that onUnload is not cloned. - return 5 - }` - ctrA := neotest.CompileSource(t, acc.ScriptHash(), strings.NewReader(srcA), &compiler.Options{ - NoEventsCheck: true, - NoPermissionsCheck: true, - Name: "contractA", - }) - e.DeployContract(t, ctrA, nil) - - ctrInvoker := e.NewInvoker(ctrA.Hash, e.Committee) - ctrInvoker.Invoke(t, stackitem.Null{}, "callHasRet") -} From 2086bca303cfb3db1706b716a56919299786947f Mon Sep 17 00:00:00 2001 From: Roman Khimov Date: Wed, 8 Jun 2022 19:31:49 +0300 Subject: [PATCH 12/21] core: move storage-related interop code into the storage package --- pkg/core/interop/storage/basic.go | 139 ++++++++++++ pkg/core/interop/storage/bench_test.go | 113 ++++++++++ pkg/core/interop/storage/find.go | 46 ++++ .../{ => interop/storage}/interops_test.go | 28 +-- .../storage/storage_test.go} | 197 +++++------------- pkg/core/interop_system.go | 181 ---------------- pkg/core/interops.go | 15 +- 7 files changed, 364 insertions(+), 355 deletions(-) create mode 100644 pkg/core/interop/storage/basic.go create mode 100644 pkg/core/interop/storage/bench_test.go rename pkg/core/{ => interop/storage}/interops_test.go (53%) rename pkg/core/{interop_system_core_test.go => interop/storage/storage_test.go} (62%) delete mode 100644 pkg/core/interop_system.go diff --git a/pkg/core/interop/storage/basic.go b/pkg/core/interop/storage/basic.go new file mode 100644 index 000000000..dcb6771c8 --- /dev/null +++ b/pkg/core/interop/storage/basic.go @@ -0,0 +1,139 @@ +package storage + +import ( + "errors" + "fmt" + + "github.com/nspcc-dev/neo-go/pkg/core/interop" + "github.com/nspcc-dev/neo-go/pkg/core/storage" + "github.com/nspcc-dev/neo-go/pkg/vm/stackitem" +) + +var ( + // ErrGasLimitExceeded is returned from interops when there is not enough + // GAS left in the execution context to complete the action. + ErrGasLimitExceeded = errors.New("gas limit exceeded") + errFindInvalidOptions = errors.New("invalid Find options") +) + +// Context contains contract ID and read/write flag, it's used as +// a context for storage manipulation functions. +type Context struct { + ID int32 + ReadOnly bool +} + +// storageDelete deletes stored key-value pair. +func Delete(ic *interop.Context) error { + stcInterface := ic.VM.Estack().Pop().Value() + stc, ok := stcInterface.(*Context) + if !ok { + return fmt.Errorf("%T is not a storage.Context", stcInterface) + } + if stc.ReadOnly { + return errors.New("storage.Context is read only") + } + key := ic.VM.Estack().Pop().Bytes() + ic.DAO.DeleteStorageItem(stc.ID, key) + return nil +} + +// Get returns stored key-value pair. +func Get(ic *interop.Context) error { + stcInterface := ic.VM.Estack().Pop().Value() + stc, ok := stcInterface.(*Context) + if !ok { + return fmt.Errorf("%T is not a storage.Context", stcInterface) + } + key := ic.VM.Estack().Pop().Bytes() + si := ic.DAO.GetStorageItem(stc.ID, key) + if si != nil { + ic.VM.Estack().PushItem(stackitem.NewByteArray([]byte(si))) + } else { + ic.VM.Estack().PushItem(stackitem.Null{}) + } + return nil +} + +// GetContext returns storage context for the currently executing contract. +func GetContext(ic *interop.Context) error { + return getContextInternal(ic, false) +} + +// GetReadOnlyContext returns read-only storage context for the currently executing contract. +func GetReadOnlyContext(ic *interop.Context) error { + return getContextInternal(ic, true) +} + +// getContextInternal is internal version of storageGetContext and +// storageGetReadOnlyContext which allows to specify ReadOnly context flag. +func getContextInternal(ic *interop.Context, isReadOnly bool) error { + contract, err := ic.GetContract(ic.VM.GetCurrentScriptHash()) + if err != nil { + return err + } + sc := &Context{ + ID: contract.ID, + ReadOnly: isReadOnly, + } + ic.VM.Estack().PushItem(stackitem.NewInterop(sc)) + return nil +} + +func putWithContext(ic *interop.Context, stc *Context, key []byte, value []byte) error { + if len(key) > storage.MaxStorageKeyLen { + return errors.New("key is too big") + } + if len(value) > storage.MaxStorageValueLen { + return errors.New("value is too big") + } + if stc.ReadOnly { + return errors.New("storage.Context is read only") + } + si := ic.DAO.GetStorageItem(stc.ID, key) + sizeInc := len(value) + if si == nil { + sizeInc = len(key) + len(value) + } else if len(value) != 0 { + if len(value) <= len(si) { + sizeInc = (len(value)-1)/4 + 1 + } else if len(si) != 0 { + sizeInc = (len(si)-1)/4 + 1 + len(value) - len(si) + } + } + if !ic.VM.AddGas(int64(sizeInc) * ic.BaseStorageFee()) { + return ErrGasLimitExceeded + } + ic.DAO.PutStorageItem(stc.ID, key, value) + return nil +} + +// Put puts key-value pair into the storage. +func Put(ic *interop.Context) error { + stcInterface := ic.VM.Estack().Pop().Value() + stc, ok := stcInterface.(*Context) + if !ok { + return fmt.Errorf("%T is not a storage.Context", stcInterface) + } + key := ic.VM.Estack().Pop().Bytes() + value := ic.VM.Estack().Pop().Bytes() + return putWithContext(ic, stc, key, value) +} + +// ContextAsReadOnly sets given context to read-only mode. +func ContextAsReadOnly(ic *interop.Context) error { + stcInterface := ic.VM.Estack().Pop().Value() + stc, ok := stcInterface.(*Context) + if !ok { + return fmt.Errorf("%T is not a storage.Context", stcInterface) + } + if !stc.ReadOnly { + stx := &Context{ + ID: stc.ID, + ReadOnly: true, + } + stc = stx + } + ic.VM.Estack().PushItem(stackitem.NewInterop(stc)) + return nil +} diff --git a/pkg/core/interop/storage/bench_test.go b/pkg/core/interop/storage/bench_test.go new file mode 100644 index 000000000..ee64de6b4 --- /dev/null +++ b/pkg/core/interop/storage/bench_test.go @@ -0,0 +1,113 @@ +package storage_test + +import ( + "fmt" + "testing" + + "github.com/nspcc-dev/neo-go/internal/random" + "github.com/nspcc-dev/neo-go/pkg/core/interop/iterator" + istorage "github.com/nspcc-dev/neo-go/pkg/core/interop/storage" + "github.com/nspcc-dev/neo-go/pkg/core/native" + "github.com/nspcc-dev/neo-go/pkg/core/state" + "github.com/nspcc-dev/neo-go/pkg/vm/stackitem" + "github.com/stretchr/testify/require" +) + +func BenchmarkStorageFind(b *testing.B) { + for count := 10; count <= 10000; count *= 10 { + b.Run(fmt.Sprintf("%dElements", count), func(b *testing.B) { + v, contractState, context, _ := createVMAndContractState(b) + require.NoError(b, native.PutContractState(context.DAO, contractState)) + + items := make(map[string]state.StorageItem) + for i := 0; i < count; i++ { + items["abc"+random.String(10)] = random.Bytes(10) + } + for k, v := range items { + context.DAO.PutStorageItem(contractState.ID, []byte(k), v) + context.DAO.PutStorageItem(contractState.ID+1, []byte(k), v) + } + changes, err := context.DAO.Persist() + require.NoError(b, err) + require.NotEqual(b, 0, changes) + + b.ResetTimer() + b.ReportAllocs() + for i := 0; i < b.N; i++ { + b.StopTimer() + v.Estack().PushVal(istorage.FindDefault) + v.Estack().PushVal("abc") + v.Estack().PushVal(stackitem.NewInterop(&istorage.Context{ID: contractState.ID})) + b.StartTimer() + err := istorage.Find(context) + if err != nil { + b.FailNow() + } + b.StopTimer() + context.Finalize() + } + }) + } +} + +func BenchmarkStorageFindIteratorNext(b *testing.B) { + for count := 10; count <= 10000; count *= 10 { + cases := map[string]int{ + "Pick1": 1, + "PickHalf": count / 2, + "PickAll": count, + } + b.Run(fmt.Sprintf("%dElements", count), func(b *testing.B) { + for name, last := range cases { + b.Run(name, func(b *testing.B) { + v, contractState, context, _ := createVMAndContractState(b) + require.NoError(b, native.PutContractState(context.DAO, contractState)) + + items := make(map[string]state.StorageItem) + for i := 0; i < count; i++ { + items["abc"+random.String(10)] = random.Bytes(10) + } + for k, v := range items { + context.DAO.PutStorageItem(contractState.ID, []byte(k), v) + context.DAO.PutStorageItem(contractState.ID+1, []byte(k), v) + } + changes, err := context.DAO.Persist() + require.NoError(b, err) + require.NotEqual(b, 0, changes) + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + b.StopTimer() + v.Estack().PushVal(istorage.FindDefault) + v.Estack().PushVal("abc") + v.Estack().PushVal(stackitem.NewInterop(&istorage.Context{ID: contractState.ID})) + b.StartTimer() + err := istorage.Find(context) + b.StopTimer() + if err != nil { + b.FailNow() + } + res := context.VM.Estack().Pop().Item() + for i := 0; i < last; i++ { + context.VM.Estack().PushVal(res) + b.StartTimer() + require.NoError(b, iterator.Next(context)) + b.StopTimer() + require.True(b, context.VM.Estack().Pop().Bool()) + } + + context.VM.Estack().PushVal(res) + require.NoError(b, iterator.Next(context)) + actual := context.VM.Estack().Pop().Bool() + if last == count { + require.False(b, actual) + } else { + require.True(b, actual) + } + context.Finalize() + } + }) + } + }) + } +} diff --git a/pkg/core/interop/storage/find.go b/pkg/core/interop/storage/find.go index f86ca96f3..62a59b688 100644 --- a/pkg/core/interop/storage/find.go +++ b/pkg/core/interop/storage/find.go @@ -1,6 +1,10 @@ package storage import ( + "context" + "fmt" + + "github.com/nspcc-dev/neo-go/pkg/core/interop" "github.com/nspcc-dev/neo-go/pkg/core/storage" "github.com/nspcc-dev/neo-go/pkg/util/slice" "github.com/nspcc-dev/neo-go/pkg/vm/stackitem" @@ -80,3 +84,45 @@ func (s *Iterator) Value() stackitem.Item { value, }) } + +// Find finds stored key-value pair. +func Find(ic *interop.Context) error { + stcInterface := ic.VM.Estack().Pop().Value() + stc, ok := stcInterface.(*Context) + if !ok { + return fmt.Errorf("%T is not a storage,Context", stcInterface) + } + prefix := ic.VM.Estack().Pop().Bytes() + opts := ic.VM.Estack().Pop().BigInt().Int64() + if opts&^FindAll != 0 { + return fmt.Errorf("%w: unknown flag", errFindInvalidOptions) + } + if opts&FindKeysOnly != 0 && + opts&(FindDeserialize|FindPick0|FindPick1) != 0 { + return fmt.Errorf("%w KeysOnly conflicts with other options", errFindInvalidOptions) + } + if opts&FindValuesOnly != 0 && + opts&(FindKeysOnly|FindRemovePrefix) != 0 { + return fmt.Errorf("%w: KeysOnly conflicts with ValuesOnly", errFindInvalidOptions) + } + if opts&FindPick0 != 0 && opts&FindPick1 != 0 { + return fmt.Errorf("%w: Pick0 conflicts with Pick1", errFindInvalidOptions) + } + if opts&FindDeserialize == 0 && (opts&FindPick0 != 0 || opts&FindPick1 != 0) { + return fmt.Errorf("%w: PickN is specified without Deserialize", errFindInvalidOptions) + } + ctx, cancel := context.WithCancel(context.Background()) + seekres := ic.DAO.SeekAsync(ctx, stc.ID, storage.SeekRange{Prefix: prefix}) + item := NewIterator(seekres, prefix, opts) + ic.VM.Estack().PushItem(stackitem.NewInterop(item)) + ic.RegisterCancelFunc(func() { + cancel() + // Underlying persistent store is likely to be a private MemCachedStore. Thus, + // to avoid concurrent map iteration and map write we need to wait until internal + // seek goroutine is finished, because it can access underlying persistent store. + for range seekres { + } + }) + + return nil +} diff --git a/pkg/core/interops_test.go b/pkg/core/interop/storage/interops_test.go similarity index 53% rename from pkg/core/interops_test.go rename to pkg/core/interop/storage/interops_test.go index fb31c0e25..74795fc21 100644 --- a/pkg/core/interops_test.go +++ b/pkg/core/interop/storage/interops_test.go @@ -1,4 +1,4 @@ -package core +package storage_test import ( "reflect" @@ -6,20 +6,10 @@ import ( "testing" "github.com/nspcc-dev/neo-go/pkg/core/interop" - "github.com/nspcc-dev/neo-go/pkg/smartcontract/trigger" - "github.com/nspcc-dev/neo-go/pkg/vm" + "github.com/nspcc-dev/neo-go/pkg/core/interop/storage" "github.com/stretchr/testify/require" ) -func testNonInterop(t *testing.T, value interface{}, f func(*interop.Context) error) { - v := vm.New() - v.Estack().PushVal(value) - chain := newTestChain(t) - context := chain.newInteropContext(trigger.Application, chain.dao, nil, nil) - context.VM = v - require.Error(t, f(context)) -} - func TestUnexpectedNonInterops(t *testing.T) { vals := map[string]interface{}{ "int": 1, @@ -30,17 +20,19 @@ func TestUnexpectedNonInterops(t *testing.T) { // All of these functions expect an interop item on the stack. funcs := []func(*interop.Context) error{ - storageContextAsReadOnly, - storageDelete, - storageFind, - storageGet, - storagePut, + storage.ContextAsReadOnly, + storage.Delete, + storage.Find, + storage.Get, + storage.Put, } for _, f := range funcs { for k, v := range vals { fname := runtime.FuncForPC(reflect.ValueOf(f).Pointer()).Name() t.Run(k+"/"+fname, func(t *testing.T) { - testNonInterop(t, v, f) + vm, ic, _ := createVM(t) + vm.Estack().PushVal(v) + require.Error(t, f(ic)) }) } } diff --git a/pkg/core/interop_system_core_test.go b/pkg/core/interop/storage/storage_test.go similarity index 62% rename from pkg/core/interop_system_core_test.go rename to pkg/core/interop/storage/storage_test.go index 2c7496c10..7cfa53a74 100644 --- a/pkg/core/interop_system_core_test.go +++ b/pkg/core/interop/storage/storage_test.go @@ -1,20 +1,21 @@ -package core +package storage_test import ( "errors" - "fmt" "math/big" - "path/filepath" "testing" - "github.com/nspcc-dev/neo-go/internal/random" + "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/interop" "github.com/nspcc-dev/neo-go/pkg/core/interop/iterator" istorage "github.com/nspcc-dev/neo-go/pkg/core/interop/storage" "github.com/nspcc-dev/neo-go/pkg/core/native" "github.com/nspcc-dev/neo-go/pkg/core/state" "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/crypto/hash" + "github.com/nspcc-dev/neo-go/pkg/neotest/chain" "github.com/nspcc-dev/neo-go/pkg/smartcontract/callflag" "github.com/nspcc-dev/neo-go/pkg/smartcontract/manifest" "github.com/nspcc-dev/neo-go/pkg/smartcontract/nef" @@ -24,9 +25,7 @@ import ( "github.com/stretchr/testify/require" ) -var pathToInternalContracts = filepath.Join("..", "..", "internal", "contracts") - -func TestStoragePut(t *testing.T) { +func TestPut(t *testing.T) { _, cs, ic, _ := createVMAndContractState(t) require.NoError(t, native.PutContractState(ic.DAO, cs)) @@ -37,53 +36,53 @@ func TestStoragePut(t *testing.T) { v.GasLimit = gas v.Estack().PushVal(value) v.Estack().PushVal(key) - require.NoError(t, storageGetContext(ic)) + require.NoError(t, istorage.GetContext(ic)) } t.Run("create, not enough gas", func(t *testing.T) { initVM(t, []byte{1}, []byte{2, 3}, 2*native.DefaultStoragePrice) - err := storagePut(ic) - require.True(t, errors.Is(err, errGasLimitExceeded), "got: %v", err) + err := istorage.Put(ic) + require.True(t, errors.Is(err, istorage.ErrGasLimitExceeded), "got: %v", err) }) initVM(t, []byte{4}, []byte{5, 6}, 3*native.DefaultStoragePrice) - require.NoError(t, storagePut(ic)) + require.NoError(t, istorage.Put(ic)) t.Run("update", func(t *testing.T) { t.Run("not enough gas", func(t *testing.T) { initVM(t, []byte{4}, []byte{5, 6, 7, 8}, native.DefaultStoragePrice) - err := storagePut(ic) - require.True(t, errors.Is(err, errGasLimitExceeded), "got: %v", err) + err := istorage.Put(ic) + require.True(t, errors.Is(err, istorage.ErrGasLimitExceeded), "got: %v", err) }) initVM(t, []byte{4}, []byte{5, 6, 7, 8}, 3*native.DefaultStoragePrice) - require.NoError(t, storagePut(ic)) + require.NoError(t, istorage.Put(ic)) initVM(t, []byte{4}, []byte{5, 6}, native.DefaultStoragePrice) - require.NoError(t, storagePut(ic)) + require.NoError(t, istorage.Put(ic)) }) t.Run("check limits", func(t *testing.T) { initVM(t, make([]byte, storage.MaxStorageKeyLen), make([]byte, storage.MaxStorageValueLen), -1) - require.NoError(t, storagePut(ic)) + require.NoError(t, istorage.Put(ic)) }) t.Run("bad", func(t *testing.T) { t.Run("readonly context", func(t *testing.T) { initVM(t, []byte{1}, []byte{1}, -1) - require.NoError(t, storageContextAsReadOnly(ic)) - require.Error(t, storagePut(ic)) + require.NoError(t, istorage.ContextAsReadOnly(ic)) + require.Error(t, istorage.Put(ic)) }) t.Run("big key", func(t *testing.T) { initVM(t, make([]byte, storage.MaxStorageKeyLen+1), []byte{1}, -1) - require.Error(t, storagePut(ic)) + require.Error(t, istorage.Put(ic)) }) t.Run("big value", func(t *testing.T) { initVM(t, []byte{1}, make([]byte, storage.MaxStorageValueLen+1), -1) - require.Error(t, storagePut(ic)) + require.Error(t, istorage.Put(ic)) }) }) } -func TestStorageDelete(t *testing.T) { +func TestDelete(t *testing.T) { v, cs, ic, _ := createVMAndContractState(t) require.NoError(t, native.PutContractState(ic.DAO, cs)) @@ -91,8 +90,8 @@ func TestStorageDelete(t *testing.T) { put := func(key, value string, flag int) { v.Estack().PushVal(value) v.Estack().PushVal(key) - require.NoError(t, storageGetContext(ic)) - require.NoError(t, storagePut(ic)) + require.NoError(t, istorage.GetContext(ic)) + require.NoError(t, istorage.Put(ic)) } put("key1", "value1", 0) put("key2", "value2", 0) @@ -100,123 +99,24 @@ func TestStorageDelete(t *testing.T) { t.Run("good", func(t *testing.T) { v.Estack().PushVal("key1") - require.NoError(t, storageGetContext(ic)) - require.NoError(t, storageDelete(ic)) + require.NoError(t, istorage.GetContext(ic)) + require.NoError(t, istorage.Delete(ic)) }) t.Run("readonly context", func(t *testing.T) { v.Estack().PushVal("key2") - require.NoError(t, storageGetReadOnlyContext(ic)) - require.Error(t, storageDelete(ic)) + require.NoError(t, istorage.GetReadOnlyContext(ic)) + require.Error(t, istorage.Delete(ic)) }) t.Run("readonly context (from normal)", func(t *testing.T) { v.Estack().PushVal("key3") - require.NoError(t, storageGetContext(ic)) - require.NoError(t, storageContextAsReadOnly(ic)) - require.Error(t, storageDelete(ic)) + require.NoError(t, istorage.GetContext(ic)) + require.NoError(t, istorage.ContextAsReadOnly(ic)) + require.Error(t, istorage.Delete(ic)) }) } -func BenchmarkStorageFind(b *testing.B) { - for count := 10; count <= 10000; count *= 10 { - b.Run(fmt.Sprintf("%dElements", count), func(b *testing.B) { - v, contractState, context, chain := createVMAndContractState(b) - require.NoError(b, native.PutContractState(chain.dao, contractState)) - - items := make(map[string]state.StorageItem) - for i := 0; i < count; i++ { - items["abc"+random.String(10)] = random.Bytes(10) - } - for k, v := range items { - context.DAO.PutStorageItem(contractState.ID, []byte(k), v) - context.DAO.PutStorageItem(contractState.ID+1, []byte(k), v) - } - changes, err := context.DAO.Persist() - require.NoError(b, err) - require.NotEqual(b, 0, changes) - - b.ResetTimer() - b.ReportAllocs() - for i := 0; i < b.N; i++ { - b.StopTimer() - v.Estack().PushVal(istorage.FindDefault) - v.Estack().PushVal("abc") - v.Estack().PushVal(stackitem.NewInterop(&StorageContext{ID: contractState.ID})) - b.StartTimer() - err := storageFind(context) - if err != nil { - b.FailNow() - } - b.StopTimer() - context.Finalize() - } - }) - } -} - -func BenchmarkStorageFindIteratorNext(b *testing.B) { - for count := 10; count <= 10000; count *= 10 { - cases := map[string]int{ - "Pick1": 1, - "PickHalf": count / 2, - "PickAll": count, - } - b.Run(fmt.Sprintf("%dElements", count), func(b *testing.B) { - for name, last := range cases { - b.Run(name, func(b *testing.B) { - v, contractState, context, chain := createVMAndContractState(b) - require.NoError(b, native.PutContractState(chain.dao, contractState)) - - items := make(map[string]state.StorageItem) - for i := 0; i < count; i++ { - items["abc"+random.String(10)] = random.Bytes(10) - } - for k, v := range items { - context.DAO.PutStorageItem(contractState.ID, []byte(k), v) - context.DAO.PutStorageItem(contractState.ID+1, []byte(k), v) - } - changes, err := context.DAO.Persist() - require.NoError(b, err) - require.NotEqual(b, 0, changes) - b.ReportAllocs() - b.ResetTimer() - for i := 0; i < b.N; i++ { - b.StopTimer() - v.Estack().PushVal(istorage.FindDefault) - v.Estack().PushVal("abc") - v.Estack().PushVal(stackitem.NewInterop(&StorageContext{ID: contractState.ID})) - b.StartTimer() - err := storageFind(context) - b.StopTimer() - if err != nil { - b.FailNow() - } - res := context.VM.Estack().Pop().Item() - for i := 0; i < last; i++ { - context.VM.Estack().PushVal(res) - b.StartTimer() - require.NoError(b, iterator.Next(context)) - b.StopTimer() - require.True(b, context.VM.Estack().Pop().Bool()) - } - - context.VM.Estack().PushVal(res) - require.NoError(b, iterator.Next(context)) - actual := context.VM.Estack().Pop().Bool() - if last == count { - require.False(b, actual) - } else { - require.True(b, actual) - } - context.Finalize() - } - }) - } - }) - } -} - -func TestStorageFind(t *testing.T) { - v, contractState, context, chain := createVMAndContractState(t) +func TestFind(t *testing.T) { + v, contractState, context, _ := createVMAndContractState(t) arr := []stackitem.Item{ stackitem.NewBigInteger(big.NewInt(42)), @@ -247,7 +147,7 @@ func TestStorageFind(t *testing.T) { []byte{222}, } - require.NoError(t, native.PutContractState(chain.dao, contractState)) + require.NoError(t, native.PutContractState(context.DAO, contractState)) id := contractState.ID @@ -258,9 +158,9 @@ func TestStorageFind(t *testing.T) { testFind := func(t *testing.T, prefix []byte, opts int64, expected []stackitem.Item) { v.Estack().PushVal(opts) v.Estack().PushVal(prefix) - v.Estack().PushVal(stackitem.NewInterop(&StorageContext{ID: id})) + v.Estack().PushVal(stackitem.NewInterop(&istorage.Context{ID: id})) - err := storageFind(context) + err := istorage.Find(context) require.NoError(t, err) var iter *stackitem.Interop @@ -327,8 +227,8 @@ func TestStorageFind(t *testing.T) { t.Run("invalid", func(t *testing.T) { v.Estack().PushVal(istorage.FindDeserialize) v.Estack().PushVal([]byte{0x05}) - v.Estack().PushVal(stackitem.NewInterop(&StorageContext{ID: id})) - err := storageFind(context) + v.Estack().PushVal(stackitem.NewInterop(&istorage.Context{ID: id})) + err := istorage.Find(context) require.NoError(t, err) var iter *stackitem.Interop @@ -371,16 +271,16 @@ func TestStorageFind(t *testing.T) { for _, opts := range invalid { v.Estack().PushVal(opts) v.Estack().PushVal([]byte{0x01}) - v.Estack().PushVal(stackitem.NewInterop(&StorageContext{ID: id})) - require.Error(t, storageFind(context)) + v.Estack().PushVal(stackitem.NewInterop(&istorage.Context{ID: id})) + require.Error(t, istorage.Find(context)) } }) - t.Run("invalid type for StorageContext", func(t *testing.T) { + t.Run("invalid type for storage.Context", func(t *testing.T) { v.Estack().PushVal(istorage.FindDefault) v.Estack().PushVal([]byte{0x01}) v.Estack().PushVal(stackitem.NewInterop(nil)) - require.Error(t, storageFind(context)) + require.Error(t, istorage.Find(context)) }) t.Run("invalid id", func(t *testing.T) { @@ -388,9 +288,9 @@ func TestStorageFind(t *testing.T) { v.Estack().PushVal(istorage.FindDefault) v.Estack().PushVal([]byte{0x01}) - v.Estack().PushVal(stackitem.NewInterop(&StorageContext{ID: invalidID})) + v.Estack().PushVal(stackitem.NewInterop(&istorage.Context{ID: invalidID})) - require.NoError(t, storageFind(context)) + require.NoError(t, istorage.Find(context)) require.NoError(t, iterator.Next(context)) require.False(t, v.Estack().Pop().Bool()) }) @@ -398,15 +298,14 @@ func TestStorageFind(t *testing.T) { // Helper functions to create VM, InteropContext, TX, Account, Contract. -func createVM(t testing.TB) (*vm.VM, *interop.Context, *Blockchain) { - chain := newTestChain(t) - context := chain.newInteropContext(trigger.Application, - chain.dao.GetWrapped(), nil, nil) - v := context.SpawnVM() - return v, context, chain +func createVM(t testing.TB) (*vm.VM, *interop.Context, *core.Blockchain) { + chain, _ := chain.NewSingle(t) + ic := chain.GetTestVM(trigger.Application, &transaction.Transaction{}, &block.Block{}) + v := ic.SpawnVM() + return v, ic, chain } -func createVMAndContractState(t testing.TB) (*vm.VM, *state.Contract, *interop.Context, *Blockchain) { +func createVMAndContractState(t testing.TB) (*vm.VM, *state.Contract, *interop.Context, *core.Blockchain) { script := []byte("testscript") m := manifest.NewManifest("Test") ne, err := nef.NewFile(script) diff --git a/pkg/core/interop_system.go b/pkg/core/interop_system.go deleted file mode 100644 index b483fb16b..000000000 --- a/pkg/core/interop_system.go +++ /dev/null @@ -1,181 +0,0 @@ -package core - -import ( - "context" - "errors" - "fmt" - - "github.com/nspcc-dev/neo-go/pkg/core/interop" - istorage "github.com/nspcc-dev/neo-go/pkg/core/interop/storage" - "github.com/nspcc-dev/neo-go/pkg/core/storage" - "github.com/nspcc-dev/neo-go/pkg/vm/stackitem" -) - -var ( - errGasLimitExceeded = errors.New("gas limit exceeded") - errFindInvalidOptions = errors.New("invalid Find options") -) - -// StorageContext contains storing id and read/write flag, it's used as -// a context for storage manipulation functions. -type StorageContext struct { - ID int32 - ReadOnly bool -} - -// storageDelete deletes stored key-value pair. -func storageDelete(ic *interop.Context) error { - stcInterface := ic.VM.Estack().Pop().Value() - stc, ok := stcInterface.(*StorageContext) - if !ok { - return fmt.Errorf("%T is not a StorageContext", stcInterface) - } - if stc.ReadOnly { - return errors.New("StorageContext is read only") - } - key := ic.VM.Estack().Pop().Bytes() - ic.DAO.DeleteStorageItem(stc.ID, key) - return nil -} - -// storageGet returns stored key-value pair. -func storageGet(ic *interop.Context) error { - stcInterface := ic.VM.Estack().Pop().Value() - stc, ok := stcInterface.(*StorageContext) - if !ok { - return fmt.Errorf("%T is not a StorageContext", stcInterface) - } - key := ic.VM.Estack().Pop().Bytes() - si := ic.DAO.GetStorageItem(stc.ID, key) - if si != nil { - ic.VM.Estack().PushItem(stackitem.NewByteArray([]byte(si))) - } else { - ic.VM.Estack().PushItem(stackitem.Null{}) - } - return nil -} - -// storageGetContext returns storage context (scripthash). -func storageGetContext(ic *interop.Context) error { - return storageGetContextInternal(ic, false) -} - -// storageGetReadOnlyContext returns read-only context (scripthash). -func storageGetReadOnlyContext(ic *interop.Context) error { - return storageGetContextInternal(ic, true) -} - -// storageGetContextInternal is internal version of storageGetContext and -// storageGetReadOnlyContext which allows to specify ReadOnly context flag. -func storageGetContextInternal(ic *interop.Context, isReadOnly bool) error { - contract, err := ic.GetContract(ic.VM.GetCurrentScriptHash()) - if err != nil { - return err - } - sc := &StorageContext{ - ID: contract.ID, - ReadOnly: isReadOnly, - } - ic.VM.Estack().PushItem(stackitem.NewInterop(sc)) - return nil -} - -func putWithContext(ic *interop.Context, stc *StorageContext, key []byte, value []byte) error { - if len(key) > storage.MaxStorageKeyLen { - return errors.New("key is too big") - } - if len(value) > storage.MaxStorageValueLen { - return errors.New("value is too big") - } - if stc.ReadOnly { - return errors.New("StorageContext is read only") - } - si := ic.DAO.GetStorageItem(stc.ID, key) - sizeInc := len(value) - if si == nil { - sizeInc = len(key) + len(value) - } else if len(value) != 0 { - if len(value) <= len(si) { - sizeInc = (len(value)-1)/4 + 1 - } else if len(si) != 0 { - sizeInc = (len(si)-1)/4 + 1 + len(value) - len(si) - } - } - if !ic.VM.AddGas(int64(sizeInc) * ic.BaseStorageFee()) { - return errGasLimitExceeded - } - ic.DAO.PutStorageItem(stc.ID, key, value) - return nil -} - -// storagePut puts key-value pair into the storage. -func storagePut(ic *interop.Context) error { - stcInterface := ic.VM.Estack().Pop().Value() - stc, ok := stcInterface.(*StorageContext) - if !ok { - return fmt.Errorf("%T is not a StorageContext", stcInterface) - } - key := ic.VM.Estack().Pop().Bytes() - value := ic.VM.Estack().Pop().Bytes() - return putWithContext(ic, stc, key, value) -} - -// storageContextAsReadOnly sets given context to read-only mode. -func storageContextAsReadOnly(ic *interop.Context) error { - stcInterface := ic.VM.Estack().Pop().Value() - stc, ok := stcInterface.(*StorageContext) - if !ok { - return fmt.Errorf("%T is not a StorageContext", stcInterface) - } - if !stc.ReadOnly { - stx := &StorageContext{ - ID: stc.ID, - ReadOnly: true, - } - stc = stx - } - ic.VM.Estack().PushItem(stackitem.NewInterop(stc)) - return nil -} - -// storageFind finds stored key-value pair. -func storageFind(ic *interop.Context) error { - stcInterface := ic.VM.Estack().Pop().Value() - stc, ok := stcInterface.(*StorageContext) - if !ok { - return fmt.Errorf("%T is not a StorageContext", stcInterface) - } - prefix := ic.VM.Estack().Pop().Bytes() - opts := ic.VM.Estack().Pop().BigInt().Int64() - if opts&^istorage.FindAll != 0 { - return fmt.Errorf("%w: unknown flag", errFindInvalidOptions) - } - if opts&istorage.FindKeysOnly != 0 && - opts&(istorage.FindDeserialize|istorage.FindPick0|istorage.FindPick1) != 0 { - return fmt.Errorf("%w KeysOnly conflicts with other options", errFindInvalidOptions) - } - if opts&istorage.FindValuesOnly != 0 && - opts&(istorage.FindKeysOnly|istorage.FindRemovePrefix) != 0 { - return fmt.Errorf("%w: KeysOnly conflicts with ValuesOnly", errFindInvalidOptions) - } - if opts&istorage.FindPick0 != 0 && opts&istorage.FindPick1 != 0 { - return fmt.Errorf("%w: Pick0 conflicts with Pick1", errFindInvalidOptions) - } - if opts&istorage.FindDeserialize == 0 && (opts&istorage.FindPick0 != 0 || opts&istorage.FindPick1 != 0) { - return fmt.Errorf("%w: PickN is specified without Deserialize", errFindInvalidOptions) - } - ctx, cancel := context.WithCancel(context.Background()) - seekres := ic.DAO.SeekAsync(ctx, stc.ID, storage.SeekRange{Prefix: prefix}) - item := istorage.NewIterator(seekres, prefix, opts) - ic.VM.Estack().PushItem(stackitem.NewInterop(item)) - ic.RegisterCancelFunc(func() { - cancel() - // Underlying persistent store is likely to be a private MemCachedStore. Thus, - // to avoid concurrent map iteration and map write we need to wait until internal - // seek goroutine is finished, because it can access underlying persistent store. - for range seekres { - } - }) - - return nil -} diff --git a/pkg/core/interops.go b/pkg/core/interops.go index 50c8ee723..5612a4e20 100644 --- a/pkg/core/interops.go +++ b/pkg/core/interops.go @@ -15,6 +15,7 @@ import ( "github.com/nspcc-dev/neo-go/pkg/core/interop/interopnames" "github.com/nspcc-dev/neo-go/pkg/core/interop/iterator" "github.com/nspcc-dev/neo-go/pkg/core/interop/runtime" + "github.com/nspcc-dev/neo-go/pkg/core/interop/storage" "github.com/nspcc-dev/neo-go/pkg/core/native" "github.com/nspcc-dev/neo-go/pkg/smartcontract/callflag" "github.com/nspcc-dev/neo-go/pkg/vm" @@ -62,19 +63,19 @@ var systemInterops = []interop.Function{ {Name: interopnames.SystemRuntimeNotify, Func: runtime.Notify, Price: 1 << 15, RequiredFlags: callflag.AllowNotify, ParamCount: 2}, {Name: interopnames.SystemRuntimePlatform, Func: runtime.Platform, Price: 1 << 3}, - {Name: interopnames.SystemStorageDelete, Func: storageDelete, Price: 1 << 15, + {Name: interopnames.SystemStorageDelete, Func: storage.Delete, Price: 1 << 15, RequiredFlags: callflag.WriteStates, ParamCount: 2}, - {Name: interopnames.SystemStorageFind, Func: storageFind, Price: 1 << 15, RequiredFlags: callflag.ReadStates, + {Name: interopnames.SystemStorageFind, Func: storage.Find, Price: 1 << 15, RequiredFlags: callflag.ReadStates, ParamCount: 3}, - {Name: interopnames.SystemStorageGet, Func: storageGet, Price: 1 << 15, RequiredFlags: callflag.ReadStates, + {Name: interopnames.SystemStorageGet, Func: storage.Get, Price: 1 << 15, RequiredFlags: callflag.ReadStates, ParamCount: 2}, - {Name: interopnames.SystemStorageGetContext, Func: storageGetContext, Price: 1 << 4, + {Name: interopnames.SystemStorageGetContext, Func: storage.GetContext, Price: 1 << 4, RequiredFlags: callflag.ReadStates}, - {Name: interopnames.SystemStorageGetReadOnlyContext, Func: storageGetReadOnlyContext, Price: 1 << 4, + {Name: interopnames.SystemStorageGetReadOnlyContext, Func: storage.GetReadOnlyContext, Price: 1 << 4, RequiredFlags: callflag.ReadStates}, - {Name: interopnames.SystemStoragePut, Func: storagePut, Price: 1 << 15, RequiredFlags: callflag.WriteStates, + {Name: interopnames.SystemStoragePut, Func: storage.Put, Price: 1 << 15, RequiredFlags: callflag.WriteStates, ParamCount: 3}, - {Name: interopnames.SystemStorageAsReadOnly, Func: storageContextAsReadOnly, Price: 1 << 4, + {Name: interopnames.SystemStorageAsReadOnly, Func: storage.ContextAsReadOnly, Price: 1 << 4, RequiredFlags: callflag.ReadStates, ParamCount: 1}, } From 8057c096c651f199322ace55f27cd3557408c32c Mon Sep 17 00:00:00 2001 From: Roman Khimov Date: Wed, 8 Jun 2022 22:25:50 +0300 Subject: [PATCH 13/21] core: move native invocation tests into native --- .../{native_contract_test.go => native/invocation_test.go} | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) rename pkg/core/{native_contract_test.go => native/invocation_test.go} (97%) diff --git a/pkg/core/native_contract_test.go b/pkg/core/native/invocation_test.go similarity index 97% rename from pkg/core/native_contract_test.go rename to pkg/core/native/invocation_test.go index 2be2c46b4..b6f55d9dc 100644 --- a/pkg/core/native_contract_test.go +++ b/pkg/core/native/invocation_test.go @@ -1,8 +1,9 @@ -package core_test +package native_test import ( "encoding/json" "fmt" + "path/filepath" "strings" "testing" @@ -22,6 +23,8 @@ import ( "github.com/stretchr/testify/require" ) +var pathToInternalContracts = filepath.Join("..", "..", "..", "internal", "contracts") + func TestNativeContract_Invoke(t *testing.T) { const ( transferCPUFee = 1 << 17 From 2e2d886a2f53cdbad4e0751f0103726e333a89e3 Mon Sep 17 00:00:00 2001 From: Roman Khimov Date: Wed, 8 Jun 2022 22:34:09 +0300 Subject: [PATCH 14/21] core: move Management contract test into native --- .../management_neotest_test.go} | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) rename pkg/core/{native_management_test.go => native/management_neotest_test.go} (95%) diff --git a/pkg/core/native_management_test.go b/pkg/core/native/management_neotest_test.go similarity index 95% rename from pkg/core/native_management_test.go rename to pkg/core/native/management_neotest_test.go index e02e022dd..f805d439b 100644 --- a/pkg/core/native_management_test.go +++ b/pkg/core/native/management_neotest_test.go @@ -1,4 +1,4 @@ -package core_test +package native_test import ( "testing" @@ -26,7 +26,7 @@ func TestManagement_GetNEP17Contracts(t *testing.T) { c.P2PSigExtensions = true // `basicchain.Init` requires Notary enabled }) e := neotest.NewExecutor(t, bc, validators, committee) - basicchain.Init(t, "../../", e) + basicchain.Init(t, "../../../", e) require.ElementsMatch(t, []util.Uint160{e.NativeHash(t, nativenames.Neo), e.NativeHash(t, nativenames.Gas), e.ContractHash(t, 1)}, bc.GetNEP17Contracts()) From a1eccc16e61416d7cd466efe7c2881d1d62b0733 Mon Sep 17 00:00:00 2001 From: Roman Khimov Date: Wed, 8 Jun 2022 22:39:57 +0300 Subject: [PATCH 15/21] core: move benchmarks into bench_test.go They share some functions and they're benchmarks, so let's have them here. --- pkg/core/bench_test.go | 112 ++++++++++++++++++++++++++++++++ pkg/core/native_neo_test.go | 125 ------------------------------------ 2 files changed, 112 insertions(+), 125 deletions(-) delete mode 100644 pkg/core/native_neo_test.go diff --git a/pkg/core/bench_test.go b/pkg/core/bench_test.go index d747a6401..8af373b80 100644 --- a/pkg/core/bench_test.go +++ b/pkg/core/bench_test.go @@ -2,6 +2,8 @@ package core_test import ( "fmt" + "math/big" + "path/filepath" "testing" "github.com/nspcc-dev/neo-go/internal/random" @@ -10,12 +12,14 @@ import ( "github.com/nspcc-dev/neo-go/pkg/core/state" "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/crypto/keys" "github.com/nspcc-dev/neo-go/pkg/io" "github.com/nspcc-dev/neo-go/pkg/neotest" "github.com/nspcc-dev/neo-go/pkg/neotest/chain" "github.com/nspcc-dev/neo-go/pkg/smartcontract/callflag" "github.com/nspcc-dev/neo-go/pkg/vm/emit" "github.com/nspcc-dev/neo-go/pkg/vm/opcode" + "github.com/nspcc-dev/neo-go/pkg/wallet" "github.com/stretchr/testify/require" ) @@ -54,6 +58,27 @@ func BenchmarkBlockchain_ForEachNEP17Transfer(t *testing.B) { } } +func BenchmarkNEO_GetGASPerVote(t *testing.B) { + var stores = map[string]func(testing.TB) storage.Store{ + "MemPS": func(t testing.TB) storage.Store { + return storage.NewMemoryStore() + }, + "BoltPS": newBoltStoreForTesting, + "LevelPS": newLevelDBForTesting, + } + for psName, newPS := range stores { + for nRewardRecords := 10; nRewardRecords <= 1000; nRewardRecords *= 10 { + for rewardDistance := 1; rewardDistance <= 1000; rewardDistance *= 10 { + t.Run(fmt.Sprintf("%s_%dRewardRecords_%dRewardDistance", psName, nRewardRecords, rewardDistance), func(t *testing.B) { + ps := newPS(t) + t.Cleanup(func() { ps.Close() }) + benchmarkGasPerVote(t, ps, nRewardRecords, rewardDistance) + }) + } + } + } +} + func benchmarkForEachNEP17Transfer(t *testing.B, ps storage.Store, startFromBlock, nBlocksToTake int) { var ( chainHeight = 2_100 // constant chain height to be able to compare paging results @@ -108,3 +133,90 @@ func benchmarkForEachNEP17Transfer(t *testing.B, ps storage.Store, startFromBloc } t.StopTimer() } + +func newLevelDBForTesting(t testing.TB) storage.Store { + dbPath := t.TempDir() + dbOptions := storage.LevelDBOptions{ + DataDirectoryPath: dbPath, + } + newLevelStore, err := storage.NewLevelDBStore(dbOptions) + require.Nil(t, err, "NewLevelDBStore error") + return newLevelStore +} + +func newBoltStoreForTesting(t testing.TB) storage.Store { + d := t.TempDir() + dbPath := filepath.Join(d, "test_bolt_db") + boltDBStore, err := storage.NewBoltDBStore(storage.BoltDBOptions{FilePath: dbPath}) + require.NoError(t, err) + return boltDBStore +} + +func benchmarkGasPerVote(t *testing.B, ps storage.Store, nRewardRecords int, rewardDistance int) { + bc, validators, committee := chain.NewMultiWithCustomConfigAndStore(t, nil, ps, true) + cfg := bc.GetConfig() + + e := neotest.NewExecutor(t, bc, validators, committee) + neoHash := e.NativeHash(t, nativenames.Neo) + gasHash := e.NativeHash(t, nativenames.Gas) + neoSuperInvoker := e.NewInvoker(neoHash, validators, committee) + neoValidatorsInvoker := e.ValidatorInvoker(neoHash) + gasValidatorsInvoker := e.ValidatorInvoker(gasHash) + + // Vote for new committee. + sz := len(cfg.StandbyCommittee) + voters := make([]*wallet.Account, sz) + candidates := make(keys.PublicKeys, sz) + txs := make([]*transaction.Transaction, 0, len(voters)*3) + for i := 0; i < sz; i++ { + priv, err := keys.NewPrivateKey() + require.NoError(t, err) + candidates[i] = priv.PublicKey() + voters[i], err = wallet.NewAccount() + require.NoError(t, err) + registerTx := neoSuperInvoker.PrepareInvoke(t, "registerCandidate", candidates[i].Bytes()) + txs = append(txs, registerTx) + + to := voters[i].Contract.ScriptHash() + transferNeoTx := neoValidatorsInvoker.PrepareInvoke(t, "transfer", e.Validator.ScriptHash(), to, big.NewInt(int64(sz-i)*1000000).Int64(), nil) + txs = append(txs, transferNeoTx) + + transferGasTx := gasValidatorsInvoker.PrepareInvoke(t, "transfer", e.Validator.ScriptHash(), to, int64(1_000_000_000), nil) + txs = append(txs, transferGasTx) + } + e.AddNewBlock(t, txs...) + for _, tx := range txs { + e.CheckHalt(t, tx.Hash()) + } + voteTxs := make([]*transaction.Transaction, 0, sz) + for i := 0; i < sz; i++ { + priv := voters[i].PrivateKey() + h := priv.GetScriptHash() + voteTx := e.NewTx(t, []neotest.Signer{neotest.NewSingleSigner(voters[i])}, neoHash, "vote", h, candidates[i].Bytes()) + voteTxs = append(voteTxs, voteTx) + } + e.AddNewBlock(t, voteTxs...) + for _, tx := range voteTxs { + e.CheckHalt(t, tx.Hash()) + } + + // Collect set of nRewardRecords reward records for each voter. + e.GenerateNewBlocks(t, len(cfg.StandbyCommittee)) + + // Transfer some more NEO to first voter to update his balance height. + to := voters[0].Contract.ScriptHash() + neoValidatorsInvoker.Invoke(t, true, "transfer", e.Validator.ScriptHash(), to, int64(1), nil) + + // Advance chain one more time to avoid same start/end rewarding bounds. + e.GenerateNewBlocks(t, rewardDistance) + end := bc.BlockHeight() + + t.ResetTimer() + t.ReportAllocs() + t.StartTimer() + for i := 0; i < t.N; i++ { + _, err := bc.CalculateClaimable(to, end) + require.NoError(t, err) + } + t.StopTimer() +} diff --git a/pkg/core/native_neo_test.go b/pkg/core/native_neo_test.go deleted file mode 100644 index 9c0c7b43c..000000000 --- a/pkg/core/native_neo_test.go +++ /dev/null @@ -1,125 +0,0 @@ -package core_test - -import ( - "fmt" - "math/big" - "path/filepath" - "testing" - - "github.com/nspcc-dev/neo-go/pkg/core/native/nativenames" - "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/crypto/keys" - "github.com/nspcc-dev/neo-go/pkg/neotest" - "github.com/nspcc-dev/neo-go/pkg/neotest/chain" - "github.com/nspcc-dev/neo-go/pkg/wallet" - "github.com/stretchr/testify/require" -) - -func BenchmarkNEO_GetGASPerVote(t *testing.B) { - var stores = map[string]func(testing.TB) storage.Store{ - "MemPS": func(t testing.TB) storage.Store { - return storage.NewMemoryStore() - }, - "BoltPS": newBoltStoreForTesting, - "LevelPS": newLevelDBForTesting, - } - for psName, newPS := range stores { - for nRewardRecords := 10; nRewardRecords <= 1000; nRewardRecords *= 10 { - for rewardDistance := 1; rewardDistance <= 1000; rewardDistance *= 10 { - t.Run(fmt.Sprintf("%s_%dRewardRecords_%dRewardDistance", psName, nRewardRecords, rewardDistance), func(t *testing.B) { - ps := newPS(t) - t.Cleanup(func() { ps.Close() }) - benchmarkGasPerVote(t, ps, nRewardRecords, rewardDistance) - }) - } - } - } -} - -func newLevelDBForTesting(t testing.TB) storage.Store { - dbPath := t.TempDir() - dbOptions := storage.LevelDBOptions{ - DataDirectoryPath: dbPath, - } - newLevelStore, err := storage.NewLevelDBStore(dbOptions) - require.Nil(t, err, "NewLevelDBStore error") - return newLevelStore -} - -func newBoltStoreForTesting(t testing.TB) storage.Store { - d := t.TempDir() - dbPath := filepath.Join(d, "test_bolt_db") - boltDBStore, err := storage.NewBoltDBStore(storage.BoltDBOptions{FilePath: dbPath}) - require.NoError(t, err) - return boltDBStore -} - -func benchmarkGasPerVote(t *testing.B, ps storage.Store, nRewardRecords int, rewardDistance int) { - bc, validators, committee := chain.NewMultiWithCustomConfigAndStore(t, nil, ps, true) - cfg := bc.GetConfig() - - e := neotest.NewExecutor(t, bc, validators, committee) - neoHash := e.NativeHash(t, nativenames.Neo) - gasHash := e.NativeHash(t, nativenames.Gas) - neoSuperInvoker := e.NewInvoker(neoHash, validators, committee) - neoValidatorsInvoker := e.ValidatorInvoker(neoHash) - gasValidatorsInvoker := e.ValidatorInvoker(gasHash) - - // Vote for new committee. - sz := len(cfg.StandbyCommittee) - voters := make([]*wallet.Account, sz) - candidates := make(keys.PublicKeys, sz) - txs := make([]*transaction.Transaction, 0, len(voters)*3) - for i := 0; i < sz; i++ { - priv, err := keys.NewPrivateKey() - require.NoError(t, err) - candidates[i] = priv.PublicKey() - voters[i], err = wallet.NewAccount() - require.NoError(t, err) - registerTx := neoSuperInvoker.PrepareInvoke(t, "registerCandidate", candidates[i].Bytes()) - txs = append(txs, registerTx) - - to := voters[i].Contract.ScriptHash() - transferNeoTx := neoValidatorsInvoker.PrepareInvoke(t, "transfer", e.Validator.ScriptHash(), to, big.NewInt(int64(sz-i)*1000000).Int64(), nil) - txs = append(txs, transferNeoTx) - - transferGasTx := gasValidatorsInvoker.PrepareInvoke(t, "transfer", e.Validator.ScriptHash(), to, int64(1_000_000_000), nil) - txs = append(txs, transferGasTx) - } - e.AddNewBlock(t, txs...) - for _, tx := range txs { - e.CheckHalt(t, tx.Hash()) - } - voteTxs := make([]*transaction.Transaction, 0, sz) - for i := 0; i < sz; i++ { - priv := voters[i].PrivateKey() - h := priv.GetScriptHash() - voteTx := e.NewTx(t, []neotest.Signer{neotest.NewSingleSigner(voters[i])}, neoHash, "vote", h, candidates[i].Bytes()) - voteTxs = append(voteTxs, voteTx) - } - e.AddNewBlock(t, voteTxs...) - for _, tx := range voteTxs { - e.CheckHalt(t, tx.Hash()) - } - - // Collect set of nRewardRecords reward records for each voter. - e.GenerateNewBlocks(t, len(cfg.StandbyCommittee)) - - // Transfer some more NEO to first voter to update his balance height. - to := voters[0].Contract.ScriptHash() - neoValidatorsInvoker.Invoke(t, true, "transfer", e.Validator.ScriptHash(), to, int64(1), nil) - - // Advance chain one more time to avoid same start/end rewarding bounds. - e.GenerateNewBlocks(t, rewardDistance) - end := bc.BlockHeight() - - t.ResetTimer() - t.ReportAllocs() - t.StartTimer() - for i := 0; i < t.N; i++ { - _, err := bc.CalculateClaimable(to, end) - require.NoError(t, err) - } - t.StopTimer() -} From 167bb72424c0e7002959bdf2752ef25d055073bc Mon Sep 17 00:00:00 2001 From: Roman Khimov Date: Wed, 8 Jun 2022 22:41:23 +0300 Subject: [PATCH 16/21] core: move Policy contract tests to native --- pkg/core/{native_policy_test.go => native/policy_test.go} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename pkg/core/{native_policy_test.go => native/policy_test.go} (99%) diff --git a/pkg/core/native_policy_test.go b/pkg/core/native/policy_test.go similarity index 99% rename from pkg/core/native_policy_test.go rename to pkg/core/native/policy_test.go index 211f7476c..9569dd6b8 100644 --- a/pkg/core/native_policy_test.go +++ b/pkg/core/native/policy_test.go @@ -1,4 +1,4 @@ -package core_test +package native_test import ( "fmt" From ff75e67610decd1098565ef8711236f73590e728 Mon Sep 17 00:00:00 2001 From: Roman Khimov Date: Wed, 8 Jun 2022 22:53:09 +0300 Subject: [PATCH 17/21] core: move stateroot tests into services/stateroot They test both module and service which is a bit wrong, but separating these tests will lead to some duplication, so it's OK for now to have them in the higher-order package (service imports module). --- .../stateroot_test.go => services/stateroot/service_test.go} | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) rename pkg/{core/stateroot_test.go => services/stateroot/service_test.go} (99%) diff --git a/pkg/core/stateroot_test.go b/pkg/services/stateroot/service_test.go similarity index 99% rename from pkg/core/stateroot_test.go rename to pkg/services/stateroot/service_test.go index f0ed8d1d5..3d11debaf 100644 --- a/pkg/core/stateroot_test.go +++ b/pkg/services/stateroot/service_test.go @@ -1,4 +1,4 @@ -package core_test +package stateroot_test import ( "crypto/elliptic" @@ -303,7 +303,7 @@ func TestStateroot_GetLatestStateHeight(t *testing.T) { c.P2PSigExtensions = true }) e := neotest.NewExecutor(t, bc, validators, committee) - basicchain.Init(t, "../../", e) + basicchain.Init(t, "../../../", e) m := bc.GetStateModule() for i := uint32(0); i < bc.BlockHeight(); i++ { From 017cd558cbe658c1dd45984bdceecd42bee9a8e6 Mon Sep 17 00:00:00 2001 From: Roman Khimov Date: Wed, 8 Jun 2022 22:57:48 +0300 Subject: [PATCH 18/21] core: move statesync tests into the statesync package --- pkg/core/{statesync_test.go => statesync/neotest_test.go} | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) rename pkg/core/{statesync_test.go => statesync/neotest_test.go} (99%) diff --git a/pkg/core/statesync_test.go b/pkg/core/statesync/neotest_test.go similarity index 99% rename from pkg/core/statesync_test.go rename to pkg/core/statesync/neotest_test.go index 326c40a58..a9dea0cf9 100644 --- a/pkg/core/statesync_test.go +++ b/pkg/core/statesync/neotest_test.go @@ -1,4 +1,4 @@ -package core_test +package statesync_test import ( "testing" @@ -298,7 +298,7 @@ func TestStateSyncModule_RestoreBasicChain(t *testing.T) { bcSpout, validators, committee := chain.NewMultiWithCustomConfigAndStore(t, spoutCfg, bcSpoutStore, false) go bcSpout.Run() // Will close it manually at the end. e := neotest.NewExecutor(t, bcSpout, validators, committee) - basicchain.Init(t, "../../", e) + basicchain.Init(t, "../../../", e) // make spout chain higher that latest state sync point (add several blocks up to stateSyncPoint+2) e.AddNewBlock(t) From 5d573895ad7c43ccb4fdffbf4ddaefd49ab70ca6 Mon Sep 17 00:00:00 2001 From: Roman Khimov Date: Wed, 8 Jun 2022 23:04:47 +0300 Subject: [PATCH 19/21] core: move oracle tests into oracle service --- pkg/core/blockchain_neotest_test.go | 2 +- pkg/{core => services/oracle}/oracle_test.go | 9 ++++----- 2 files changed, 5 insertions(+), 6 deletions(-) rename pkg/{core => services/oracle}/oracle_test.go (98%) diff --git a/pkg/core/blockchain_neotest_test.go b/pkg/core/blockchain_neotest_test.go index ef765774f..50788ed90 100644 --- a/pkg/core/blockchain_neotest_test.go +++ b/pkg/core/blockchain_neotest_test.go @@ -1330,7 +1330,7 @@ func TestBlockchain_VerifyTx(t *testing.T) { cInvoker := e.ValidatorInvoker(cs.Hash) const gasForResponse int64 = 10_000_000 - putOracleRequest(t, cInvoker, "https://get.1234", new(string), "handle", []byte{}, gasForResponse) + cInvoker.Invoke(t, stackitem.Null{}, "requestURL", "https://get.1234", "", "handle", []byte{}, gasForResponse) oracleScript, err := smartcontract.CreateMajorityMultiSigRedeemScript(oraclePubs) require.NoError(t, err) diff --git a/pkg/core/oracle_test.go b/pkg/services/oracle/oracle_test.go similarity index 98% rename from pkg/core/oracle_test.go rename to pkg/services/oracle/oracle_test.go index 6a6decd8a..f8fc6c023 100644 --- a/pkg/core/oracle_test.go +++ b/pkg/services/oracle/oracle_test.go @@ -1,4 +1,4 @@ -package core_test +package oracle_test import ( "bytes" @@ -8,7 +8,6 @@ import ( "fmt" gio "io" "net/http" - "path" "path/filepath" "strings" "sync" @@ -38,7 +37,7 @@ import ( "go.uber.org/zap/zaptest" ) -var oracleModulePath = filepath.Join("..", "services", "oracle") +var pathToInternalContracts = filepath.Join("..", "..", "..", "internal", "contracts") func putOracleRequest(t *testing.T, oracleValidatorInvoker *neotest.ContractInvoker, url string, filter *string, cb string, userData []byte, gas int64) util.Uint256 { @@ -57,7 +56,7 @@ func getOracleConfig(t *testing.T, bc *core.Blockchain, w, pass string, returnOr RefreshInterval: time.Second, AllowedContentTypes: []string{"application/json"}, UnlockWallet: config.Wallet{ - Path: filepath.Join(oracleModulePath, w), + Path: w, Password: pass, }, }, @@ -81,7 +80,7 @@ func getTestOracle(t *testing.T, bc *core.Blockchain, walletPath, pass string) ( orc, err := oracle.NewOracle(orcCfg) require.NoError(t, err) - w, err := wallet.NewWalletFromFile(path.Join(oracleModulePath, walletPath)) + w, err := wallet.NewWalletFromFile(walletPath) require.NoError(t, err) require.NoError(t, w.Accounts[0].Decrypt(pass, w.Scrypt)) return w.Accounts[0], orc, m, ch From ef50518b831302c44edfde42d10f79893a634864 Mon Sep 17 00:00:00 2001 From: Roman Khimov Date: Wed, 8 Jun 2022 23:07:28 +0300 Subject: [PATCH 20/21] core: move notary tests into the notary package --- pkg/core/basic_chain_test.go | 1 - pkg/{core/notary_test.go => services/notary/core_test.go} | 8 +++----- 2 files changed, 3 insertions(+), 6 deletions(-) rename pkg/{core/notary_test.go => services/notary/core_test.go} (99%) diff --git a/pkg/core/basic_chain_test.go b/pkg/core/basic_chain_test.go index ad87db912..aac129172 100644 --- a/pkg/core/basic_chain_test.go +++ b/pkg/core/basic_chain_test.go @@ -26,7 +26,6 @@ const ( ) var ( - notaryModulePath = filepath.Join("..", "services", "notary") pathToInternalContracts = filepath.Join("..", "..", "internal", "contracts") ) diff --git a/pkg/core/notary_test.go b/pkg/services/notary/core_test.go similarity index 99% rename from pkg/core/notary_test.go rename to pkg/services/notary/core_test.go index 6a9ae5422..3aaeb7837 100644 --- a/pkg/core/notary_test.go +++ b/pkg/services/notary/core_test.go @@ -1,12 +1,10 @@ -package core_test +package notary_test import ( "errors" "fmt" "math/big" "math/rand" - "path" - "path/filepath" "sync" "testing" "time" @@ -40,7 +38,7 @@ func getTestNotary(t *testing.T, bc *core.Blockchain, walletPath, pass string, o mainCfg := config.P2PNotary{ Enabled: true, UnlockWallet: config.Wallet{ - Path: filepath.Join(notaryModulePath, walletPath), + Path: walletPath, Password: pass, }, } @@ -53,7 +51,7 @@ func getTestNotary(t *testing.T, bc *core.Blockchain, walletPath, pass string, o ntr, err := notary.NewNotary(cfg, netmode.UnitTestNet, mp, onTx) require.NoError(t, err) - w, err := wallet.NewWalletFromFile(path.Join(notaryModulePath, walletPath)) + w, err := wallet.NewWalletFromFile(walletPath) require.NoError(t, err) require.NoError(t, w.Accounts[0].Decrypt(pass, w.Scrypt)) return w.Accounts[0], ntr, mp From 1b7c8f262b67cd8b0ec361ac166f0e0bdc165f59 Mon Sep 17 00:00:00 2001 From: Roman Khimov Date: Wed, 8 Jun 2022 23:11:14 +0300 Subject: [PATCH 21/21] core: add some comment for TestDesignate_DesignateAsRole Closes #1472 at last. --- pkg/core/native_designate_test.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pkg/core/native_designate_test.go b/pkg/core/native_designate_test.go index 3dd68f05f..533127859 100644 --- a/pkg/core/native_designate_test.go +++ b/pkg/core/native_designate_test.go @@ -15,6 +15,9 @@ import ( "github.com/stretchr/testify/require" ) +// Technically this test belongs to the native package, but it's so deeply tied to +// the core internals that it needs to be rewritten to be moved. So let it be +// there just to remind us about the imperfect world we live in. func TestDesignate_DesignateAsRole(t *testing.T) { bc := newTestChain(t)