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..aac129172 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. @@ -39,7 +26,6 @@ const ( ) var ( - notaryModulePath = filepath.Join("..", "services", "notary") pathToInternalContracts = filepath.Join("..", "..", "internal", "contracts") ) @@ -56,7 +42,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 +59,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/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/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/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/blockchain_neotest_test.go b/pkg/core/blockchain_neotest_test.go index 91a25d12f..50788ed90 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" @@ -17,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" @@ -48,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) - - initBasicChain(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() @@ -147,7 +70,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. @@ -1407,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/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) + }) + }) +} 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/account_test.go b/pkg/core/interop/contract/account_test.go new file mode 100644 index 000000000..84a655571 --- /dev/null +++ b/pkg/core/interop/contract/account_test.go @@ -0,0 +1,173 @@ +package contract_test + +import ( + "math" + "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" + "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/util/slice" + "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") + }) +} + +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.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_system_neotest_test.go b/pkg/core/interop/contract/call_test.go similarity index 62% rename from pkg/core/interop_system_neotest_test.go rename to pkg/core/interop/contract/call_test.go index 7fa7ba060..f4159acd3 100644 --- a/pkg/core/interop_system_neotest_test.go +++ b/pkg/core/interop/contract/call_test.go @@ -1,182 +1,175 @@ -package core_test +package contract_test import ( "encoding/json" "fmt" - "math" "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/config" + "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/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/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/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/util/slice" - "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" ) -func TestSystemRuntimeGetRandom_DifferentTransactions(t *testing.T) { - bc, acc := chain.NewSingle(t) - e := neotest.NewExecutor(t, bc, acc, acc) +var pathToInternalContracts = filepath.Join("..", "..", "..", "..", "internal", "contracts") - w := io.NewBufBinWriter() - emit.Syscall(w.BinWriter, interopnames.SystemRuntimeGetRandom) - require.NoError(t, w.Err) - script := w.Bytes() +func TestGetCallFlags(t *testing.T) { + bc, _ := chain.NewSingle(t) + ic := bc.GetTestVM(trigger.Application, &transaction.Transaction{}, &block.Block{}) - 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) + 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()) } -func TestSystemContractCreateStandardAccount(t *testing.T) { - bc, acc := chain.NewSingle(t) - e := neotest.NewExecutor(t, bc, acc, acc) - w := io.NewBufBinWriter() +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) { - 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("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("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") + 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)) }) -} -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() + 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) + } + }) } - 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("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("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("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("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("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("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") + + 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 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) @@ -204,118 +197,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 // `initBasicChain` 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) @@ -707,3 +588,21 @@ func TestCALLL_from_VoidContext(t *testing.T) { 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) + 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/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/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/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/storage/storage_test.go b/pkg/core/interop/storage/storage_test.go new file mode 100644 index 000000000..7cfa53a74 --- /dev/null +++ b/pkg/core/interop/storage/storage_test.go @@ -0,0 +1,324 @@ +package storage_test + +import ( + "errors" + "math/big" + "testing" + + "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" + "github.com/nspcc-dev/neo-go/pkg/smartcontract/trigger" + "github.com/nspcc-dev/neo-go/pkg/vm" + "github.com/nspcc-dev/neo-go/pkg/vm/stackitem" + "github.com/stretchr/testify/require" +) + +func TestPut(t *testing.T) { + _, cs, ic, _ := createVMAndContractState(t) + + require.NoError(t, native.PutContractState(ic.DAO, cs)) + + initVM := func(t *testing.T, key, value []byte, gas int64) { + v := ic.SpawnVM() + v.LoadScript(cs.NEF.Script) + v.GasLimit = gas + v.Estack().PushVal(value) + v.Estack().PushVal(key) + 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 := 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, 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 := 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, istorage.Put(ic)) + initVM(t, []byte{4}, []byte{5, 6}, native.DefaultStoragePrice) + 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, 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, 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, istorage.Put(ic)) + }) + t.Run("big value", func(t *testing.T) { + initVM(t, []byte{1}, make([]byte, storage.MaxStorageValueLen+1), -1) + require.Error(t, istorage.Put(ic)) + }) + }) +} + +func TestDelete(t *testing.T) { + v, cs, ic, _ := createVMAndContractState(t) + + 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) + v.Estack().PushVal(key) + require.NoError(t, istorage.GetContext(ic)) + require.NoError(t, istorage.Put(ic)) + } + put("key1", "value1", 0) + put("key2", "value2", 0) + put("key3", "value3", 0) + + t.Run("good", func(t *testing.T) { + v.Estack().PushVal("key1") + 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, 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, istorage.GetContext(ic)) + require.NoError(t, istorage.ContextAsReadOnly(ic)) + require.Error(t, istorage.Delete(ic)) + }) +} + +func TestFind(t *testing.T) { + v, contractState, context, _ := createVMAndContractState(t) + + arr := []stackitem.Item{ + stackitem.NewBigInteger(big.NewInt(42)), + stackitem.NewByteArray([]byte("second")), + stackitem.Null{}, + } + rawArr, err := stackitem.Serialize(stackitem.NewArray(arr)) + require.NoError(t, err) + rawArr0, err := stackitem.Serialize(stackitem.NewArray(arr[:0])) + require.NoError(t, err) + rawArr1, err := stackitem.Serialize(stackitem.NewArray(arr[:1])) + require.NoError(t, err) + + skeys := [][]byte{{0x01, 0x02}, {0x02, 0x01}, {0x01, 0x01}, + {0x04, 0x00}, {0x05, 0x00}, {0x06}, {0x07}, {0x08}, + {0x09, 0x12, 0x34}, {0x09, 0x12, 0x56}, + } + items := []state.StorageItem{ + []byte{0x01, 0x02, 0x03, 0x04}, + []byte{0x04, 0x03, 0x02, 0x01}, + []byte{0x03, 0x04, 0x05, 0x06}, + []byte{byte(stackitem.ByteArrayT), 2, 0xCA, 0xFE}, + []byte{0xFF, 0xFF}, + rawArr, + rawArr0, + rawArr1, + []byte{111}, + []byte{222}, + } + + require.NoError(t, native.PutContractState(context.DAO, contractState)) + + id := contractState.ID + + for i := range skeys { + context.DAO.PutStorageItem(id, skeys[i], items[i]) + } + + 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(&istorage.Context{ID: id})) + + err := istorage.Find(context) + require.NoError(t, err) + + var iter *stackitem.Interop + require.NotPanics(t, func() { iter = v.Estack().Pop().Interop() }) + + for i := range expected { // sorted indices with mathing prefix + v.Estack().PushVal(iter) + require.NoError(t, iterator.Next(context)) + require.True(t, v.Estack().Pop().Bool()) + + v.Estack().PushVal(iter) + if expected[i] == nil { + require.Panics(t, func() { _ = iterator.Value(context) }) + return + } + require.NoError(t, iterator.Value(context)) + require.Equal(t, expected[i], v.Estack().Pop().Item()) + } + + v.Estack().PushVal(iter) + require.NoError(t, iterator.Next(context)) + require.False(t, v.Estack().Pop().Bool()) + } + + t.Run("normal invocation", func(t *testing.T) { + testFind(t, []byte{0x01}, istorage.FindDefault, []stackitem.Item{ + stackitem.NewStruct([]stackitem.Item{ + stackitem.NewByteArray(skeys[2]), + stackitem.NewByteArray(items[2]), + }), + stackitem.NewStruct([]stackitem.Item{ + stackitem.NewByteArray(skeys[0]), + stackitem.NewByteArray(items[0]), + }), + }) + }) + + t.Run("keys only", func(t *testing.T) { + testFind(t, []byte{0x01}, istorage.FindKeysOnly, []stackitem.Item{ + stackitem.NewByteArray(skeys[2]), + stackitem.NewByteArray(skeys[0]), + }) + }) + t.Run("remove prefix", func(t *testing.T) { + testFind(t, []byte{0x01}, istorage.FindKeysOnly|istorage.FindRemovePrefix, []stackitem.Item{ + stackitem.NewByteArray(skeys[2][1:]), + stackitem.NewByteArray(skeys[0][1:]), + }) + testFind(t, []byte{0x09, 0x12}, istorage.FindKeysOnly|istorage.FindRemovePrefix, []stackitem.Item{ + stackitem.NewByteArray(skeys[8][2:]), + stackitem.NewByteArray(skeys[9][2:]), + }) + }) + t.Run("values only", func(t *testing.T) { + testFind(t, []byte{0x01}, istorage.FindValuesOnly, []stackitem.Item{ + stackitem.NewByteArray(items[2]), + stackitem.NewByteArray(items[0]), + }) + }) + t.Run("deserialize values", func(t *testing.T) { + testFind(t, []byte{0x04}, istorage.FindValuesOnly|istorage.FindDeserialize, []stackitem.Item{ + stackitem.NewByteArray(items[3][2:]), + }) + t.Run("invalid", func(t *testing.T) { + v.Estack().PushVal(istorage.FindDeserialize) + v.Estack().PushVal([]byte{0x05}) + v.Estack().PushVal(stackitem.NewInterop(&istorage.Context{ID: id})) + err := istorage.Find(context) + require.NoError(t, err) + + var iter *stackitem.Interop + require.NotPanics(t, func() { iter = v.Estack().Pop().Interop() }) + + v.Estack().PushVal(iter) + require.NoError(t, iterator.Next(context)) + + v.Estack().PushVal(iter) + require.Panics(t, func() { _ = iterator.Value(context) }) + }) + }) + t.Run("PickN", func(t *testing.T) { + testFind(t, []byte{0x06}, istorage.FindPick0|istorage.FindValuesOnly|istorage.FindDeserialize, arr[:1]) + testFind(t, []byte{0x06}, istorage.FindPick1|istorage.FindValuesOnly|istorage.FindDeserialize, arr[1:2]) + // Array with 0 elements. + testFind(t, []byte{0x07}, istorage.FindPick0|istorage.FindValuesOnly|istorage.FindDeserialize, + []stackitem.Item{nil}) + // Array with 1 element. + testFind(t, []byte{0x08}, istorage.FindPick1|istorage.FindValuesOnly|istorage.FindDeserialize, + []stackitem.Item{nil}) + // Not an array, but serialized ByteArray. + testFind(t, []byte{0x04}, istorage.FindPick1|istorage.FindValuesOnly|istorage.FindDeserialize, + []stackitem.Item{nil}) + }) + + t.Run("normal invocation, empty result", func(t *testing.T) { + testFind(t, []byte{0x03}, istorage.FindDefault, nil) + }) + + t.Run("invalid options", func(t *testing.T) { + invalid := []int64{ + istorage.FindKeysOnly | istorage.FindValuesOnly, + ^istorage.FindAll, + istorage.FindKeysOnly | istorage.FindDeserialize, + istorage.FindPick0, + istorage.FindPick0 | istorage.FindPick1 | istorage.FindDeserialize, + istorage.FindPick0 | istorage.FindPick1, + } + for _, opts := range invalid { + v.Estack().PushVal(opts) + v.Estack().PushVal([]byte{0x01}) + v.Estack().PushVal(stackitem.NewInterop(&istorage.Context{ID: id})) + require.Error(t, istorage.Find(context)) + } + }) + 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, istorage.Find(context)) + }) + + t.Run("invalid id", func(t *testing.T) { + invalidID := id + 1 + + v.Estack().PushVal(istorage.FindDefault) + v.Estack().PushVal([]byte{0x01}) + v.Estack().PushVal(stackitem.NewInterop(&istorage.Context{ID: invalidID})) + + require.NoError(t, istorage.Find(context)) + require.NoError(t, iterator.Next(context)) + require.False(t, v.Estack().Pop().Bool()) + }) +} + +// Helper functions to create VM, InteropContext, TX, Account, Contract. + +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, *core.Blockchain) { + script := []byte("testscript") + m := manifest.NewManifest("Test") + ne, err := nef.NewFile(script) + require.NoError(t, err) + contractState := &state.Contract{ + ContractBase: state.ContractBase{ + NEF: *ne, + Hash: hash.Hash160(script), + Manifest: *m, + ID: 123, + }, + } + + v, context, chain := createVM(t) + return v, contractState, context, chain +} diff --git a/pkg/core/interop_system.go b/pkg/core/interop_system.go deleted file mode 100644 index 96d9b0a09..000000000 --- a/pkg/core/interop_system.go +++ /dev/null @@ -1,270 +0,0 @@ -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" -) - -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 -} - -// 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() - 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 -} - -// 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 deleted file mode 100644 index e0211922f..000000000 --- a/pkg/core/interop_system_core_test.go +++ /dev/null @@ -1,984 +0,0 @@ -package core - -import ( - "errors" - "fmt" - "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/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" - "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, bc := 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)) - - 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, bc := createVMAndContractState(t) - - require.NoError(t, bc.contracts.Management.PutContractState(ic.DAO, cs)) - - initVM := func(t *testing.T, key, value []byte, gas int64) { - v := ic.SpawnVM() - v.LoadScript(cs.NEF.Script) - v.GasLimit = gas - v.Estack().PushVal(value) - v.Estack().PushVal(key) - require.NoError(t, storageGetContext(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) - }) - - initVM(t, []byte{4}, []byte{5, 6}, 3*native.DefaultStoragePrice) - require.NoError(t, storagePut(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) - }) - initVM(t, []byte{4}, []byte{5, 6, 7, 8}, 3*native.DefaultStoragePrice) - require.NoError(t, storagePut(ic)) - initVM(t, []byte{4}, []byte{5, 6}, native.DefaultStoragePrice) - require.NoError(t, storagePut(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)) - }) - - 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)) - }) - t.Run("big key", func(t *testing.T) { - initVM(t, make([]byte, storage.MaxStorageKeyLen+1), []byte{1}, -1) - require.Error(t, storagePut(ic)) - }) - t.Run("big value", func(t *testing.T) { - initVM(t, []byte{1}, make([]byte, storage.MaxStorageValueLen+1), -1) - require.Error(t, storagePut(ic)) - }) - }) -} - -func TestStorageDelete(t *testing.T) { - v, cs, ic, bc := createVMAndContractState(t) - - require.NoError(t, bc.contracts.Management.PutContractState(ic.DAO, cs)) - v.LoadScriptWithHash(cs.NEF.Script, cs.Hash, callflag.All) - 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)) - } - put("key1", "value1", 0) - put("key2", "value2", 0) - put("key3", "value3", 0) - - t.Run("good", func(t *testing.T) { - v.Estack().PushVal("key1") - require.NoError(t, storageGetContext(ic)) - require.NoError(t, storageDelete(ic)) - }) - t.Run("readonly context", func(t *testing.T) { - v.Estack().PushVal("key2") - require.NoError(t, storageGetReadOnlyContext(ic)) - require.Error(t, storageDelete(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)) - }) -} - -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)) - - 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, chain.contracts.Management.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) - - arr := []stackitem.Item{ - stackitem.NewBigInteger(big.NewInt(42)), - stackitem.NewByteArray([]byte("second")), - stackitem.Null{}, - } - rawArr, err := stackitem.Serialize(stackitem.NewArray(arr)) - require.NoError(t, err) - rawArr0, err := stackitem.Serialize(stackitem.NewArray(arr[:0])) - require.NoError(t, err) - rawArr1, err := stackitem.Serialize(stackitem.NewArray(arr[:1])) - require.NoError(t, err) - - skeys := [][]byte{{0x01, 0x02}, {0x02, 0x01}, {0x01, 0x01}, - {0x04, 0x00}, {0x05, 0x00}, {0x06}, {0x07}, {0x08}, - {0x09, 0x12, 0x34}, {0x09, 0x12, 0x56}, - } - items := []state.StorageItem{ - []byte{0x01, 0x02, 0x03, 0x04}, - []byte{0x04, 0x03, 0x02, 0x01}, - []byte{0x03, 0x04, 0x05, 0x06}, - []byte{byte(stackitem.ByteArrayT), 2, 0xCA, 0xFE}, - []byte{0xFF, 0xFF}, - rawArr, - rawArr0, - rawArr1, - []byte{111}, - []byte{222}, - } - - require.NoError(t, chain.contracts.Management.PutContractState(chain.dao, contractState)) - - id := contractState.ID - - for i := range skeys { - context.DAO.PutStorageItem(id, skeys[i], items[i]) - } - - 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})) - - err := storageFind(context) - require.NoError(t, err) - - var iter *stackitem.Interop - require.NotPanics(t, func() { iter = v.Estack().Pop().Interop() }) - - for i := range expected { // sorted indices with mathing prefix - v.Estack().PushVal(iter) - require.NoError(t, iterator.Next(context)) - require.True(t, v.Estack().Pop().Bool()) - - v.Estack().PushVal(iter) - if expected[i] == nil { - require.Panics(t, func() { _ = iterator.Value(context) }) - return - } - require.NoError(t, iterator.Value(context)) - require.Equal(t, expected[i], v.Estack().Pop().Item()) - } - - v.Estack().PushVal(iter) - require.NoError(t, iterator.Next(context)) - require.False(t, v.Estack().Pop().Bool()) - } - - t.Run("normal invocation", func(t *testing.T) { - testFind(t, []byte{0x01}, istorage.FindDefault, []stackitem.Item{ - stackitem.NewStruct([]stackitem.Item{ - stackitem.NewByteArray(skeys[2]), - stackitem.NewByteArray(items[2]), - }), - stackitem.NewStruct([]stackitem.Item{ - stackitem.NewByteArray(skeys[0]), - stackitem.NewByteArray(items[0]), - }), - }) - }) - - t.Run("keys only", func(t *testing.T) { - testFind(t, []byte{0x01}, istorage.FindKeysOnly, []stackitem.Item{ - stackitem.NewByteArray(skeys[2]), - stackitem.NewByteArray(skeys[0]), - }) - }) - t.Run("remove prefix", func(t *testing.T) { - testFind(t, []byte{0x01}, istorage.FindKeysOnly|istorage.FindRemovePrefix, []stackitem.Item{ - stackitem.NewByteArray(skeys[2][1:]), - stackitem.NewByteArray(skeys[0][1:]), - }) - testFind(t, []byte{0x09, 0x12}, istorage.FindKeysOnly|istorage.FindRemovePrefix, []stackitem.Item{ - stackitem.NewByteArray(skeys[8][2:]), - stackitem.NewByteArray(skeys[9][2:]), - }) - }) - t.Run("values only", func(t *testing.T) { - testFind(t, []byte{0x01}, istorage.FindValuesOnly, []stackitem.Item{ - stackitem.NewByteArray(items[2]), - stackitem.NewByteArray(items[0]), - }) - }) - t.Run("deserialize values", func(t *testing.T) { - testFind(t, []byte{0x04}, istorage.FindValuesOnly|istorage.FindDeserialize, []stackitem.Item{ - stackitem.NewByteArray(items[3][2:]), - }) - 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) - require.NoError(t, err) - - var iter *stackitem.Interop - require.NotPanics(t, func() { iter = v.Estack().Pop().Interop() }) - - v.Estack().PushVal(iter) - require.NoError(t, iterator.Next(context)) - - v.Estack().PushVal(iter) - require.Panics(t, func() { _ = iterator.Value(context) }) - }) - }) - t.Run("PickN", func(t *testing.T) { - testFind(t, []byte{0x06}, istorage.FindPick0|istorage.FindValuesOnly|istorage.FindDeserialize, arr[:1]) - testFind(t, []byte{0x06}, istorage.FindPick1|istorage.FindValuesOnly|istorage.FindDeserialize, arr[1:2]) - // Array with 0 elements. - testFind(t, []byte{0x07}, istorage.FindPick0|istorage.FindValuesOnly|istorage.FindDeserialize, - []stackitem.Item{nil}) - // Array with 1 element. - testFind(t, []byte{0x08}, istorage.FindPick1|istorage.FindValuesOnly|istorage.FindDeserialize, - []stackitem.Item{nil}) - // Not an array, but serialized ByteArray. - testFind(t, []byte{0x04}, istorage.FindPick1|istorage.FindValuesOnly|istorage.FindDeserialize, - []stackitem.Item{nil}) - }) - - t.Run("normal invocation, empty result", func(t *testing.T) { - testFind(t, []byte{0x03}, istorage.FindDefault, nil) - }) - - t.Run("invalid options", func(t *testing.T) { - invalid := []int64{ - istorage.FindKeysOnly | istorage.FindValuesOnly, - ^istorage.FindAll, - istorage.FindKeysOnly | istorage.FindDeserialize, - istorage.FindPick0, - istorage.FindPick0 | istorage.FindPick1 | istorage.FindDeserialize, - istorage.FindPick0 | istorage.FindPick1, - } - 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)) - } - }) - t.Run("invalid type for StorageContext", 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)) - }) - - t.Run("invalid id", func(t *testing.T) { - invalidID := id + 1 - - v.Estack().PushVal(istorage.FindDefault) - v.Estack().PushVal([]byte{0x01}) - v.Estack().PushVal(stackitem.NewInterop(&StorageContext{ID: invalidID})) - - require.NoError(t, storageFind(context)) - require.NoError(t, iterator.Next(context)) - require.False(t, v.Estack().Pop().Bool()) - }) -} - -// 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 createVMAndContractState(t testing.TB) (*vm.VM, *state.Contract, *interop.Context, *Blockchain) { - script := []byte("testscript") - m := manifest.NewManifest("Test") - ne, err := nef.NewFile(script) - require.NoError(t, err) - contractState := &state.Contract{ - ContractBase: state.ContractBase{ - NEF: *ne, - Hash: hash.Hash160(script), - Manifest: *m, - ID: 123, - }, - } - - v, context, chain := createVM(t) - 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) - for i := range args { - ic.VM.Estack().PushVal(args[i]) - } - ic.VM.GasLimit = -1 -} - -func TestContractCall(t *testing.T) { - _, ic, bc := 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)) - - 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 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) - - 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, bc.contracts.Management.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) { - 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/interops.go b/pkg/core/interops.go index cf2e3982b..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" @@ -33,9 +34,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}, @@ -54,7 +55,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, @@ -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}, } 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) + } + }) + } +} 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 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/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 diff --git a/pkg/core/native_management_test.go b/pkg/core/native/management_neotest_test.go similarity index 84% rename from pkg/core/native_management_test.go rename to pkg/core/native/management_neotest_test.go index e38a8cf96..f805d439b 100644 --- a/pkg/core/native_management_test.go +++ b/pkg/core/native/management_neotest_test.go @@ -1,8 +1,9 @@ -package core_test +package native_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/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_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" 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) 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() -} diff --git a/pkg/core/statesync_test.go b/pkg/core/statesync/neotest_test.go similarity index 98% rename from pkg/core/statesync_test.go rename to pkg/core/statesync/neotest_test.go index 4162c921b..a9dea0cf9 100644 --- a/pkg/core/statesync_test.go +++ b/pkg/core/statesync/neotest_test.go @@ -1,8 +1,9 @@ -package core_test +package statesync_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) 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), + }) +} 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" 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 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 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 86df6c8b5..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" @@ -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++ {